1 import express from 'express'
2 import { body, param, query } from 'express-validator'
3 import { forceNumber } from '@shared/core-utils'
4 import { HttpStatusCode, UserRight, UserRole } from '@shared/models'
5 import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
6 import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
9 isUserAutoPlayNextVideoValid,
10 isUserAutoPlayVideoValid,
11 isUserBlockedReasonValid,
12 isUserDescriptionValid,
13 isUserDisplayNameValid,
15 isUserNSFWPolicyValid,
16 isUserP2PEnabledValid,
18 isUserPasswordValidOrEmpty,
22 isUserVideoQuotaDailyValid,
23 isUserVideoQuotaValid,
24 isUserVideosHistoryEnabledValid
25 } from '../../helpers/custom-validators/users'
26 import { isVideoChannelUsernameValid } from '../../helpers/custom-validators/video-channels'
27 import { logger } from '../../helpers/logger'
28 import { isThemeRegistered } from '../../lib/plugins/theme-utils'
29 import { Redis } from '../../lib/redis'
30 import { ActorModel } from '../../models/actor/actor'
35 checkUserNameOrEmailDoNotAlreadyExist,
36 doesVideoChannelIdExist,
41 const usersListValidator = [
44 .customSanitizer(toBooleanOrNull)
45 .isBoolean().withMessage('Should be a valid blocked boolean'),
47 (req: express.Request, res: express.Response, next: express.NextFunction) => {
48 if (areValidationErrors(req, res)) return
54 const usersAddValidator = [
56 .custom(isUserUsernameValid)
57 .withMessage('Should have a valid username (lowercase alphanumeric characters)'),
59 .custom(isUserPasswordValidOrEmpty),
65 .custom(isVideoChannelUsernameValid),
68 .custom(isUserVideoQuotaValid),
69 body('videoQuotaDaily')
70 .custom(isUserVideoQuotaDailyValid),
73 .customSanitizer(toIntOrNull)
74 .custom(isUserRoleValid),
78 .custom(isUserAdminFlagsValid),
80 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
81 if (areValidationErrors(req, res, { omitBodyLog: true })) return
82 if (!await checkUserNameOrEmailDoNotAlreadyExist(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 usersRemoveValidator = [
114 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
115 if (areValidationErrors(req, res)) return
116 if (!await checkUserIdExist(req.params.id, res)) return
118 const user = res.locals.user
119 if (user.username === 'root') {
120 return res.fail({ message: 'Cannot remove the root user' })
127 const usersBlockingValidator = [
132 .custom(isUserBlockedReasonValid),
134 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
135 if (areValidationErrors(req, res)) return
136 if (!await checkUserIdExist(req.params.id, res)) return
138 const user = res.locals.user
139 if (user.username === 'root') {
140 return res.fail({ message: 'Cannot block the root user' })
147 const deleteMeValidator = [
148 (req: express.Request, res: express.Response, next: express.NextFunction) => {
149 const user = res.locals.oauth.token.User
150 if (user.username === 'root') {
151 return res.fail({ message: 'You cannot delete your root account.' })
158 const usersUpdateValidator = [
159 param('id').custom(isIdValid),
163 .custom(isUserPasswordValid),
167 body('emailVerified')
172 .custom(isUserVideoQuotaValid),
173 body('videoQuotaDaily')
175 .custom(isUserVideoQuotaDailyValid),
181 .customSanitizer(toIntOrNull)
182 .custom(isUserRoleValid),
185 .custom(isUserAdminFlagsValid),
187 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
188 if (areValidationErrors(req, res, { omitBodyLog: true })) return
189 if (!await checkUserIdExist(req.params.id, res)) return
191 const user = res.locals.user
192 if (user.username === 'root' && req.body.role !== undefined && user.role !== req.body.role) {
193 return res.fail({ message: 'Cannot change root role.' })
200 const usersUpdateMeValidator = [
203 .custom(isUserDisplayNameValid),
206 .custom(isUserDescriptionValid),
207 body('currentPassword')
209 .custom(isUserPasswordValid),
212 .custom(isUserPasswordValid),
218 .custom(isUserNSFWPolicyValid),
219 body('autoPlayVideo')
221 .custom(isUserAutoPlayVideoValid),
224 .custom(isUserP2PEnabledValid).withMessage('Should have a valid p2p enabled boolean'),
225 body('videoLanguages')
227 .custom(isUserVideoLanguages),
228 body('videosHistoryEnabled')
230 .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled boolean'),
233 .custom(v => isThemeNameValid(v) && isThemeRegistered(v)),
235 body('noInstanceConfigWarningModal')
237 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noInstanceConfigWarningModal boolean'),
238 body('noWelcomeModal')
240 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noWelcomeModal boolean'),
241 body('noAccountSetupWarningModal')
243 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noAccountSetupWarningModal boolean'),
245 body('autoPlayNextVideo')
247 .custom(v => isUserAutoPlayNextVideoValid(v)).withMessage('Should have a valid autoPlayNextVideo boolean'),
249 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
250 const user = res.locals.oauth.token.User
252 if (req.body.password || req.body.email) {
253 if (user.pluginAuth !== null) {
254 return res.fail({ message: 'You cannot update your email or password that is associated with an external auth system.' })
257 if (!req.body.currentPassword) {
258 return res.fail({ message: 'currentPassword parameter is missing.' })
261 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
263 status: HttpStatusCode.UNAUTHORIZED_401,
264 message: 'currentPassword is invalid.'
269 if (areValidationErrors(req, res, { omitBodyLog: true })) return
275 const usersGetValidator = [
280 .isBoolean().withMessage('Should have a valid withStats boolean'),
282 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
283 if (areValidationErrors(req, res)) return
284 if (!await checkUserIdExist(req.params.id, res, req.query.withStats)) return
290 const usersVideoRatingValidator = [
291 isValidVideoIdParam('videoId'),
293 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
294 if (areValidationErrors(req, res)) return
295 if (!await doesVideoExist(req.params.videoId, res, 'id')) return
301 const usersVideosValidator = [
304 .customSanitizer(toBooleanOrNull)
305 .custom(isBooleanValid).withMessage('Should have a valid isLive boolean'),
309 .customSanitizer(toIntOrNull)
312 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
313 if (areValidationErrors(req, res)) return
315 if (req.query.channelId && !await doesVideoChannelIdExist(req.query.channelId, res)) return
321 const usersAskResetPasswordValidator = [
325 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
326 if (areValidationErrors(req, res)) return
328 const exists = await checkUserEmailExist(req.body.email, res, false)
330 logger.debug('User with email %s does not exist (asking reset password).', req.body.email)
331 // Do not leak our emails
332 return res.status(HttpStatusCode.NO_CONTENT_204).end()
335 if (res.locals.user.pluginAuth) {
337 status: HttpStatusCode.CONFLICT_409,
338 message: 'Cannot recover password of a user that uses a plugin authentication.'
346 const usersResetPasswordValidator = [
349 body('verificationString')
352 .custom(isUserPasswordValid),
354 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
355 if (areValidationErrors(req, res)) return
356 if (!await checkUserIdExist(req.params.id, res)) return
358 const user = res.locals.user
359 const redisVerificationString = await Redis.Instance.getResetPasswordVerificationString(user.id)
361 if (redisVerificationString !== req.body.verificationString) {
363 status: HttpStatusCode.FORBIDDEN_403,
364 message: 'Invalid verification string.'
372 const usersCheckCurrentPasswordFactory = (targetUserIdGetter: (req: express.Request) => number | string) => {
374 body('currentPassword').optional().custom(exists),
376 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
377 if (areValidationErrors(req, res)) return
379 const user = res.locals.oauth.token.User
380 const isAdminOrModerator = user.role === UserRole.ADMINISTRATOR || user.role === UserRole.MODERATOR
381 const targetUserId = forceNumber(targetUserIdGetter(req))
383 // Admin/moderator action on another user, skip the password check
384 if (isAdminOrModerator && targetUserId !== user.id) {
388 if (!req.body.currentPassword) {
390 status: HttpStatusCode.BAD_REQUEST_400,
391 message: 'currentPassword is missing'
395 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
397 status: HttpStatusCode.FORBIDDEN_403,
398 message: 'currentPassword is invalid.'
407 const userAutocompleteValidator = [
413 const ensureAuthUserOwnsAccountValidator = [
414 (req: express.Request, res: express.Response, next: express.NextFunction) => {
415 const user = res.locals.oauth.token.User
417 if (res.locals.account.id !== user.Account.id) {
419 status: HttpStatusCode.FORBIDDEN_403,
420 message: 'Only owner of this account can access this resource.'
428 const ensureCanManageChannelOrAccount = [
429 (req: express.Request, res: express.Response, next: express.NextFunction) => {
430 const user = res.locals.oauth.token.user
431 const account = res.locals.videoChannel?.Account ?? res.locals.account
432 const isUserOwner = account.userId === user.id
434 if (!isUserOwner && user.hasRight(UserRight.MANAGE_ANY_VIDEO_CHANNEL) === false) {
435 const message = `User ${user.username} does not have right this channel or account.`
438 status: HttpStatusCode.FORBIDDEN_403,
447 const ensureCanModerateUser = [
448 (req: express.Request, res: express.Response, next: express.NextFunction) => {
449 const authUser = res.locals.oauth.token.User
450 const onUser = res.locals.user
452 if (authUser.role === UserRole.ADMINISTRATOR) return next()
453 if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next()
456 status: HttpStatusCode.FORBIDDEN_403,
457 message: 'A moderator can only manage users.'
462 // ---------------------------------------------------------------------------
468 usersBlockingValidator,
469 usersRemoveValidator,
470 usersUpdateValidator,
471 usersUpdateMeValidator,
472 usersVideoRatingValidator,
473 usersCheckCurrentPasswordFactory,
475 usersVideosValidator,
476 usersAskResetPasswordValidator,
477 usersResetPasswordValidator,
478 userAutocompleteValidator,
479 ensureAuthUserOwnsAccountValidator,
480 ensureCanModerateUser,
481 ensureCanManageChannelOrAccount