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