Remove references to author
[oweals/peertube.git] / server / models / account / user.ts
1 import * as Sequelize from 'sequelize'
2
3 import { getSort, addMethodsToModel } from '../utils'
4 import {
5   cryptPassword,
6   comparePassword,
7   isUserPasswordValid,
8   isUserUsernameValid,
9   isUserDisplayNSFWValid,
10   isUserVideoQuotaValid,
11   isUserRoleValid
12 } from '../../helpers'
13 import { UserRight, USER_ROLE_LABELS, hasUserRight } from '../../../shared'
14
15 import {
16   UserInstance,
17   UserAttributes,
18
19   UserMethods
20 } from './user-interface'
21
22 let User: Sequelize.Model<UserInstance, UserAttributes>
23 let isPasswordMatch: UserMethods.IsPasswordMatch
24 let hasRight: UserMethods.HasRight
25 let toFormattedJSON: UserMethods.ToFormattedJSON
26 let countTotal: UserMethods.CountTotal
27 let getByUsername: UserMethods.GetByUsername
28 let listForApi: UserMethods.ListForApi
29 let loadById: UserMethods.LoadById
30 let loadByUsername: UserMethods.LoadByUsername
31 let loadByUsernameAndPopulateChannels: UserMethods.LoadByUsernameAndPopulateChannels
32 let loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail
33 let isAbleToUploadVideo: UserMethods.IsAbleToUploadVideo
34
35 export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
36   User = sequelize.define<UserInstance, UserAttributes>('User',
37     {
38       password: {
39         type: DataTypes.STRING,
40         allowNull: false,
41         validate: {
42           passwordValid: value => {
43             const res = isUserPasswordValid(value)
44             if (res === false) throw new Error('Password not valid.')
45           }
46         }
47       },
48       username: {
49         type: DataTypes.STRING,
50         allowNull: false,
51         validate: {
52           usernameValid: value => {
53             const res = isUserUsernameValid(value)
54             if (res === false) throw new Error('Username not valid.')
55           }
56         }
57       },
58       email: {
59         type: DataTypes.STRING(400),
60         allowNull: false,
61         validate: {
62           isEmail: true
63         }
64       },
65       displayNSFW: {
66         type: DataTypes.BOOLEAN,
67         allowNull: false,
68         defaultValue: false,
69         validate: {
70           nsfwValid: value => {
71             const res = isUserDisplayNSFWValid(value)
72             if (res === false) throw new Error('Display NSFW is not valid.')
73           }
74         }
75       },
76       role: {
77         type: DataTypes.INTEGER,
78         allowNull: false,
79         validate: {
80           roleValid: value => {
81             const res = isUserRoleValid(value)
82             if (res === false) throw new Error('Role is not valid.')
83           }
84         }
85       },
86       videoQuota: {
87         type: DataTypes.BIGINT,
88         allowNull: false,
89         validate: {
90           videoQuotaValid: value => {
91             const res = isUserVideoQuotaValid(value)
92             if (res === false) throw new Error('Video quota is not valid.')
93           }
94         }
95       }
96     },
97     {
98       indexes: [
99         {
100           fields: [ 'username' ],
101           unique: true
102         },
103         {
104           fields: [ 'email' ],
105           unique: true
106         }
107       ],
108       hooks: {
109         beforeCreate: beforeCreateOrUpdate,
110         beforeUpdate: beforeCreateOrUpdate
111       }
112     }
113   )
114
115   const classMethods = [
116     associate,
117
118     countTotal,
119     getByUsername,
120     listForApi,
121     loadById,
122     loadByUsername,
123     loadByUsernameAndPopulateChannels,
124     loadByUsernameOrEmail
125   ]
126   const instanceMethods = [
127     hasRight,
128     isPasswordMatch,
129     toFormattedJSON,
130     isAbleToUploadVideo
131   ]
132   addMethodsToModel(User, classMethods, instanceMethods)
133
134   return User
135 }
136
137 function beforeCreateOrUpdate (user: UserInstance) {
138   if (user.changed('password')) {
139     return cryptPassword(user.password)
140       .then(hash => {
141         user.password = hash
142         return undefined
143       })
144   }
145 }
146
147 // ------------------------------ METHODS ------------------------------
148
149 hasRight = function (this: UserInstance, right: UserRight) {
150   return hasUserRight(this.role, right)
151 }
152
153 isPasswordMatch = function (this: UserInstance, password: string) {
154   return comparePassword(password, this.password)
155 }
156
157 toFormattedJSON = function (this: UserInstance) {
158   const json = {
159     id: this.id,
160     username: this.username,
161     email: this.email,
162     displayNSFW: this.displayNSFW,
163     role: this.role,
164     roleLabel: USER_ROLE_LABELS[this.role],
165     videoQuota: this.videoQuota,
166     createdAt: this.createdAt,
167     account: {
168       id: this.Account.id,
169       uuid: this.Account.uuid
170     }
171   }
172
173   if (Array.isArray(this.Account.VideoChannels) === true) {
174     const videoChannels = this.Account.VideoChannels
175       .map(c => c.toFormattedJSON())
176       .sort((v1, v2) => {
177         if (v1.createdAt < v2.createdAt) return -1
178         if (v1.createdAt === v2.createdAt) return 0
179
180         return 1
181       })
182
183     json['videoChannels'] = videoChannels
184   }
185
186   return json
187 }
188
189 isAbleToUploadVideo = function (this: UserInstance, videoFile: Express.Multer.File) {
190   if (this.videoQuota === -1) return Promise.resolve(true)
191
192   return getOriginalVideoFileTotalFromUser(this).then(totalBytes => {
193     return (videoFile.size + totalBytes) < this.videoQuota
194   })
195 }
196
197 // ------------------------------ STATICS ------------------------------
198
199 function associate (models) {
200   User.hasOne(models.Account, {
201     foreignKey: 'userId',
202     onDelete: 'cascade'
203   })
204
205   User.hasMany(models.OAuthToken, {
206     foreignKey: 'userId',
207     onDelete: 'cascade'
208   })
209 }
210
211 countTotal = function () {
212   return this.count()
213 }
214
215 getByUsername = function (username: string) {
216   const query = {
217     where: {
218       username: username
219     },
220     include: [ { model: User['sequelize'].models.Account, required: true } ]
221   }
222
223   return User.findOne(query)
224 }
225
226 listForApi = function (start: number, count: number, sort: string) {
227   const query = {
228     offset: start,
229     limit: count,
230     order: [ getSort(sort) ],
231     include: [ { model: User['sequelize'].models.Account, required: true } ]
232   }
233
234   return User.findAndCountAll(query).then(({ rows, count }) => {
235     return {
236       data: rows,
237       total: count
238     }
239   })
240 }
241
242 loadById = function (id: number) {
243   const options = {
244     include: [ { model: User['sequelize'].models.Account, required: true } ]
245   }
246
247   return User.findById(id, options)
248 }
249
250 loadByUsername = function (username: string) {
251   const query = {
252     where: {
253       username
254     },
255     include: [ { model: User['sequelize'].models.Account, required: true } ]
256   }
257
258   return User.findOne(query)
259 }
260
261 loadByUsernameAndPopulateChannels = function (username: string) {
262   const query = {
263     where: {
264       username
265     },
266     include: [
267       {
268         model: User['sequelize'].models.Account,
269         required: true,
270         include: [ User['sequelize'].models.VideoChannel ]
271       }
272     ]
273   }
274
275   return User.findOne(query)
276 }
277
278 loadByUsernameOrEmail = function (username: string, email: string) {
279   const query = {
280     include: [ { model: User['sequelize'].models.Account, required: true } ],
281     where: {
282       [Sequelize.Op.or]: [ { username }, { email } ]
283     }
284   }
285
286   // FIXME: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18387
287   return (User as any).findOne(query)
288 }
289
290 // ---------------------------------------------------------------------------
291
292 function getOriginalVideoFileTotalFromUser (user: UserInstance) {
293   // Don't use sequelize because we need to use a sub query
294   const query = 'SELECT SUM("size") AS "total" FROM ' +
295                 '(SELECT MAX("VideoFiles"."size") AS "size" FROM "VideoFiles" ' +
296                 'INNER JOIN "Videos" ON "VideoFiles"."videoId" = "Videos"."id" ' +
297                 'INNER JOIN "VideoChannels" ON "VideoChannels"."id" = "Videos"."channelId" ' +
298                 'INNER JOIN "Accounts" ON "VideoChannels"."accountId" = "Accounts"."id" ' +
299                 'INNER JOIN "Users" ON "Accounts"."userId" = "Users"."id" ' +
300                 'WHERE "Users"."id" = $userId GROUP BY "Videos"."id") t'
301
302   const options = {
303     bind: { userId: user.id },
304     type: Sequelize.QueryTypes.SELECT
305   }
306   return User['sequelize'].query(query, options).then(([ { total } ]) => {
307     if (total === null) return 0
308
309     return parseInt(total, 10)
310   })
311 }