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 usersCheckCurrentPassword = [
510 body('currentPassword').custom(exists),
512 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
513 if (areValidationErrors(req, res)) return
515 const user = res.locals.oauth.token.User
516 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
518 status: HttpStatusCode.FORBIDDEN_403,
519 message: 'currentPassword is invalid.'
527 const userAutocompleteValidator = [
533 const ensureAuthUserOwnsAccountValidator = [
534 (req: express.Request, res: express.Response, next: express.NextFunction) => {
535 const user = res.locals.oauth.token.User
537 if (res.locals.account.id !== user.Account.id) {
539 status: HttpStatusCode.FORBIDDEN_403,
540 message: 'Only owner of this account can access this resource.'
548 const ensureCanManageChannelOrAccount = [
549 (req: express.Request, res: express.Response, next: express.NextFunction) => {
550 const user = res.locals.oauth.token.user
551 const account = res.locals.videoChannel?.Account ?? res.locals.account
552 const isUserOwner = account.userId === user.id
554 if (!isUserOwner && user.hasRight(UserRight.MANAGE_ANY_VIDEO_CHANNEL) === false) {
555 const message = `User ${user.username} does not have right this channel or account.`
558 status: HttpStatusCode.FORBIDDEN_403,
567 const ensureCanModerateUser = [
568 (req: express.Request, res: express.Response, next: express.NextFunction) => {
569 const authUser = res.locals.oauth.token.User
570 const onUser = res.locals.user
572 if (authUser.role === UserRole.ADMINISTRATOR) return next()
573 if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next()
576 status: HttpStatusCode.FORBIDDEN_403,
577 message: 'A moderator can only manage users.'
582 // ---------------------------------------------------------------------------
588 usersRegisterValidator,
589 usersBlockingValidator,
590 usersRemoveValidator,
591 usersUpdateValidator,
592 usersUpdateMeValidator,
593 usersVideoRatingValidator,
594 usersCheckCurrentPassword,
595 ensureUserRegistrationAllowed,
596 ensureUserRegistrationAllowedForIP,
598 usersVideosValidator,
599 usersAskResetPasswordValidator,
600 usersResetPasswordValidator,
601 usersAskSendVerifyEmailValidator,
602 usersVerifyEmailValidator,
603 userAutocompleteValidator,
604 ensureAuthUserOwnsAccountValidator,
605 ensureCanModerateUser,
606 ensureCanManageChannelOrAccount