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