]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/middlewares/validators/users.ts
Remove uneccessary details to link titles
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / users.ts
CommitLineData
ecb4e35f 1import * as Bluebird from 'bluebird'
69818c93 2import * as express from 'express'
76314386 3import { body, param, query } from 'express-validator'
ecb4e35f 4import { omit } from 'lodash'
d11c28a3 5import { isIdOrUUIDValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
b60e5f38 6import {
c1e5bd23
C
7 isNoInstanceConfigWarningModal,
8 isNoWelcomeModal,
1eddc9a7 9 isUserAdminFlagsValid,
c1e5bd23 10 isUserAutoPlayNextVideoValid,
1a12adcd
C
11 isUserAutoPlayVideoValid,
12 isUserBlockedReasonValid,
4bbfc6c6
C
13 isUserDescriptionValid,
14 isUserDisplayNameValid,
0883b324 15 isUserNSFWPolicyValid,
ecb4e35f 16 isUserPasswordValid,
45f1bd72 17 isUserPasswordValidOrEmpty,
ecb4e35f 18 isUserRoleValid,
3e753302
C
19 isUserUsernameValid,
20 isUserVideoLanguages,
1a12adcd 21 isUserVideoQuotaDailyValid,
dae86118
C
22 isUserVideoQuotaValid,
23 isUserVideosHistoryEnabledValid
3fd3ab2d 24} from '../../helpers/custom-validators/users'
da854ddd 25import { logger } from '../../helpers/logger'
06215f15 26import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup'
ecb4e35f 27import { Redis } from '../../lib/redis'
3fd3ab2d 28import { UserModel } from '../../models/account/user'
a2431b7d 29import { areValidationErrors } from './utils'
2ef6a063 30import { ActorModel } from '../../models/activitypub/actor'
e590b4a5
C
31import { isActorPreferredUsernameValid } from '../../helpers/custom-validators/activitypub/actor'
32import { isVideoChannelNameValid } from '../../helpers/custom-validators/video-channels'
e590b4a5 33import { UserRegister } from '../../../shared/models/users/user-register.model'
503c6f44
C
34import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
35import { isThemeRegistered } from '../../lib/plugins/theme-utils'
3e753302 36import { doesVideoExist } from '../../helpers/middlewares'
a95a4cc8 37import { UserRole } from '../../../shared/models/users'
453e83ea 38import { MUserDefault } from '@server/typings/models'
4ce7eb71 39import { Hooks } from '@server/lib/plugins/hooks'
9bd26629 40
b60e5f38 41const usersAddValidator = [
563d032e 42 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'),
45f1bd72 43 body('password').custom(isUserPasswordValidOrEmpty).withMessage('Should have a valid password'),
b60e5f38
C
44 body('email').isEmail().withMessage('Should have a valid email'),
45 body('videoQuota').custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'),
bee0abff 46 body('videoQuotaDaily').custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'),
dea16773
C
47 body('role')
48 .customSanitizer(toIntOrNull)
49 .custom(isUserRoleValid).withMessage('Should have a valid role'),
1eddc9a7 50 body('adminFlags').optional().custom(isUserAdminFlagsValid).withMessage('Should have a valid admin flags'),
9bd26629 51
a2431b7d 52 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
ce97fe36 53 logger.debug('Checking usersAdd parameters', { parameters: omit(req.body, 'password') })
9bd26629 54
a2431b7d
C
55 if (areValidationErrors(req, res)) return
56 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
57
a95a4cc8
C
58 const authUser = res.locals.oauth.token.User
59 if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) {
60 return res.status(403)
dea16773 61 .json({ error: 'You can only create users (and not administrators or moderators)' })
a95a4cc8
C
62 }
63
a2431b7d 64 return next()
b60e5f38
C
65 }
66]
6fcd19ba 67
b60e5f38
C
68const usersRegisterValidator = [
69 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username'),
70 body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'),
71 body('email').isEmail().withMessage('Should have a valid email'),
1f20622f
C
72 body('displayName')
73 .optional()
74 .custom(isUserDisplayNameValid).withMessage('Should have a valid display name'),
75
76 body('channel.name')
77 .optional()
78 .custom(isActorPreferredUsernameValid).withMessage('Should have a valid channel name'),
79 body('channel.displayName')
80 .optional()
81 .custom(isVideoChannelNameValid).withMessage('Should have a valid display name'),
77a5501f 82
a2431b7d 83 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
ce97fe36 84 logger.debug('Checking usersRegister parameters', { parameters: omit(req.body, 'password') })
77a5501f 85
a2431b7d
C
86 if (areValidationErrors(req, res)) return
87 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
88
e590b4a5
C
89 const body: UserRegister = req.body
90 if (body.channel) {
91 if (!body.channel.name || !body.channel.displayName) {
92 return res.status(400)
a95a4cc8 93 .json({ error: 'Channel is optional but if you specify it, channel.name and channel.displayName are required.' })
e590b4a5
C
94 }
95
1d5342ab
C
96 if (body.channel.name === body.username) {
97 return res.status(400)
32d7f2b7 98 .json({ error: 'Channel name cannot be the same as user username.' })
1d5342ab
C
99 }
100
e590b4a5
C
101 const existing = await ActorModel.loadLocalByName(body.channel.name)
102 if (existing) {
103 return res.status(409)
a95a4cc8 104 .json({ error: `Channel with name ${body.channel.name} already exists.` })
e590b4a5
C
105 }
106 }
107
a2431b7d 108 return next()
b60e5f38
C
109 }
110]
9bd26629 111
b60e5f38
C
112const usersRemoveValidator = [
113 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
9bd26629 114
a2431b7d 115 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 116 logger.debug('Checking usersRemove parameters', { parameters: req.params })
9bd26629 117
a2431b7d
C
118 if (areValidationErrors(req, res)) return
119 if (!await checkUserIdExist(req.params.id, res)) return
120
121 const user = res.locals.user
122 if (user.username === 'root') {
123 return res.status(400)
a95a4cc8 124 .json({ error: 'Cannot remove the root user' })
a2431b7d
C
125 }
126
127 return next()
b60e5f38
C
128 }
129]
8094a898 130
e6921918
C
131const usersBlockingValidator = [
132 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
eacb25c4 133 body('reason').optional().custom(isUserBlockedReasonValid).withMessage('Should have a valid blocking reason'),
e6921918
C
134
135 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
eacb25c4 136 logger.debug('Checking usersBlocking parameters', { parameters: req.params })
e6921918
C
137
138 if (areValidationErrors(req, res)) return
139 if (!await checkUserIdExist(req.params.id, res)) return
140
141 const user = res.locals.user
142 if (user.username === 'root') {
143 return res.status(400)
a95a4cc8 144 .json({ error: 'Cannot block the root user' })
e6921918
C
145 }
146
147 return next()
148 }
149]
150
92b9d60c 151const deleteMeValidator = [
a1587156 152 (req: express.Request, res: express.Response, next: express.NextFunction) => {
dae86118 153 const user = res.locals.oauth.token.User
92b9d60c
C
154 if (user.username === 'root') {
155 return res.status(400)
a95a4cc8 156 .json({ error: 'You cannot delete your root account.' })
92b9d60c
C
157 .end()
158 }
159
160 return next()
161 }
162]
163
b60e5f38
C
164const usersUpdateValidator = [
165 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
b426edd4 166 body('password').optional().custom(isUserPasswordValid).withMessage('Should have a valid password'),
b60e5f38 167 body('email').optional().isEmail().withMessage('Should have a valid email attribute'),
fc2ec87a 168 body('emailVerified').optional().isBoolean().withMessage('Should have a valid email verified attribute'),
b60e5f38 169 body('videoQuota').optional().custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'),
bee0abff 170 body('videoQuotaDaily').optional().custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'),
dea16773
C
171 body('role')
172 .optional()
173 .customSanitizer(toIntOrNull)
174 .custom(isUserRoleValid).withMessage('Should have a valid role'),
1eddc9a7 175 body('adminFlags').optional().custom(isUserAdminFlagsValid).withMessage('Should have a valid admin flags'),
8094a898 176
a2431b7d 177 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 178 logger.debug('Checking usersUpdate parameters', { parameters: req.body })
9bd26629 179
a2431b7d
C
180 if (areValidationErrors(req, res)) return
181 if (!await checkUserIdExist(req.params.id, res)) return
182
f8b8c36b
C
183 const user = res.locals.user
184 if (user.username === 'root' && req.body.role !== undefined && user.role !== req.body.role) {
185 return res.status(400)
a95a4cc8 186 .json({ error: 'Cannot change root role.' })
f8b8c36b
C
187 }
188
a2431b7d 189 return next()
b60e5f38
C
190 }
191]
9bd26629 192
b60e5f38 193const usersUpdateMeValidator = [
d1ab89de
C
194 body('displayName')
195 .optional()
196 .custom(isUserDisplayNameValid).withMessage('Should have a valid display name'),
197 body('description')
198 .optional()
199 .custom(isUserDescriptionValid).withMessage('Should have a valid description'),
200 body('currentPassword')
201 .optional()
202 .custom(isUserPasswordValid).withMessage('Should have a valid current password'),
203 body('password')
204 .optional()
205 .custom(isUserPasswordValid).withMessage('Should have a valid password'),
206 body('email')
207 .optional()
208 .isEmail().withMessage('Should have a valid email attribute'),
209 body('nsfwPolicy')
210 .optional()
211 .custom(isUserNSFWPolicyValid).withMessage('Should have a valid display Not Safe For Work policy'),
212 body('autoPlayVideo')
213 .optional()
214 .custom(isUserAutoPlayVideoValid).withMessage('Should have a valid automatically plays video attribute'),
3caf77d3
C
215 body('videoLanguages')
216 .optional()
217 .custom(isUserVideoLanguages).withMessage('Should have a valid video languages attribute'),
1a12adcd
C
218 body('videosHistoryEnabled')
219 .optional()
220 .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled attribute'),
7cd4d2ba
C
221 body('theme')
222 .optional()
503c6f44 223 .custom(v => isThemeNameValid(v) && isThemeRegistered(v)).withMessage('Should have a valid theme'),
43d0ea7f
C
224 body('noInstanceConfigWarningModal')
225 .optional()
226 .custom(v => isNoInstanceConfigWarningModal(v)).withMessage('Should have a valid noInstanceConfigWarningModal boolean'),
227 body('noWelcomeModal')
228 .optional()
229 .custom(v => isNoWelcomeModal(v)).withMessage('Should have a valid noWelcomeModal boolean'),
c1e5bd23
C
230 body('autoPlayNextVideo')
231 .optional()
232 .custom(v => isUserAutoPlayNextVideoValid(v)).withMessage('Should have a valid autoPlayNextVideo boolean'),
9bd26629 233
a890d1e0 234 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
ce97fe36 235 logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') })
8094a898 236
9a7fd960
C
237 const user = res.locals.oauth.token.User
238
0ba5f5ba 239 if (req.body.password || req.body.email) {
9a7fd960
C
240 if (user.pluginAuth !== null) {
241 return res.status(400)
242 .json({ error: 'You cannot update your email or password that is associated with an external auth system.' })
243 }
244
a890d1e0
C
245 if (!req.body.currentPassword) {
246 return res.status(400)
a95a4cc8 247 .json({ error: 'currentPassword parameter is missing.' })
a890d1e0
C
248 }
249
a890d1e0
C
250 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
251 return res.status(401)
a95a4cc8 252 .json({ error: 'currentPassword is invalid.' })
a890d1e0
C
253 }
254 }
255
a2431b7d
C
256 if (areValidationErrors(req, res)) return
257
258 return next()
b60e5f38
C
259 }
260]
8094a898 261
b60e5f38
C
262const usersGetValidator = [
263 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
76314386 264 query('withStats').optional().isBoolean().withMessage('Should have a valid stats flag'),
d38b8281 265
a2431b7d 266 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
ce97fe36 267 logger.debug('Checking usersGet parameters', { parameters: req.params })
a2431b7d
C
268
269 if (areValidationErrors(req, res)) return
76314386 270 if (!await checkUserIdExist(req.params.id, res, req.query.withStats)) return
a2431b7d
C
271
272 return next()
b60e5f38
C
273 }
274]
d38b8281 275
b60e5f38 276const usersVideoRatingValidator = [
72c7248b 277 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'),
0a6658fd 278
a2431b7d 279 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 280 logger.debug('Checking usersVideoRating parameters', { parameters: req.params })
0a6658fd 281
a2431b7d 282 if (areValidationErrors(req, res)) return
0f6acda1 283 if (!await doesVideoExist(req.params.videoId, res, 'id')) return
a2431b7d
C
284
285 return next()
b60e5f38
C
286 }
287]
288
289const ensureUserRegistrationAllowed = [
a2431b7d 290 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
4ce7eb71 291 const allowedParams = {
ba7b7e57
RK
292 body: req.body,
293 ip: req.ip
4ce7eb71
C
294 }
295
296 const allowedResult = await Hooks.wrapPromiseFun(
297 isSignupAllowed,
298 allowedParams,
299 'filter:api.user.signup.allowed.result'
300 )
301
302 if (allowedResult.allowed === false) {
a2431b7d 303 return res.status(403)
4ce7eb71 304 .json({ error: allowedResult.errorMessage || 'User registration is not enabled or user limit is reached.' })
a2431b7d
C
305 }
306
307 return next()
b60e5f38
C
308 }
309]
291e8d3e 310
ff2c1fe8 311const ensureUserRegistrationAllowedForIP = [
a1587156 312 (req: express.Request, res: express.Response, next: express.NextFunction) => {
ff2c1fe8
RK
313 const allowed = isSignupAllowedForCurrentIP(req.ip)
314
315 if (allowed === false) {
316 return res.status(403)
a95a4cc8 317 .json({ error: 'You are not on a network authorized for registration.' })
ff2c1fe8
RK
318 }
319
320 return next()
321 }
322]
323
ecb4e35f
C
324const usersAskResetPasswordValidator = [
325 body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
326
327 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
328 logger.debug('Checking usersAskResetPassword parameters', { parameters: req.body })
329
330 if (areValidationErrors(req, res)) return
b426edd4 331
ecb4e35f
C
332 const exists = await checkUserEmailExist(req.body.email, res, false)
333 if (!exists) {
334 logger.debug('User with email %s does not exist (asking reset password).', req.body.email)
335 // Do not leak our emails
336 return res.status(204).end()
337 }
338
339 return next()
340 }
341]
342
343const usersResetPasswordValidator = [
344 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
345 body('verificationString').not().isEmpty().withMessage('Should have a valid verification string'),
346 body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'),
347
348 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
349 logger.debug('Checking usersResetPassword parameters', { parameters: req.params })
350
351 if (areValidationErrors(req, res)) return
352 if (!await checkUserIdExist(req.params.id, res)) return
353
dae86118 354 const user = res.locals.user
ecb4e35f
C
355 const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id)
356
357 if (redisVerificationString !== req.body.verificationString) {
358 return res
359 .status(403)
a95a4cc8 360 .json({ error: 'Invalid verification string.' })
ecb4e35f
C
361 }
362
363 return next()
364 }
365]
366
d9eaee39
JM
367const usersAskSendVerifyEmailValidator = [
368 body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
369
370 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
371 logger.debug('Checking askUsersSendVerifyEmail parameters', { parameters: req.body })
372
373 if (areValidationErrors(req, res)) return
374 const exists = await checkUserEmailExist(req.body.email, res, false)
375 if (!exists) {
376 logger.debug('User with email %s does not exist (asking verify email).', req.body.email)
377 // Do not leak our emails
378 return res.status(204).end()
379 }
380
381 return next()
382 }
383]
384
385const usersVerifyEmailValidator = [
d1ab89de
C
386 param('id')
387 .isInt().not().isEmpty().withMessage('Should have a valid id'),
388
389 body('verificationString')
390 .not().isEmpty().withMessage('Should have a valid verification string'),
391 body('isPendingEmail')
392 .optional()
2b65c4e5 393 .customSanitizer(toBooleanOrNull),
d9eaee39
JM
394
395 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
396 logger.debug('Checking usersVerifyEmail parameters', { parameters: req.params })
397
398 if (areValidationErrors(req, res)) return
399 if (!await checkUserIdExist(req.params.id, res)) return
400
dae86118 401 const user = res.locals.user
d9eaee39
JM
402 const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id)
403
404 if (redisVerificationString !== req.body.verificationString) {
405 return res
406 .status(403)
a95a4cc8 407 .json({ error: 'Invalid verification string.' })
d9eaee39
JM
408 }
409
410 return next()
411 }
412]
413
74d63469
GR
414const userAutocompleteValidator = [
415 param('search').isString().not().isEmpty().withMessage('Should have a search parameter')
416]
417
c100a614 418const ensureAuthUserOwnsAccountValidator = [
a1587156 419 (req: express.Request, res: express.Response, next: express.NextFunction) => {
c100a614
YB
420 const user = res.locals.oauth.token.User
421
422 if (res.locals.account.id !== user.Account.id) {
423 return res.status(403)
a95a4cc8 424 .json({ error: 'Only owner can access ratings list.' })
c100a614
YB
425 }
426
427 return next()
428 }
429]
430
a95a4cc8
C
431const ensureCanManageUser = [
432 (req: express.Request, res: express.Response, next: express.NextFunction) => {
433 const authUser = res.locals.oauth.token.User
434 const onUser = res.locals.user
435
436 if (authUser.role === UserRole.ADMINISTRATOR) return next()
437 if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next()
438
439 return res.status(403)
440 .json({ error: 'A moderator can only manager users.' })
441 }
442]
443
9bd26629
C
444// ---------------------------------------------------------------------------
445
65fcc311
C
446export {
447 usersAddValidator,
92b9d60c 448 deleteMeValidator,
77a5501f 449 usersRegisterValidator,
e6921918 450 usersBlockingValidator,
65fcc311
C
451 usersRemoveValidator,
452 usersUpdateValidator,
8094a898 453 usersUpdateMeValidator,
291e8d3e 454 usersVideoRatingValidator,
8094a898 455 ensureUserRegistrationAllowed,
ff2c1fe8 456 ensureUserRegistrationAllowedForIP,
c5911fd3 457 usersGetValidator,
ecb4e35f 458 usersAskResetPasswordValidator,
d9eaee39
JM
459 usersResetPasswordValidator,
460 usersAskSendVerifyEmailValidator,
74d63469 461 usersVerifyEmailValidator,
c100a614 462 userAutocompleteValidator,
a95a4cc8
C
463 ensureAuthUserOwnsAccountValidator,
464 ensureCanManageUser
8094a898
C
465}
466
467// ---------------------------------------------------------------------------
468
76314386 469function checkUserIdExist (idArg: number | string, res: express.Response, withStats = false) {
d5d9b6d7 470 const id = parseInt(idArg + '', 10)
76314386 471 return checkUserExist(() => UserModel.loadById(id, withStats), res)
ecb4e35f 472}
a2431b7d 473
ecb4e35f
C
474function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) {
475 return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse)
65fcc311 476}
77a5501f 477
a2431b7d 478async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email: string, res: express.Response) {
3fd3ab2d 479 const user = await UserModel.loadByUsernameOrEmail(username, email)
a2431b7d
C
480
481 if (user) {
482 res.status(409)
a95a4cc8 483 .json({ error: 'User with this username or email already exists.' })
a2431b7d
C
484 return false
485 }
486
2ef6a063
C
487 const actor = await ActorModel.loadLocalByName(username)
488 if (actor) {
489 res.status(409)
a95a4cc8 490 .json({ error: 'Another actor (account/channel) with this name on this instance already exists or has already existed.' })
2ef6a063
C
491 return false
492 }
493
a2431b7d 494 return true
77a5501f 495}
ecb4e35f 496
453e83ea 497async function checkUserExist (finder: () => Bluebird<MUserDefault>, res: express.Response, abortResponse = true) {
ecb4e35f
C
498 const user = await finder()
499
500 if (!user) {
501 if (abortResponse === true) {
502 res.status(404)
a95a4cc8 503 .json({ error: 'User not found' })
ecb4e35f
C
504 }
505
506 return false
507 }
508
509 res.locals.user = user
510
511 return true
512}