]>
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) { |
6fcd19ba C |
139 | return cryptPassword(user.password).then(hash => { |
140 | user.password = hash | |
141 | return undefined | |
26d7d31b | 142 | }) |
feb4bdfd | 143 | } |
69b0a27c | 144 | |
26d7d31b C |
145 | // ------------------------------ METHODS ------------------------------ |
146 | ||
954605a8 C |
147 | hasRight = function (this: UserInstance, right: UserRight) { |
148 | return hasUserRight(this.role, right) | |
149 | } | |
150 | ||
6fcd19ba C |
151 | isPasswordMatch = function (this: UserInstance, password: string) { |
152 | return comparePassword(password, this.password) | |
26d7d31b C |
153 | } |
154 | ||
0aef76c4 | 155 | toFormattedJSON = function (this: UserInstance) { |
72c7248b | 156 | const json = { |
feb4bdfd | 157 | id: this.id, |
26d7d31b | 158 | username: this.username, |
ad4a8a1c | 159 | email: this.email, |
1d49e1e2 | 160 | displayNSFW: this.displayNSFW, |
d74a0680 | 161 | role: this.role, |
954605a8 | 162 | roleLabel: USER_ROLE_LABELS[this.role], |
b0f9f39e | 163 | videoQuota: this.videoQuota, |
72c7248b C |
164 | createdAt: this.createdAt, |
165 | author: { | |
166 | id: this.Author.id, | |
167 | uuid: this.Author.uuid | |
168 | } | |
26d7d31b | 169 | } |
72c7248b C |
170 | |
171 | if (Array.isArray(this.Author.VideoChannels) === true) { | |
172 | const videoChannels = this.Author.VideoChannels | |
173 | .map(c => c.toFormattedJSON()) | |
174 | .sort((v1, v2) => { | |
175 | if (v1.createdAt < v2.createdAt) return -1 | |
176 | if (v1.createdAt === v2.createdAt) return 0 | |
177 | ||
178 | return 1 | |
179 | }) | |
180 | ||
181 | json['videoChannels'] = videoChannels | |
182 | } | |
183 | ||
184 | return json | |
26d7d31b | 185 | } |
198b205c | 186 | |
b0f9f39e C |
187 | isAbleToUploadVideo = function (this: UserInstance, videoFile: Express.Multer.File) { |
188 | if (this.videoQuota === -1) return Promise.resolve(true) | |
189 | ||
190 | return getOriginalVideoFileTotalFromUser(this).then(totalBytes => { | |
191 | return (videoFile.size + totalBytes) < this.videoQuota | |
192 | }) | |
193 | } | |
194 | ||
26d7d31b | 195 | // ------------------------------ STATICS ------------------------------ |
69b0a27c | 196 | |
feb4bdfd | 197 | function associate (models) { |
e02643f3 | 198 | User.hasOne(models.Author, { |
4712081f C |
199 | foreignKey: 'userId', |
200 | onDelete: 'cascade' | |
201 | }) | |
202 | ||
e02643f3 | 203 | User.hasMany(models.OAuthToken, { |
feb4bdfd C |
204 | foreignKey: 'userId', |
205 | onDelete: 'cascade' | |
206 | }) | |
207 | } | |
208 | ||
6fcd19ba C |
209 | countTotal = function () { |
210 | return this.count() | |
089ff2f2 C |
211 | } |
212 | ||
69818c93 | 213 | getByUsername = function (username: string) { |
feb4bdfd C |
214 | const query = { |
215 | where: { | |
216 | username: username | |
72c7248b C |
217 | }, |
218 | include: [ { model: User['sequelize'].models.Author, required: true } ] | |
feb4bdfd C |
219 | } |
220 | ||
e02643f3 | 221 | return User.findOne(query) |
9bd26629 C |
222 | } |
223 | ||
6fcd19ba | 224 | listForApi = function (start: number, count: number, sort: string) { |
feb4bdfd C |
225 | const query = { |
226 | offset: start, | |
227 | limit: count, | |
72c7248b C |
228 | order: [ getSort(sort) ], |
229 | include: [ { model: User['sequelize'].models.Author, required: true } ] | |
feb4bdfd C |
230 | } |
231 | ||
6fcd19ba C |
232 | return User.findAndCountAll(query).then(({ rows, count }) => { |
233 | return { | |
234 | data: rows, | |
235 | total: count | |
236 | } | |
feb4bdfd | 237 | }) |
69b0a27c C |
238 | } |
239 | ||
6fcd19ba | 240 | loadById = function (id: number) { |
72c7248b C |
241 | const options = { |
242 | include: [ { model: User['sequelize'].models.Author, required: true } ] | |
243 | } | |
244 | ||
245 | return User.findById(id, options) | |
68a3b9f2 C |
246 | } |
247 | ||
6fcd19ba | 248 | loadByUsername = function (username: string) { |
feb4bdfd C |
249 | const query = { |
250 | where: { | |
556ddc31 | 251 | username |
72c7248b C |
252 | }, |
253 | include: [ { model: User['sequelize'].models.Author, required: true } ] | |
254 | } | |
255 | ||
256 | return User.findOne(query) | |
257 | } | |
258 | ||
259 | loadByUsernameAndPopulateChannels = function (username: string) { | |
260 | const query = { | |
261 | where: { | |
262 | username | |
263 | }, | |
264 | include: [ | |
265 | { | |
266 | model: User['sequelize'].models.Author, | |
267 | required: true, | |
268 | include: [ User['sequelize'].models.VideoChannel ] | |
269 | } | |
270 | ] | |
feb4bdfd C |
271 | } |
272 | ||
6fcd19ba | 273 | return User.findOne(query) |
9bd26629 | 274 | } |
ad4a8a1c | 275 | |
6fcd19ba | 276 | loadByUsernameOrEmail = function (username: string, email: string) { |
ad4a8a1c | 277 | const query = { |
72c7248b | 278 | include: [ { model: User['sequelize'].models.Author, required: true } ], |
ad4a8a1c | 279 | where: { |
c2962505 | 280 | [Sequelize.Op.or]: [ { username }, { email } ] |
ad4a8a1c C |
281 | } |
282 | } | |
283 | ||
556ddc31 C |
284 | // FIXME: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18387 |
285 | return (User as any).findOne(query) | |
ad4a8a1c | 286 | } |
b0f9f39e C |
287 | |
288 | // --------------------------------------------------------------------------- | |
289 | ||
290 | function getOriginalVideoFileTotalFromUser (user: UserInstance) { | |
72c7248b | 291 | // Don't use sequelize because we need to use a sub query |
14d3270f C |
292 | const query = 'SELECT SUM("size") AS "total" FROM ' + |
293 | '(SELECT MAX("VideoFiles"."size") AS "size" FROM "VideoFiles" ' + | |
294 | 'INNER JOIN "Videos" ON "VideoFiles"."videoId" = "Videos"."id" ' + | |
72c7248b C |
295 | 'INNER JOIN "VideoChannels" ON "VideoChannels"."id" = "Videos"."channelId" ' + |
296 | 'INNER JOIN "Authors" ON "VideoChannels"."authorId" = "Authors"."id" ' + | |
14d3270f C |
297 | 'INNER JOIN "Users" ON "Authors"."userId" = "Users"."id" ' + |
298 | 'WHERE "Users"."id" = $userId GROUP BY "Videos"."id") t' | |
299 | ||
300 | const options = { | |
301 | bind: { userId: user.id }, | |
302 | type: Sequelize.QueryTypes.SELECT | |
b0f9f39e | 303 | } |
14d3270f C |
304 | return User['sequelize'].query(query, options).then(([ { total } ]) => { |
305 | if (total === null) return 0 | |
b0f9f39e | 306 | |
14d3270f C |
307 | return parseInt(total, 10) |
308 | }) | |
b0f9f39e | 309 | } |