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, UserRole } from '@shared/models'
7 import { 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,
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'
33 import { UserModel } from '../../models/user/user'
34 import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from './shared'
36 const usersListValidator = [
39 .customSanitizer(toBooleanOrNull)
40 .isBoolean().withMessage('Should be a valid boolean banned state'),
42 (req: express.Request, res: express.Response, next: express.NextFunction) => {
43 logger.debug('Checking usersList parameters', { parameters: req.query })
45 if (areValidationErrors(req, res)) return
51 const usersAddValidator = [
52 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'),
53 body('password').custom(isUserPasswordValidOrEmpty).withMessage('Should have a valid password'),
54 body('email').isEmail().withMessage('Should have a valid email'),
56 body('channelName').optional().custom(isVideoChannelUsernameValid).withMessage('Should have a valid channel name'),
58 body('videoQuota').custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'),
59 body('videoQuotaDaily').custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'),
62 .customSanitizer(toIntOrNull)
63 .custom(isUserRoleValid).withMessage('Should have a valid role'),
64 body('adminFlags').optional().custom(isUserAdminFlagsValid).withMessage('Should have a valid admin flags'),
66 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
67 logger.debug('Checking usersAdd parameters', { parameters: omit(req.body, 'password') })
69 if (areValidationErrors(req, res)) return
70 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
72 const authUser = res.locals.oauth.token.User
73 if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) {
75 status: HttpStatusCode.FORBIDDEN_403,
76 message: 'You can only create users (and not administrators or moderators)'
80 if (req.body.channelName) {
81 if (req.body.channelName === req.body.username) {
82 return res.fail({ message: 'Channel name cannot be the same as user username.' })
85 const existing = await ActorModel.loadLocalByName(req.body.channelName)
88 status: HttpStatusCode.CONFLICT_409,
89 message: `Channel with name ${req.body.channelName} already exists.`
98 const usersRegisterValidator = [
99 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username'),
100 body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'),
101 body('email').isEmail().withMessage('Should have a valid email'),
104 .custom(isUserDisplayNameValid).withMessage('Should have a valid display name'),
108 .custom(isVideoChannelUsernameValid).withMessage('Should have a valid channel name'),
109 body('channel.displayName')
111 .custom(isVideoChannelDisplayNameValid).withMessage('Should have a valid display name'),
113 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
114 logger.debug('Checking usersRegister parameters', { parameters: omit(req.body, 'password') })
116 if (areValidationErrors(req, res)) return
117 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
119 const body: UserRegister = req.body
121 if (!body.channel.name || !body.channel.displayName) {
122 return res.fail({ message: 'Channel is optional but if you specify it, channel.name and channel.displayName are required.' })
125 if (body.channel.name === body.username) {
126 return res.fail({ message: 'Channel name cannot be the same as user username.' })
129 const existing = await ActorModel.loadLocalByName(body.channel.name)
132 status: HttpStatusCode.CONFLICT_409,
133 message: `Channel with name ${body.channel.name} already exists.`
142 const usersRemoveValidator = [
143 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
145 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
146 logger.debug('Checking usersRemove parameters', { parameters: req.params })
148 if (areValidationErrors(req, res)) return
149 if (!await checkUserIdExist(req.params.id, res)) return
151 const user = res.locals.user
152 if (user.username === 'root') {
153 return res.fail({ message: 'Cannot remove the root user' })
160 const usersBlockingValidator = [
161 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
162 body('reason').optional().custom(isUserBlockedReasonValid).withMessage('Should have a valid blocking reason'),
164 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
165 logger.debug('Checking usersBlocking parameters', { parameters: req.params })
167 if (areValidationErrors(req, res)) return
168 if (!await checkUserIdExist(req.params.id, res)) return
170 const user = res.locals.user
171 if (user.username === 'root') {
172 return res.fail({ message: 'Cannot block the root user' })
179 const deleteMeValidator = [
180 (req: express.Request, res: express.Response, next: express.NextFunction) => {
181 const user = res.locals.oauth.token.User
182 if (user.username === 'root') {
183 return res.fail({ message: 'You cannot delete your root account.' })
190 const usersUpdateValidator = [
191 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
193 body('password').optional().custom(isUserPasswordValid).withMessage('Should have a valid password'),
194 body('email').optional().isEmail().withMessage('Should have a valid email attribute'),
195 body('emailVerified').optional().isBoolean().withMessage('Should have a valid email verified attribute'),
196 body('videoQuota').optional().custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'),
197 body('videoQuotaDaily').optional().custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'),
198 body('pluginAuth').optional(),
201 .customSanitizer(toIntOrNull)
202 .custom(isUserRoleValid).withMessage('Should have a valid role'),
203 body('adminFlags').optional().custom(isUserAdminFlagsValid).withMessage('Should have a valid admin flags'),
205 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
206 logger.debug('Checking usersUpdate parameters', { parameters: req.body })
208 if (areValidationErrors(req, res)) return
209 if (!await checkUserIdExist(req.params.id, res)) return
211 const user = res.locals.user
212 if (user.username === 'root' && req.body.role !== undefined && user.role !== req.body.role) {
213 return res.fail({ message: 'Cannot change root role.' })
220 const usersUpdateMeValidator = [
223 .custom(isUserDisplayNameValid).withMessage('Should have a valid display name'),
226 .custom(isUserDescriptionValid).withMessage('Should have a valid description'),
227 body('currentPassword')
229 .custom(isUserPasswordValid).withMessage('Should have a valid current password'),
232 .custom(isUserPasswordValid).withMessage('Should have a valid password'),
235 .isEmail().withMessage('Should have a valid email attribute'),
238 .custom(isUserNSFWPolicyValid).withMessage('Should have a valid display Not Safe For Work policy'),
239 body('autoPlayVideo')
241 .custom(isUserAutoPlayVideoValid).withMessage('Should have a valid automatically plays video attribute'),
242 body('videoLanguages')
244 .custom(isUserVideoLanguages).withMessage('Should have a valid video languages attribute'),
245 body('videosHistoryEnabled')
247 .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled attribute'),
250 .custom(v => isThemeNameValid(v) && isThemeRegistered(v)).withMessage('Should have a valid theme'),
252 body('noInstanceConfigWarningModal')
254 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noInstanceConfigWarningModal boolean'),
255 body('noWelcomeModal')
257 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noWelcomeModal boolean'),
258 body('noAccountSetupWarningModal')
260 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noAccountSetupWarningModal boolean'),
262 body('autoPlayNextVideo')
264 .custom(v => isUserAutoPlayNextVideoValid(v)).withMessage('Should have a valid autoPlayNextVideo boolean'),
266 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
267 logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') })
269 const user = res.locals.oauth.token.User
271 if (req.body.password || req.body.email) {
272 if (user.pluginAuth !== null) {
273 return res.fail({ message: 'You cannot update your email or password that is associated with an external auth system.' })
276 if (!req.body.currentPassword) {
277 return res.fail({ message: 'currentPassword parameter is missing.' })
280 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
282 status: HttpStatusCode.UNAUTHORIZED_401,
283 message: 'currentPassword is invalid.'
288 if (areValidationErrors(req, res)) return
294 const usersGetValidator = [
295 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
296 query('withStats').optional().isBoolean().withMessage('Should have a valid stats flag'),
298 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
299 logger.debug('Checking usersGet parameters', { parameters: req.params })
301 if (areValidationErrors(req, res)) return
302 if (!await checkUserIdExist(req.params.id, res, req.query.withStats)) return
308 const usersVideoRatingValidator = [
309 isValidVideoIdParam('videoId'),
311 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
312 logger.debug('Checking usersVideoRating parameters', { parameters: req.params })
314 if (areValidationErrors(req, res)) return
315 if (!await doesVideoExist(req.params.videoId, res, 'id')) return
321 const ensureUserRegistrationAllowed = [
322 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
323 const allowedParams = {
328 const allowedResult = await Hooks.wrapPromiseFun(
331 'filter:api.user.signup.allowed.result'
334 if (allowedResult.allowed === false) {
336 status: HttpStatusCode.FORBIDDEN_403,
337 message: allowedResult.errorMessage || 'User registration is not enabled or user limit is reached.'
345 const ensureUserRegistrationAllowedForIP = [
346 (req: express.Request, res: express.Response, next: express.NextFunction) => {
347 const allowed = isSignupAllowedForCurrentIP(req.ip)
349 if (allowed === false) {
351 status: HttpStatusCode.FORBIDDEN_403,
352 message: 'You are not on a network authorized for registration.'
360 const usersAskResetPasswordValidator = [
361 body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
363 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
364 logger.debug('Checking usersAskResetPassword parameters', { parameters: req.body })
366 if (areValidationErrors(req, res)) return
368 const exists = await checkUserEmailExist(req.body.email, res, false)
370 logger.debug('User with email %s does not exist (asking reset password).', req.body.email)
371 // Do not leak our emails
372 return res.status(HttpStatusCode.NO_CONTENT_204).end()
379 const usersResetPasswordValidator = [
380 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
381 body('verificationString').not().isEmpty().withMessage('Should have a valid verification string'),
382 body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'),
384 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
385 logger.debug('Checking usersResetPassword parameters', { parameters: req.params })
387 if (areValidationErrors(req, res)) return
388 if (!await checkUserIdExist(req.params.id, res)) return
390 const user = res.locals.user
391 const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id)
393 if (redisVerificationString !== req.body.verificationString) {
395 status: HttpStatusCode.FORBIDDEN_403,
396 message: 'Invalid verification string.'
404 const usersAskSendVerifyEmailValidator = [
405 body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
407 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
408 logger.debug('Checking askUsersSendVerifyEmail parameters', { parameters: req.body })
410 if (areValidationErrors(req, res)) return
411 const exists = await checkUserEmailExist(req.body.email, res, false)
413 logger.debug('User with email %s does not exist (asking verify email).', req.body.email)
414 // Do not leak our emails
415 return res.status(HttpStatusCode.NO_CONTENT_204).end()
422 const usersVerifyEmailValidator = [
424 .isInt().not().isEmpty().withMessage('Should have a valid id'),
426 body('verificationString')
427 .not().isEmpty().withMessage('Should have a valid verification string'),
428 body('isPendingEmail')
430 .customSanitizer(toBooleanOrNull),
432 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
433 logger.debug('Checking usersVerifyEmail parameters', { parameters: req.params })
435 if (areValidationErrors(req, res)) return
436 if (!await checkUserIdExist(req.params.id, res)) return
438 const user = res.locals.user
439 const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id)
441 if (redisVerificationString !== req.body.verificationString) {
443 status: HttpStatusCode.FORBIDDEN_403,
444 message: 'Invalid verification string.'
452 const userAutocompleteValidator = [
453 param('search').isString().not().isEmpty().withMessage('Should have a search parameter')
456 const ensureAuthUserOwnsAccountValidator = [
457 (req: express.Request, res: express.Response, next: express.NextFunction) => {
458 const user = res.locals.oauth.token.User
460 if (res.locals.account.id !== user.Account.id) {
462 status: HttpStatusCode.FORBIDDEN_403,
463 message: 'Only owner of this account can access this ressource.'
471 const ensureAuthUserOwnsChannelValidator = [
472 (req: express.Request, res: express.Response, next: express.NextFunction) => {
473 const user = res.locals.oauth.token.User
475 if (res.locals.videoChannel.Account.userId !== user.id) {
477 status: HttpStatusCode.FORBIDDEN_403,
478 message: 'Only owner of this video channel can access this ressource'
486 const ensureCanManageUser = [
487 (req: express.Request, res: express.Response, next: express.NextFunction) => {
488 const authUser = res.locals.oauth.token.User
489 const onUser = res.locals.user
491 if (authUser.role === UserRole.ADMINISTRATOR) return next()
492 if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next()
495 status: HttpStatusCode.FORBIDDEN_403,
496 message: 'A moderator can only manager users.'
501 // ---------------------------------------------------------------------------
507 usersRegisterValidator,
508 usersBlockingValidator,
509 usersRemoveValidator,
510 usersUpdateValidator,
511 usersUpdateMeValidator,
512 usersVideoRatingValidator,
513 ensureUserRegistrationAllowed,
514 ensureUserRegistrationAllowedForIP,
516 usersAskResetPasswordValidator,
517 usersResetPasswordValidator,
518 usersAskSendVerifyEmailValidator,
519 usersVerifyEmailValidator,
520 userAutocompleteValidator,
521 ensureAuthUserOwnsAccountValidator,
522 ensureAuthUserOwnsChannelValidator,
526 // ---------------------------------------------------------------------------
528 function checkUserIdExist (idArg: number | string, res: express.Response, withStats = false) {
529 const id = parseInt(idArg + '', 10)
530 return checkUserExist(() => UserModel.loadByIdWithChannels(id, withStats), res)
533 function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) {
534 return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse)
537 async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email: string, res: express.Response) {
538 const user = await UserModel.loadByUsernameOrEmail(username, email)
542 status: HttpStatusCode.CONFLICT_409,
543 message: 'User with this username or email already exists.'
548 const actor = await ActorModel.loadLocalByName(username)
551 status: HttpStatusCode.CONFLICT_409,
552 message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.'
560 async function checkUserExist (finder: () => Promise<MUserDefault>, res: express.Response, abortResponse = true) {
561 const user = await finder()
564 if (abortResponse === true) {
566 status: HttpStatusCode.NOT_FOUND_404,
567 message: 'User not found'
574 res.locals.user = user