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