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