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