]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/middlewares/validators/users.ts
Merge branch 'feature/admin-comments' into develop
[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 .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.status(403)
77 .json({ error: 'You can only create users (and not administrators or moderators)' })
78 }
79
80 if (req.body.channelName) {
81 if (req.body.channelName === req.body.username) {
82 return res.status(400)
83 .json({ error: '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.status(409)
89 .json({ error: `Channel with name ${req.body.channelName} already exists.` })
90 }
91 }
92
93 return next()
94 }
95 ]
96
97 const usersRegisterValidator = [
98 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username'),
99 body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'),
100 body('email').isEmail().withMessage('Should have a valid email'),
101 body('displayName')
102 .optional()
103 .custom(isUserDisplayNameValid).withMessage('Should have a valid display name'),
104
105 body('channel.name')
106 .optional()
107 .custom(isActorPreferredUsernameValid).withMessage('Should have a valid channel name'),
108 body('channel.displayName')
109 .optional()
110 .custom(isVideoChannelNameValid).withMessage('Should have a valid display name'),
111
112 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
113 logger.debug('Checking usersRegister parameters', { parameters: omit(req.body, 'password') })
114
115 if (areValidationErrors(req, res)) return
116 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
117
118 const body: UserRegister = req.body
119 if (body.channel) {
120 if (!body.channel.name || !body.channel.displayName) {
121 return res.status(400)
122 .json({ error: 'Channel is optional but if you specify it, channel.name and channel.displayName are required.' })
123 }
124
125 if (body.channel.name === body.username) {
126 return res.status(400)
127 .json({ error: '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.status(409)
133 .json({ error: `Channel with name ${body.channel.name} already exists.` })
134 }
135 }
136
137 return next()
138 }
139 ]
140
141 const usersRemoveValidator = [
142 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
143
144 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
145 logger.debug('Checking usersRemove parameters', { parameters: req.params })
146
147 if (areValidationErrors(req, res)) return
148 if (!await checkUserIdExist(req.params.id, res)) return
149
150 const user = res.locals.user
151 if (user.username === 'root') {
152 return res.status(400)
153 .json({ error: 'Cannot remove the root user' })
154 }
155
156 return next()
157 }
158 ]
159
160 const usersBlockingValidator = [
161 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
162 body('reason').optional().custom(isUserBlockedReasonValid).withMessage('Should have a valid blocking reason'),
163
164 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
165 logger.debug('Checking usersBlocking parameters', { parameters: req.params })
166
167 if (areValidationErrors(req, res)) return
168 if (!await checkUserIdExist(req.params.id, res)) return
169
170 const user = res.locals.user
171 if (user.username === 'root') {
172 return res.status(400)
173 .json({ error: '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.status(400)
185 .json({ error: 'You cannot delete your root account.' })
186 .end()
187 }
188
189 return next()
190 }
191 ]
192
193 const usersUpdateValidator = [
194 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
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('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.status(400)
215 .json({ error: '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 body('noInstanceConfigWarningModal')
254 .optional()
255 .custom(v => isNoInstanceConfigWarningModal(v)).withMessage('Should have a valid noInstanceConfigWarningModal boolean'),
256 body('noWelcomeModal')
257 .optional()
258 .custom(v => isNoWelcomeModal(v)).withMessage('Should have a valid noWelcomeModal boolean'),
259 body('autoPlayNextVideo')
260 .optional()
261 .custom(v => isUserAutoPlayNextVideoValid(v)).withMessage('Should have a valid autoPlayNextVideo boolean'),
262
263 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
264 logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') })
265
266 const user = res.locals.oauth.token.User
267
268 if (req.body.password || req.body.email) {
269 if (user.pluginAuth !== null) {
270 return res.status(400)
271 .json({ error: '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.status(400)
276 .json({ error: 'currentPassword parameter is missing.' })
277 }
278
279 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
280 return res.status(401)
281 .json({ error: 'currentPassword is invalid.' })
282 }
283 }
284
285 if (areValidationErrors(req, res)) return
286
287 return next()
288 }
289 ]
290
291 const usersGetValidator = [
292 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
293 query('withStats').optional().isBoolean().withMessage('Should have a valid stats flag'),
294
295 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
296 logger.debug('Checking usersGet parameters', { parameters: req.params })
297
298 if (areValidationErrors(req, res)) return
299 if (!await checkUserIdExist(req.params.id, res, req.query.withStats)) return
300
301 return next()
302 }
303 ]
304
305 const usersVideoRatingValidator = [
306 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'),
307
308 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
309 logger.debug('Checking usersVideoRating parameters', { parameters: req.params })
310
311 if (areValidationErrors(req, res)) return
312 if (!await doesVideoExist(req.params.videoId, res, 'id')) return
313
314 return next()
315 }
316 ]
317
318 const ensureUserRegistrationAllowed = [
319 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
320 const allowedParams = {
321 body: req.body,
322 ip: req.ip
323 }
324
325 const allowedResult = await Hooks.wrapPromiseFun(
326 isSignupAllowed,
327 allowedParams,
328 'filter:api.user.signup.allowed.result'
329 )
330
331 if (allowedResult.allowed === false) {
332 return res.status(403)
333 .json({ error: allowedResult.errorMessage || 'User registration is not enabled or user limit is reached.' })
334 }
335
336 return next()
337 }
338 ]
339
340 const ensureUserRegistrationAllowedForIP = [
341 (req: express.Request, res: express.Response, next: express.NextFunction) => {
342 const allowed = isSignupAllowedForCurrentIP(req.ip)
343
344 if (allowed === false) {
345 return res.status(403)
346 .json({ error: 'You are not on a network authorized for registration.' })
347 }
348
349 return next()
350 }
351 ]
352
353 const usersAskResetPasswordValidator = [
354 body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
355
356 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
357 logger.debug('Checking usersAskResetPassword parameters', { parameters: req.body })
358
359 if (areValidationErrors(req, res)) return
360
361 const exists = await checkUserEmailExist(req.body.email, res, false)
362 if (!exists) {
363 logger.debug('User with email %s does not exist (asking reset password).', req.body.email)
364 // Do not leak our emails
365 return res.status(204).end()
366 }
367
368 return next()
369 }
370 ]
371
372 const usersResetPasswordValidator = [
373 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
374 body('verificationString').not().isEmpty().withMessage('Should have a valid verification string'),
375 body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'),
376
377 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
378 logger.debug('Checking usersResetPassword parameters', { parameters: req.params })
379
380 if (areValidationErrors(req, res)) return
381 if (!await checkUserIdExist(req.params.id, res)) return
382
383 const user = res.locals.user
384 const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id)
385
386 if (redisVerificationString !== req.body.verificationString) {
387 return res
388 .status(403)
389 .json({ error: 'Invalid verification string.' })
390 }
391
392 return next()
393 }
394 ]
395
396 const usersAskSendVerifyEmailValidator = [
397 body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
398
399 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
400 logger.debug('Checking askUsersSendVerifyEmail parameters', { parameters: req.body })
401
402 if (areValidationErrors(req, res)) return
403 const exists = await checkUserEmailExist(req.body.email, res, false)
404 if (!exists) {
405 logger.debug('User with email %s does not exist (asking verify email).', req.body.email)
406 // Do not leak our emails
407 return res.status(204).end()
408 }
409
410 return next()
411 }
412 ]
413
414 const usersVerifyEmailValidator = [
415 param('id')
416 .isInt().not().isEmpty().withMessage('Should have a valid id'),
417
418 body('verificationString')
419 .not().isEmpty().withMessage('Should have a valid verification string'),
420 body('isPendingEmail')
421 .optional()
422 .customSanitizer(toBooleanOrNull),
423
424 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
425 logger.debug('Checking usersVerifyEmail parameters', { parameters: req.params })
426
427 if (areValidationErrors(req, res)) return
428 if (!await checkUserIdExist(req.params.id, res)) return
429
430 const user = res.locals.user
431 const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id)
432
433 if (redisVerificationString !== req.body.verificationString) {
434 return res
435 .status(403)
436 .json({ error: 'Invalid verification string.' })
437 }
438
439 return next()
440 }
441 ]
442
443 const userAutocompleteValidator = [
444 param('search').isString().not().isEmpty().withMessage('Should have a search parameter')
445 ]
446
447 const ensureAuthUserOwnsAccountValidator = [
448 (req: express.Request, res: express.Response, next: express.NextFunction) => {
449 const user = res.locals.oauth.token.User
450
451 if (res.locals.account.id !== user.Account.id) {
452 return res.status(403)
453 .json({ error: 'Only owner can access ratings list.' })
454 }
455
456 return next()
457 }
458 ]
459
460 const ensureCanManageUser = [
461 (req: express.Request, res: express.Response, next: express.NextFunction) => {
462 const authUser = res.locals.oauth.token.User
463 const onUser = res.locals.user
464
465 if (authUser.role === UserRole.ADMINISTRATOR) return next()
466 if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next()
467
468 return res.status(403)
469 .json({ error: 'A moderator can only manager users.' })
470 }
471 ]
472
473 // ---------------------------------------------------------------------------
474
475 export {
476 usersListValidator,
477 usersAddValidator,
478 deleteMeValidator,
479 usersRegisterValidator,
480 usersBlockingValidator,
481 usersRemoveValidator,
482 usersUpdateValidator,
483 usersUpdateMeValidator,
484 usersVideoRatingValidator,
485 ensureUserRegistrationAllowed,
486 ensureUserRegistrationAllowedForIP,
487 usersGetValidator,
488 usersAskResetPasswordValidator,
489 usersResetPasswordValidator,
490 usersAskSendVerifyEmailValidator,
491 usersVerifyEmailValidator,
492 userAutocompleteValidator,
493 ensureAuthUserOwnsAccountValidator,
494 ensureCanManageUser
495 }
496
497 // ---------------------------------------------------------------------------
498
499 function checkUserIdExist (idArg: number | string, res: express.Response, withStats = false) {
500 const id = parseInt(idArg + '', 10)
501 return checkUserExist(() => UserModel.loadByIdWithChannels(id, withStats), res)
502 }
503
504 function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) {
505 return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse)
506 }
507
508 async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email: string, res: express.Response) {
509 const user = await UserModel.loadByUsernameOrEmail(username, email)
510
511 if (user) {
512 res.status(409)
513 .json({ error: 'User with this username or email already exists.' })
514 return false
515 }
516
517 const actor = await ActorModel.loadLocalByName(username)
518 if (actor) {
519 res.status(409)
520 .json({ error: 'Another actor (account/channel) with this name on this instance already exists or has already existed.' })
521 return false
522 }
523
524 return true
525 }
526
527 async function checkUserExist (finder: () => Bluebird<MUserDefault>, res: express.Response, abortResponse = true) {
528 const user = await finder()
529
530 if (!user) {
531 if (abortResponse === true) {
532 res.status(404)
533 .json({ error: 'User not found' })
534 }
535
536 return false
537 }
538
539 res.locals.user = user
540
541 return true
542 }