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 { 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,
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, doesVideoChannelIdExist, 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 usersVideosValidator = [
324 .customSanitizer(toBooleanOrNull)
325 .custom(isBooleanValid).withMessage('Should have a valid live boolean'),
329 .customSanitizer(toIntOrNull)
330 .custom(isIdValid).withMessage('Should have a valid channel id'),
332 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
333 logger.debug('Checking usersVideosValidator parameters', { parameters: req.query })
335 if (areValidationErrors(req, res)) return
337 if (req.query.channelId && !await doesVideoChannelIdExist(req.query.channelId, res)) return
343 const ensureUserRegistrationAllowed = [
344 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
345 const allowedParams = {
350 const allowedResult = await Hooks.wrapPromiseFun(
353 'filter:api.user.signup.allowed.result'
356 if (allowedResult.allowed === false) {
358 status: HttpStatusCode.FORBIDDEN_403,
359 message: allowedResult.errorMessage || 'User registration is not enabled or user limit is reached.'
367 const ensureUserRegistrationAllowedForIP = [
368 (req: express.Request, res: express.Response, next: express.NextFunction) => {
369 const allowed = isSignupAllowedForCurrentIP(req.ip)
371 if (allowed === false) {
373 status: HttpStatusCode.FORBIDDEN_403,
374 message: 'You are not on a network authorized for registration.'
382 const usersAskResetPasswordValidator = [
383 body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
385 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
386 logger.debug('Checking usersAskResetPassword parameters', { parameters: req.body })
388 if (areValidationErrors(req, res)) return
390 const exists = await checkUserEmailExist(req.body.email, res, false)
392 logger.debug('User with email %s does not exist (asking reset password).', req.body.email)
393 // Do not leak our emails
394 return res.status(HttpStatusCode.NO_CONTENT_204).end()
401 const usersResetPasswordValidator = [
402 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
403 body('verificationString').not().isEmpty().withMessage('Should have a valid verification string'),
404 body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'),
406 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
407 logger.debug('Checking usersResetPassword parameters', { parameters: req.params })
409 if (areValidationErrors(req, res)) return
410 if (!await checkUserIdExist(req.params.id, res)) return
412 const user = res.locals.user
413 const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id)
415 if (redisVerificationString !== req.body.verificationString) {
417 status: HttpStatusCode.FORBIDDEN_403,
418 message: 'Invalid verification string.'
426 const usersAskSendVerifyEmailValidator = [
427 body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
429 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
430 logger.debug('Checking askUsersSendVerifyEmail parameters', { parameters: req.body })
432 if (areValidationErrors(req, res)) return
433 const exists = await checkUserEmailExist(req.body.email, res, false)
435 logger.debug('User with email %s does not exist (asking verify email).', req.body.email)
436 // Do not leak our emails
437 return res.status(HttpStatusCode.NO_CONTENT_204).end()
444 const usersVerifyEmailValidator = [
446 .isInt().not().isEmpty().withMessage('Should have a valid id'),
448 body('verificationString')
449 .not().isEmpty().withMessage('Should have a valid verification string'),
450 body('isPendingEmail')
452 .customSanitizer(toBooleanOrNull),
454 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
455 logger.debug('Checking usersVerifyEmail parameters', { parameters: req.params })
457 if (areValidationErrors(req, res)) return
458 if (!await checkUserIdExist(req.params.id, res)) return
460 const user = res.locals.user
461 const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id)
463 if (redisVerificationString !== req.body.verificationString) {
465 status: HttpStatusCode.FORBIDDEN_403,
466 message: 'Invalid verification string.'
474 const userAutocompleteValidator = [
475 param('search').isString().not().isEmpty().withMessage('Should have a search parameter')
478 const ensureAuthUserOwnsAccountValidator = [
479 (req: express.Request, res: express.Response, next: express.NextFunction) => {
480 const user = res.locals.oauth.token.User
482 if (res.locals.account.id !== user.Account.id) {
484 status: HttpStatusCode.FORBIDDEN_403,
485 message: 'Only owner of this account can access this ressource.'
493 const ensureAuthUserOwnsChannelValidator = [
494 (req: express.Request, res: express.Response, next: express.NextFunction) => {
495 const user = res.locals.oauth.token.User
497 if (res.locals.videoChannel.Account.userId !== user.id) {
499 status: HttpStatusCode.FORBIDDEN_403,
500 message: 'Only owner of this video channel can access this ressource'
508 const ensureCanManageUser = [
509 (req: express.Request, res: express.Response, next: express.NextFunction) => {
510 const authUser = res.locals.oauth.token.User
511 const onUser = res.locals.user
513 if (authUser.role === UserRole.ADMINISTRATOR) return next()
514 if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next()
517 status: HttpStatusCode.FORBIDDEN_403,
518 message: 'A moderator can only manager users.'
523 // ---------------------------------------------------------------------------
529 usersRegisterValidator,
530 usersBlockingValidator,
531 usersRemoveValidator,
532 usersUpdateValidator,
533 usersUpdateMeValidator,
534 usersVideoRatingValidator,
535 ensureUserRegistrationAllowed,
536 ensureUserRegistrationAllowedForIP,
538 usersVideosValidator,
539 usersAskResetPasswordValidator,
540 usersResetPasswordValidator,
541 usersAskSendVerifyEmailValidator,
542 usersVerifyEmailValidator,
543 userAutocompleteValidator,
544 ensureAuthUserOwnsAccountValidator,
545 ensureAuthUserOwnsChannelValidator,
549 // ---------------------------------------------------------------------------
551 function checkUserIdExist (idArg: number | string, res: express.Response, withStats = false) {
552 const id = parseInt(idArg + '', 10)
553 return checkUserExist(() => UserModel.loadByIdWithChannels(id, withStats), res)
556 function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) {
557 return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse)
560 async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email: string, res: express.Response) {
561 const user = await UserModel.loadByUsernameOrEmail(username, email)
565 status: HttpStatusCode.CONFLICT_409,
566 message: 'User with this username or email already exists.'
571 const actor = await ActorModel.loadLocalByName(username)
574 status: HttpStatusCode.CONFLICT_409,
575 message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.'
583 async function checkUserExist (finder: () => Promise<MUserDefault>, res: express.Response, abortResponse = true) {
584 const user = await finder()
587 if (abortResponse === true) {
589 status: HttpStatusCode.NOT_FOUND_404,
590 message: 'User not found'
597 res.locals.user = user