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