]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/models/account/user.ts
Move models to typescript-sequelize
[github/Chocobozzz/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 }