]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/middlewares/validators/users.ts
Remove unnecessary logs
[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 ensureCanManageChannel = [
511 (req: express.Request, res: express.Response, next: express.NextFunction) => {
512 const user = res.locals.oauth.token.user
513 const isUserOwner = res.locals.videoChannel.Account.userId === user.id
514
515 if (!isUserOwner && user.hasRight(UserRight.MANAGE_ANY_VIDEO_CHANNEL) === false) {
516 const message = `User ${user.username} does not have right to manage channel ${req.params.nameWithHost}.`
517
518 return res.fail({
519 status: HttpStatusCode.FORBIDDEN_403,
520 message
521 })
522 }
523
524 return next()
525 }
526 ]
527
528 const ensureCanManageUser = [
529 (req: express.Request, res: express.Response, next: express.NextFunction) => {
530 const authUser = res.locals.oauth.token.User
531 const onUser = res.locals.user
532
533 if (authUser.role === UserRole.ADMINISTRATOR) return next()
534 if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next()
535
536 return res.fail({
537 status: HttpStatusCode.FORBIDDEN_403,
538 message: 'A moderator can only manager users.'
539 })
540 }
541 ]
542
543 // ---------------------------------------------------------------------------
544
545 export {
546 usersListValidator,
547 usersAddValidator,
548 deleteMeValidator,
549 usersRegisterValidator,
550 usersBlockingValidator,
551 usersRemoveValidator,
552 usersUpdateValidator,
553 usersUpdateMeValidator,
554 usersVideoRatingValidator,
555 ensureUserRegistrationAllowed,
556 ensureUserRegistrationAllowedForIP,
557 usersGetValidator,
558 usersVideosValidator,
559 usersAskResetPasswordValidator,
560 usersResetPasswordValidator,
561 usersAskSendVerifyEmailValidator,
562 usersVerifyEmailValidator,
563 userAutocompleteValidator,
564 ensureAuthUserOwnsAccountValidator,
565 ensureCanManageUser,
566 ensureCanManageChannel
567 }
568
569 // ---------------------------------------------------------------------------
570
571 function checkUserIdExist (idArg: number | string, res: express.Response, withStats = false) {
572 const id = parseInt(idArg + '', 10)
573 return checkUserExist(() => UserModel.loadByIdWithChannels(id, withStats), res)
574 }
575
576 function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) {
577 return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse)
578 }
579
580 async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email: string, res: express.Response) {
581 const user = await UserModel.loadByUsernameOrEmail(username, email)
582
583 if (user) {
584 res.fail({
585 status: HttpStatusCode.CONFLICT_409,
586 message: 'User with this username or email already exists.'
587 })
588 return false
589 }
590
591 const actor = await ActorModel.loadLocalByName(username)
592 if (actor) {
593 res.fail({
594 status: HttpStatusCode.CONFLICT_409,
595 message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.'
596 })
597 return false
598 }
599
600 return true
601 }
602
603 async function checkUserExist (finder: () => Promise<MUserDefault>, res: express.Response, abortResponse = true) {
604 const user = await finder()
605
606 if (!user) {
607 if (abortResponse === true) {
608 res.fail({
609 status: HttpStatusCode.NOT_FOUND_404,
610 message: 'User not found'
611 })
612 }
613
614 return false
615 }
616
617 res.locals.user = user
618 return true
619 }