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 blocked boolena'),
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 = [
54 .custom(isUserUsernameValid)
55 .withMessage('Should have a valid username (lowercase alphanumeric characters)'),
57 .custom(isUserPasswordValidOrEmpty),
63 .custom(isVideoChannelUsernameValid),
66 .custom(isUserVideoQuotaValid),
67 body('videoQuotaDaily')
68 .custom(isUserVideoQuotaDailyValid),
71 .customSanitizer(toIntOrNull)
72 .custom(isUserRoleValid),
76 .custom(isUserAdminFlagsValid),
78 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
79 logger.debug('Checking usersAdd parameters', { parameters: omit(req.body, 'password') })
81 if (areValidationErrors(req, res)) return
82 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
84 const authUser = res.locals.oauth.token.User
85 if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) {
87 status: HttpStatusCode.FORBIDDEN_403,
88 message: 'You can only create users (and not administrators or moderators)'
92 if (req.body.channelName) {
93 if (req.body.channelName === req.body.username) {
94 return res.fail({ message: 'Channel name cannot be the same as user username.' })
97 const existing = await ActorModel.loadLocalByName(req.body.channelName)
100 status: HttpStatusCode.CONFLICT_409,
101 message: `Channel with name ${req.body.channelName} already exists.`
110 const usersRegisterValidator = [
112 .custom(isUserUsernameValid),
114 .custom(isUserPasswordValid),
119 .custom(isUserDisplayNameValid),
123 .custom(isVideoChannelUsernameValid),
124 body('channel.displayName')
126 .custom(isVideoChannelDisplayNameValid),
128 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
129 logger.debug('Checking usersRegister parameters', { parameters: omit(req.body, 'password') })
131 if (areValidationErrors(req, res)) 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 logger.debug('Checking usersRemove parameters', { parameters: req.params })
164 if (areValidationErrors(req, res)) return
165 if (!await checkUserIdExist(req.params.id, res)) return
167 const user = res.locals.user
168 if (user.username === 'root') {
169 return res.fail({ message: 'Cannot remove the root user' })
176 const usersBlockingValidator = [
181 .custom(isUserBlockedReasonValid),
183 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
184 logger.debug('Checking usersBlocking parameters', { parameters: req.params })
186 if (areValidationErrors(req, res)) return
187 if (!await checkUserIdExist(req.params.id, res)) return
189 const user = res.locals.user
190 if (user.username === 'root') {
191 return res.fail({ message: 'Cannot block the root user' })
198 const deleteMeValidator = [
199 (req: express.Request, res: express.Response, next: express.NextFunction) => {
200 const user = res.locals.oauth.token.User
201 if (user.username === 'root') {
202 return res.fail({ message: 'You cannot delete your root account.' })
209 const usersUpdateValidator = [
210 param('id').custom(isIdValid),
214 .custom(isUserPasswordValid),
218 body('emailVerified')
223 .custom(isUserVideoQuotaValid),
224 body('videoQuotaDaily')
226 .custom(isUserVideoQuotaDailyValid),
232 .customSanitizer(toIntOrNull)
233 .custom(isUserRoleValid),
236 .custom(isUserAdminFlagsValid),
238 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
239 logger.debug('Checking usersUpdate parameters', { parameters: req.body })
241 if (areValidationErrors(req, res)) return
242 if (!await checkUserIdExist(req.params.id, res)) return
244 const user = res.locals.user
245 if (user.username === 'root' && req.body.role !== undefined && user.role !== req.body.role) {
246 return res.fail({ message: 'Cannot change root role.' })
253 const usersUpdateMeValidator = [
256 .custom(isUserDisplayNameValid),
259 .custom(isUserDescriptionValid),
260 body('currentPassword')
262 .custom(isUserPasswordValid),
265 .custom(isUserPasswordValid),
271 .custom(isUserNSFWPolicyValid),
272 body('autoPlayVideo')
274 .custom(isUserAutoPlayVideoValid),
277 .custom(isUserP2PEnabledValid).withMessage('Should have a valid p2p enabled boolean'),
278 body('videoLanguages')
280 .custom(isUserVideoLanguages),
281 body('videosHistoryEnabled')
283 .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled boolean'),
286 .custom(v => isThemeNameValid(v) && isThemeRegistered(v)),
288 body('noInstanceConfigWarningModal')
290 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noInstanceConfigWarningModal boolean'),
291 body('noWelcomeModal')
293 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noWelcomeModal boolean'),
294 body('noAccountSetupWarningModal')
296 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noAccountSetupWarningModal boolean'),
298 body('autoPlayNextVideo')
300 .custom(v => isUserAutoPlayNextVideoValid(v)).withMessage('Should have a valid autoPlayNextVideo boolean'),
302 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
303 logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') })
305 const user = res.locals.oauth.token.User
307 if (req.body.password || req.body.email) {
308 if (user.pluginAuth !== null) {
309 return res.fail({ message: 'You cannot update your email or password that is associated with an external auth system.' })
312 if (!req.body.currentPassword) {
313 return res.fail({ message: 'currentPassword parameter is missing.' })
316 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
318 status: HttpStatusCode.UNAUTHORIZED_401,
319 message: 'currentPassword is invalid.'
324 if (areValidationErrors(req, res)) return
330 const usersGetValidator = [
335 .isBoolean().withMessage('Should have a valid withStats boolean'),
337 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
338 logger.debug('Checking usersGet parameters', { parameters: req.params })
340 if (areValidationErrors(req, res)) return
341 if (!await checkUserIdExist(req.params.id, res, req.query.withStats)) return
347 const usersVideoRatingValidator = [
348 isValidVideoIdParam('videoId'),
350 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
351 logger.debug('Checking usersVideoRating parameters', { parameters: req.params })
353 if (areValidationErrors(req, res)) return
354 if (!await doesVideoExist(req.params.videoId, res, 'id')) return
360 const usersVideosValidator = [
363 .customSanitizer(toBooleanOrNull)
364 .custom(isBooleanValid).withMessage('Should have a valid isLive boolean'),
368 .customSanitizer(toIntOrNull)
371 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
372 logger.debug('Checking usersVideosValidator parameters', { parameters: req.query })
374 if (areValidationErrors(req, res)) return
376 if (req.query.channelId && !await doesVideoChannelIdExist(req.query.channelId, res)) return
382 const ensureUserRegistrationAllowed = [
383 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
384 const allowedParams = {
389 const allowedResult = await Hooks.wrapPromiseFun(
392 'filter:api.user.signup.allowed.result'
395 if (allowedResult.allowed === false) {
397 status: HttpStatusCode.FORBIDDEN_403,
398 message: allowedResult.errorMessage || 'User registration is not enabled or user limit is reached.'
406 const ensureUserRegistrationAllowedForIP = [
407 (req: express.Request, res: express.Response, next: express.NextFunction) => {
408 const allowed = isSignupAllowedForCurrentIP(req.ip)
410 if (allowed === false) {
412 status: HttpStatusCode.FORBIDDEN_403,
413 message: 'You are not on a network authorized for registration.'
421 const usersAskResetPasswordValidator = [
425 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
426 logger.debug('Checking usersAskResetPassword parameters', { parameters: req.body })
428 if (areValidationErrors(req, res)) return
430 const exists = await checkUserEmailExist(req.body.email, res, false)
432 logger.debug('User with email %s does not exist (asking reset password).', req.body.email)
433 // Do not leak our emails
434 return res.status(HttpStatusCode.NO_CONTENT_204).end()
441 const usersResetPasswordValidator = [
444 body('verificationString')
447 .custom(isUserPasswordValid),
449 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
450 logger.debug('Checking usersResetPassword parameters', { parameters: req.params })
452 if (areValidationErrors(req, res)) return
453 if (!await checkUserIdExist(req.params.id, res)) return
455 const user = res.locals.user
456 const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id)
458 if (redisVerificationString !== req.body.verificationString) {
460 status: HttpStatusCode.FORBIDDEN_403,
461 message: 'Invalid verification string.'
469 const usersAskSendVerifyEmailValidator = [
470 body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
472 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
473 logger.debug('Checking askUsersSendVerifyEmail parameters', { parameters: req.body })
475 if (areValidationErrors(req, res)) return
476 const exists = await checkUserEmailExist(req.body.email, res, false)
478 logger.debug('User with email %s does not exist (asking verify email).', req.body.email)
479 // Do not leak our emails
480 return res.status(HttpStatusCode.NO_CONTENT_204).end()
487 const usersVerifyEmailValidator = [
489 .isInt().not().isEmpty().withMessage('Should have a valid id'),
491 body('verificationString')
492 .not().isEmpty().withMessage('Should have a valid verification string'),
493 body('isPendingEmail')
495 .customSanitizer(toBooleanOrNull),
497 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
498 logger.debug('Checking usersVerifyEmail parameters', { parameters: req.params })
500 if (areValidationErrors(req, res)) return
501 if (!await checkUserIdExist(req.params.id, res)) return
503 const user = res.locals.user
504 const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id)
506 if (redisVerificationString !== req.body.verificationString) {
508 status: HttpStatusCode.FORBIDDEN_403,
509 message: 'Invalid verification string.'
517 const userAutocompleteValidator = [
518 param('search').isString().not().isEmpty().withMessage('Should have a search parameter')
521 const ensureAuthUserOwnsAccountValidator = [
522 (req: express.Request, res: express.Response, next: express.NextFunction) => {
523 const user = res.locals.oauth.token.User
525 if (res.locals.account.id !== user.Account.id) {
527 status: HttpStatusCode.FORBIDDEN_403,
528 message: 'Only owner of this account can access this resource.'
536 const ensureCanManageChannel = [
537 (req: express.Request, res: express.Response, next: express.NextFunction) => {
538 const user = res.locals.oauth.token.user
539 const isUserOwner = res.locals.videoChannel.Account.userId === user.id
541 if (!isUserOwner && user.hasRight(UserRight.MANAGE_ANY_VIDEO_CHANNEL) === false) {
542 const message = `User ${user.username} does not have right to manage channel ${req.params.nameWithHost}.`
545 status: HttpStatusCode.FORBIDDEN_403,
554 const ensureCanManageUser = [
555 (req: express.Request, res: express.Response, next: express.NextFunction) => {
556 const authUser = res.locals.oauth.token.User
557 const onUser = res.locals.user
559 if (authUser.role === UserRole.ADMINISTRATOR) return next()
560 if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next()
563 status: HttpStatusCode.FORBIDDEN_403,
564 message: 'A moderator can only manager users.'
569 // ---------------------------------------------------------------------------
575 usersRegisterValidator,
576 usersBlockingValidator,
577 usersRemoveValidator,
578 usersUpdateValidator,
579 usersUpdateMeValidator,
580 usersVideoRatingValidator,
581 ensureUserRegistrationAllowed,
582 ensureUserRegistrationAllowedForIP,
584 usersVideosValidator,
585 usersAskResetPasswordValidator,
586 usersResetPasswordValidator,
587 usersAskSendVerifyEmailValidator,
588 usersVerifyEmailValidator,
589 userAutocompleteValidator,
590 ensureAuthUserOwnsAccountValidator,
592 ensureCanManageChannel
595 // ---------------------------------------------------------------------------
597 function checkUserIdExist (idArg: number | string, res: express.Response, withStats = false) {
598 const id = parseInt(idArg + '', 10)
599 return checkUserExist(() => UserModel.loadByIdWithChannels(id, withStats), res)
602 function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) {
603 return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse)
606 async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email: string, res: express.Response) {
607 const user = await UserModel.loadByUsernameOrEmail(username, email)
611 status: HttpStatusCode.CONFLICT_409,
612 message: 'User with this username or email already exists.'
617 const actor = await ActorModel.loadLocalByName(username)
620 status: HttpStatusCode.CONFLICT_409,
621 message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.'
629 async function checkUserExist (finder: () => Promise<MUserDefault>, res: express.Response, abortResponse = true) {
630 const user = await finder()
633 if (abortResponse === true) {
635 status: HttpStatusCode.NOT_FOUND_404,
636 message: 'User not found'
643 res.locals.user = user