]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/middlewares/validators/users.ts
Cleanup useless express validator messages
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / users.ts
1 import express from 'express'
2 import { body, param, query } from 'express-validator'
3 import { omit } from 'lodash'
4 import { Hooks } from '@server/lib/plugins/hooks'
5 import { MUserDefault } from '@server/types/models'
6 import { HttpStatusCode, UserRegister, UserRight, UserRole } from '@shared/models'
7 import { isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
8 import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
9 import {
10 isUserAdminFlagsValid,
11 isUserAutoPlayNextVideoValid,
12 isUserAutoPlayVideoValid,
13 isUserBlockedReasonValid,
14 isUserDescriptionValid,
15 isUserDisplayNameValid,
16 isUserNoModal,
17 isUserNSFWPolicyValid,
18 isUserP2PEnabledValid,
19 isUserPasswordValid,
20 isUserPasswordValidOrEmpty,
21 isUserRoleValid,
22 isUserUsernameValid,
23 isUserVideoLanguages,
24 isUserVideoQuotaDailyValid,
25 isUserVideoQuotaValid,
26 isUserVideosHistoryEnabledValid
27 } from '../../helpers/custom-validators/users'
28 import { isVideoChannelDisplayNameValid, isVideoChannelUsernameValid } from '../../helpers/custom-validators/video-channels'
29 import { logger } from '../../helpers/logger'
30 import { isThemeRegistered } from '../../lib/plugins/theme-utils'
31 import { Redis } from '../../lib/redis'
32 import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../lib/signup'
33 import { ActorModel } from '../../models/actor/actor'
34 import { UserModel } from '../../models/user/user'
35 import { areValidationErrors, doesVideoChannelIdExist, doesVideoExist, isValidVideoIdParam } from './shared'
36
37 const usersListValidator = [
38 query('blocked')
39 .optional()
40 .customSanitizer(toBooleanOrNull)
41 .isBoolean().withMessage('Should be a valid blocked boolena'),
42
43 (req: express.Request, res: express.Response, next: express.NextFunction) => {
44 logger.debug('Checking usersList parameters', { parameters: req.query })
45
46 if (areValidationErrors(req, res)) return
47
48 return next()
49 }
50 ]
51
52 const usersAddValidator = [
53 body('username')
54 .custom(isUserUsernameValid)
55 .withMessage('Should have a valid username (lowercase alphanumeric characters)'),
56 body('password')
57 .custom(isUserPasswordValidOrEmpty),
58 body('email')
59 .isEmail(),
60
61 body('channelName')
62 .optional()
63 .custom(isVideoChannelUsernameValid),
64
65 body('videoQuota')
66 .custom(isUserVideoQuotaValid),
67 body('videoQuotaDaily')
68 .custom(isUserVideoQuotaDailyValid),
69
70 body('role')
71 .customSanitizer(toIntOrNull)
72 .custom(isUserRoleValid),
73
74 body('adminFlags')
75 .optional()
76 .custom(isUserAdminFlagsValid),
77
78 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
79 logger.debug('Checking usersAdd parameters', { parameters: omit(req.body, 'password') })
80
81 if (areValidationErrors(req, res)) return
82 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
83
84 const authUser = res.locals.oauth.token.User
85 if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) {
86 return res.fail({
87 status: HttpStatusCode.FORBIDDEN_403,
88 message: 'You can only create users (and not administrators or moderators)'
89 })
90 }
91
92 if (req.body.channelName) {
93 if (req.body.channelName === req.body.username) {
94 return res.fail({ message: 'Channel name cannot be the same as user username.' })
95 }
96
97 const existing = await ActorModel.loadLocalByName(req.body.channelName)
98 if (existing) {
99 return res.fail({
100 status: HttpStatusCode.CONFLICT_409,
101 message: `Channel with name ${req.body.channelName} already exists.`
102 })
103 }
104 }
105
106 return next()
107 }
108 ]
109
110 const usersRegisterValidator = [
111 body('username')
112 .custom(isUserUsernameValid),
113 body('password')
114 .custom(isUserPasswordValid),
115 body('email')
116 .isEmail(),
117 body('displayName')
118 .optional()
119 .custom(isUserDisplayNameValid),
120
121 body('channel.name')
122 .optional()
123 .custom(isVideoChannelUsernameValid),
124 body('channel.displayName')
125 .optional()
126 .custom(isVideoChannelDisplayNameValid),
127
128 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
129 logger.debug('Checking usersRegister parameters', { parameters: omit(req.body, 'password') })
130
131 if (areValidationErrors(req, res)) return
132 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
133
134 const body: UserRegister = req.body
135 if (body.channel) {
136 if (!body.channel.name || !body.channel.displayName) {
137 return res.fail({ message: 'Channel is optional but if you specify it, channel.name and channel.displayName are required.' })
138 }
139
140 if (body.channel.name === body.username) {
141 return res.fail({ message: 'Channel name cannot be the same as user username.' })
142 }
143
144 const existing = await ActorModel.loadLocalByName(body.channel.name)
145 if (existing) {
146 return res.fail({
147 status: HttpStatusCode.CONFLICT_409,
148 message: `Channel with name ${body.channel.name} already exists.`
149 })
150 }
151 }
152
153 return next()
154 }
155 ]
156
157 const usersRemoveValidator = [
158 param('id')
159 .custom(isIdValid),
160
161 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
162 logger.debug('Checking usersRemove parameters', { parameters: req.params })
163
164 if (areValidationErrors(req, res)) return
165 if (!await checkUserIdExist(req.params.id, res)) return
166
167 const user = res.locals.user
168 if (user.username === 'root') {
169 return res.fail({ message: 'Cannot remove the root user' })
170 }
171
172 return next()
173 }
174 ]
175
176 const usersBlockingValidator = [
177 param('id')
178 .custom(isIdValid),
179 body('reason')
180 .optional()
181 .custom(isUserBlockedReasonValid),
182
183 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
184 logger.debug('Checking usersBlocking parameters', { parameters: req.params })
185
186 if (areValidationErrors(req, res)) return
187 if (!await checkUserIdExist(req.params.id, res)) return
188
189 const user = res.locals.user
190 if (user.username === 'root') {
191 return res.fail({ message: 'Cannot block the root user' })
192 }
193
194 return next()
195 }
196 ]
197
198 const deleteMeValidator = [
199 (req: express.Request, res: express.Response, next: express.NextFunction) => {
200 const user = res.locals.oauth.token.User
201 if (user.username === 'root') {
202 return res.fail({ message: 'You cannot delete your root account.' })
203 }
204
205 return next()
206 }
207 ]
208
209 const usersUpdateValidator = [
210 param('id').custom(isIdValid),
211
212 body('password')
213 .optional()
214 .custom(isUserPasswordValid),
215 body('email')
216 .optional()
217 .isEmail(),
218 body('emailVerified')
219 .optional()
220 .isBoolean(),
221 body('videoQuota')
222 .optional()
223 .custom(isUserVideoQuotaValid),
224 body('videoQuotaDaily')
225 .optional()
226 .custom(isUserVideoQuotaDailyValid),
227 body('pluginAuth')
228 .optional()
229 .exists(),
230 body('role')
231 .optional()
232 .customSanitizer(toIntOrNull)
233 .custom(isUserRoleValid),
234 body('adminFlags')
235 .optional()
236 .custom(isUserAdminFlagsValid),
237
238 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
239 logger.debug('Checking usersUpdate parameters', { parameters: req.body })
240
241 if (areValidationErrors(req, res)) return
242 if (!await checkUserIdExist(req.params.id, res)) return
243
244 const user = res.locals.user
245 if (user.username === 'root' && req.body.role !== undefined && user.role !== req.body.role) {
246 return res.fail({ message: 'Cannot change root role.' })
247 }
248
249 return next()
250 }
251 ]
252
253 const usersUpdateMeValidator = [
254 body('displayName')
255 .optional()
256 .custom(isUserDisplayNameValid),
257 body('description')
258 .optional()
259 .custom(isUserDescriptionValid),
260 body('currentPassword')
261 .optional()
262 .custom(isUserPasswordValid),
263 body('password')
264 .optional()
265 .custom(isUserPasswordValid),
266 body('email')
267 .optional()
268 .isEmail(),
269 body('nsfwPolicy')
270 .optional()
271 .custom(isUserNSFWPolicyValid),
272 body('autoPlayVideo')
273 .optional()
274 .custom(isUserAutoPlayVideoValid),
275 body('p2pEnabled')
276 .optional()
277 .custom(isUserP2PEnabledValid).withMessage('Should have a valid p2p enabled boolean'),
278 body('videoLanguages')
279 .optional()
280 .custom(isUserVideoLanguages),
281 body('videosHistoryEnabled')
282 .optional()
283 .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled boolean'),
284 body('theme')
285 .optional()
286 .custom(v => isThemeNameValid(v) && isThemeRegistered(v)),
287
288 body('noInstanceConfigWarningModal')
289 .optional()
290 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noInstanceConfigWarningModal boolean'),
291 body('noWelcomeModal')
292 .optional()
293 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noWelcomeModal boolean'),
294 body('noAccountSetupWarningModal')
295 .optional()
296 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noAccountSetupWarningModal boolean'),
297
298 body('autoPlayNextVideo')
299 .optional()
300 .custom(v => isUserAutoPlayNextVideoValid(v)).withMessage('Should have a valid autoPlayNextVideo boolean'),
301
302 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
303 logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') })
304
305 const user = res.locals.oauth.token.User
306
307 if (req.body.password || req.body.email) {
308 if (user.pluginAuth !== null) {
309 return res.fail({ message: 'You cannot update your email or password that is associated with an external auth system.' })
310 }
311
312 if (!req.body.currentPassword) {
313 return res.fail({ message: 'currentPassword parameter is missing.' })
314 }
315
316 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
317 return res.fail({
318 status: HttpStatusCode.UNAUTHORIZED_401,
319 message: 'currentPassword is invalid.'
320 })
321 }
322 }
323
324 if (areValidationErrors(req, res)) return
325
326 return next()
327 }
328 ]
329
330 const usersGetValidator = [
331 param('id')
332 .custom(isIdValid),
333 query('withStats')
334 .optional()
335 .isBoolean().withMessage('Should have a valid withStats boolean'),
336
337 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
338 logger.debug('Checking usersGet parameters', { parameters: req.params })
339
340 if (areValidationErrors(req, res)) return
341 if (!await checkUserIdExist(req.params.id, res, req.query.withStats)) return
342
343 return next()
344 }
345 ]
346
347 const usersVideoRatingValidator = [
348 isValidVideoIdParam('videoId'),
349
350 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
351 logger.debug('Checking usersVideoRating parameters', { parameters: req.params })
352
353 if (areValidationErrors(req, res)) return
354 if (!await doesVideoExist(req.params.videoId, res, 'id')) return
355
356 return next()
357 }
358 ]
359
360 const usersVideosValidator = [
361 query('isLive')
362 .optional()
363 .customSanitizer(toBooleanOrNull)
364 .custom(isBooleanValid).withMessage('Should have a valid isLive boolean'),
365
366 query('channelId')
367 .optional()
368 .customSanitizer(toIntOrNull)
369 .custom(isIdValid),
370
371 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
372 logger.debug('Checking usersVideosValidator parameters', { parameters: req.query })
373
374 if (areValidationErrors(req, res)) return
375
376 if (req.query.channelId && !await doesVideoChannelIdExist(req.query.channelId, res)) return
377
378 return next()
379 }
380 ]
381
382 const ensureUserRegistrationAllowed = [
383 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
384 const allowedParams = {
385 body: req.body,
386 ip: req.ip
387 }
388
389 const allowedResult = await Hooks.wrapPromiseFun(
390 isSignupAllowed,
391 allowedParams,
392 'filter:api.user.signup.allowed.result'
393 )
394
395 if (allowedResult.allowed === false) {
396 return res.fail({
397 status: HttpStatusCode.FORBIDDEN_403,
398 message: allowedResult.errorMessage || 'User registration is not enabled or user limit is reached.'
399 })
400 }
401
402 return next()
403 }
404 ]
405
406 const ensureUserRegistrationAllowedForIP = [
407 (req: express.Request, res: express.Response, next: express.NextFunction) => {
408 const allowed = isSignupAllowedForCurrentIP(req.ip)
409
410 if (allowed === false) {
411 return res.fail({
412 status: HttpStatusCode.FORBIDDEN_403,
413 message: 'You are not on a network authorized for registration.'
414 })
415 }
416
417 return next()
418 }
419 ]
420
421 const usersAskResetPasswordValidator = [
422 body('email')
423 .isEmail(),
424
425 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
426 logger.debug('Checking usersAskResetPassword parameters', { parameters: req.body })
427
428 if (areValidationErrors(req, res)) return
429
430 const exists = await checkUserEmailExist(req.body.email, res, false)
431 if (!exists) {
432 logger.debug('User with email %s does not exist (asking reset password).', req.body.email)
433 // Do not leak our emails
434 return res.status(HttpStatusCode.NO_CONTENT_204).end()
435 }
436
437 return next()
438 }
439 ]
440
441 const usersResetPasswordValidator = [
442 param('id')
443 .custom(isIdValid),
444 body('verificationString')
445 .not().isEmpty(),
446 body('password')
447 .custom(isUserPasswordValid),
448
449 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
450 logger.debug('Checking usersResetPassword parameters', { parameters: req.params })
451
452 if (areValidationErrors(req, res)) return
453 if (!await checkUserIdExist(req.params.id, res)) return
454
455 const user = res.locals.user
456 const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id)
457
458 if (redisVerificationString !== req.body.verificationString) {
459 return res.fail({
460 status: HttpStatusCode.FORBIDDEN_403,
461 message: 'Invalid verification string.'
462 })
463 }
464
465 return next()
466 }
467 ]
468
469 const usersAskSendVerifyEmailValidator = [
470 body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
471
472 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
473 logger.debug('Checking askUsersSendVerifyEmail parameters', { parameters: req.body })
474
475 if (areValidationErrors(req, res)) return
476 const exists = await checkUserEmailExist(req.body.email, res, false)
477 if (!exists) {
478 logger.debug('User with email %s does not exist (asking verify email).', req.body.email)
479 // Do not leak our emails
480 return res.status(HttpStatusCode.NO_CONTENT_204).end()
481 }
482
483 return next()
484 }
485 ]
486
487 const usersVerifyEmailValidator = [
488 param('id')
489 .isInt().not().isEmpty().withMessage('Should have a valid id'),
490
491 body('verificationString')
492 .not().isEmpty().withMessage('Should have a valid verification string'),
493 body('isPendingEmail')
494 .optional()
495 .customSanitizer(toBooleanOrNull),
496
497 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
498 logger.debug('Checking usersVerifyEmail parameters', { parameters: req.params })
499
500 if (areValidationErrors(req, res)) return
501 if (!await checkUserIdExist(req.params.id, res)) return
502
503 const user = res.locals.user
504 const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id)
505
506 if (redisVerificationString !== req.body.verificationString) {
507 return res.fail({
508 status: HttpStatusCode.FORBIDDEN_403,
509 message: 'Invalid verification string.'
510 })
511 }
512
513 return next()
514 }
515 ]
516
517 const userAutocompleteValidator = [
518 param('search').isString().not().isEmpty().withMessage('Should have a search parameter')
519 ]
520
521 const ensureAuthUserOwnsAccountValidator = [
522 (req: express.Request, res: express.Response, next: express.NextFunction) => {
523 const user = res.locals.oauth.token.User
524
525 if (res.locals.account.id !== user.Account.id) {
526 return res.fail({
527 status: HttpStatusCode.FORBIDDEN_403,
528 message: 'Only owner of this account can access this resource.'
529 })
530 }
531
532 return next()
533 }
534 ]
535
536 const ensureCanManageChannel = [
537 (req: express.Request, res: express.Response, next: express.NextFunction) => {
538 const user = res.locals.oauth.token.user
539 const isUserOwner = res.locals.videoChannel.Account.userId === user.id
540
541 if (!isUserOwner && user.hasRight(UserRight.MANAGE_ANY_VIDEO_CHANNEL) === false) {
542 const message = `User ${user.username} does not have right to manage channel ${req.params.nameWithHost}.`
543
544 return res.fail({
545 status: HttpStatusCode.FORBIDDEN_403,
546 message
547 })
548 }
549
550 return next()
551 }
552 ]
553
554 const ensureCanManageUser = [
555 (req: express.Request, res: express.Response, next: express.NextFunction) => {
556 const authUser = res.locals.oauth.token.User
557 const onUser = res.locals.user
558
559 if (authUser.role === UserRole.ADMINISTRATOR) return next()
560 if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next()
561
562 return res.fail({
563 status: HttpStatusCode.FORBIDDEN_403,
564 message: 'A moderator can only manager users.'
565 })
566 }
567 ]
568
569 // ---------------------------------------------------------------------------
570
571 export {
572 usersListValidator,
573 usersAddValidator,
574 deleteMeValidator,
575 usersRegisterValidator,
576 usersBlockingValidator,
577 usersRemoveValidator,
578 usersUpdateValidator,
579 usersUpdateMeValidator,
580 usersVideoRatingValidator,
581 ensureUserRegistrationAllowed,
582 ensureUserRegistrationAllowedForIP,
583 usersGetValidator,
584 usersVideosValidator,
585 usersAskResetPasswordValidator,
586 usersResetPasswordValidator,
587 usersAskSendVerifyEmailValidator,
588 usersVerifyEmailValidator,
589 userAutocompleteValidator,
590 ensureAuthUserOwnsAccountValidator,
591 ensureCanManageUser,
592 ensureCanManageChannel
593 }
594
595 // ---------------------------------------------------------------------------
596
597 function checkUserIdExist (idArg: number | string, res: express.Response, withStats = false) {
598 const id = parseInt(idArg + '', 10)
599 return checkUserExist(() => UserModel.loadByIdWithChannels(id, withStats), res)
600 }
601
602 function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) {
603 return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse)
604 }
605
606 async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email: string, res: express.Response) {
607 const user = await UserModel.loadByUsernameOrEmail(username, email)
608
609 if (user) {
610 res.fail({
611 status: HttpStatusCode.CONFLICT_409,
612 message: 'User with this username or email already exists.'
613 })
614 return false
615 }
616
617 const actor = await ActorModel.loadLocalByName(username)
618 if (actor) {
619 res.fail({
620 status: HttpStatusCode.CONFLICT_409,
621 message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.'
622 })
623 return false
624 }
625
626 return true
627 }
628
629 async function checkUserExist (finder: () => Promise<MUserDefault>, res: express.Response, abortResponse = true) {
630 const user = await finder()
631
632 if (!user) {
633 if (abortResponse === true) {
634 res.fail({
635 status: HttpStatusCode.NOT_FOUND_404,
636 message: 'User not found'
637 })
638 }
639
640 return false
641 }
642
643 res.locals.user = user
644 return true
645 }