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()
418 const usersResetPasswordValidator = [
421 body('verificationString')
424 .custom(isUserPasswordValid),
426 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
427 if (areValidationErrors(req, res)) return
428 if (!await checkUserIdExist(req.params.id, res)) return
430 const user = res.locals.user
431 const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id)
433 if (redisVerificationString !== req.body.verificationString) {
435 status: HttpStatusCode.FORBIDDEN_403,
436 message: 'Invalid verification string.'
444 const usersAskSendVerifyEmailValidator = [
445 body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
447 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
448 if (areValidationErrors(req, res)) return
450 const exists = await checkUserEmailExist(req.body.email, res, false)
452 logger.debug('User with email %s does not exist (asking verify email).', req.body.email)
453 // Do not leak our emails
454 return res.status(HttpStatusCode.NO_CONTENT_204).end()
461 const usersVerifyEmailValidator = [
463 .isInt().not().isEmpty().withMessage('Should have a valid id'),
465 body('verificationString')
466 .not().isEmpty().withMessage('Should have a valid verification string'),
467 body('isPendingEmail')
469 .customSanitizer(toBooleanOrNull),
471 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
472 if (areValidationErrors(req, res)) return
473 if (!await checkUserIdExist(req.params.id, res)) return
475 const user = res.locals.user
476 const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id)
478 if (redisVerificationString !== req.body.verificationString) {
480 status: HttpStatusCode.FORBIDDEN_403,
481 message: 'Invalid verification string.'
489 const userAutocompleteValidator = [
495 const ensureAuthUserOwnsAccountValidator = [
496 (req: express.Request, res: express.Response, next: express.NextFunction) => {
497 const user = res.locals.oauth.token.User
499 if (res.locals.account.id !== user.Account.id) {
501 status: HttpStatusCode.FORBIDDEN_403,
502 message: 'Only owner of this account can access this resource.'
510 const ensureCanManageChannel = [
511 (req: express.Request, res: express.Response, next: express.NextFunction) => {
512 const user = res.locals.oauth.token.user
513 const isUserOwner = res.locals.videoChannel.Account.userId === user.id
515 if (!isUserOwner && user.hasRight(UserRight.MANAGE_ANY_VIDEO_CHANNEL) === false) {
516 const message = `User ${user.username} does not have right to manage channel ${req.params.nameWithHost}.`
519 status: HttpStatusCode.FORBIDDEN_403,
528 const ensureCanManageUser = [
529 (req: express.Request, res: express.Response, next: express.NextFunction) => {
530 const authUser = res.locals.oauth.token.User
531 const onUser = res.locals.user
533 if (authUser.role === UserRole.ADMINISTRATOR) return next()
534 if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next()
537 status: HttpStatusCode.FORBIDDEN_403,
538 message: 'A moderator can only manager users.'
543 // ---------------------------------------------------------------------------
549 usersRegisterValidator,
550 usersBlockingValidator,
551 usersRemoveValidator,
552 usersUpdateValidator,
553 usersUpdateMeValidator,
554 usersVideoRatingValidator,
555 ensureUserRegistrationAllowed,
556 ensureUserRegistrationAllowedForIP,
558 usersVideosValidator,
559 usersAskResetPasswordValidator,
560 usersResetPasswordValidator,
561 usersAskSendVerifyEmailValidator,
562 usersVerifyEmailValidator,
563 userAutocompleteValidator,
564 ensureAuthUserOwnsAccountValidator,
566 ensureCanManageChannel
569 // ---------------------------------------------------------------------------
571 function checkUserIdExist (idArg: number | string, res: express.Response, withStats = false) {
572 const id = parseInt(idArg + '', 10)
573 return checkUserExist(() => UserModel.loadByIdWithChannels(id, withStats), res)
576 function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) {
577 return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse)
580 async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email: string, res: express.Response) {
581 const user = await UserModel.loadByUsernameOrEmail(username, email)
585 status: HttpStatusCode.CONFLICT_409,
586 message: 'User with this username or email already exists.'
591 const actor = await ActorModel.loadLocalByName(username)
594 status: HttpStatusCode.CONFLICT_409,
595 message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.'
603 async function checkUserExist (finder: () => Promise<MUserDefault>, res: express.Response, abortResponse = true) {
604 const user = await finder()
607 if (abortResponse === true) {
609 status: HttpStatusCode.NOT_FOUND_404,
610 message: 'User not found'
617 res.locals.user = user