]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/middlewares/validators/users.ts
Support only ffmpeg >= 4.3
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / users.ts
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'
8 import {
9 isUserAdminFlagsValid,
10 isUserAutoPlayNextVideoValid,
11 isUserAutoPlayVideoValid,
12 isUserBlockedReasonValid,
13 isUserDescriptionValid,
14 isUserDisplayNameValid,
15 isUserNoModal,
16 isUserNSFWPolicyValid,
17 isUserP2PEnabledValid,
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 blocked boolena'),
41
42 (req: express.Request, res: express.Response, next: express.NextFunction) => {
43 if (areValidationErrors(req, res)) return
44
45 return next()
46 }
47 ]
48
49 const usersAddValidator = [
50 body('username')
51 .custom(isUserUsernameValid)
52 .withMessage('Should have a valid username (lowercase alphanumeric characters)'),
53 body('password')
54 .custom(isUserPasswordValidOrEmpty),
55 body('email')
56 .isEmail(),
57
58 body('channelName')
59 .optional()
60 .custom(isVideoChannelUsernameValid),
61
62 body('videoQuota')
63 .custom(isUserVideoQuotaValid),
64 body('videoQuotaDaily')
65 .custom(isUserVideoQuotaDailyValid),
66
67 body('role')
68 .customSanitizer(toIntOrNull)
69 .custom(isUserRoleValid),
70
71 body('adminFlags')
72 .optional()
73 .custom(isUserAdminFlagsValid),
74
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
78
79 const authUser = res.locals.oauth.token.User
80 if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) {
81 return res.fail({
82 status: HttpStatusCode.FORBIDDEN_403,
83 message: 'You can only create users (and not administrators or moderators)'
84 })
85 }
86
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.' })
90 }
91
92 const existing = await ActorModel.loadLocalByName(req.body.channelName)
93 if (existing) {
94 return res.fail({
95 status: HttpStatusCode.CONFLICT_409,
96 message: `Channel with name ${req.body.channelName} already exists.`
97 })
98 }
99 }
100
101 return next()
102 }
103 ]
104
105 const usersRegisterValidator = [
106 body('username')
107 .custom(isUserUsernameValid),
108 body('password')
109 .custom(isUserPasswordValid),
110 body('email')
111 .isEmail(),
112 body('displayName')
113 .optional()
114 .custom(isUserDisplayNameValid),
115
116 body('channel.name')
117 .optional()
118 .custom(isVideoChannelUsernameValid),
119 body('channel.displayName')
120 .optional()
121 .custom(isVideoChannelDisplayNameValid),
122
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
126
127 const body: UserRegister = req.body
128 if (body.channel) {
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.' })
131 }
132
133 if (body.channel.name === body.username) {
134 return res.fail({ message: 'Channel name cannot be the same as user username.' })
135 }
136
137 const existing = await ActorModel.loadLocalByName(body.channel.name)
138 if (existing) {
139 return res.fail({
140 status: HttpStatusCode.CONFLICT_409,
141 message: `Channel with name ${body.channel.name} already exists.`
142 })
143 }
144 }
145
146 return next()
147 }
148 ]
149
150 const usersRemoveValidator = [
151 param('id')
152 .custom(isIdValid),
153
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
157
158 const user = res.locals.user
159 if (user.username === 'root') {
160 return res.fail({ message: 'Cannot remove the root user' })
161 }
162
163 return next()
164 }
165 ]
166
167 const usersBlockingValidator = [
168 param('id')
169 .custom(isIdValid),
170 body('reason')
171 .optional()
172 .custom(isUserBlockedReasonValid),
173
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
177
178 const user = res.locals.user
179 if (user.username === 'root') {
180 return res.fail({ message: 'Cannot block the root user' })
181 }
182
183 return next()
184 }
185 ]
186
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.' })
192 }
193
194 return next()
195 }
196 ]
197
198 const usersUpdateValidator = [
199 param('id').custom(isIdValid),
200
201 body('password')
202 .optional()
203 .custom(isUserPasswordValid),
204 body('email')
205 .optional()
206 .isEmail(),
207 body('emailVerified')
208 .optional()
209 .isBoolean(),
210 body('videoQuota')
211 .optional()
212 .custom(isUserVideoQuotaValid),
213 body('videoQuotaDaily')
214 .optional()
215 .custom(isUserVideoQuotaDailyValid),
216 body('pluginAuth')
217 .optional()
218 .exists(),
219 body('role')
220 .optional()
221 .customSanitizer(toIntOrNull)
222 .custom(isUserRoleValid),
223 body('adminFlags')
224 .optional()
225 .custom(isUserAdminFlagsValid),
226
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
230
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.' })
234 }
235
236 return next()
237 }
238 ]
239
240 const usersUpdateMeValidator = [
241 body('displayName')
242 .optional()
243 .custom(isUserDisplayNameValid),
244 body('description')
245 .optional()
246 .custom(isUserDescriptionValid),
247 body('currentPassword')
248 .optional()
249 .custom(isUserPasswordValid),
250 body('password')
251 .optional()
252 .custom(isUserPasswordValid),
253 body('email')
254 .optional()
255 .isEmail(),
256 body('nsfwPolicy')
257 .optional()
258 .custom(isUserNSFWPolicyValid),
259 body('autoPlayVideo')
260 .optional()
261 .custom(isUserAutoPlayVideoValid),
262 body('p2pEnabled')
263 .optional()
264 .custom(isUserP2PEnabledValid).withMessage('Should have a valid p2p enabled boolean'),
265 body('videoLanguages')
266 .optional()
267 .custom(isUserVideoLanguages),
268 body('videosHistoryEnabled')
269 .optional()
270 .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled boolean'),
271 body('theme')
272 .optional()
273 .custom(v => isThemeNameValid(v) && isThemeRegistered(v)),
274
275 body('noInstanceConfigWarningModal')
276 .optional()
277 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noInstanceConfigWarningModal boolean'),
278 body('noWelcomeModal')
279 .optional()
280 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noWelcomeModal boolean'),
281 body('noAccountSetupWarningModal')
282 .optional()
283 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noAccountSetupWarningModal boolean'),
284
285 body('autoPlayNextVideo')
286 .optional()
287 .custom(v => isUserAutoPlayNextVideoValid(v)).withMessage('Should have a valid autoPlayNextVideo boolean'),
288
289 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
290 const user = res.locals.oauth.token.User
291
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.' })
295 }
296
297 if (!req.body.currentPassword) {
298 return res.fail({ message: 'currentPassword parameter is missing.' })
299 }
300
301 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
302 return res.fail({
303 status: HttpStatusCode.UNAUTHORIZED_401,
304 message: 'currentPassword is invalid.'
305 })
306 }
307 }
308
309 if (areValidationErrors(req, res, { omitBodyLog: true })) return
310
311 return next()
312 }
313 ]
314
315 const usersGetValidator = [
316 param('id')
317 .custom(isIdValid),
318 query('withStats')
319 .optional()
320 .isBoolean().withMessage('Should have a valid withStats boolean'),
321
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
325
326 return next()
327 }
328 ]
329
330 const usersVideoRatingValidator = [
331 isValidVideoIdParam('videoId'),
332
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
336
337 return next()
338 }
339 ]
340
341 const usersVideosValidator = [
342 query('isLive')
343 .optional()
344 .customSanitizer(toBooleanOrNull)
345 .custom(isBooleanValid).withMessage('Should have a valid isLive boolean'),
346
347 query('channelId')
348 .optional()
349 .customSanitizer(toIntOrNull)
350 .custom(isIdValid),
351
352 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
353 if (areValidationErrors(req, res)) return
354
355 if (req.query.channelId && !await doesVideoChannelIdExist(req.query.channelId, res)) return
356
357 return next()
358 }
359 ]
360
361 const ensureUserRegistrationAllowed = [
362 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
363 const allowedParams = {
364 body: req.body,
365 ip: req.ip
366 }
367
368 const allowedResult = await Hooks.wrapPromiseFun(
369 isSignupAllowed,
370 allowedParams,
371 'filter:api.user.signup.allowed.result'
372 )
373
374 if (allowedResult.allowed === false) {
375 return res.fail({
376 status: HttpStatusCode.FORBIDDEN_403,
377 message: allowedResult.errorMessage || 'User registration is not enabled or user limit is reached.'
378 })
379 }
380
381 return next()
382 }
383 ]
384
385 const ensureUserRegistrationAllowedForIP = [
386 (req: express.Request, res: express.Response, next: express.NextFunction) => {
387 const allowed = isSignupAllowedForCurrentIP(req.ip)
388
389 if (allowed === false) {
390 return res.fail({
391 status: HttpStatusCode.FORBIDDEN_403,
392 message: 'You are not on a network authorized for registration.'
393 })
394 }
395
396 return next()
397 }
398 ]
399
400 const usersAskResetPasswordValidator = [
401 body('email')
402 .isEmail(),
403
404 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
405 if (areValidationErrors(req, res)) return
406
407 const exists = await checkUserEmailExist(req.body.email, res, false)
408 if (!exists) {
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()
412 }
413
414 return next()
415 }
416 ]
417
418 const usersResetPasswordValidator = [
419 param('id')
420 .custom(isIdValid),
421 body('verificationString')
422 .not().isEmpty(),
423 body('password')
424 .custom(isUserPasswordValid),
425
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
429
430 const user = res.locals.user
431 const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id)
432
433 if (redisVerificationString !== req.body.verificationString) {
434 return res.fail({
435 status: HttpStatusCode.FORBIDDEN_403,
436 message: 'Invalid verification string.'
437 })
438 }
439
440 return next()
441 }
442 ]
443
444 const usersAskSendVerifyEmailValidator = [
445 body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
446
447 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
448 if (areValidationErrors(req, res)) return
449
450 const exists = await checkUserEmailExist(req.body.email, res, false)
451 if (!exists) {
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()
455 }
456
457 return next()
458 }
459 ]
460
461 const usersVerifyEmailValidator = [
462 param('id')
463 .isInt().not().isEmpty().withMessage('Should have a valid id'),
464
465 body('verificationString')
466 .not().isEmpty().withMessage('Should have a valid verification string'),
467 body('isPendingEmail')
468 .optional()
469 .customSanitizer(toBooleanOrNull),
470
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
474
475 const user = res.locals.user
476 const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id)
477
478 if (redisVerificationString !== req.body.verificationString) {
479 return res.fail({
480 status: HttpStatusCode.FORBIDDEN_403,
481 message: 'Invalid verification string.'
482 })
483 }
484
485 return next()
486 }
487 ]
488
489 const userAutocompleteValidator = [
490 param('search')
491 .isString()
492 .not().isEmpty()
493 ]
494
495 const ensureAuthUserOwnsAccountValidator = [
496 (req: express.Request, res: express.Response, next: express.NextFunction) => {
497 const user = res.locals.oauth.token.User
498
499 if (res.locals.account.id !== user.Account.id) {
500 return res.fail({
501 status: HttpStatusCode.FORBIDDEN_403,
502 message: 'Only owner of this account can access this resource.'
503 })
504 }
505
506 return next()
507 }
508 ]
509
510 const ensureCanManageChannelOrAccount = [
511 (req: express.Request, res: express.Response, next: express.NextFunction) => {
512 const user = res.locals.oauth.token.user
513 const account = res.locals.videoChannel?.Account ?? res.locals.account
514 const isUserOwner = account.userId === user.id
515
516 if (!isUserOwner && user.hasRight(UserRight.MANAGE_ANY_VIDEO_CHANNEL) === false) {
517 const message = `User ${user.username} does not have right this channel or account.`
518
519 return res.fail({
520 status: HttpStatusCode.FORBIDDEN_403,
521 message
522 })
523 }
524
525 return next()
526 }
527 ]
528
529 const ensureCanModerateUser = [
530 (req: express.Request, res: express.Response, next: express.NextFunction) => {
531 const authUser = res.locals.oauth.token.User
532 const onUser = res.locals.user
533
534 if (authUser.role === UserRole.ADMINISTRATOR) return next()
535 if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next()
536
537 return res.fail({
538 status: HttpStatusCode.FORBIDDEN_403,
539 message: 'A moderator can only manage users.'
540 })
541 }
542 ]
543
544 // ---------------------------------------------------------------------------
545
546 export {
547 usersListValidator,
548 usersAddValidator,
549 deleteMeValidator,
550 usersRegisterValidator,
551 usersBlockingValidator,
552 usersRemoveValidator,
553 usersUpdateValidator,
554 usersUpdateMeValidator,
555 usersVideoRatingValidator,
556 ensureUserRegistrationAllowed,
557 ensureUserRegistrationAllowedForIP,
558 usersGetValidator,
559 usersVideosValidator,
560 usersAskResetPasswordValidator,
561 usersResetPasswordValidator,
562 usersAskSendVerifyEmailValidator,
563 usersVerifyEmailValidator,
564 userAutocompleteValidator,
565 ensureAuthUserOwnsAccountValidator,
566 ensureCanModerateUser,
567 ensureCanManageChannelOrAccount
568 }
569
570 // ---------------------------------------------------------------------------
571
572 function checkUserIdExist (idArg: number | string, res: express.Response, withStats = false) {
573 const id = parseInt(idArg + '', 10)
574 return checkUserExist(() => UserModel.loadByIdWithChannels(id, withStats), res)
575 }
576
577 function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) {
578 return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse)
579 }
580
581 async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email: string, res: express.Response) {
582 const user = await UserModel.loadByUsernameOrEmail(username, email)
583
584 if (user) {
585 res.fail({
586 status: HttpStatusCode.CONFLICT_409,
587 message: 'User with this username or email already exists.'
588 })
589 return false
590 }
591
592 const actor = await ActorModel.loadLocalByName(username)
593 if (actor) {
594 res.fail({
595 status: HttpStatusCode.CONFLICT_409,
596 message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.'
597 })
598 return false
599 }
600
601 return true
602 }
603
604 async function checkUserExist (finder: () => Promise<MUserDefault>, res: express.Response, abortResponse = true) {
605 const user = await finder()
606
607 if (!user) {
608 if (abortResponse === true) {
609 res.fail({
610 status: HttpStatusCode.NOT_FOUND_404,
611 message: 'User not found'
612 })
613 }
614
615 return false
616 }
617
618 res.locals.user = user
619 return true
620 }