-import 'express-validator'
-import * as express from 'express'
-import * as Promise from 'bluebird'
-import * as validator from 'validator'
-
-import { database as db } from '../../initializers/database'
-import { checkErrors } from './utils'
-import { isSignupAllowed, logger } from '../../helpers'
-import { UserInstance, VideoInstance } from '../../models'
-
-function usersAddValidator (req: express.Request, res: express.Response, next: express.NextFunction) {
- req.checkBody('username', 'Should have a valid username').isUserUsernameValid()
- req.checkBody('password', 'Should have a valid password').isUserPasswordValid()
- req.checkBody('email', 'Should have a valid email').isEmail()
- req.checkBody('videoQuota', 'Should have a valid user quota').isUserVideoQuotaValid()
-
- logger.debug('Checking usersAdd parameters', { parameters: req.body })
-
- checkErrors(req, res, () => {
- checkUserDoesNotAlreadyExist(req.body.username, req.body.email, res, next)
- })
-}
+import express from 'express'
+import { body, param, query } from 'express-validator'
+import { forceNumber } from '@shared/core-utils'
+import { HttpStatusCode, UserRight, UserRole } from '@shared/models'
+import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
+import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
+import {
+ isUserAdminFlagsValid,
+ isUserAutoPlayNextVideoValid,
+ isUserAutoPlayVideoValid,
+ isUserBlockedReasonValid,
+ isUserDescriptionValid,
+ isUserDisplayNameValid,
+ isUserEmailPublicValid,
+ isUserNoModal,
+ isUserNSFWPolicyValid,
+ isUserP2PEnabledValid,
+ isUserPasswordValid,
+ isUserPasswordValidOrEmpty,
+ isUserRoleValid,
+ isUserUsernameValid,
+ isUserVideoLanguages,
+ isUserVideoQuotaDailyValid,
+ isUserVideoQuotaValid,
+ isUserVideosHistoryEnabledValid
+} from '../../helpers/custom-validators/users'
+import { isVideoChannelUsernameValid } from '../../helpers/custom-validators/video-channels'
+import { logger } from '../../helpers/logger'
+import { isThemeRegistered } from '../../lib/plugins/theme-utils'
+import { Redis } from '../../lib/redis'
+import { ActorModel } from '../../models/actor/actor'
+import {
+ areValidationErrors,
+ checkUserEmailExist,
+ checkUserIdExist,
+ checkUserNameOrEmailDoNotAlreadyExist,
+ doesVideoChannelIdExist,
+ doesVideoExist,
+ isValidVideoIdParam
+} from './shared'
+
+const usersListValidator = [
+ query('blocked')
+ .optional()
+ .customSanitizer(toBooleanOrNull)
+ .isBoolean().withMessage('Should be a valid blocked boolean'),
+
+ (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ if (areValidationErrors(req, res)) return
-function usersRegisterValidator (req: express.Request, res: express.Response, next: express.NextFunction) {
- req.checkBody('username', 'Should have a valid username').isUserUsernameValid()
- req.checkBody('password', 'Should have a valid password').isUserPasswordValid()
- req.checkBody('email', 'Should have a valid email').isEmail()
+ return next()
+ }
+]
+
+const usersAddValidator = [
+ body('username')
+ .custom(isUserUsernameValid)
+ .withMessage('Should have a valid username (lowercase alphanumeric characters)'),
+ body('password')
+ .custom(isUserPasswordValidOrEmpty),
+ body('email')
+ .isEmail(),
+
+ body('channelName')
+ .optional()
+ .custom(isVideoChannelUsernameValid),
+
+ body('videoQuota')
+ .optional()
+ .custom(isUserVideoQuotaValid),
+
+ body('videoQuotaDaily')
+ .optional()
+ .custom(isUserVideoQuotaDailyValid),
+
+ body('role')
+ .customSanitizer(toIntOrNull)
+ .custom(isUserRoleValid),
+
+ body('adminFlags')
+ .optional()
+ .custom(isUserAdminFlagsValid),
+
+ async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ if (areValidationErrors(req, res, { omitBodyLog: true })) return
+ if (!await checkUserNameOrEmailDoNotAlreadyExist(req.body.username, req.body.email, res)) return
+
+ const authUser = res.locals.oauth.token.User
+ if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) {
+ return res.fail({
+ status: HttpStatusCode.FORBIDDEN_403,
+ message: 'You can only create users (and not administrators or moderators)'
+ })
+ }
- logger.debug('Checking usersRegister parameters', { parameters: req.body })
+ if (req.body.channelName) {
+ if (req.body.channelName === req.body.username) {
+ return res.fail({ message: 'Channel name cannot be the same as user username.' })
+ }
- checkErrors(req, res, () => {
- checkUserDoesNotAlreadyExist(req.body.username, req.body.email, res, next)
- })
-}
+ const existing = await ActorModel.loadLocalByName(req.body.channelName)
+ if (existing) {
+ return res.fail({
+ status: HttpStatusCode.CONFLICT_409,
+ message: `Channel with name ${req.body.channelName} already exists.`
+ })
+ }
+ }
+
+ return next()
+ }
+]
+
+const usersRemoveValidator = [
+ param('id')
+ .custom(isIdValid),
+
+ async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ if (areValidationErrors(req, res)) return
+ if (!await checkUserIdExist(req.params.id, res)) return
+
+ const user = res.locals.user
+ if (user.username === 'root') {
+ return res.fail({ message: 'Cannot remove the root user' })
+ }
+
+ return next()
+ }
+]
+
+const usersBlockingValidator = [
+ param('id')
+ .custom(isIdValid),
+ body('reason')
+ .optional()
+ .custom(isUserBlockedReasonValid),
+
+ async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ if (areValidationErrors(req, res)) return
+ if (!await checkUserIdExist(req.params.id, res)) return
+
+ const user = res.locals.user
+ if (user.username === 'root') {
+ return res.fail({ message: 'Cannot block the root user' })
+ }
+
+ return next()
+ }
+]
+
+const deleteMeValidator = [
+ (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ const user = res.locals.oauth.token.User
+ if (user.username === 'root') {
+ return res.fail({ message: 'You cannot delete your root account.' })
+ }
+
+ return next()
+ }
+]
+
+const usersUpdateValidator = [
+ param('id').custom(isIdValid),
+
+ body('password')
+ .optional()
+ .custom(isUserPasswordValid),
+ body('email')
+ .optional()
+ .isEmail(),
+ body('emailVerified')
+ .optional()
+ .isBoolean(),
+ body('videoQuota')
+ .optional()
+ .custom(isUserVideoQuotaValid),
+ body('videoQuotaDaily')
+ .optional()
+ .custom(isUserVideoQuotaDailyValid),
+ body('pluginAuth')
+ .optional()
+ .exists(),
+ body('role')
+ .optional()
+ .customSanitizer(toIntOrNull)
+ .custom(isUserRoleValid),
+ body('adminFlags')
+ .optional()
+ .custom(isUserAdminFlagsValid),
+
+ async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ if (areValidationErrors(req, res, { omitBodyLog: true })) return
+ if (!await checkUserIdExist(req.params.id, res)) return
+
+ const user = res.locals.user
+ if (user.username === 'root' && req.body.role !== undefined && user.role !== req.body.role) {
+ return res.fail({ message: 'Cannot change root role.' })
+ }
-function usersRemoveValidator (req: express.Request, res: express.Response, next: express.NextFunction) {
- req.checkParams('id', 'Should have a valid id').notEmpty().isInt()
+ return next()
+ }
+]
+
+const usersUpdateMeValidator = [
+ body('displayName')
+ .optional()
+ .custom(isUserDisplayNameValid),
+ body('description')
+ .optional()
+ .custom(isUserDescriptionValid),
+ body('currentPassword')
+ .optional()
+ .custom(isUserPasswordValid),
+ body('password')
+ .optional()
+ .custom(isUserPasswordValid),
+ body('emailPublic')
+ .optional()
+ .custom(isUserEmailPublicValid),
+ body('email')
+ .optional()
+ .isEmail(),
+ body('nsfwPolicy')
+ .optional()
+ .custom(isUserNSFWPolicyValid),
+ body('autoPlayVideo')
+ .optional()
+ .custom(isUserAutoPlayVideoValid),
+ body('p2pEnabled')
+ .optional()
+ .custom(isUserP2PEnabledValid).withMessage('Should have a valid p2p enabled boolean'),
+ body('videoLanguages')
+ .optional()
+ .custom(isUserVideoLanguages),
+ body('videosHistoryEnabled')
+ .optional()
+ .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled boolean'),
+ body('theme')
+ .optional()
+ .custom(v => isThemeNameValid(v) && isThemeRegistered(v)),
+
+ body('noInstanceConfigWarningModal')
+ .optional()
+ .custom(v => isUserNoModal(v)).withMessage('Should have a valid noInstanceConfigWarningModal boolean'),
+ body('noWelcomeModal')
+ .optional()
+ .custom(v => isUserNoModal(v)).withMessage('Should have a valid noWelcomeModal boolean'),
+ body('noAccountSetupWarningModal')
+ .optional()
+ .custom(v => isUserNoModal(v)).withMessage('Should have a valid noAccountSetupWarningModal boolean'),
+
+ body('autoPlayNextVideo')
+ .optional()
+ .custom(v => isUserAutoPlayNextVideoValid(v)).withMessage('Should have a valid autoPlayNextVideo boolean'),
+
+ async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ const user = res.locals.oauth.token.User
+
+ if (req.body.password || req.body.email) {
+ if (user.pluginAuth !== null) {
+ return res.fail({ message: 'You cannot update your email or password that is associated with an external auth system.' })
+ }
- logger.debug('Checking usersRemove parameters', { parameters: req.params })
+ if (!req.body.currentPassword) {
+ return res.fail({ message: 'currentPassword parameter is missing.' })
+ }
- checkErrors(req, res, () => {
- checkUserExists(req.params.id, res, (err, user) => {
- if (err) {
- logger.error('Error in usersRemoveValidator.', err)
- return res.sendStatus(500)
+ if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
+ return res.fail({
+ status: HttpStatusCode.UNAUTHORIZED_401,
+ message: 'currentPassword is invalid.'
+ })
}
+ }
- if (user.username === 'root') return res.status(400).send('Cannot remove the root user')
+ if (areValidationErrors(req, res, { omitBodyLog: true })) return
- next()
- })
- })
-}
+ return next()
+ }
+]
-function usersUpdateValidator (req: express.Request, res: express.Response, next: express.NextFunction) {
- req.checkParams('id', 'Should have a valid id').notEmpty().isInt()
- req.checkBody('email', 'Should have a valid email attribute').optional().isEmail()
- req.checkBody('videoQuota', 'Should have a valid user quota').optional().isUserVideoQuotaValid()
+const usersGetValidator = [
+ param('id')
+ .custom(isIdValid),
+ query('withStats')
+ .optional()
+ .isBoolean().withMessage('Should have a valid withStats boolean'),
- logger.debug('Checking usersUpdate parameters', { parameters: req.body })
+ async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ if (areValidationErrors(req, res)) return
+ if (!await checkUserIdExist(req.params.id, res, req.query.withStats)) return
- checkErrors(req, res, () => {
- checkUserExists(req.params.id, res, next)
- })
-}
+ return next()
+ }
+]
-function usersUpdateMeValidator (req: express.Request, res: express.Response, next: express.NextFunction) {
- // Add old password verification
- req.checkBody('password', 'Should have a valid password').optional().isUserPasswordValid()
- req.checkBody('email', 'Should have a valid email attribute').optional().isEmail()
- req.checkBody('displayNSFW', 'Should have a valid display Not Safe For Work attribute').optional().isUserDisplayNSFWValid()
+const usersVideoRatingValidator = [
+ isValidVideoIdParam('videoId'),
- logger.debug('Checking usersUpdateMe parameters', { parameters: req.body })
+ async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ if (areValidationErrors(req, res)) return
+ if (!await doesVideoExist(req.params.videoId, res, 'id')) return
- checkErrors(req, res, next)
-}
+ return next()
+ }
+]
-function usersGetValidator (req: express.Request, res: express.Response, next: express.NextFunction) {
- req.checkParams('id', 'Should have a valid id').notEmpty().isInt()
+const usersVideosValidator = [
+ query('isLive')
+ .optional()
+ .customSanitizer(toBooleanOrNull)
+ .custom(isBooleanValid).withMessage('Should have a valid isLive boolean'),
- checkErrors(req, res, () => {
- checkUserExists(req.params.id, res, next)
- })
-}
+ query('channelId')
+ .optional()
+ .customSanitizer(toIntOrNull)
+ .custom(isIdValid),
-function usersVideoRatingValidator (req: express.Request, res: express.Response, next: express.NextFunction) {
- req.checkParams('videoId', 'Should have a valid video id').notEmpty().isVideoIdOrUUIDValid()
+ async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ if (areValidationErrors(req, res)) return
- logger.debug('Checking usersVideoRating parameters', { parameters: req.params })
+ if (req.query.channelId && !await doesVideoChannelIdExist(req.query.channelId, res)) return
- checkErrors(req, res, () => {
- let videoPromise: Promise<VideoInstance>
+ return next()
+ }
+]
- if (validator.isUUID(req.params.videoId)) {
- videoPromise = db.Video.loadByUUID(req.params.videoId)
- } else {
- videoPromise = db.Video.load(req.params.videoId)
- }
+const usersAskResetPasswordValidator = [
+ body('email')
+ .isEmail(),
- videoPromise
- .then(video => {
- if (!video) return res.status(404).send('Video not found')
+ async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ if (areValidationErrors(req, res)) return
- next()
+ const exists = await checkUserEmailExist(req.body.email, res, false)
+ if (!exists) {
+ logger.debug('User with email %s does not exist (asking reset password).', req.body.email)
+ // Do not leak our emails
+ return res.status(HttpStatusCode.NO_CONTENT_204).end()
+ }
+
+ if (res.locals.user.pluginAuth) {
+ return res.fail({
+ status: HttpStatusCode.CONFLICT_409,
+ message: 'Cannot recover password of a user that uses a plugin authentication.'
})
- .catch(err => {
- logger.error('Error in user request validator.', err)
- return res.sendStatus(500)
+ }
+
+ return next()
+ }
+]
+
+const usersResetPasswordValidator = [
+ param('id')
+ .custom(isIdValid),
+ body('verificationString')
+ .not().isEmpty(),
+ body('password')
+ .custom(isUserPasswordValid),
+
+ async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ if (areValidationErrors(req, res)) return
+ if (!await checkUserIdExist(req.params.id, res)) return
+
+ const user = res.locals.user
+ const redisVerificationString = await Redis.Instance.getResetPasswordVerificationString(user.id)
+
+ if (redisVerificationString !== req.body.verificationString) {
+ return res.fail({
+ status: HttpStatusCode.FORBIDDEN_403,
+ message: 'Invalid verification string.'
})
- })
+ }
+
+ return next()
+ }
+]
+
+const usersCheckCurrentPasswordFactory = (targetUserIdGetter: (req: express.Request) => number | string) => {
+ return [
+ body('currentPassword').optional().custom(exists),
+
+ async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ if (areValidationErrors(req, res)) return
+
+ const user = res.locals.oauth.token.User
+ const isAdminOrModerator = user.role === UserRole.ADMINISTRATOR || user.role === UserRole.MODERATOR
+ const targetUserId = forceNumber(targetUserIdGetter(req))
+
+ // Admin/moderator action on another user, skip the password check
+ if (isAdminOrModerator && targetUserId !== user.id) {
+ return next()
+ }
+
+ if (!req.body.currentPassword) {
+ return res.fail({
+ status: HttpStatusCode.BAD_REQUEST_400,
+ message: 'currentPassword is missing'
+ })
+ }
+
+ if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
+ return res.fail({
+ status: HttpStatusCode.FORBIDDEN_403,
+ message: 'currentPassword is invalid.'
+ })
+ }
+
+ return next()
+ }
+ ]
}
-function ensureUserRegistrationAllowed (req: express.Request, res: express.Response, next: express.NextFunction) {
- isSignupAllowed().then(allowed => {
- if (allowed === false) {
- return res.status(403).send('User registration is not enabled or user limit is reached.')
+const userAutocompleteValidator = [
+ param('search')
+ .isString()
+ .not().isEmpty()
+]
+
+const ensureAuthUserOwnsAccountValidator = [
+ (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ const user = res.locals.oauth.token.User
+
+ if (res.locals.account.id !== user.Account.id) {
+ return res.fail({
+ status: HttpStatusCode.FORBIDDEN_403,
+ message: 'Only owner of this account can access this resource.'
+ })
}
return next()
- })
-}
+ }
+]
+
+const ensureCanManageChannelOrAccount = [
+ (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ const user = res.locals.oauth.token.user
+ const account = res.locals.videoChannel?.Account ?? res.locals.account
+ const isUserOwner = account.userId === user.id
+
+ if (!isUserOwner && user.hasRight(UserRight.MANAGE_ANY_VIDEO_CHANNEL) === false) {
+ const message = `User ${user.username} does not have right this channel or account.`
+
+ return res.fail({
+ status: HttpStatusCode.FORBIDDEN_403,
+ message
+ })
+ }
+
+ return next()
+ }
+]
+
+const ensureCanModerateUser = [
+ (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ const authUser = res.locals.oauth.token.User
+ const onUser = res.locals.user
+
+ if (authUser.role === UserRole.ADMINISTRATOR) return next()
+ if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next()
+
+ return res.fail({
+ status: HttpStatusCode.FORBIDDEN_403,
+ message: 'A moderator can only manage users.'
+ })
+ }
+]
// ---------------------------------------------------------------------------
export {
+ usersListValidator,
usersAddValidator,
- usersRegisterValidator,
+ deleteMeValidator,
+ usersBlockingValidator,
usersRemoveValidator,
usersUpdateValidator,
usersUpdateMeValidator,
usersVideoRatingValidator,
- ensureUserRegistrationAllowed,
- usersGetValidator
-}
-
-// ---------------------------------------------------------------------------
-
-function checkUserExists (id: number, res: express.Response, callback: (err: Error, user: UserInstance) => void) {
- db.User.loadById(id)
- .then(user => {
- if (!user) return res.status(404).send('User not found')
-
- res.locals.user = user
- callback(null, user)
- })
- .catch(err => {
- logger.error('Error in user request validator.', err)
- return res.sendStatus(500)
- })
-}
-
-function checkUserDoesNotAlreadyExist (username: string, email: string, res: express.Response, callback: () => void) {
- db.User.loadByUsernameOrEmail(username, email)
- .then(user => {
- if (user) return res.status(409).send('User already exists.')
-
- callback()
- })
- .catch(err => {
- logger.error('Error in usersAdd request validator.', err)
- return res.sendStatus(500)
- })
+ usersCheckCurrentPasswordFactory,
+ usersGetValidator,
+ usersVideosValidator,
+ usersAskResetPasswordValidator,
+ usersResetPasswordValidator,
+ userAutocompleteValidator,
+ ensureAuthUserOwnsAccountValidator,
+ ensureCanModerateUser,
+ ensureCanManageChannelOrAccount
}