]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - server/middlewares/validators/users.ts
Add Podcast RSS feeds (#5487)
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / users.ts
index aec6324bf0be21acf1d83f97f56dd46282ebf527..3d311b15be055f541ae6f75005e0b70a052956f0 100644 (file)
-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
 }