Add ability to choose what policy we have for NSFW videos
[oweals/peertube.git] / server / models / account / user.ts
1 import * as Sequelize from 'sequelize'
2 import {
3   AllowNull,
4   BeforeCreate,
5   BeforeUpdate,
6   Column,
7   CreatedAt,
8   DataType,
9   Default,
10   DefaultScope,
11   HasMany,
12   HasOne,
13   Is,
14   IsEmail,
15   Model,
16   Scopes,
17   Table,
18   UpdatedAt
19 } from 'sequelize-typescript'
20 import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared'
21 import { User, UserRole } from '../../../shared/models/users'
22 import {
23   isUserAutoPlayVideoValid,
24   isUserNSFWPolicyValid,
25   isUserPasswordValid,
26   isUserRoleValid,
27   isUserUsernameValid,
28   isUserVideoQuotaValid
29 } from '../../helpers/custom-validators/users'
30 import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto'
31 import { OAuthTokenModel } from '../oauth/oauth-token'
32 import { getSort, throwIfNotValid } from '../utils'
33 import { VideoChannelModel } from '../video/video-channel'
34 import { AccountModel } from './account'
35 import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type'
36 import { values } from 'lodash'
37 import { NSFW_POLICY_TYPES } from '../../initializers'
38
39 @DefaultScope({
40   include: [
41     {
42       model: () => AccountModel,
43       required: true
44     }
45   ]
46 })
47 @Scopes({
48   withVideoChannel: {
49     include: [
50       {
51         model: () => AccountModel,
52         required: true,
53         include: [ () => VideoChannelModel ]
54       }
55     ]
56   }
57 })
58 @Table({
59   tableName: 'user',
60   indexes: [
61     {
62       fields: [ 'username' ],
63       unique: true
64     },
65     {
66       fields: [ 'email' ],
67       unique: true
68     }
69   ]
70 })
71 export class UserModel extends Model<UserModel> {
72
73   @AllowNull(false)
74   @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password'))
75   @Column
76   password: string
77
78   @AllowNull(false)
79   @Is('UserPassword', value => throwIfNotValid(value, isUserUsernameValid, 'user name'))
80   @Column
81   username: string
82
83   @AllowNull(false)
84   @IsEmail
85   @Column(DataType.STRING(400))
86   email: string
87
88   @AllowNull(false)
89   @Is('UserNSFWPolicy', value => throwIfNotValid(value, isUserNSFWPolicyValid, 'NSFW policy'))
90   @Column(DataType.ENUM(values(NSFW_POLICY_TYPES)))
91   nsfwPolicy: NSFWPolicyType
92
93   @AllowNull(false)
94   @Default(true)
95   @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean'))
96   @Column
97   autoPlayVideo: boolean
98
99   @AllowNull(false)
100   @Is('UserRole', value => throwIfNotValid(value, isUserRoleValid, 'role'))
101   @Column
102   role: number
103
104   @AllowNull(false)
105   @Is('UserVideoQuota', value => throwIfNotValid(value, isUserVideoQuotaValid, 'video quota'))
106   @Column(DataType.BIGINT)
107   videoQuota: number
108
109   @CreatedAt
110   createdAt: Date
111
112   @UpdatedAt
113   updatedAt: Date
114
115   @HasOne(() => AccountModel, {
116     foreignKey: 'userId',
117     onDelete: 'cascade',
118     hooks: true
119   })
120   Account: AccountModel
121
122   @HasMany(() => OAuthTokenModel, {
123     foreignKey: 'userId',
124     onDelete: 'cascade'
125   })
126   OAuthTokens: OAuthTokenModel[]
127
128   @BeforeCreate
129   @BeforeUpdate
130   static cryptPasswordIfNeeded (instance: UserModel) {
131     if (instance.changed('password')) {
132       return cryptPassword(instance.password)
133         .then(hash => {
134           instance.password = hash
135           return undefined
136         })
137     }
138   }
139
140   static countTotal () {
141     return this.count()
142   }
143
144   static listForApi (start: number, count: number, sort: string) {
145     const query = {
146       offset: start,
147       limit: count,
148       order: getSort(sort)
149     }
150
151     return UserModel.findAndCountAll(query)
152       .then(({ rows, count }) => {
153         return {
154           data: rows,
155           total: count
156         }
157       })
158   }
159
160   static listEmailsWithRight (right: UserRight) {
161     const roles = Object.keys(USER_ROLE_LABELS)
162       .map(k => parseInt(k, 10) as UserRole)
163       .filter(role => hasUserRight(role, right))
164
165     console.log(roles)
166
167     const query = {
168       attribute: [ 'email' ],
169       where: {
170         role: {
171           [Sequelize.Op.in]: roles
172         }
173       }
174     }
175
176     return UserModel.unscoped()
177       .findAll(query)
178       .then(u => u.map(u => u.email))
179   }
180
181   static loadById (id: number) {
182     return UserModel.findById(id)
183   }
184
185   static loadByUsername (username: string) {
186     const query = {
187       where: {
188         username
189       }
190     }
191
192     return UserModel.findOne(query)
193   }
194
195   static loadByUsernameAndPopulateChannels (username: string) {
196     const query = {
197       where: {
198         username
199       }
200     }
201
202     return UserModel.scope('withVideoChannel').findOne(query)
203   }
204
205   static loadByEmail (email: string) {
206     const query = {
207       where: {
208         email
209       }
210     }
211
212     return UserModel.findOne(query)
213   }
214
215   static loadByUsernameOrEmail (username: string, email?: string) {
216     if (!email) email = username
217
218     const query = {
219       where: {
220         [ Sequelize.Op.or ]: [ { username }, { email } ]
221       }
222     }
223
224     return UserModel.findOne(query)
225   }
226
227   static getOriginalVideoFileTotalFromUser (user: UserModel) {
228     // Don't use sequelize because we need to use a sub query
229     const query = 'SELECT SUM("size") AS "total" FROM ' +
230       '(SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' +
231       'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' +
232       'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
233       'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
234       'INNER JOIN "user" ON "account"."userId" = "user"."id" ' +
235       'WHERE "user"."id" = $userId GROUP BY "video"."id") t'
236
237     const options = {
238       bind: { userId: user.id },
239       type: Sequelize.QueryTypes.SELECT
240     }
241     return UserModel.sequelize.query(query, options)
242       .then(([ { total } ]) => {
243         if (total === null) return 0
244
245         return parseInt(total, 10)
246       })
247   }
248
249   static async getStats () {
250     const totalUsers = await UserModel.count()
251
252     return {
253       totalUsers
254     }
255   }
256
257   hasRight (right: UserRight) {
258     return hasUserRight(this.role, right)
259   }
260
261   isPasswordMatch (password: string) {
262     return comparePassword(password, this.password)
263   }
264
265   toFormattedJSON (): User {
266     const json = {
267       id: this.id,
268       username: this.username,
269       email: this.email,
270       nsfwPolicy: this.nsfwPolicy,
271       autoPlayVideo: this.autoPlayVideo,
272       role: this.role,
273       roleLabel: USER_ROLE_LABELS[ this.role ],
274       videoQuota: this.videoQuota,
275       createdAt: this.createdAt,
276       account: this.Account.toFormattedJSON(),
277       videoChannels: []
278     }
279
280     if (Array.isArray(this.Account.VideoChannels) === true) {
281       json.videoChannels = this.Account.VideoChannels
282         .map(c => c.toFormattedJSON())
283         .sort((v1, v2) => {
284           if (v1.createdAt < v2.createdAt) return -1
285           if (v1.createdAt === v2.createdAt) return 0
286
287           return 1
288         })
289     }
290
291     return json
292   }
293
294   isAbleToUploadVideo (videoFile: Express.Multer.File) {
295     if (this.videoQuota === -1) return Promise.resolve(true)
296
297     return UserModel.getOriginalVideoFileTotalFromUser(this)
298       .then(totalBytes => {
299         return (videoFile.size + totalBytes) < this.videoQuota
300       })
301   }
302 }