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