1 import express from 'express'
2 import { body, param, query } from 'express-validator'
3 import { Hooks } from '@server/lib/plugins/hooks'
4 import { MUserDefault } from '@server/types/models'
5 import { HttpStatusCode, UserRegister, UserRight, UserRole } from '@shared/models'
6 import { isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
7 import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
10 isUserAutoPlayNextVideoValid,
11 isUserAutoPlayVideoValid,
12 isUserBlockedReasonValid,
13 isUserDescriptionValid,
14 isUserDisplayNameValid,
16 isUserNSFWPolicyValid,
17 isUserP2PEnabledValid,
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 blocked boolena'),
42 (req: express.Request, res: express.Response, next: express.NextFunction) => {
43 if (areValidationErrors(req, res)) return
49 const usersAddValidator = [
51 .custom(isUserUsernameValid)
52 .withMessage('Should have a valid username (lowercase alphanumeric characters)'),
54 .custom(isUserPasswordValidOrEmpty),
60 .custom(isVideoChannelUsernameValid),
63 .custom(isUserVideoQuotaValid),
64 body('videoQuotaDaily')
65 .custom(isUserVideoQuotaDailyValid),
68 .customSanitizer(toIntOrNull)
69 .custom(isUserRoleValid),
73 .custom(isUserAdminFlagsValid),
75 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
76 if (areValidationErrors(req, res, { omitBodyLog: true })) return
77 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
79 const authUser = res.locals.oauth.token.User
80 if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) {
82 status: HttpStatusCode.FORBIDDEN_403,
83 message: 'You can only create users (and not administrators or moderators)'
87 if (req.body.channelName) {
88 if (req.body.channelName === req.body.username) {
89 return res.fail({ message: 'Channel name cannot be the same as user username.' })
92 const existing = await ActorModel.loadLocalByName(req.body.channelName)
95 status: HttpStatusCode.CONFLICT_409,
96 message: `Channel with name ${req.body.channelName} already exists.`
105 const usersRegisterValidator = [
107 .custom(isUserUsernameValid),
109 .custom(isUserPasswordValid),
114 .custom(isUserDisplayNameValid),
118 .custom(isVideoChannelUsernameValid),
119 body('channel.displayName')
121 .custom(isVideoChannelDisplayNameValid),
123 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
124 if (areValidationErrors(req, res, { omitBodyLog: true })) return
125 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
127 const body: UserRegister = req.body
129 if (!body.channel.name || !body.channel.displayName) {
130 return res.fail({ message: 'Channel is optional but if you specify it, channel.name and channel.displayName are required.' })
133 if (body.channel.name === body.username) {
134 return res.fail({ message: 'Channel name cannot be the same as user username.' })
137 const existing = await ActorModel.loadLocalByName(body.channel.name)
140 status: HttpStatusCode.CONFLICT_409,
141 message: `Channel with name ${body.channel.name} already exists.`
150 const usersRemoveValidator = [
154 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
155 if (areValidationErrors(req, res)) return
156 if (!await checkUserIdExist(req.params.id, res)) return
158 const user = res.locals.user
159 if (user.username === 'root') {
160 return res.fail({ message: 'Cannot remove the root user' })
167 const usersBlockingValidator = [
172 .custom(isUserBlockedReasonValid),
174 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
175 if (areValidationErrors(req, res)) return
176 if (!await checkUserIdExist(req.params.id, res)) return
178 const user = res.locals.user
179 if (user.username === 'root') {
180 return res.fail({ message: 'Cannot block the root user' })
187 const deleteMeValidator = [
188 (req: express.Request, res: express.Response, next: express.NextFunction) => {
189 const user = res.locals.oauth.token.User
190 if (user.username === 'root') {
191 return res.fail({ message: 'You cannot delete your root account.' })
198 const usersUpdateValidator = [
199 param('id').custom(isIdValid),
203 .custom(isUserPasswordValid),
207 body('emailVerified')
212 .custom(isUserVideoQuotaValid),
213 body('videoQuotaDaily')
215 .custom(isUserVideoQuotaDailyValid),
221 .customSanitizer(toIntOrNull)
222 .custom(isUserRoleValid),
225 .custom(isUserAdminFlagsValid),
227 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
228 if (areValidationErrors(req, res, { omitBodyLog: true })) return
229 if (!await checkUserIdExist(req.params.id, res)) return
231 const user = res.locals.user
232 if (user.username === 'root' && req.body.role !== undefined && user.role !== req.body.role) {
233 return res.fail({ message: 'Cannot change root role.' })
240 const usersUpdateMeValidator = [
243 .custom(isUserDisplayNameValid),
246 .custom(isUserDescriptionValid),
247 body('currentPassword')
249 .custom(isUserPasswordValid),
252 .custom(isUserPasswordValid),
258 .custom(isUserNSFWPolicyValid),
259 body('autoPlayVideo')
261 .custom(isUserAutoPlayVideoValid),
264 .custom(isUserP2PEnabledValid).withMessage('Should have a valid p2p enabled boolean'),
265 body('videoLanguages')
267 .custom(isUserVideoLanguages),
268 body('videosHistoryEnabled')
270 .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled boolean'),
273 .custom(v => isThemeNameValid(v) && isThemeRegistered(v)),
275 body('noInstanceConfigWarningModal')
277 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noInstanceConfigWarningModal boolean'),
278 body('noWelcomeModal')
280 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noWelcomeModal boolean'),
281 body('noAccountSetupWarningModal')
283 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noAccountSetupWarningModal boolean'),
285 body('autoPlayNextVideo')
287 .custom(v => isUserAutoPlayNextVideoValid(v)).withMessage('Should have a valid autoPlayNextVideo boolean'),
289 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
290 const user = res.locals.oauth.token.User
292 if (req.body.password || req.body.email) {
293 if (user.pluginAuth !== null) {
294 return res.fail({ message: 'You cannot update your email or password that is associated with an external auth system.' })
297 if (!req.body.currentPassword) {
298 return res.fail({ message: 'currentPassword parameter is missing.' })
301 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
303 status: HttpStatusCode.UNAUTHORIZED_401,
304 message: 'currentPassword is invalid.'
309 if (areValidationErrors(req, res, { omitBodyLog: true })) return
315 const usersGetValidator = [
320 .isBoolean().withMessage('Should have a valid withStats boolean'),
322 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
323 if (areValidationErrors(req, res)) return
324 if (!await checkUserIdExist(req.params.id, res, req.query.withStats)) return
330 const usersVideoRatingValidator = [
331 isValidVideoIdParam('videoId'),
333 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
334 if (areValidationErrors(req, res)) return
335 if (!await doesVideoExist(req.params.videoId, res, 'id')) return
341 const usersVideosValidator = [
344 .customSanitizer(toBooleanOrNull)
345 .custom(isBooleanValid).withMessage('Should have a valid isLive boolean'),
349 .customSanitizer(toIntOrNull)
352 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
353 if (areValidationErrors(req, res)) return
355 if (req.query.channelId && !await doesVideoChannelIdExist(req.query.channelId, res)) return
361 const ensureUserRegistrationAllowed = [
362 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
363 const allowedParams = {
368 const allowedResult = await Hooks.wrapPromiseFun(
371 'filter:api.user.signup.allowed.result'
374 if (allowedResult.allowed === false) {
376 status: HttpStatusCode.FORBIDDEN_403,
377 message: allowedResult.errorMessage || 'User registration is not enabled or user limit is reached.'
385 const ensureUserRegistrationAllowedForIP = [
386 (req: express.Request, res: express.Response, next: express.NextFunction) => {
387 const allowed = isSignupAllowedForCurrentIP(req.ip)
389 if (allowed === false) {
391 status: HttpStatusCode.FORBIDDEN_403,
392 message: 'You are not on a network authorized for registration.'
400 const usersAskResetPasswordValidator = [
404 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
405 if (areValidationErrors(req, res)) return
407 const exists = await checkUserEmailExist(req.body.email, res, false)
409 logger.debug('User with email %s does not exist (asking reset password).', req.body.email)
410 // Do not leak our emails
411 return res.status(HttpStatusCode.NO_CONTENT_204).end()
414 if (res.locals.user.pluginAuth) {
416 status: HttpStatusCode.CONFLICT_409,
417 message: 'Cannot recover password of a user that uses a plugin authentication.'
425 const usersResetPasswordValidator = [
428 body('verificationString')
431 .custom(isUserPasswordValid),
433 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
434 if (areValidationErrors(req, res)) return
435 if (!await checkUserIdExist(req.params.id, res)) return
437 const user = res.locals.user
438 const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id)
440 if (redisVerificationString !== req.body.verificationString) {
442 status: HttpStatusCode.FORBIDDEN_403,
443 message: 'Invalid verification string.'
451 const usersAskSendVerifyEmailValidator = [
452 body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
454 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
455 if (areValidationErrors(req, res)) return
457 const exists = await checkUserEmailExist(req.body.email, res, false)
459 logger.debug('User with email %s does not exist (asking verify email).', req.body.email)
460 // Do not leak our emails
461 return res.status(HttpStatusCode.NO_CONTENT_204).end()
464 if (res.locals.user.pluginAuth) {
466 status: HttpStatusCode.CONFLICT_409,
467 message: 'Cannot ask verification email of a user that uses a plugin authentication.'
475 const usersVerifyEmailValidator = [
477 .isInt().not().isEmpty().withMessage('Should have a valid id'),
479 body('verificationString')
480 .not().isEmpty().withMessage('Should have a valid verification string'),
481 body('isPendingEmail')
483 .customSanitizer(toBooleanOrNull),
485 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
486 if (areValidationErrors(req, res)) return
487 if (!await checkUserIdExist(req.params.id, res)) return
489 const user = res.locals.user
490 const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id)
492 if (redisVerificationString !== req.body.verificationString) {
494 status: HttpStatusCode.FORBIDDEN_403,
495 message: 'Invalid verification string.'
503 const userAutocompleteValidator = [
509 const ensureAuthUserOwnsAccountValidator = [
510 (req: express.Request, res: express.Response, next: express.NextFunction) => {
511 const user = res.locals.oauth.token.User
513 if (res.locals.account.id !== user.Account.id) {
515 status: HttpStatusCode.FORBIDDEN_403,
516 message: 'Only owner of this account can access this resource.'
524 const ensureCanManageChannelOrAccount = [
525 (req: express.Request, res: express.Response, next: express.NextFunction) => {
526 const user = res.locals.oauth.token.user
527 const account = res.locals.videoChannel?.Account ?? res.locals.account
528 const isUserOwner = account.userId === user.id
530 if (!isUserOwner && user.hasRight(UserRight.MANAGE_ANY_VIDEO_CHANNEL) === false) {
531 const message = `User ${user.username} does not have right this channel or account.`
534 status: HttpStatusCode.FORBIDDEN_403,
543 const ensureCanModerateUser = [
544 (req: express.Request, res: express.Response, next: express.NextFunction) => {
545 const authUser = res.locals.oauth.token.User
546 const onUser = res.locals.user
548 if (authUser.role === UserRole.ADMINISTRATOR) return next()
549 if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next()
552 status: HttpStatusCode.FORBIDDEN_403,
553 message: 'A moderator can only manage users.'
558 // ---------------------------------------------------------------------------
564 usersRegisterValidator,
565 usersBlockingValidator,
566 usersRemoveValidator,
567 usersUpdateValidator,
568 usersUpdateMeValidator,
569 usersVideoRatingValidator,
570 ensureUserRegistrationAllowed,
571 ensureUserRegistrationAllowedForIP,
573 usersVideosValidator,
574 usersAskResetPasswordValidator,
575 usersResetPasswordValidator,
576 usersAskSendVerifyEmailValidator,
577 usersVerifyEmailValidator,
578 userAutocompleteValidator,
579 ensureAuthUserOwnsAccountValidator,
580 ensureCanModerateUser,
581 ensureCanManageChannelOrAccount
584 // ---------------------------------------------------------------------------
586 function checkUserIdExist (idArg: number | string, res: express.Response, withStats = false) {
587 const id = parseInt(idArg + '', 10)
588 return checkUserExist(() => UserModel.loadByIdWithChannels(id, withStats), res)
591 function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) {
592 return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse)
595 async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email: string, res: express.Response) {
596 const user = await UserModel.loadByUsernameOrEmail(username, email)
600 status: HttpStatusCode.CONFLICT_409,
601 message: 'User with this username or email already exists.'
606 const actor = await ActorModel.loadLocalByName(username)
609 status: HttpStatusCode.CONFLICT_409,
610 message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.'
618 async function checkUserExist (finder: () => Promise<MUserDefault>, res: express.Response, abortResponse = true) {
619 const user = await finder()
622 if (abortResponse === true) {
624 status: HttpStatusCode.NOT_FOUND_404,
625 message: 'User not found'
632 res.locals.user = user