diff options
Diffstat (limited to 'server/middlewares/validators/users.ts')
-rw-r--r-- | server/middlewares/validators/users.ts | 489 |
1 files changed, 0 insertions, 489 deletions
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts deleted file mode 100644 index 3d311b15b..000000000 --- a/server/middlewares/validators/users.ts +++ /dev/null | |||
@@ -1,489 +0,0 @@ | |||
1 | import express from 'express' | ||
2 | import { body, param, query } from 'express-validator' | ||
3 | import { forceNumber } from '@shared/core-utils' | ||
4 | import { HttpStatusCode, UserRight, UserRole } from '@shared/models' | ||
5 | import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' | ||
6 | import { isThemeNameValid } from '../../helpers/custom-validators/plugins' | ||
7 | import { | ||
8 | isUserAdminFlagsValid, | ||
9 | isUserAutoPlayNextVideoValid, | ||
10 | isUserAutoPlayVideoValid, | ||
11 | isUserBlockedReasonValid, | ||
12 | isUserDescriptionValid, | ||
13 | isUserDisplayNameValid, | ||
14 | isUserEmailPublicValid, | ||
15 | isUserNoModal, | ||
16 | isUserNSFWPolicyValid, | ||
17 | isUserP2PEnabledValid, | ||
18 | isUserPasswordValid, | ||
19 | isUserPasswordValidOrEmpty, | ||
20 | isUserRoleValid, | ||
21 | isUserUsernameValid, | ||
22 | isUserVideoLanguages, | ||
23 | isUserVideoQuotaDailyValid, | ||
24 | isUserVideoQuotaValid, | ||
25 | isUserVideosHistoryEnabledValid | ||
26 | } from '../../helpers/custom-validators/users' | ||
27 | import { isVideoChannelUsernameValid } from '../../helpers/custom-validators/video-channels' | ||
28 | import { logger } from '../../helpers/logger' | ||
29 | import { isThemeRegistered } from '../../lib/plugins/theme-utils' | ||
30 | import { Redis } from '../../lib/redis' | ||
31 | import { ActorModel } from '../../models/actor/actor' | ||
32 | import { | ||
33 | areValidationErrors, | ||
34 | checkUserEmailExist, | ||
35 | checkUserIdExist, | ||
36 | checkUserNameOrEmailDoNotAlreadyExist, | ||
37 | doesVideoChannelIdExist, | ||
38 | doesVideoExist, | ||
39 | isValidVideoIdParam | ||
40 | } from './shared' | ||
41 | |||
42 | const usersListValidator = [ | ||
43 | query('blocked') | ||
44 | .optional() | ||
45 | .customSanitizer(toBooleanOrNull) | ||
46 | .isBoolean().withMessage('Should be a valid blocked boolean'), | ||
47 | |||
48 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
49 | if (areValidationErrors(req, res)) return | ||
50 | |||
51 | return next() | ||
52 | } | ||
53 | ] | ||
54 | |||
55 | const usersAddValidator = [ | ||
56 | body('username') | ||
57 | .custom(isUserUsernameValid) | ||
58 | .withMessage('Should have a valid username (lowercase alphanumeric characters)'), | ||
59 | body('password') | ||
60 | .custom(isUserPasswordValidOrEmpty), | ||
61 | body('email') | ||
62 | .isEmail(), | ||
63 | |||
64 | body('channelName') | ||
65 | .optional() | ||
66 | .custom(isVideoChannelUsernameValid), | ||
67 | |||
68 | body('videoQuota') | ||
69 | .optional() | ||
70 | .custom(isUserVideoQuotaValid), | ||
71 | |||
72 | body('videoQuotaDaily') | ||
73 | .optional() | ||
74 | .custom(isUserVideoQuotaDailyValid), | ||
75 | |||
76 | body('role') | ||
77 | .customSanitizer(toIntOrNull) | ||
78 | .custom(isUserRoleValid), | ||
79 | |||
80 | body('adminFlags') | ||
81 | .optional() | ||
82 | .custom(isUserAdminFlagsValid), | ||
83 | |||
84 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
85 | if (areValidationErrors(req, res, { omitBodyLog: true })) return | ||
86 | if (!await checkUserNameOrEmailDoNotAlreadyExist(req.body.username, req.body.email, res)) return | ||
87 | |||
88 | const authUser = res.locals.oauth.token.User | ||
89 | if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) { | ||
90 | return res.fail({ | ||
91 | status: HttpStatusCode.FORBIDDEN_403, | ||
92 | message: 'You can only create users (and not administrators or moderators)' | ||
93 | }) | ||
94 | } | ||
95 | |||
96 | if (req.body.channelName) { | ||
97 | if (req.body.channelName === req.body.username) { | ||
98 | return res.fail({ message: 'Channel name cannot be the same as user username.' }) | ||
99 | } | ||
100 | |||
101 | const existing = await ActorModel.loadLocalByName(req.body.channelName) | ||
102 | if (existing) { | ||
103 | return res.fail({ | ||
104 | status: HttpStatusCode.CONFLICT_409, | ||
105 | message: `Channel with name ${req.body.channelName} already exists.` | ||
106 | }) | ||
107 | } | ||
108 | } | ||
109 | |||
110 | return next() | ||
111 | } | ||
112 | ] | ||
113 | |||
114 | const usersRemoveValidator = [ | ||
115 | param('id') | ||
116 | .custom(isIdValid), | ||
117 | |||
118 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
119 | if (areValidationErrors(req, res)) return | ||
120 | if (!await checkUserIdExist(req.params.id, res)) return | ||
121 | |||
122 | const user = res.locals.user | ||
123 | if (user.username === 'root') { | ||
124 | return res.fail({ message: 'Cannot remove the root user' }) | ||
125 | } | ||
126 | |||
127 | return next() | ||
128 | } | ||
129 | ] | ||
130 | |||
131 | const usersBlockingValidator = [ | ||
132 | param('id') | ||
133 | .custom(isIdValid), | ||
134 | body('reason') | ||
135 | .optional() | ||
136 | .custom(isUserBlockedReasonValid), | ||
137 | |||
138 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
139 | if (areValidationErrors(req, res)) return | ||
140 | if (!await checkUserIdExist(req.params.id, res)) return | ||
141 | |||
142 | const user = res.locals.user | ||
143 | if (user.username === 'root') { | ||
144 | return res.fail({ message: 'Cannot block the root user' }) | ||
145 | } | ||
146 | |||
147 | return next() | ||
148 | } | ||
149 | ] | ||
150 | |||
151 | const deleteMeValidator = [ | ||
152 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
153 | const user = res.locals.oauth.token.User | ||
154 | if (user.username === 'root') { | ||
155 | return res.fail({ message: 'You cannot delete your root account.' }) | ||
156 | } | ||
157 | |||
158 | return next() | ||
159 | } | ||
160 | ] | ||
161 | |||
162 | const usersUpdateValidator = [ | ||
163 | param('id').custom(isIdValid), | ||
164 | |||
165 | body('password') | ||
166 | .optional() | ||
167 | .custom(isUserPasswordValid), | ||
168 | body('email') | ||
169 | .optional() | ||
170 | .isEmail(), | ||
171 | body('emailVerified') | ||
172 | .optional() | ||
173 | .isBoolean(), | ||
174 | body('videoQuota') | ||
175 | .optional() | ||
176 | .custom(isUserVideoQuotaValid), | ||
177 | body('videoQuotaDaily') | ||
178 | .optional() | ||
179 | .custom(isUserVideoQuotaDailyValid), | ||
180 | body('pluginAuth') | ||
181 | .optional() | ||
182 | .exists(), | ||
183 | body('role') | ||
184 | .optional() | ||
185 | .customSanitizer(toIntOrNull) | ||
186 | .custom(isUserRoleValid), | ||
187 | body('adminFlags') | ||
188 | .optional() | ||
189 | .custom(isUserAdminFlagsValid), | ||
190 | |||
191 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
192 | if (areValidationErrors(req, res, { omitBodyLog: true })) return | ||
193 | if (!await checkUserIdExist(req.params.id, res)) return | ||
194 | |||
195 | const user = res.locals.user | ||
196 | if (user.username === 'root' && req.body.role !== undefined && user.role !== req.body.role) { | ||
197 | return res.fail({ message: 'Cannot change root role.' }) | ||
198 | } | ||
199 | |||
200 | return next() | ||
201 | } | ||
202 | ] | ||
203 | |||
204 | const usersUpdateMeValidator = [ | ||
205 | body('displayName') | ||
206 | .optional() | ||
207 | .custom(isUserDisplayNameValid), | ||
208 | body('description') | ||
209 | .optional() | ||
210 | .custom(isUserDescriptionValid), | ||
211 | body('currentPassword') | ||
212 | .optional() | ||
213 | .custom(isUserPasswordValid), | ||
214 | body('password') | ||
215 | .optional() | ||
216 | .custom(isUserPasswordValid), | ||
217 | body('emailPublic') | ||
218 | .optional() | ||
219 | .custom(isUserEmailPublicValid), | ||
220 | body('email') | ||
221 | .optional() | ||
222 | .isEmail(), | ||
223 | body('nsfwPolicy') | ||
224 | .optional() | ||
225 | .custom(isUserNSFWPolicyValid), | ||
226 | body('autoPlayVideo') | ||
227 | .optional() | ||
228 | .custom(isUserAutoPlayVideoValid), | ||
229 | body('p2pEnabled') | ||
230 | .optional() | ||
231 | .custom(isUserP2PEnabledValid).withMessage('Should have a valid p2p enabled boolean'), | ||
232 | body('videoLanguages') | ||
233 | .optional() | ||
234 | .custom(isUserVideoLanguages), | ||
235 | body('videosHistoryEnabled') | ||
236 | .optional() | ||
237 | .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled boolean'), | ||
238 | body('theme') | ||
239 | .optional() | ||
240 | .custom(v => isThemeNameValid(v) && isThemeRegistered(v)), | ||
241 | |||
242 | body('noInstanceConfigWarningModal') | ||
243 | .optional() | ||
244 | .custom(v => isUserNoModal(v)).withMessage('Should have a valid noInstanceConfigWarningModal boolean'), | ||
245 | body('noWelcomeModal') | ||
246 | .optional() | ||
247 | .custom(v => isUserNoModal(v)).withMessage('Should have a valid noWelcomeModal boolean'), | ||
248 | body('noAccountSetupWarningModal') | ||
249 | .optional() | ||
250 | .custom(v => isUserNoModal(v)).withMessage('Should have a valid noAccountSetupWarningModal boolean'), | ||
251 | |||
252 | body('autoPlayNextVideo') | ||
253 | .optional() | ||
254 | .custom(v => isUserAutoPlayNextVideoValid(v)).withMessage('Should have a valid autoPlayNextVideo boolean'), | ||
255 | |||
256 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
257 | const user = res.locals.oauth.token.User | ||
258 | |||
259 | if (req.body.password || req.body.email) { | ||
260 | if (user.pluginAuth !== null) { | ||
261 | return res.fail({ message: 'You cannot update your email or password that is associated with an external auth system.' }) | ||
262 | } | ||
263 | |||
264 | if (!req.body.currentPassword) { | ||
265 | return res.fail({ message: 'currentPassword parameter is missing.' }) | ||
266 | } | ||
267 | |||
268 | if (await user.isPasswordMatch(req.body.currentPassword) !== true) { | ||
269 | return res.fail({ | ||
270 | status: HttpStatusCode.UNAUTHORIZED_401, | ||
271 | message: 'currentPassword is invalid.' | ||
272 | }) | ||
273 | } | ||
274 | } | ||
275 | |||
276 | if (areValidationErrors(req, res, { omitBodyLog: true })) return | ||
277 | |||
278 | return next() | ||
279 | } | ||
280 | ] | ||
281 | |||
282 | const usersGetValidator = [ | ||
283 | param('id') | ||
284 | .custom(isIdValid), | ||
285 | query('withStats') | ||
286 | .optional() | ||
287 | .isBoolean().withMessage('Should have a valid withStats boolean'), | ||
288 | |||
289 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
290 | if (areValidationErrors(req, res)) return | ||
291 | if (!await checkUserIdExist(req.params.id, res, req.query.withStats)) return | ||
292 | |||
293 | return next() | ||
294 | } | ||
295 | ] | ||
296 | |||
297 | const usersVideoRatingValidator = [ | ||
298 | isValidVideoIdParam('videoId'), | ||
299 | |||
300 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
301 | if (areValidationErrors(req, res)) return | ||
302 | if (!await doesVideoExist(req.params.videoId, res, 'id')) return | ||
303 | |||
304 | return next() | ||
305 | } | ||
306 | ] | ||
307 | |||
308 | const usersVideosValidator = [ | ||
309 | query('isLive') | ||
310 | .optional() | ||
311 | .customSanitizer(toBooleanOrNull) | ||
312 | .custom(isBooleanValid).withMessage('Should have a valid isLive boolean'), | ||
313 | |||
314 | query('channelId') | ||
315 | .optional() | ||
316 | .customSanitizer(toIntOrNull) | ||
317 | .custom(isIdValid), | ||
318 | |||
319 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
320 | if (areValidationErrors(req, res)) return | ||
321 | |||
322 | if (req.query.channelId && !await doesVideoChannelIdExist(req.query.channelId, res)) return | ||
323 | |||
324 | return next() | ||
325 | } | ||
326 | ] | ||
327 | |||
328 | const usersAskResetPasswordValidator = [ | ||
329 | body('email') | ||
330 | .isEmail(), | ||
331 | |||
332 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
333 | if (areValidationErrors(req, res)) return | ||
334 | |||
335 | const exists = await checkUserEmailExist(req.body.email, res, false) | ||
336 | if (!exists) { | ||
337 | logger.debug('User with email %s does not exist (asking reset password).', req.body.email) | ||
338 | // Do not leak our emails | ||
339 | return res.status(HttpStatusCode.NO_CONTENT_204).end() | ||
340 | } | ||
341 | |||
342 | if (res.locals.user.pluginAuth) { | ||
343 | return res.fail({ | ||
344 | status: HttpStatusCode.CONFLICT_409, | ||
345 | message: 'Cannot recover password of a user that uses a plugin authentication.' | ||
346 | }) | ||
347 | } | ||
348 | |||
349 | return next() | ||
350 | } | ||
351 | ] | ||
352 | |||
353 | const usersResetPasswordValidator = [ | ||
354 | param('id') | ||
355 | .custom(isIdValid), | ||
356 | body('verificationString') | ||
357 | .not().isEmpty(), | ||
358 | body('password') | ||
359 | .custom(isUserPasswordValid), | ||
360 | |||
361 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
362 | if (areValidationErrors(req, res)) return | ||
363 | if (!await checkUserIdExist(req.params.id, res)) return | ||
364 | |||
365 | const user = res.locals.user | ||
366 | const redisVerificationString = await Redis.Instance.getResetPasswordVerificationString(user.id) | ||
367 | |||
368 | if (redisVerificationString !== req.body.verificationString) { | ||
369 | return res.fail({ | ||
370 | status: HttpStatusCode.FORBIDDEN_403, | ||
371 | message: 'Invalid verification string.' | ||
372 | }) | ||
373 | } | ||
374 | |||
375 | return next() | ||
376 | } | ||
377 | ] | ||
378 | |||
379 | const usersCheckCurrentPasswordFactory = (targetUserIdGetter: (req: express.Request) => number | string) => { | ||
380 | return [ | ||
381 | body('currentPassword').optional().custom(exists), | ||
382 | |||
383 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
384 | if (areValidationErrors(req, res)) return | ||
385 | |||
386 | const user = res.locals.oauth.token.User | ||
387 | const isAdminOrModerator = user.role === UserRole.ADMINISTRATOR || user.role === UserRole.MODERATOR | ||
388 | const targetUserId = forceNumber(targetUserIdGetter(req)) | ||
389 | |||
390 | // Admin/moderator action on another user, skip the password check | ||
391 | if (isAdminOrModerator && targetUserId !== user.id) { | ||
392 | return next() | ||
393 | } | ||
394 | |||
395 | if (!req.body.currentPassword) { | ||
396 | return res.fail({ | ||
397 | status: HttpStatusCode.BAD_REQUEST_400, | ||
398 | message: 'currentPassword is missing' | ||
399 | }) | ||
400 | } | ||
401 | |||
402 | if (await user.isPasswordMatch(req.body.currentPassword) !== true) { | ||
403 | return res.fail({ | ||
404 | status: HttpStatusCode.FORBIDDEN_403, | ||
405 | message: 'currentPassword is invalid.' | ||
406 | }) | ||
407 | } | ||
408 | |||
409 | return next() | ||
410 | } | ||
411 | ] | ||
412 | } | ||
413 | |||
414 | const userAutocompleteValidator = [ | ||
415 | param('search') | ||
416 | .isString() | ||
417 | .not().isEmpty() | ||
418 | ] | ||
419 | |||
420 | const ensureAuthUserOwnsAccountValidator = [ | ||
421 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
422 | const user = res.locals.oauth.token.User | ||
423 | |||
424 | if (res.locals.account.id !== user.Account.id) { | ||
425 | return res.fail({ | ||
426 | status: HttpStatusCode.FORBIDDEN_403, | ||
427 | message: 'Only owner of this account can access this resource.' | ||
428 | }) | ||
429 | } | ||
430 | |||
431 | return next() | ||
432 | } | ||
433 | ] | ||
434 | |||
435 | const ensureCanManageChannelOrAccount = [ | ||
436 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
437 | const user = res.locals.oauth.token.user | ||
438 | const account = res.locals.videoChannel?.Account ?? res.locals.account | ||
439 | const isUserOwner = account.userId === user.id | ||
440 | |||
441 | if (!isUserOwner && user.hasRight(UserRight.MANAGE_ANY_VIDEO_CHANNEL) === false) { | ||
442 | const message = `User ${user.username} does not have right this channel or account.` | ||
443 | |||
444 | return res.fail({ | ||
445 | status: HttpStatusCode.FORBIDDEN_403, | ||
446 | message | ||
447 | }) | ||
448 | } | ||
449 | |||
450 | return next() | ||
451 | } | ||
452 | ] | ||
453 | |||
454 | const ensureCanModerateUser = [ | ||
455 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
456 | const authUser = res.locals.oauth.token.User | ||
457 | const onUser = res.locals.user | ||
458 | |||
459 | if (authUser.role === UserRole.ADMINISTRATOR) return next() | ||
460 | if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next() | ||
461 | |||
462 | return res.fail({ | ||
463 | status: HttpStatusCode.FORBIDDEN_403, | ||
464 | message: 'A moderator can only manage users.' | ||
465 | }) | ||
466 | } | ||
467 | ] | ||
468 | |||
469 | // --------------------------------------------------------------------------- | ||
470 | |||
471 | export { | ||
472 | usersListValidator, | ||
473 | usersAddValidator, | ||
474 | deleteMeValidator, | ||
475 | usersBlockingValidator, | ||
476 | usersRemoveValidator, | ||
477 | usersUpdateValidator, | ||
478 | usersUpdateMeValidator, | ||
479 | usersVideoRatingValidator, | ||
480 | usersCheckCurrentPasswordFactory, | ||
481 | usersGetValidator, | ||
482 | usersVideosValidator, | ||
483 | usersAskResetPasswordValidator, | ||
484 | usersResetPasswordValidator, | ||
485 | userAutocompleteValidator, | ||
486 | ensureAuthUserOwnsAccountValidator, | ||
487 | ensureCanModerateUser, | ||
488 | ensureCanManageChannelOrAccount | ||
489 | } | ||