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