1 import express from 'express'
2 import { body, param, query } from 'express-validator'
3 import { omit } from 'lodash'
4 import { Hooks } from '@server/lib/plugins/hooks'
5 import { MUserDefault } from '@server/types/models'
6 import { HttpStatusCode, UserRegister, UserRight, UserRole } from '@shared/models'
7 import { isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
8 import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
10 isUserAdminFlagsValid,
11 isUserAutoPlayNextVideoValid,
12 isUserAutoPlayVideoValid,
13 isUserBlockedReasonValid,
14 isUserDescriptionValid,
15 isUserDisplayNameValid,
17 isUserNSFWPolicyValid,
18 isUserP2PEnabledValid,
20 isUserPasswordValidOrEmpty,
24 isUserVideoQuotaDailyValid,
25 isUserVideoQuotaValid,
26 isUserVideosHistoryEnabledValid
27 } from '../../helpers/custom-validators/users'
28 import { isVideoChannelDisplayNameValid, isVideoChannelUsernameValid } from '../../helpers/custom-validators/video-channels'
29 import { logger } from '../../helpers/logger'
30 import { isThemeRegistered } from '../../lib/plugins/theme-utils'
31 import { Redis } from '../../lib/redis'
32 import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../lib/signup'
33 import { ActorModel } from '../../models/actor/actor'
34 import { UserModel } from '../../models/user/user'
35 import { areValidationErrors, doesVideoChannelIdExist, doesVideoExist, isValidVideoIdParam } from './shared'
37 const usersListValidator = [
40 .customSanitizer(toBooleanOrNull)
41 .isBoolean().withMessage('Should be a valid boolean banned state'),
43 (req: express.Request, res: express.Response, next: express.NextFunction) => {
44 logger.debug('Checking usersList parameters', { parameters: req.query })
46 if (areValidationErrors(req, res)) return
52 const usersAddValidator = [
53 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'),
54 body('password').custom(isUserPasswordValidOrEmpty).withMessage('Should have a valid password'),
55 body('email').isEmail().withMessage('Should have a valid email'),
57 body('channelName').optional().custom(isVideoChannelUsernameValid).withMessage('Should have a valid channel name'),
59 body('videoQuota').custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'),
60 body('videoQuotaDaily').custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'),
63 .customSanitizer(toIntOrNull)
64 .custom(isUserRoleValid).withMessage('Should have a valid role'),
65 body('adminFlags').optional().custom(isUserAdminFlagsValid).withMessage('Should have a valid admin flags'),
67 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
68 logger.debug('Checking usersAdd parameters', { parameters: omit(req.body, 'password') })
70 if (areValidationErrors(req, res)) return
71 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
73 const authUser = res.locals.oauth.token.User
74 if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) {
76 status: HttpStatusCode.FORBIDDEN_403,
77 message: 'You can only create users (and not administrators or moderators)'
81 if (req.body.channelName) {
82 if (req.body.channelName === req.body.username) {
83 return res.fail({ message: 'Channel name cannot be the same as user username.' })
86 const existing = await ActorModel.loadLocalByName(req.body.channelName)
89 status: HttpStatusCode.CONFLICT_409,
90 message: `Channel with name ${req.body.channelName} already exists.`
99 const usersRegisterValidator = [
100 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username'),
101 body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'),
102 body('email').isEmail().withMessage('Should have a valid email'),
105 .custom(isUserDisplayNameValid).withMessage('Should have a valid display name'),
109 .custom(isVideoChannelUsernameValid).withMessage('Should have a valid channel name'),
110 body('channel.displayName')
112 .custom(isVideoChannelDisplayNameValid).withMessage('Should have a valid display name'),
114 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
115 logger.debug('Checking usersRegister parameters', { parameters: omit(req.body, 'password') })
117 if (areValidationErrors(req, res)) return
118 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
120 const body: UserRegister = req.body
122 if (!body.channel.name || !body.channel.displayName) {
123 return res.fail({ message: 'Channel is optional but if you specify it, channel.name and channel.displayName are required.' })
126 if (body.channel.name === body.username) {
127 return res.fail({ message: 'Channel name cannot be the same as user username.' })
130 const existing = await ActorModel.loadLocalByName(body.channel.name)
133 status: HttpStatusCode.CONFLICT_409,
134 message: `Channel with name ${body.channel.name} already exists.`
143 const usersRemoveValidator = [
144 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
146 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
147 logger.debug('Checking usersRemove parameters', { parameters: req.params })
149 if (areValidationErrors(req, res)) return
150 if (!await checkUserIdExist(req.params.id, res)) return
152 const user = res.locals.user
153 if (user.username === 'root') {
154 return res.fail({ message: 'Cannot remove the root user' })
161 const usersBlockingValidator = [
162 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
163 body('reason').optional().custom(isUserBlockedReasonValid).withMessage('Should have a valid blocking reason'),
165 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
166 logger.debug('Checking usersBlocking parameters', { parameters: req.params })
168 if (areValidationErrors(req, res)) return
169 if (!await checkUserIdExist(req.params.id, res)) return
171 const user = res.locals.user
172 if (user.username === 'root') {
173 return res.fail({ message: 'Cannot block the root user' })
180 const deleteMeValidator = [
181 (req: express.Request, res: express.Response, next: express.NextFunction) => {
182 const user = res.locals.oauth.token.User
183 if (user.username === 'root') {
184 return res.fail({ message: 'You cannot delete your root account.' })
191 const usersUpdateValidator = [
192 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
194 body('password').optional().custom(isUserPasswordValid).withMessage('Should have a valid password'),
195 body('email').optional().isEmail().withMessage('Should have a valid email attribute'),
196 body('emailVerified').optional().isBoolean().withMessage('Should have a valid email verified attribute'),
197 body('videoQuota').optional().custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'),
198 body('videoQuotaDaily').optional().custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'),
199 body('pluginAuth').optional(),
202 .customSanitizer(toIntOrNull)
203 .custom(isUserRoleValid).withMessage('Should have a valid role'),
204 body('adminFlags').optional().custom(isUserAdminFlagsValid).withMessage('Should have a valid admin flags'),
206 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
207 logger.debug('Checking usersUpdate parameters', { parameters: req.body })
209 if (areValidationErrors(req, res)) return
210 if (!await checkUserIdExist(req.params.id, res)) return
212 const user = res.locals.user
213 if (user.username === 'root' && req.body.role !== undefined && user.role !== req.body.role) {
214 return res.fail({ message: 'Cannot change root role.' })
221 const usersUpdateMeValidator = [
224 .custom(isUserDisplayNameValid).withMessage('Should have a valid display name'),
227 .custom(isUserDescriptionValid).withMessage('Should have a valid description'),
228 body('currentPassword')
230 .custom(isUserPasswordValid).withMessage('Should have a valid current password'),
233 .custom(isUserPasswordValid).withMessage('Should have a valid password'),
236 .isEmail().withMessage('Should have a valid email attribute'),
239 .custom(isUserNSFWPolicyValid).withMessage('Should have a valid display Not Safe For Work policy'),
240 body('autoPlayVideo')
242 .custom(isUserAutoPlayVideoValid).withMessage('Should have a valid automatically plays video attribute'),
245 .custom(isUserP2PEnabledValid).withMessage('Should have a valid p2p enabled boolean'),
246 body('videoLanguages')
248 .custom(isUserVideoLanguages).withMessage('Should have a valid video languages attribute'),
249 body('videosHistoryEnabled')
251 .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled attribute'),
254 .custom(v => isThemeNameValid(v) && isThemeRegistered(v)).withMessage('Should have a valid theme'),
256 body('noInstanceConfigWarningModal')
258 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noInstanceConfigWarningModal boolean'),
259 body('noWelcomeModal')
261 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noWelcomeModal boolean'),
262 body('noAccountSetupWarningModal')
264 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noAccountSetupWarningModal boolean'),
266 body('autoPlayNextVideo')
268 .custom(v => isUserAutoPlayNextVideoValid(v)).withMessage('Should have a valid autoPlayNextVideo boolean'),
270 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
271 logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') })
273 const user = res.locals.oauth.token.User
275 if (req.body.password || req.body.email) {
276 if (user.pluginAuth !== null) {
277 return res.fail({ message: 'You cannot update your email or password that is associated with an external auth system.' })
280 if (!req.body.currentPassword) {
281 return res.fail({ message: 'currentPassword parameter is missing.' })
284 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
286 status: HttpStatusCode.UNAUTHORIZED_401,
287 message: 'currentPassword is invalid.'
292 if (areValidationErrors(req, res)) return
298 const usersGetValidator = [
299 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
300 query('withStats').optional().isBoolean().withMessage('Should have a valid stats flag'),
302 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
303 logger.debug('Checking usersGet parameters', { parameters: req.params })
305 if (areValidationErrors(req, res)) return
306 if (!await checkUserIdExist(req.params.id, res, req.query.withStats)) return
312 const usersVideoRatingValidator = [
313 isValidVideoIdParam('videoId'),
315 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
316 logger.debug('Checking usersVideoRating parameters', { parameters: req.params })
318 if (areValidationErrors(req, res)) return
319 if (!await doesVideoExist(req.params.videoId, res, 'id')) return
325 const usersVideosValidator = [
328 .customSanitizer(toBooleanOrNull)
329 .custom(isBooleanValid).withMessage('Should have a valid live boolean'),
333 .customSanitizer(toIntOrNull)
334 .custom(isIdValid).withMessage('Should have a valid channel id'),
336 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
337 logger.debug('Checking usersVideosValidator parameters', { parameters: req.query })
339 if (areValidationErrors(req, res)) return
341 if (req.query.channelId && !await doesVideoChannelIdExist(req.query.channelId, res)) return
347 const ensureUserRegistrationAllowed = [
348 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
349 const allowedParams = {
354 const allowedResult = await Hooks.wrapPromiseFun(
357 'filter:api.user.signup.allowed.result'
360 if (allowedResult.allowed === false) {
362 status: HttpStatusCode.FORBIDDEN_403,
363 message: allowedResult.errorMessage || 'User registration is not enabled or user limit is reached.'
371 const ensureUserRegistrationAllowedForIP = [
372 (req: express.Request, res: express.Response, next: express.NextFunction) => {
373 const allowed = isSignupAllowedForCurrentIP(req.ip)
375 if (allowed === false) {
377 status: HttpStatusCode.FORBIDDEN_403,
378 message: 'You are not on a network authorized for registration.'
386 const usersAskResetPasswordValidator = [
387 body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
389 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
390 logger.debug('Checking usersAskResetPassword parameters', { parameters: req.body })
392 if (areValidationErrors(req, res)) return
394 const exists = await checkUserEmailExist(req.body.email, res, false)
396 logger.debug('User with email %s does not exist (asking reset password).', req.body.email)
397 // Do not leak our emails
398 return res.status(HttpStatusCode.NO_CONTENT_204).end()
405 const usersResetPasswordValidator = [
406 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
407 body('verificationString').not().isEmpty().withMessage('Should have a valid verification string'),
408 body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'),
410 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
411 logger.debug('Checking usersResetPassword parameters', { parameters: req.params })
413 if (areValidationErrors(req, res)) return
414 if (!await checkUserIdExist(req.params.id, res)) return
416 const user = res.locals.user
417 const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id)
419 if (redisVerificationString !== req.body.verificationString) {
421 status: HttpStatusCode.FORBIDDEN_403,
422 message: 'Invalid verification string.'
430 const usersAskSendVerifyEmailValidator = [
431 body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
433 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
434 logger.debug('Checking askUsersSendVerifyEmail parameters', { parameters: req.body })
436 if (areValidationErrors(req, res)) return
437 const exists = await checkUserEmailExist(req.body.email, res, false)
439 logger.debug('User with email %s does not exist (asking verify email).', req.body.email)
440 // Do not leak our emails
441 return res.status(HttpStatusCode.NO_CONTENT_204).end()
448 const usersVerifyEmailValidator = [
450 .isInt().not().isEmpty().withMessage('Should have a valid id'),
452 body('verificationString')
453 .not().isEmpty().withMessage('Should have a valid verification string'),
454 body('isPendingEmail')
456 .customSanitizer(toBooleanOrNull),
458 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
459 logger.debug('Checking usersVerifyEmail parameters', { parameters: req.params })
461 if (areValidationErrors(req, res)) return
462 if (!await checkUserIdExist(req.params.id, res)) return
464 const user = res.locals.user
465 const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id)
467 if (redisVerificationString !== req.body.verificationString) {
469 status: HttpStatusCode.FORBIDDEN_403,
470 message: 'Invalid verification string.'
478 const userAutocompleteValidator = [
479 param('search').isString().not().isEmpty().withMessage('Should have a search parameter')
482 const ensureAuthUserOwnsAccountValidator = [
483 (req: express.Request, res: express.Response, next: express.NextFunction) => {
484 const user = res.locals.oauth.token.User
486 if (res.locals.account.id !== user.Account.id) {
488 status: HttpStatusCode.FORBIDDEN_403,
489 message: 'Only owner of this account can access this resource.'
497 const ensureCanManageChannel = [
498 (req: express.Request, res: express.Response, next: express.NextFunction) => {
499 const user = res.locals.oauth.token.user
500 const isUserOwner = res.locals.videoChannel.Account.userId === user.id
502 if (!isUserOwner && user.hasRight(UserRight.MANAGE_ANY_VIDEO_CHANNEL) === false) {
503 const message = `User ${user.username} does not have right to manage channel ${req.params.nameWithHost}.`
506 status: HttpStatusCode.FORBIDDEN_403,
515 const ensureCanManageUser = [
516 (req: express.Request, res: express.Response, next: express.NextFunction) => {
517 const authUser = res.locals.oauth.token.User
518 const onUser = res.locals.user
520 if (authUser.role === UserRole.ADMINISTRATOR) return next()
521 if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next()
524 status: HttpStatusCode.FORBIDDEN_403,
525 message: 'A moderator can only manager users.'
530 // ---------------------------------------------------------------------------
536 usersRegisterValidator,
537 usersBlockingValidator,
538 usersRemoveValidator,
539 usersUpdateValidator,
540 usersUpdateMeValidator,
541 usersVideoRatingValidator,
542 ensureUserRegistrationAllowed,
543 ensureUserRegistrationAllowedForIP,
545 usersVideosValidator,
546 usersAskResetPasswordValidator,
547 usersResetPasswordValidator,
548 usersAskSendVerifyEmailValidator,
549 usersVerifyEmailValidator,
550 userAutocompleteValidator,
551 ensureAuthUserOwnsAccountValidator,
553 ensureCanManageChannel
556 // ---------------------------------------------------------------------------
558 function checkUserIdExist (idArg: number | string, res: express.Response, withStats = false) {
559 const id = parseInt(idArg + '', 10)
560 return checkUserExist(() => UserModel.loadByIdWithChannels(id, withStats), res)
563 function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) {
564 return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse)
567 async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email: string, res: express.Response) {
568 const user = await UserModel.loadByUsernameOrEmail(username, email)
572 status: HttpStatusCode.CONFLICT_409,
573 message: 'User with this username or email already exists.'
578 const actor = await ActorModel.loadLocalByName(username)
581 status: HttpStatusCode.CONFLICT_409,
582 message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.'
590 async function checkUserExist (finder: () => Promise<MUserDefault>, res: express.Response, abortResponse = true) {
591 const user = await finder()
594 if (abortResponse === true) {
596 status: HttpStatusCode.NOT_FOUND_404,
597 message: 'User not found'
604 res.locals.user = user