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