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