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