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