1 import express from 'express'
2 import { body, param, query } from 'express-validator'
3 import { Hooks } from '@server/lib/plugins/hooks'
4 import { HttpStatusCode, UserRegister, 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'
9 isUserAutoPlayNextVideoValid,
10 isUserAutoPlayVideoValid,
11 isUserBlockedReasonValid,
12 isUserDescriptionValid,
13 isUserDisplayNameValid,
15 isUserNSFWPolicyValid,
16 isUserP2PEnabledValid,
18 isUserPasswordValidOrEmpty,
22 isUserVideoQuotaDailyValid,
23 isUserVideoQuotaValid,
24 isUserVideosHistoryEnabledValid
25 } from '../../helpers/custom-validators/users'
26 import { isVideoChannelDisplayNameValid, isVideoChannelUsernameValid } from '../../helpers/custom-validators/video-channels'
27 import { logger } from '../../helpers/logger'
28 import { isThemeRegistered } from '../../lib/plugins/theme-utils'
29 import { Redis } from '../../lib/redis'
30 import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../lib/signup'
31 import { ActorModel } from '../../models/actor/actor'
36 checkUserNameOrEmailDoesNotAlreadyExist,
37 doesVideoChannelIdExist,
42 const usersListValidator = [
45 .customSanitizer(toBooleanOrNull)
46 .isBoolean().withMessage('Should be a valid blocked boolena'),
48 (req: express.Request, res: express.Response, next: express.NextFunction) => {
49 if (areValidationErrors(req, res)) return
55 const usersAddValidator = [
57 .custom(isUserUsernameValid)
58 .withMessage('Should have a valid username (lowercase alphanumeric characters)'),
60 .custom(isUserPasswordValidOrEmpty),
66 .custom(isVideoChannelUsernameValid),
69 .custom(isUserVideoQuotaValid),
70 body('videoQuotaDaily')
71 .custom(isUserVideoQuotaDailyValid),
74 .customSanitizer(toIntOrNull)
75 .custom(isUserRoleValid),
79 .custom(isUserAdminFlagsValid),
81 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
82 if (areValidationErrors(req, res, { omitBodyLog: true })) return
83 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
85 const authUser = res.locals.oauth.token.User
86 if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) {
88 status: HttpStatusCode.FORBIDDEN_403,
89 message: 'You can only create users (and not administrators or moderators)'
93 if (req.body.channelName) {
94 if (req.body.channelName === req.body.username) {
95 return res.fail({ message: 'Channel name cannot be the same as user username.' })
98 const existing = await ActorModel.loadLocalByName(req.body.channelName)
101 status: HttpStatusCode.CONFLICT_409,
102 message: `Channel with name ${req.body.channelName} already exists.`
111 const usersRegisterValidator = [
113 .custom(isUserUsernameValid),
115 .custom(isUserPasswordValid),
120 .custom(isUserDisplayNameValid),
124 .custom(isVideoChannelUsernameValid),
125 body('channel.displayName')
127 .custom(isVideoChannelDisplayNameValid),
129 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
130 if (areValidationErrors(req, res, { omitBodyLog: true })) return
131 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
133 const body: UserRegister = req.body
135 if (!body.channel.name || !body.channel.displayName) {
136 return res.fail({ message: 'Channel is optional but if you specify it, channel.name and channel.displayName are required.' })
139 if (body.channel.name === body.username) {
140 return res.fail({ message: 'Channel name cannot be the same as user username.' })
143 const existing = await ActorModel.loadLocalByName(body.channel.name)
146 status: HttpStatusCode.CONFLICT_409,
147 message: `Channel with name ${body.channel.name} already exists.`
156 const usersRemoveValidator = [
160 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
161 if (areValidationErrors(req, res)) return
162 if (!await checkUserIdExist(req.params.id, res)) return
164 const user = res.locals.user
165 if (user.username === 'root') {
166 return res.fail({ message: 'Cannot remove the root user' })
173 const usersBlockingValidator = [
178 .custom(isUserBlockedReasonValid),
180 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
181 if (areValidationErrors(req, res)) return
182 if (!await checkUserIdExist(req.params.id, res)) return
184 const user = res.locals.user
185 if (user.username === 'root') {
186 return res.fail({ message: 'Cannot block the root user' })
193 const deleteMeValidator = [
194 (req: express.Request, res: express.Response, next: express.NextFunction) => {
195 const user = res.locals.oauth.token.User
196 if (user.username === 'root') {
197 return res.fail({ message: 'You cannot delete your root account.' })
204 const usersUpdateValidator = [
205 param('id').custom(isIdValid),
209 .custom(isUserPasswordValid),
213 body('emailVerified')
218 .custom(isUserVideoQuotaValid),
219 body('videoQuotaDaily')
221 .custom(isUserVideoQuotaDailyValid),
227 .customSanitizer(toIntOrNull)
228 .custom(isUserRoleValid),
231 .custom(isUserAdminFlagsValid),
233 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
234 if (areValidationErrors(req, res, { omitBodyLog: true })) return
235 if (!await checkUserIdExist(req.params.id, res)) return
237 const user = res.locals.user
238 if (user.username === 'root' && req.body.role !== undefined && user.role !== req.body.role) {
239 return res.fail({ message: 'Cannot change root role.' })
246 const usersUpdateMeValidator = [
249 .custom(isUserDisplayNameValid),
252 .custom(isUserDescriptionValid),
253 body('currentPassword')
255 .custom(isUserPasswordValid),
258 .custom(isUserPasswordValid),
264 .custom(isUserNSFWPolicyValid),
265 body('autoPlayVideo')
267 .custom(isUserAutoPlayVideoValid),
270 .custom(isUserP2PEnabledValid).withMessage('Should have a valid p2p enabled boolean'),
271 body('videoLanguages')
273 .custom(isUserVideoLanguages),
274 body('videosHistoryEnabled')
276 .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled boolean'),
279 .custom(v => isThemeNameValid(v) && isThemeRegistered(v)),
281 body('noInstanceConfigWarningModal')
283 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noInstanceConfigWarningModal boolean'),
284 body('noWelcomeModal')
286 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noWelcomeModal boolean'),
287 body('noAccountSetupWarningModal')
289 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noAccountSetupWarningModal boolean'),
291 body('autoPlayNextVideo')
293 .custom(v => isUserAutoPlayNextVideoValid(v)).withMessage('Should have a valid autoPlayNextVideo boolean'),
295 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
296 const user = res.locals.oauth.token.User
298 if (req.body.password || req.body.email) {
299 if (user.pluginAuth !== null) {
300 return res.fail({ message: 'You cannot update your email or password that is associated with an external auth system.' })
303 if (!req.body.currentPassword) {
304 return res.fail({ message: 'currentPassword parameter is missing.' })
307 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
309 status: HttpStatusCode.UNAUTHORIZED_401,
310 message: 'currentPassword is invalid.'
315 if (areValidationErrors(req, res, { omitBodyLog: true })) return
321 const usersGetValidator = [
326 .isBoolean().withMessage('Should have a valid withStats boolean'),
328 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
329 if (areValidationErrors(req, res)) return
330 if (!await checkUserIdExist(req.params.id, res, req.query.withStats)) return
336 const usersVideoRatingValidator = [
337 isValidVideoIdParam('videoId'),
339 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
340 if (areValidationErrors(req, res)) return
341 if (!await doesVideoExist(req.params.videoId, res, 'id')) return
347 const usersVideosValidator = [
350 .customSanitizer(toBooleanOrNull)
351 .custom(isBooleanValid).withMessage('Should have a valid isLive boolean'),
355 .customSanitizer(toIntOrNull)
358 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
359 if (areValidationErrors(req, res)) return
361 if (req.query.channelId && !await doesVideoChannelIdExist(req.query.channelId, res)) return
367 const ensureUserRegistrationAllowed = [
368 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
369 const allowedParams = {
374 const allowedResult = await Hooks.wrapPromiseFun(
377 'filter:api.user.signup.allowed.result'
380 if (allowedResult.allowed === false) {
382 status: HttpStatusCode.FORBIDDEN_403,
383 message: allowedResult.errorMessage || 'User registration is not enabled or user limit is reached.'
391 const ensureUserRegistrationAllowedForIP = [
392 (req: express.Request, res: express.Response, next: express.NextFunction) => {
393 const allowed = isSignupAllowedForCurrentIP(req.ip)
395 if (allowed === false) {
397 status: HttpStatusCode.FORBIDDEN_403,
398 message: 'You are not on a network authorized for registration.'
406 const usersAskResetPasswordValidator = [
410 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
411 if (areValidationErrors(req, res)) return
413 const exists = await checkUserEmailExist(req.body.email, res, false)
415 logger.debug('User with email %s does not exist (asking reset password).', req.body.email)
416 // Do not leak our emails
417 return res.status(HttpStatusCode.NO_CONTENT_204).end()
420 if (res.locals.user.pluginAuth) {
422 status: HttpStatusCode.CONFLICT_409,
423 message: 'Cannot recover password of a user that uses a plugin authentication.'
431 const usersResetPasswordValidator = [
434 body('verificationString')
437 .custom(isUserPasswordValid),
439 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
440 if (areValidationErrors(req, res)) return
441 if (!await checkUserIdExist(req.params.id, res)) return
443 const user = res.locals.user
444 const redisVerificationString = await Redis.Instance.getResetPasswordVerificationString(user.id)
446 if (redisVerificationString !== req.body.verificationString) {
448 status: HttpStatusCode.FORBIDDEN_403,
449 message: 'Invalid verification string.'
457 const usersAskSendVerifyEmailValidator = [
458 body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
460 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
461 if (areValidationErrors(req, res)) return
463 const exists = await checkUserEmailExist(req.body.email, res, false)
465 logger.debug('User with email %s does not exist (asking verify email).', req.body.email)
466 // Do not leak our emails
467 return res.status(HttpStatusCode.NO_CONTENT_204).end()
470 if (res.locals.user.pluginAuth) {
472 status: HttpStatusCode.CONFLICT_409,
473 message: 'Cannot ask verification email of a user that uses a plugin authentication.'
481 const usersVerifyEmailValidator = [
483 .isInt().not().isEmpty().withMessage('Should have a valid id'),
485 body('verificationString')
486 .not().isEmpty().withMessage('Should have a valid verification string'),
487 body('isPendingEmail')
489 .customSanitizer(toBooleanOrNull),
491 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
492 if (areValidationErrors(req, res)) return
493 if (!await checkUserIdExist(req.params.id, res)) return
495 const user = res.locals.user
496 const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id)
498 if (redisVerificationString !== req.body.verificationString) {
500 status: HttpStatusCode.FORBIDDEN_403,
501 message: 'Invalid verification string.'
509 const usersCheckCurrentPasswordFactory = (targetUserIdGetter: (req: express.Request) => number | string) => {
511 body('currentPassword').optional().custom(exists),
513 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
514 if (areValidationErrors(req, res)) return
516 const user = res.locals.oauth.token.User
517 const isAdminOrModerator = user.role === UserRole.ADMINISTRATOR || user.role === UserRole.MODERATOR
518 const targetUserId = parseInt(targetUserIdGetter(req) + '')
520 // Admin/moderator action on another user, skip the password check
521 if (isAdminOrModerator && targetUserId !== user.id) {
525 if (!req.body.currentPassword) {
527 status: HttpStatusCode.BAD_REQUEST_400,
528 message: 'currentPassword is missing'
532 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
534 status: HttpStatusCode.FORBIDDEN_403,
535 message: 'currentPassword is invalid.'
544 const userAutocompleteValidator = [
550 const ensureAuthUserOwnsAccountValidator = [
551 (req: express.Request, res: express.Response, next: express.NextFunction) => {
552 const user = res.locals.oauth.token.User
554 if (res.locals.account.id !== user.Account.id) {
556 status: HttpStatusCode.FORBIDDEN_403,
557 message: 'Only owner of this account can access this resource.'
565 const ensureCanManageChannelOrAccount = [
566 (req: express.Request, res: express.Response, next: express.NextFunction) => {
567 const user = res.locals.oauth.token.user
568 const account = res.locals.videoChannel?.Account ?? res.locals.account
569 const isUserOwner = account.userId === user.id
571 if (!isUserOwner && user.hasRight(UserRight.MANAGE_ANY_VIDEO_CHANNEL) === false) {
572 const message = `User ${user.username} does not have right this channel or account.`
575 status: HttpStatusCode.FORBIDDEN_403,
584 const ensureCanModerateUser = [
585 (req: express.Request, res: express.Response, next: express.NextFunction) => {
586 const authUser = res.locals.oauth.token.User
587 const onUser = res.locals.user
589 if (authUser.role === UserRole.ADMINISTRATOR) return next()
590 if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next()
593 status: HttpStatusCode.FORBIDDEN_403,
594 message: 'A moderator can only manage users.'
599 // ---------------------------------------------------------------------------
605 usersRegisterValidator,
606 usersBlockingValidator,
607 usersRemoveValidator,
608 usersUpdateValidator,
609 usersUpdateMeValidator,
610 usersVideoRatingValidator,
611 usersCheckCurrentPasswordFactory,
612 ensureUserRegistrationAllowed,
613 ensureUserRegistrationAllowedForIP,
615 usersVideosValidator,
616 usersAskResetPasswordValidator,
617 usersResetPasswordValidator,
618 usersAskSendVerifyEmailValidator,
619 usersVerifyEmailValidator,
620 userAutocompleteValidator,
621 ensureAuthUserOwnsAccountValidator,
622 ensureCanModerateUser,
623 ensureCanManageChannelOrAccount