]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/middlewares/validators/users.ts
Add ability for admins to set default p2p policy
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / users.ts
1 import express from 'express'
2 import { body, param, query } from 'express-validator'
3 import { omit } from 'lodash'
4 import { Hooks } from '@server/lib/plugins/hooks'
5 import { MUserDefault } from '@server/types/models'
6 import { HttpStatusCode, UserRegister, UserRight, UserRole } from '@shared/models'
7 import { isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
8 import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
9 import {
10 isUserAdminFlagsValid,
11 isUserAutoPlayNextVideoValid,
12 isUserAutoPlayVideoValid,
13 isUserBlockedReasonValid,
14 isUserDescriptionValid,
15 isUserDisplayNameValid,
16 isUserNoModal,
17 isUserNSFWPolicyValid,
18 isUserP2PEnabledValid,
19 isUserPasswordValid,
20 isUserPasswordValidOrEmpty,
21 isUserRoleValid,
22 isUserUsernameValid,
23 isUserVideoLanguages,
24 isUserVideoQuotaDailyValid,
25 isUserVideoQuotaValid,
26 isUserVideosHistoryEnabledValid
27 } from '../../helpers/custom-validators/users'
28 import { isVideoChannelDisplayNameValid, isVideoChannelUsernameValid } from '../../helpers/custom-validators/video-channels'
29 import { logger } from '../../helpers/logger'
30 import { isThemeRegistered } from '../../lib/plugins/theme-utils'
31 import { Redis } from '../../lib/redis'
32 import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../lib/signup'
33 import { ActorModel } from '../../models/actor/actor'
34 import { UserModel } from '../../models/user/user'
35 import { areValidationErrors, doesVideoChannelIdExist, doesVideoExist, isValidVideoIdParam } from './shared'
36
37 const usersListValidator = [
38 query('blocked')
39 .optional()
40 .customSanitizer(toBooleanOrNull)
41 .isBoolean().withMessage('Should be a valid boolean banned state'),
42
43 (req: express.Request, res: express.Response, next: express.NextFunction) => {
44 logger.debug('Checking usersList parameters', { parameters: req.query })
45
46 if (areValidationErrors(req, res)) return
47
48 return next()
49 }
50 ]
51
52 const usersAddValidator = [
53 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'),
54 body('password').custom(isUserPasswordValidOrEmpty).withMessage('Should have a valid password'),
55 body('email').isEmail().withMessage('Should have a valid email'),
56
57 body('channelName').optional().custom(isVideoChannelUsernameValid).withMessage('Should have a valid channel name'),
58
59 body('videoQuota').custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'),
60 body('videoQuotaDaily').custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'),
61
62 body('role')
63 .customSanitizer(toIntOrNull)
64 .custom(isUserRoleValid).withMessage('Should have a valid role'),
65 body('adminFlags').optional().custom(isUserAdminFlagsValid).withMessage('Should have a valid admin flags'),
66
67 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
68 logger.debug('Checking usersAdd parameters', { parameters: omit(req.body, 'password') })
69
70 if (areValidationErrors(req, res)) return
71 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
72
73 const authUser = res.locals.oauth.token.User
74 if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) {
75 return res.fail({
76 status: HttpStatusCode.FORBIDDEN_403,
77 message: 'You can only create users (and not administrators or moderators)'
78 })
79 }
80
81 if (req.body.channelName) {
82 if (req.body.channelName === req.body.username) {
83 return res.fail({ message: 'Channel name cannot be the same as user username.' })
84 }
85
86 const existing = await ActorModel.loadLocalByName(req.body.channelName)
87 if (existing) {
88 return res.fail({
89 status: HttpStatusCode.CONFLICT_409,
90 message: `Channel with name ${req.body.channelName} already exists.`
91 })
92 }
93 }
94
95 return next()
96 }
97 ]
98
99 const usersRegisterValidator = [
100 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username'),
101 body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'),
102 body('email').isEmail().withMessage('Should have a valid email'),
103 body('displayName')
104 .optional()
105 .custom(isUserDisplayNameValid).withMessage('Should have a valid display name'),
106
107 body('channel.name')
108 .optional()
109 .custom(isVideoChannelUsernameValid).withMessage('Should have a valid channel name'),
110 body('channel.displayName')
111 .optional()
112 .custom(isVideoChannelDisplayNameValid).withMessage('Should have a valid display name'),
113
114 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
115 logger.debug('Checking usersRegister parameters', { parameters: omit(req.body, 'password') })
116
117 if (areValidationErrors(req, res)) return
118 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
119
120 const body: UserRegister = req.body
121 if (body.channel) {
122 if (!body.channel.name || !body.channel.displayName) {
123 return res.fail({ message: 'Channel is optional but if you specify it, channel.name and channel.displayName are required.' })
124 }
125
126 if (body.channel.name === body.username) {
127 return res.fail({ message: 'Channel name cannot be the same as user username.' })
128 }
129
130 const existing = await ActorModel.loadLocalByName(body.channel.name)
131 if (existing) {
132 return res.fail({
133 status: HttpStatusCode.CONFLICT_409,
134 message: `Channel with name ${body.channel.name} already exists.`
135 })
136 }
137 }
138
139 return next()
140 }
141 ]
142
143 const usersRemoveValidator = [
144 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
145
146 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
147 logger.debug('Checking usersRemove parameters', { parameters: req.params })
148
149 if (areValidationErrors(req, res)) return
150 if (!await checkUserIdExist(req.params.id, res)) return
151
152 const user = res.locals.user
153 if (user.username === 'root') {
154 return res.fail({ message: 'Cannot remove the root user' })
155 }
156
157 return next()
158 }
159 ]
160
161 const usersBlockingValidator = [
162 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
163 body('reason').optional().custom(isUserBlockedReasonValid).withMessage('Should have a valid blocking reason'),
164
165 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
166 logger.debug('Checking usersBlocking parameters', { parameters: req.params })
167
168 if (areValidationErrors(req, res)) return
169 if (!await checkUserIdExist(req.params.id, res)) return
170
171 const user = res.locals.user
172 if (user.username === 'root') {
173 return res.fail({ message: 'Cannot block the root user' })
174 }
175
176 return next()
177 }
178 ]
179
180 const deleteMeValidator = [
181 (req: express.Request, res: express.Response, next: express.NextFunction) => {
182 const user = res.locals.oauth.token.User
183 if (user.username === 'root') {
184 return res.fail({ message: 'You cannot delete your root account.' })
185 }
186
187 return next()
188 }
189 ]
190
191 const usersUpdateValidator = [
192 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
193
194 body('password').optional().custom(isUserPasswordValid).withMessage('Should have a valid password'),
195 body('email').optional().isEmail().withMessage('Should have a valid email attribute'),
196 body('emailVerified').optional().isBoolean().withMessage('Should have a valid email verified attribute'),
197 body('videoQuota').optional().custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'),
198 body('videoQuotaDaily').optional().custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'),
199 body('pluginAuth').optional(),
200 body('role')
201 .optional()
202 .customSanitizer(toIntOrNull)
203 .custom(isUserRoleValid).withMessage('Should have a valid role'),
204 body('adminFlags').optional().custom(isUserAdminFlagsValid).withMessage('Should have a valid admin flags'),
205
206 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
207 logger.debug('Checking usersUpdate parameters', { parameters: req.body })
208
209 if (areValidationErrors(req, res)) return
210 if (!await checkUserIdExist(req.params.id, res)) return
211
212 const user = res.locals.user
213 if (user.username === 'root' && req.body.role !== undefined && user.role !== req.body.role) {
214 return res.fail({ message: 'Cannot change root role.' })
215 }
216
217 return next()
218 }
219 ]
220
221 const usersUpdateMeValidator = [
222 body('displayName')
223 .optional()
224 .custom(isUserDisplayNameValid).withMessage('Should have a valid display name'),
225 body('description')
226 .optional()
227 .custom(isUserDescriptionValid).withMessage('Should have a valid description'),
228 body('currentPassword')
229 .optional()
230 .custom(isUserPasswordValid).withMessage('Should have a valid current password'),
231 body('password')
232 .optional()
233 .custom(isUserPasswordValid).withMessage('Should have a valid password'),
234 body('email')
235 .optional()
236 .isEmail().withMessage('Should have a valid email attribute'),
237 body('nsfwPolicy')
238 .optional()
239 .custom(isUserNSFWPolicyValid).withMessage('Should have a valid display Not Safe For Work policy'),
240 body('autoPlayVideo')
241 .optional()
242 .custom(isUserAutoPlayVideoValid).withMessage('Should have a valid automatically plays video attribute'),
243 body('p2pEnabled')
244 .optional()
245 .custom(isUserP2PEnabledValid).withMessage('Should have a valid p2p enabled boolean'),
246 body('videoLanguages')
247 .optional()
248 .custom(isUserVideoLanguages).withMessage('Should have a valid video languages attribute'),
249 body('videosHistoryEnabled')
250 .optional()
251 .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled attribute'),
252 body('theme')
253 .optional()
254 .custom(v => isThemeNameValid(v) && isThemeRegistered(v)).withMessage('Should have a valid theme'),
255
256 body('noInstanceConfigWarningModal')
257 .optional()
258 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noInstanceConfigWarningModal boolean'),
259 body('noWelcomeModal')
260 .optional()
261 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noWelcomeModal boolean'),
262 body('noAccountSetupWarningModal')
263 .optional()
264 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noAccountSetupWarningModal boolean'),
265
266 body('autoPlayNextVideo')
267 .optional()
268 .custom(v => isUserAutoPlayNextVideoValid(v)).withMessage('Should have a valid autoPlayNextVideo boolean'),
269
270 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
271 logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') })
272
273 const user = res.locals.oauth.token.User
274
275 if (req.body.password || req.body.email) {
276 if (user.pluginAuth !== null) {
277 return res.fail({ message: 'You cannot update your email or password that is associated with an external auth system.' })
278 }
279
280 if (!req.body.currentPassword) {
281 return res.fail({ message: 'currentPassword parameter is missing.' })
282 }
283
284 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
285 return res.fail({
286 status: HttpStatusCode.UNAUTHORIZED_401,
287 message: 'currentPassword is invalid.'
288 })
289 }
290 }
291
292 if (areValidationErrors(req, res)) return
293
294 return next()
295 }
296 ]
297
298 const usersGetValidator = [
299 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
300 query('withStats').optional().isBoolean().withMessage('Should have a valid stats flag'),
301
302 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
303 logger.debug('Checking usersGet parameters', { parameters: req.params })
304
305 if (areValidationErrors(req, res)) return
306 if (!await checkUserIdExist(req.params.id, res, req.query.withStats)) return
307
308 return next()
309 }
310 ]
311
312 const usersVideoRatingValidator = [
313 isValidVideoIdParam('videoId'),
314
315 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
316 logger.debug('Checking usersVideoRating parameters', { parameters: req.params })
317
318 if (areValidationErrors(req, res)) return
319 if (!await doesVideoExist(req.params.videoId, res, 'id')) return
320
321 return next()
322 }
323 ]
324
325 const usersVideosValidator = [
326 query('isLive')
327 .optional()
328 .customSanitizer(toBooleanOrNull)
329 .custom(isBooleanValid).withMessage('Should have a valid live boolean'),
330
331 query('channelId')
332 .optional()
333 .customSanitizer(toIntOrNull)
334 .custom(isIdValid).withMessage('Should have a valid channel id'),
335
336 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
337 logger.debug('Checking usersVideosValidator parameters', { parameters: req.query })
338
339 if (areValidationErrors(req, res)) return
340
341 if (req.query.channelId && !await doesVideoChannelIdExist(req.query.channelId, res)) return
342
343 return next()
344 }
345 ]
346
347 const ensureUserRegistrationAllowed = [
348 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
349 const allowedParams = {
350 body: req.body,
351 ip: req.ip
352 }
353
354 const allowedResult = await Hooks.wrapPromiseFun(
355 isSignupAllowed,
356 allowedParams,
357 'filter:api.user.signup.allowed.result'
358 )
359
360 if (allowedResult.allowed === false) {
361 return res.fail({
362 status: HttpStatusCode.FORBIDDEN_403,
363 message: allowedResult.errorMessage || 'User registration is not enabled or user limit is reached.'
364 })
365 }
366
367 return next()
368 }
369 ]
370
371 const ensureUserRegistrationAllowedForIP = [
372 (req: express.Request, res: express.Response, next: express.NextFunction) => {
373 const allowed = isSignupAllowedForCurrentIP(req.ip)
374
375 if (allowed === false) {
376 return res.fail({
377 status: HttpStatusCode.FORBIDDEN_403,
378 message: 'You are not on a network authorized for registration.'
379 })
380 }
381
382 return next()
383 }
384 ]
385
386 const usersAskResetPasswordValidator = [
387 body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
388
389 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
390 logger.debug('Checking usersAskResetPassword parameters', { parameters: req.body })
391
392 if (areValidationErrors(req, res)) return
393
394 const exists = await checkUserEmailExist(req.body.email, res, false)
395 if (!exists) {
396 logger.debug('User with email %s does not exist (asking reset password).', req.body.email)
397 // Do not leak our emails
398 return res.status(HttpStatusCode.NO_CONTENT_204).end()
399 }
400
401 return next()
402 }
403 ]
404
405 const usersResetPasswordValidator = [
406 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
407 body('verificationString').not().isEmpty().withMessage('Should have a valid verification string'),
408 body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'),
409
410 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
411 logger.debug('Checking usersResetPassword parameters', { parameters: req.params })
412
413 if (areValidationErrors(req, res)) return
414 if (!await checkUserIdExist(req.params.id, res)) return
415
416 const user = res.locals.user
417 const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id)
418
419 if (redisVerificationString !== req.body.verificationString) {
420 return res.fail({
421 status: HttpStatusCode.FORBIDDEN_403,
422 message: 'Invalid verification string.'
423 })
424 }
425
426 return next()
427 }
428 ]
429
430 const usersAskSendVerifyEmailValidator = [
431 body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
432
433 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
434 logger.debug('Checking askUsersSendVerifyEmail parameters', { parameters: req.body })
435
436 if (areValidationErrors(req, res)) return
437 const exists = await checkUserEmailExist(req.body.email, res, false)
438 if (!exists) {
439 logger.debug('User with email %s does not exist (asking verify email).', req.body.email)
440 // Do not leak our emails
441 return res.status(HttpStatusCode.NO_CONTENT_204).end()
442 }
443
444 return next()
445 }
446 ]
447
448 const usersVerifyEmailValidator = [
449 param('id')
450 .isInt().not().isEmpty().withMessage('Should have a valid id'),
451
452 body('verificationString')
453 .not().isEmpty().withMessage('Should have a valid verification string'),
454 body('isPendingEmail')
455 .optional()
456 .customSanitizer(toBooleanOrNull),
457
458 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
459 logger.debug('Checking usersVerifyEmail parameters', { parameters: req.params })
460
461 if (areValidationErrors(req, res)) return
462 if (!await checkUserIdExist(req.params.id, res)) return
463
464 const user = res.locals.user
465 const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id)
466
467 if (redisVerificationString !== req.body.verificationString) {
468 return res.fail({
469 status: HttpStatusCode.FORBIDDEN_403,
470 message: 'Invalid verification string.'
471 })
472 }
473
474 return next()
475 }
476 ]
477
478 const userAutocompleteValidator = [
479 param('search').isString().not().isEmpty().withMessage('Should have a search parameter')
480 ]
481
482 const ensureAuthUserOwnsAccountValidator = [
483 (req: express.Request, res: express.Response, next: express.NextFunction) => {
484 const user = res.locals.oauth.token.User
485
486 if (res.locals.account.id !== user.Account.id) {
487 return res.fail({
488 status: HttpStatusCode.FORBIDDEN_403,
489 message: 'Only owner of this account can access this ressource.'
490 })
491 }
492
493 return next()
494 }
495 ]
496
497 const ensureCanManageChannel = [
498 (req: express.Request, res: express.Response, next: express.NextFunction) => {
499 const user = res.locals.oauth.token.user
500 const isUserOwner = res.locals.videoChannel.Account.userId === user.id
501
502 if (!isUserOwner && user.hasRight(UserRight.MANAGE_ANY_VIDEO_CHANNEL) === false) {
503 const message = `User ${user.username} does not have right to manage channel ${req.params.nameWithHost}.`
504
505 return res.fail({
506 status: HttpStatusCode.FORBIDDEN_403,
507 message
508 })
509 }
510
511 return next()
512 }
513 ]
514
515 const ensureCanManageUser = [
516 (req: express.Request, res: express.Response, next: express.NextFunction) => {
517 const authUser = res.locals.oauth.token.User
518 const onUser = res.locals.user
519
520 if (authUser.role === UserRole.ADMINISTRATOR) return next()
521 if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next()
522
523 return res.fail({
524 status: HttpStatusCode.FORBIDDEN_403,
525 message: 'A moderator can only manager users.'
526 })
527 }
528 ]
529
530 // ---------------------------------------------------------------------------
531
532 export {
533 usersListValidator,
534 usersAddValidator,
535 deleteMeValidator,
536 usersRegisterValidator,
537 usersBlockingValidator,
538 usersRemoveValidator,
539 usersUpdateValidator,
540 usersUpdateMeValidator,
541 usersVideoRatingValidator,
542 ensureUserRegistrationAllowed,
543 ensureUserRegistrationAllowedForIP,
544 usersGetValidator,
545 usersVideosValidator,
546 usersAskResetPasswordValidator,
547 usersResetPasswordValidator,
548 usersAskSendVerifyEmailValidator,
549 usersVerifyEmailValidator,
550 userAutocompleteValidator,
551 ensureAuthUserOwnsAccountValidator,
552 ensureCanManageUser,
553 ensureCanManageChannel
554 }
555
556 // ---------------------------------------------------------------------------
557
558 function checkUserIdExist (idArg: number | string, res: express.Response, withStats = false) {
559 const id = parseInt(idArg + '', 10)
560 return checkUserExist(() => UserModel.loadByIdWithChannels(id, withStats), res)
561 }
562
563 function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) {
564 return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse)
565 }
566
567 async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email: string, res: express.Response) {
568 const user = await UserModel.loadByUsernameOrEmail(username, email)
569
570 if (user) {
571 res.fail({
572 status: HttpStatusCode.CONFLICT_409,
573 message: 'User with this username or email already exists.'
574 })
575 return false
576 }
577
578 const actor = await ActorModel.loadLocalByName(username)
579 if (actor) {
580 res.fail({
581 status: HttpStatusCode.CONFLICT_409,
582 message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.'
583 })
584 return false
585 }
586
587 return true
588 }
589
590 async function checkUserExist (finder: () => Promise<MUserDefault>, res: express.Response, abortResponse = true) {
591 const user = await finder()
592
593 if (!user) {
594 if (abortResponse === true) {
595 res.fail({
596 status: HttpStatusCode.NOT_FOUND_404,
597 message: 'User not found'
598 })
599 }
600
601 return false
602 }
603
604 res.locals.user = user
605 return true
606 }