1 import express from 'express'
2 import { body, param, query } from 'express-validator'
3 import { Hooks } from '@server/lib/plugins/hooks'
4 import { forceNumber } from '@shared/core-utils'
5 import { HttpStatusCode, UserRegister, UserRight, UserRole } from '@shared/models'
6 import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
7 import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
10 isUserAutoPlayNextVideoValid,
11 isUserAutoPlayVideoValid,
12 isUserBlockedReasonValid,
13 isUserDescriptionValid,
14 isUserDisplayNameValid,
16 isUserNSFWPolicyValid,
17 isUserP2PEnabledValid,
19 isUserPasswordValidOrEmpty,
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'
37 checkUserNameOrEmailDoesNotAlreadyExist,
38 doesVideoChannelIdExist,
43 const usersListValidator = [
46 .customSanitizer(toBooleanOrNull)
47 .isBoolean().withMessage('Should be a valid blocked boolena'),
49 (req: express.Request, res: express.Response, next: express.NextFunction) => {
50 if (areValidationErrors(req, res)) return
56 const usersAddValidator = [
58 .custom(isUserUsernameValid)
59 .withMessage('Should have a valid username (lowercase alphanumeric characters)'),
61 .custom(isUserPasswordValidOrEmpty),
67 .custom(isVideoChannelUsernameValid),
70 .custom(isUserVideoQuotaValid),
71 body('videoQuotaDaily')
72 .custom(isUserVideoQuotaDailyValid),
75 .customSanitizer(toIntOrNull)
76 .custom(isUserRoleValid),
80 .custom(isUserAdminFlagsValid),
82 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
83 if (areValidationErrors(req, res, { omitBodyLog: true })) return
84 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
86 const authUser = res.locals.oauth.token.User
87 if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) {
89 status: HttpStatusCode.FORBIDDEN_403,
90 message: 'You can only create users (and not administrators or moderators)'
94 if (req.body.channelName) {
95 if (req.body.channelName === req.body.username) {
96 return res.fail({ message: 'Channel name cannot be the same as user username.' })
99 const existing = await ActorModel.loadLocalByName(req.body.channelName)
102 status: HttpStatusCode.CONFLICT_409,
103 message: `Channel with name ${req.body.channelName} already exists.`
112 const usersRegisterValidator = [
114 .custom(isUserUsernameValid),
116 .custom(isUserPasswordValid),
121 .custom(isUserDisplayNameValid),
125 .custom(isVideoChannelUsernameValid),
126 body('channel.displayName')
128 .custom(isVideoChannelDisplayNameValid),
130 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
131 if (areValidationErrors(req, res, { omitBodyLog: true })) return
132 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
134 const body: UserRegister = req.body
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.' })
140 if (body.channel.name === body.username) {
141 return res.fail({ message: 'Channel name cannot be the same as user username.' })
144 const existing = await ActorModel.loadLocalByName(body.channel.name)
147 status: HttpStatusCode.CONFLICT_409,
148 message: `Channel with name ${body.channel.name} already exists.`
157 const usersRemoveValidator = [
161 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
162 if (areValidationErrors(req, res)) return
163 if (!await checkUserIdExist(req.params.id, res)) return
165 const user = res.locals.user
166 if (user.username === 'root') {
167 return res.fail({ message: 'Cannot remove the root user' })
174 const usersBlockingValidator = [
179 .custom(isUserBlockedReasonValid),
181 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
182 if (areValidationErrors(req, res)) return
183 if (!await checkUserIdExist(req.params.id, res)) return
185 const user = res.locals.user
186 if (user.username === 'root') {
187 return res.fail({ message: 'Cannot block the root user' })
194 const deleteMeValidator = [
195 (req: express.Request, res: express.Response, next: express.NextFunction) => {
196 const user = res.locals.oauth.token.User
197 if (user.username === 'root') {
198 return res.fail({ message: 'You cannot delete your root account.' })
205 const usersUpdateValidator = [
206 param('id').custom(isIdValid),
210 .custom(isUserPasswordValid),
214 body('emailVerified')
219 .custom(isUserVideoQuotaValid),
220 body('videoQuotaDaily')
222 .custom(isUserVideoQuotaDailyValid),
228 .customSanitizer(toIntOrNull)
229 .custom(isUserRoleValid),
232 .custom(isUserAdminFlagsValid),
234 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
235 if (areValidationErrors(req, res, { omitBodyLog: true })) return
236 if (!await checkUserIdExist(req.params.id, res)) return
238 const user = res.locals.user
239 if (user.username === 'root' && req.body.role !== undefined && user.role !== req.body.role) {
240 return res.fail({ message: 'Cannot change root role.' })
247 const usersUpdateMeValidator = [
250 .custom(isUserDisplayNameValid),
253 .custom(isUserDescriptionValid),
254 body('currentPassword')
256 .custom(isUserPasswordValid),
259 .custom(isUserPasswordValid),
265 .custom(isUserNSFWPolicyValid),
266 body('autoPlayVideo')
268 .custom(isUserAutoPlayVideoValid),
271 .custom(isUserP2PEnabledValid).withMessage('Should have a valid p2p enabled boolean'),
272 body('videoLanguages')
274 .custom(isUserVideoLanguages),
275 body('videosHistoryEnabled')
277 .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled boolean'),
280 .custom(v => isThemeNameValid(v) && isThemeRegistered(v)),
282 body('noInstanceConfigWarningModal')
284 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noInstanceConfigWarningModal boolean'),
285 body('noWelcomeModal')
287 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noWelcomeModal boolean'),
288 body('noAccountSetupWarningModal')
290 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noAccountSetupWarningModal boolean'),
292 body('autoPlayNextVideo')
294 .custom(v => isUserAutoPlayNextVideoValid(v)).withMessage('Should have a valid autoPlayNextVideo boolean'),
296 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
297 const user = res.locals.oauth.token.User
299 if (req.body.password || req.body.email) {
300 if (user.pluginAuth !== null) {
301 return res.fail({ message: 'You cannot update your email or password that is associated with an external auth system.' })
304 if (!req.body.currentPassword) {
305 return res.fail({ message: 'currentPassword parameter is missing.' })
308 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
310 status: HttpStatusCode.UNAUTHORIZED_401,
311 message: 'currentPassword is invalid.'
316 if (areValidationErrors(req, res, { omitBodyLog: true })) return
322 const usersGetValidator = [
327 .isBoolean().withMessage('Should have a valid withStats boolean'),
329 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
330 if (areValidationErrors(req, res)) return
331 if (!await checkUserIdExist(req.params.id, res, req.query.withStats)) return
337 const usersVideoRatingValidator = [
338 isValidVideoIdParam('videoId'),
340 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
341 if (areValidationErrors(req, res)) return
342 if (!await doesVideoExist(req.params.videoId, res, 'id')) return
348 const usersVideosValidator = [
351 .customSanitizer(toBooleanOrNull)
352 .custom(isBooleanValid).withMessage('Should have a valid isLive boolean'),
356 .customSanitizer(toIntOrNull)
359 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
360 if (areValidationErrors(req, res)) return
362 if (req.query.channelId && !await doesVideoChannelIdExist(req.query.channelId, res)) return
368 const ensureUserRegistrationAllowed = [
369 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
370 const allowedParams = {
375 const allowedResult = await Hooks.wrapPromiseFun(
378 'filter:api.user.signup.allowed.result'
381 if (allowedResult.allowed === false) {
383 status: HttpStatusCode.FORBIDDEN_403,
384 message: allowedResult.errorMessage || 'User registration is not enabled or user limit is reached.'
392 const ensureUserRegistrationAllowedForIP = [
393 (req: express.Request, res: express.Response, next: express.NextFunction) => {
394 const allowed = isSignupAllowedForCurrentIP(req.ip)
396 if (allowed === false) {
398 status: HttpStatusCode.FORBIDDEN_403,
399 message: 'You are not on a network authorized for registration.'
407 const usersAskResetPasswordValidator = [
411 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
412 if (areValidationErrors(req, res)) return
414 const exists = await checkUserEmailExist(req.body.email, res, false)
416 logger.debug('User with email %s does not exist (asking reset password).', req.body.email)
417 // Do not leak our emails
418 return res.status(HttpStatusCode.NO_CONTENT_204).end()
421 if (res.locals.user.pluginAuth) {
423 status: HttpStatusCode.CONFLICT_409,
424 message: 'Cannot recover password of a user that uses a plugin authentication.'
432 const usersResetPasswordValidator = [
435 body('verificationString')
438 .custom(isUserPasswordValid),
440 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
441 if (areValidationErrors(req, res)) return
442 if (!await checkUserIdExist(req.params.id, res)) return
444 const user = res.locals.user
445 const redisVerificationString = await Redis.Instance.getResetPasswordVerificationString(user.id)
447 if (redisVerificationString !== req.body.verificationString) {
449 status: HttpStatusCode.FORBIDDEN_403,
450 message: 'Invalid verification string.'
458 const usersAskSendVerifyEmailValidator = [
459 body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
461 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
462 if (areValidationErrors(req, res)) return
464 const exists = await checkUserEmailExist(req.body.email, res, false)
466 logger.debug('User with email %s does not exist (asking verify email).', req.body.email)
467 // Do not leak our emails
468 return res.status(HttpStatusCode.NO_CONTENT_204).end()
471 if (res.locals.user.pluginAuth) {
473 status: HttpStatusCode.CONFLICT_409,
474 message: 'Cannot ask verification email of a user that uses a plugin authentication.'
482 const usersVerifyEmailValidator = [
484 .isInt().not().isEmpty().withMessage('Should have a valid id'),
486 body('verificationString')
487 .not().isEmpty().withMessage('Should have a valid verification string'),
488 body('isPendingEmail')
490 .customSanitizer(toBooleanOrNull),
492 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
493 if (areValidationErrors(req, res)) return
494 if (!await checkUserIdExist(req.params.id, res)) return
496 const user = res.locals.user
497 const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id)
499 if (redisVerificationString !== req.body.verificationString) {
501 status: HttpStatusCode.FORBIDDEN_403,
502 message: 'Invalid verification string.'
510 const usersCheckCurrentPasswordFactory = (targetUserIdGetter: (req: express.Request) => number | string) => {
512 body('currentPassword').optional().custom(exists),
514 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
515 if (areValidationErrors(req, res)) return
517 const user = res.locals.oauth.token.User
518 const isAdminOrModerator = user.role === UserRole.ADMINISTRATOR || user.role === UserRole.MODERATOR
519 const targetUserId = forceNumber(targetUserIdGetter(req))
521 // Admin/moderator action on another user, skip the password check
522 if (isAdminOrModerator && targetUserId !== user.id) {
526 if (!req.body.currentPassword) {
528 status: HttpStatusCode.BAD_REQUEST_400,
529 message: 'currentPassword is missing'
533 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
535 status: HttpStatusCode.FORBIDDEN_403,
536 message: 'currentPassword is invalid.'
545 const userAutocompleteValidator = [
551 const ensureAuthUserOwnsAccountValidator = [
552 (req: express.Request, res: express.Response, next: express.NextFunction) => {
553 const user = res.locals.oauth.token.User
555 if (res.locals.account.id !== user.Account.id) {
557 status: HttpStatusCode.FORBIDDEN_403,
558 message: 'Only owner of this account can access this resource.'
566 const ensureCanManageChannelOrAccount = [
567 (req: express.Request, res: express.Response, next: express.NextFunction) => {
568 const user = res.locals.oauth.token.user
569 const account = res.locals.videoChannel?.Account ?? res.locals.account
570 const isUserOwner = account.userId === user.id
572 if (!isUserOwner && user.hasRight(UserRight.MANAGE_ANY_VIDEO_CHANNEL) === false) {
573 const message = `User ${user.username} does not have right this channel or account.`
576 status: HttpStatusCode.FORBIDDEN_403,
585 const ensureCanModerateUser = [
586 (req: express.Request, res: express.Response, next: express.NextFunction) => {
587 const authUser = res.locals.oauth.token.User
588 const onUser = res.locals.user
590 if (authUser.role === UserRole.ADMINISTRATOR) return next()
591 if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next()
594 status: HttpStatusCode.FORBIDDEN_403,
595 message: 'A moderator can only manage users.'
600 // ---------------------------------------------------------------------------
606 usersRegisterValidator,
607 usersBlockingValidator,
608 usersRemoveValidator,
609 usersUpdateValidator,
610 usersUpdateMeValidator,
611 usersVideoRatingValidator,
612 usersCheckCurrentPasswordFactory,
613 ensureUserRegistrationAllowed,
614 ensureUserRegistrationAllowedForIP,
616 usersVideosValidator,
617 usersAskResetPasswordValidator,
618 usersResetPasswordValidator,
619 usersAskSendVerifyEmailValidator,
620 usersVerifyEmailValidator,
621 userAutocompleteValidator,
622 ensureAuthUserOwnsAccountValidator,
623 ensureCanModerateUser,
624 ensureCanManageChannelOrAccount