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