Move models to typescript-sequelize
[oweals/peertube.git] / server / models / account / user.ts
1 import * as Sequelize from 'sequelize'
2 import {
3   AllowNull,
4   BeforeCreate,
5   BeforeUpdate,
6   Column, CreatedAt,
7   DataType,
8   Default,
9   HasMany,
10   HasOne,
11   Is,
12   IsEmail,
13   Model,
14   Table, UpdatedAt
15 } from 'sequelize-typescript'
16 import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared'
17 import {
18   comparePassword,
19   cryptPassword
20 } from '../../helpers'
21 import {
22   isUserDisplayNSFWValid, isUserPasswordValid, isUserRoleValid, isUserUsernameValid,
23   isUserVideoQuotaValid
24 } from '../../helpers/custom-validators/users'
25 import { OAuthTokenModel } from '../oauth/oauth-token'
26 import { getSort, throwIfNotValid } from '../utils'
27 import { VideoChannelModel } from '../video/video-channel'
28 import { AccountModel } from './account'
29
30 @Table({
31   tableName: 'user',
32   indexes: [
33     {
34       fields: [ 'username' ],
35       unique: true
36     },
37     {
38       fields: [ 'email' ],
39       unique: true
40     }
41   ]
42 })
43 export class UserModel extends Model<UserModel> {
44
45   @AllowNull(false)
46   @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password'))
47   @Column
48   password: string
49
50   @AllowNull(false)
51   @Is('UserPassword', value => throwIfNotValid(value, isUserUsernameValid, 'user name'))
52   @Column
53   username: string
54
55   @AllowNull(false)
56   @IsEmail
57   @Column(DataType.STRING(400))
58   email: string
59
60   @AllowNull(false)
61   @Default(false)
62   @Is('UserDisplayNSFW', value => throwIfNotValid(value, isUserDisplayNSFWValid, 'display NSFW boolean'))
63   @Column
64   displayNSFW: boolean
65
66   @AllowNull(false)
67   @Is('UserRole', value => throwIfNotValid(value, isUserRoleValid, 'role'))
68   @Column
69   role: number
70
71   @AllowNull(false)
72   @Is('UserVideoQuota', value => throwIfNotValid(value, isUserVideoQuotaValid, 'video quota'))
73   @Column(DataType.BIGINT)
74   videoQuota: number
75
76   @CreatedAt
77   createdAt: Date
78
79   @UpdatedAt
80   updatedAt: Date
81
82   @HasOne(() => AccountModel, {
83     foreignKey: 'userId',
84     onDelete: 'cascade'
85   })
86   Account: AccountModel
87
88   @HasMany(() => OAuthTokenModel, {
89     foreignKey: 'userId',
90     onDelete: 'cascade'
91   })
92   OAuthTokens: OAuthTokenModel[]
93
94   @BeforeCreate
95   @BeforeUpdate
96   static cryptPasswordIfNeeded (instance: UserModel) {
97     if (instance.changed('password')) {
98       return cryptPassword(instance.password)
99         .then(hash => {
100           instance.password = hash
101           return undefined
102         })
103     }
104   }
105
106   static countTotal () {
107     return this.count()
108   }
109
110   static getByUsername (username: string) {
111     const query = {
112       where: {
113         username: username
114       },
115       include: [ { model: AccountModel, required: true } ]
116     }
117
118     return UserModel.findOne(query)
119   }
120
121   static listForApi (start: number, count: number, sort: string) {
122     const query = {
123       offset: start,
124       limit: count,
125       order: [ getSort(sort) ],
126       include: [ { model: AccountModel, required: true } ]
127     }
128
129     return UserModel.findAndCountAll(query)
130       .then(({ rows, count }) => {
131         return {
132           data: rows,
133           total: count
134         }
135       })
136   }
137
138   static loadById (id: number) {
139     const options = {
140       include: [ { model: AccountModel, required: true } ]
141     }
142
143     return UserModel.findById(id, options)
144   }
145
146   static loadByUsername (username: string) {
147     const query = {
148       where: {
149         username
150       },
151       include: [ { model: AccountModel, required: true } ]
152     }
153
154     return UserModel.findOne(query)
155   }
156
157   static loadByUsernameAndPopulateChannels (username: string) {
158     const query = {
159       where: {
160         username
161       },
162       include: [
163         {
164           model: AccountModel,
165           required: true,
166           include: [ VideoChannelModel ]
167         }
168       ]
169     }
170
171     return UserModel.findOne(query)
172   }
173
174   static loadByUsernameOrEmail (username: string, email: string) {
175     const query = {
176       include: [ { model: AccountModel, required: true } ],
177       where: {
178         [ Sequelize.Op.or ]: [ { username }, { email } ]
179       }
180     }
181
182     // FIXME: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18387
183     return (UserModel as any).findOne(query)
184   }
185
186   private static getOriginalVideoFileTotalFromUser (user: UserModel) {
187     // Don't use sequelize because we need to use a sub query
188     const query = 'SELECT SUM("size") AS "total" FROM ' +
189       '(SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' +
190       'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' +
191       'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
192       'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
193       'INNER JOIN "user" ON "account"."userId" = "user"."id" ' +
194       'WHERE "user"."id" = $userId GROUP BY "video"."id") t'
195
196     const options = {
197       bind: { userId: user.id },
198       type: Sequelize.QueryTypes.SELECT
199     }
200     return UserModel.sequelize.query(query, options)
201       .then(([ { total } ]) => {
202         if (total === null) return 0
203
204         return parseInt(total, 10)
205       })
206   }
207
208   hasRight (right: UserRight) {
209     return hasUserRight(this.role, right)
210   }
211
212   isPasswordMatch (password: string) {
213     return comparePassword(password, this.password)
214   }
215
216   toFormattedJSON () {
217     const json = {
218       id: this.id,
219       username: this.username,
220       email: this.email,
221       displayNSFW: this.displayNSFW,
222       role: this.role,
223       roleLabel: USER_ROLE_LABELS[ this.role ],
224       videoQuota: this.videoQuota,
225       createdAt: this.createdAt,
226       account: this.Account.toFormattedJSON()
227     }
228
229     if (Array.isArray(this.Account.VideoChannels) === true) {
230       json['videoChannels'] = this.Account.VideoChannels
231         .map(c => c.toFormattedJSON())
232         .sort((v1, v2) => {
233           if (v1.createdAt < v2.createdAt) return -1
234           if (v1.createdAt === v2.createdAt) return 0
235
236           return 1
237         })
238     }
239
240     return json
241   }
242
243   isAbleToUploadVideo (videoFile: Express.Multer.File) {
244     if (this.videoQuota === -1) return Promise.resolve(true)
245
246     return UserModel.getOriginalVideoFileTotalFromUser(this)
247       .then(totalBytes => {
248         return (videoFile.size + totalBytes) < this.videoQuota
249       })
250   }
251 }