]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/middlewares/validators/users.ts
Check live duration and size
[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('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.status(403)
76 .json({ error: 'You can only create users (and not administrators or moderators)' })
77 }
78
79 if (req.body.channelName) {
80 if (req.body.channelName === req.body.username) {
81 return res.status(400)
82 .json({ error: 'Channel name cannot be the same as user username.' })
83 }
84
85 const existing = await ActorModel.loadLocalByName(req.body.channelName)
86 if (existing) {
87 return res.status(409)
88 .json({ error: `Channel with name ${req.body.channelName} already exists.` })
89 }
90 }
91
92 return next()
93 }
94 ]
95
96 const usersRegisterValidator = [
97 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username'),
98 body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'),
99 body('email').isEmail().withMessage('Should have a valid email'),
100 body('displayName')
101 .optional()
102 .custom(isUserDisplayNameValid).withMessage('Should have a valid display name'),
103
104 body('channel.name')
105 .optional()
106 .custom(isActorPreferredUsernameValid).withMessage('Should have a valid channel name'),
107 body('channel.displayName')
108 .optional()
109 .custom(isVideoChannelNameValid).withMessage('Should have a valid display name'),
110
111 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
112 logger.debug('Checking usersRegister parameters', { parameters: omit(req.body, 'password') })
113
114 if (areValidationErrors(req, res)) return
115 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
116
117 const body: UserRegister = req.body
118 if (body.channel) {
119 if (!body.channel.name || !body.channel.displayName) {
120 return res.status(400)
121 .json({ error: 'Channel is optional but if you specify it, channel.name and channel.displayName are required.' })
122 }
123
124 if (body.channel.name === body.username) {
125 return res.status(400)
126 .json({ error: 'Channel name cannot be the same as user username.' })
127 }
128
129 const existing = await ActorModel.loadLocalByName(body.channel.name)
130 if (existing) {
131 return res.status(409)
132 .json({ error: `Channel with name ${body.channel.name} already exists.` })
133 }
134 }
135
136 return next()
137 }
138 ]
139
140 const usersRemoveValidator = [
141 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
142
143 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
144 logger.debug('Checking usersRemove parameters', { parameters: req.params })
145
146 if (areValidationErrors(req, res)) return
147 if (!await checkUserIdExist(req.params.id, res)) return
148
149 const user = res.locals.user
150 if (user.username === 'root') {
151 return res.status(400)
152 .json({ error: 'Cannot remove the root user' })
153 }
154
155 return next()
156 }
157 ]
158
159 const usersBlockingValidator = [
160 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
161 body('reason').optional().custom(isUserBlockedReasonValid).withMessage('Should have a valid blocking reason'),
162
163 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
164 logger.debug('Checking usersBlocking parameters', { parameters: req.params })
165
166 if (areValidationErrors(req, res)) return
167 if (!await checkUserIdExist(req.params.id, res)) return
168
169 const user = res.locals.user
170 if (user.username === 'root') {
171 return res.status(400)
172 .json({ error: 'Cannot block the root user' })
173 }
174
175 return next()
176 }
177 ]
178
179 const deleteMeValidator = [
180 (req: express.Request, res: express.Response, next: express.NextFunction) => {
181 const user = res.locals.oauth.token.User
182 if (user.username === 'root') {
183 return res.status(400)
184 .json({ error: 'You cannot delete your root account.' })
185 .end()
186 }
187
188 return next()
189 }
190 ]
191
192 const usersUpdateValidator = [
193 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
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('role')
200 .optional()
201 .customSanitizer(toIntOrNull)
202 .custom(isUserRoleValid).withMessage('Should have a valid role'),
203 body('adminFlags').optional().custom(isUserAdminFlagsValid).withMessage('Should have a valid admin flags'),
204
205 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
206 logger.debug('Checking usersUpdate parameters', { parameters: req.body })
207
208 if (areValidationErrors(req, res)) return
209 if (!await checkUserIdExist(req.params.id, res)) return
210
211 const user = res.locals.user
212 if (user.username === 'root' && req.body.role !== undefined && user.role !== req.body.role) {
213 return res.status(400)
214 .json({ error: '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.status(400)
270 .json({ error: 'You cannot update your email or password that is associated with an external auth system.' })
271 }
272
273 if (!req.body.currentPassword) {
274 return res.status(400)
275 .json({ error: 'currentPassword parameter is missing.' })
276 }
277
278 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
279 return res.status(401)
280 .json({ error: 'currentPassword is invalid.' })
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 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'),
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.status(403)
332 .json({ error: allowedResult.errorMessage || 'User registration is not enabled or user limit is reached.' })
333 }
334
335 return next()
336 }
337 ]
338
339 const ensureUserRegistrationAllowedForIP = [
340 (req: express.Request, res: express.Response, next: express.NextFunction) => {
341 const allowed = isSignupAllowedForCurrentIP(req.ip)
342
343 if (allowed === false) {
344 return res.status(403)
345 .json({ error: 'You are not on a network authorized for registration.' })
346 }
347
348 return next()
349 }
350 ]
351
352 const usersAskResetPasswordValidator = [
353 body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
354
355 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
356 logger.debug('Checking usersAskResetPassword parameters', { parameters: req.body })
357
358 if (areValidationErrors(req, res)) return
359
360 const exists = await checkUserEmailExist(req.body.email, res, false)
361 if (!exists) {
362 logger.debug('User with email %s does not exist (asking reset password).', req.body.email)
363 // Do not leak our emails
364 return res.status(204).end()
365 }
366
367 return next()
368 }
369 ]
370
371 const usersResetPasswordValidator = [
372 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
373 body('verificationString').not().isEmpty().withMessage('Should have a valid verification string'),
374 body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'),
375
376 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
377 logger.debug('Checking usersResetPassword parameters', { parameters: req.params })
378
379 if (areValidationErrors(req, res)) return
380 if (!await checkUserIdExist(req.params.id, res)) return
381
382 const user = res.locals.user
383 const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id)
384
385 if (redisVerificationString !== req.body.verificationString) {
386 return res
387 .status(403)
388 .json({ error: 'Invalid verification string.' })
389 }
390
391 return next()
392 }
393 ]
394
395 const usersAskSendVerifyEmailValidator = [
396 body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
397
398 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
399 logger.debug('Checking askUsersSendVerifyEmail parameters', { parameters: req.body })
400
401 if (areValidationErrors(req, res)) return
402 const exists = await checkUserEmailExist(req.body.email, res, false)
403 if (!exists) {
404 logger.debug('User with email %s does not exist (asking verify email).', req.body.email)
405 // Do not leak our emails
406 return res.status(204).end()
407 }
408
409 return next()
410 }
411 ]
412
413 const usersVerifyEmailValidator = [
414 param('id')
415 .isInt().not().isEmpty().withMessage('Should have a valid id'),
416
417 body('verificationString')
418 .not().isEmpty().withMessage('Should have a valid verification string'),
419 body('isPendingEmail')
420 .optional()
421 .customSanitizer(toBooleanOrNull),
422
423 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
424 logger.debug('Checking usersVerifyEmail parameters', { parameters: req.params })
425
426 if (areValidationErrors(req, res)) return
427 if (!await checkUserIdExist(req.params.id, res)) return
428
429 const user = res.locals.user
430 const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id)
431
432 if (redisVerificationString !== req.body.verificationString) {
433 return res
434 .status(403)
435 .json({ error: 'Invalid verification string.' })
436 }
437
438 return next()
439 }
440 ]
441
442 const userAutocompleteValidator = [
443 param('search').isString().not().isEmpty().withMessage('Should have a search parameter')
444 ]
445
446 const ensureAuthUserOwnsAccountValidator = [
447 (req: express.Request, res: express.Response, next: express.NextFunction) => {
448 const user = res.locals.oauth.token.User
449
450 if (res.locals.account.id !== user.Account.id) {
451 return res.status(403)
452 .json({ error: 'Only owner can access ratings list.' })
453 }
454
455 return next()
456 }
457 ]
458
459 const ensureCanManageUser = [
460 (req: express.Request, res: express.Response, next: express.NextFunction) => {
461 const authUser = res.locals.oauth.token.User
462 const onUser = res.locals.user
463
464 if (authUser.role === UserRole.ADMINISTRATOR) return next()
465 if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next()
466
467 return res.status(403)
468 .json({ error: 'A moderator can only manager users.' })
469 }
470 ]
471
472 // ---------------------------------------------------------------------------
473
474 export {
475 usersListValidator,
476 usersAddValidator,
477 deleteMeValidator,
478 usersRegisterValidator,
479 usersBlockingValidator,
480 usersRemoveValidator,
481 usersUpdateValidator,
482 usersUpdateMeValidator,
483 usersVideoRatingValidator,
484 ensureUserRegistrationAllowed,
485 ensureUserRegistrationAllowedForIP,
486 usersGetValidator,
487 usersAskResetPasswordValidator,
488 usersResetPasswordValidator,
489 usersAskSendVerifyEmailValidator,
490 usersVerifyEmailValidator,
491 userAutocompleteValidator,
492 ensureAuthUserOwnsAccountValidator,
493 ensureCanManageUser
494 }
495
496 // ---------------------------------------------------------------------------
497
498 function checkUserIdExist (idArg: number | string, res: express.Response, withStats = false) {
499 const id = parseInt(idArg + '', 10)
500 return checkUserExist(() => UserModel.loadByIdWithChannels(id, withStats), res)
501 }
502
503 function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) {
504 return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse)
505 }
506
507 async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email: string, res: express.Response) {
508 const user = await UserModel.loadByUsernameOrEmail(username, email)
509
510 if (user) {
511 res.status(409)
512 .json({ error: 'User with this username or email already exists.' })
513 return false
514 }
515
516 const actor = await ActorModel.loadLocalByName(username)
517 if (actor) {
518 res.status(409)
519 .json({ error: 'Another actor (account/channel) with this name on this instance already exists or has already existed.' })
520 return false
521 }
522
523 return true
524 }
525
526 async function checkUserExist (finder: () => Bluebird<MUserDefault>, res: express.Response, abortResponse = true) {
527 const user = await finder()
528
529 if (!user) {
530 if (abortResponse === true) {
531 res.status(404)
532 .json({ error: 'User not found' })
533 }
534
535 return false
536 }
537
538 res.locals.user = user
539
540 return true
541 }