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