]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/middlewares/validators/users.ts
Add a test for refreshing remote keys
[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('pluginAuth').optional(),
205 body('role')
206 .optional()
207 .customSanitizer(toIntOrNull)
208 .custom(isUserRoleValid).withMessage('Should have a valid role'),
209 body('adminFlags').optional().custom(isUserAdminFlagsValid).withMessage('Should have a valid admin flags'),
210
211 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
212 logger.debug('Checking usersUpdate parameters', { parameters: req.body })
213
214 if (areValidationErrors(req, res)) return
215 if (!await checkUserIdExist(req.params.id, res)) return
216
217 const user = res.locals.user
218 if (user.username === 'root' && req.body.role !== undefined && user.role !== req.body.role) {
219 return res.status(HttpStatusCode.BAD_REQUEST_400)
220 .json({ error: 'Cannot change root role.' })
221 }
222
223 return next()
224 }
225 ]
226
227 const usersUpdateMeValidator = [
228 body('displayName')
229 .optional()
230 .custom(isUserDisplayNameValid).withMessage('Should have a valid display name'),
231 body('description')
232 .optional()
233 .custom(isUserDescriptionValid).withMessage('Should have a valid description'),
234 body('currentPassword')
235 .optional()
236 .custom(isUserPasswordValid).withMessage('Should have a valid current password'),
237 body('password')
238 .optional()
239 .custom(isUserPasswordValid).withMessage('Should have a valid password'),
240 body('email')
241 .optional()
242 .isEmail().withMessage('Should have a valid email attribute'),
243 body('nsfwPolicy')
244 .optional()
245 .custom(isUserNSFWPolicyValid).withMessage('Should have a valid display Not Safe For Work policy'),
246 body('autoPlayVideo')
247 .optional()
248 .custom(isUserAutoPlayVideoValid).withMessage('Should have a valid automatically plays video attribute'),
249 body('videoLanguages')
250 .optional()
251 .custom(isUserVideoLanguages).withMessage('Should have a valid video languages attribute'),
252 body('videosHistoryEnabled')
253 .optional()
254 .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled attribute'),
255 body('theme')
256 .optional()
257 .custom(v => isThemeNameValid(v) && isThemeRegistered(v)).withMessage('Should have a valid theme'),
258 body('noInstanceConfigWarningModal')
259 .optional()
260 .custom(v => isNoInstanceConfigWarningModal(v)).withMessage('Should have a valid noInstanceConfigWarningModal boolean'),
261 body('noWelcomeModal')
262 .optional()
263 .custom(v => isNoWelcomeModal(v)).withMessage('Should have a valid noWelcomeModal boolean'),
264 body('autoPlayNextVideo')
265 .optional()
266 .custom(v => isUserAutoPlayNextVideoValid(v)).withMessage('Should have a valid autoPlayNextVideo boolean'),
267
268 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
269 logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') })
270
271 const user = res.locals.oauth.token.User
272
273 if (req.body.password || req.body.email) {
274 if (user.pluginAuth !== null) {
275 return res.status(HttpStatusCode.BAD_REQUEST_400)
276 .json({ error: 'You cannot update your email or password that is associated with an external auth system.' })
277 }
278
279 if (!req.body.currentPassword) {
280 return res.status(HttpStatusCode.BAD_REQUEST_400)
281 .json({ error: 'currentPassword parameter is missing.' })
282 }
283
284 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
285 return res.status(HttpStatusCode.UNAUTHORIZED_401)
286 .json({ error: 'currentPassword is invalid.' })
287 }
288 }
289
290 if (areValidationErrors(req, res)) return
291
292 return next()
293 }
294 ]
295
296 const usersGetValidator = [
297 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
298 query('withStats').optional().isBoolean().withMessage('Should have a valid stats flag'),
299
300 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
301 logger.debug('Checking usersGet parameters', { parameters: req.params })
302
303 if (areValidationErrors(req, res)) return
304 if (!await checkUserIdExist(req.params.id, res, req.query.withStats)) return
305
306 return next()
307 }
308 ]
309
310 const usersVideoRatingValidator = [
311 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'),
312
313 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
314 logger.debug('Checking usersVideoRating parameters', { parameters: req.params })
315
316 if (areValidationErrors(req, res)) return
317 if (!await doesVideoExist(req.params.videoId, res, 'id')) return
318
319 return next()
320 }
321 ]
322
323 const ensureUserRegistrationAllowed = [
324 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
325 const allowedParams = {
326 body: req.body,
327 ip: req.ip
328 }
329
330 const allowedResult = await Hooks.wrapPromiseFun(
331 isSignupAllowed,
332 allowedParams,
333 'filter:api.user.signup.allowed.result'
334 )
335
336 if (allowedResult.allowed === false) {
337 return res.status(HttpStatusCode.FORBIDDEN_403)
338 .json({ error: allowedResult.errorMessage || 'User registration is not enabled or user limit is reached.' })
339 }
340
341 return next()
342 }
343 ]
344
345 const ensureUserRegistrationAllowedForIP = [
346 (req: express.Request, res: express.Response, next: express.NextFunction) => {
347 const allowed = isSignupAllowedForCurrentIP(req.ip)
348
349 if (allowed === false) {
350 return res.status(HttpStatusCode.FORBIDDEN_403)
351 .json({ error: 'You are not on a network authorized for registration.' })
352 }
353
354 return next()
355 }
356 ]
357
358 const usersAskResetPasswordValidator = [
359 body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
360
361 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
362 logger.debug('Checking usersAskResetPassword parameters', { parameters: req.body })
363
364 if (areValidationErrors(req, res)) return
365
366 const exists = await checkUserEmailExist(req.body.email, res, false)
367 if (!exists) {
368 logger.debug('User with email %s does not exist (asking reset password).', req.body.email)
369 // Do not leak our emails
370 return res.status(HttpStatusCode.NO_CONTENT_204).end()
371 }
372
373 return next()
374 }
375 ]
376
377 const usersResetPasswordValidator = [
378 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
379 body('verificationString').not().isEmpty().withMessage('Should have a valid verification string'),
380 body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'),
381
382 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
383 logger.debug('Checking usersResetPassword parameters', { parameters: req.params })
384
385 if (areValidationErrors(req, res)) return
386 if (!await checkUserIdExist(req.params.id, res)) return
387
388 const user = res.locals.user
389 const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id)
390
391 if (redisVerificationString !== req.body.verificationString) {
392 return res
393 .status(HttpStatusCode.FORBIDDEN_403)
394 .json({ error: 'Invalid verification string.' })
395 }
396
397 return next()
398 }
399 ]
400
401 const usersAskSendVerifyEmailValidator = [
402 body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
403
404 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
405 logger.debug('Checking askUsersSendVerifyEmail parameters', { parameters: req.body })
406
407 if (areValidationErrors(req, res)) return
408 const exists = await checkUserEmailExist(req.body.email, res, false)
409 if (!exists) {
410 logger.debug('User with email %s does not exist (asking verify email).', req.body.email)
411 // Do not leak our emails
412 return res.status(HttpStatusCode.NO_CONTENT_204).end()
413 }
414
415 return next()
416 }
417 ]
418
419 const usersVerifyEmailValidator = [
420 param('id')
421 .isInt().not().isEmpty().withMessage('Should have a valid id'),
422
423 body('verificationString')
424 .not().isEmpty().withMessage('Should have a valid verification string'),
425 body('isPendingEmail')
426 .optional()
427 .customSanitizer(toBooleanOrNull),
428
429 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
430 logger.debug('Checking usersVerifyEmail parameters', { parameters: req.params })
431
432 if (areValidationErrors(req, res)) return
433 if (!await checkUserIdExist(req.params.id, res)) return
434
435 const user = res.locals.user
436 const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id)
437
438 if (redisVerificationString !== req.body.verificationString) {
439 return res
440 .status(HttpStatusCode.FORBIDDEN_403)
441 .json({ error: 'Invalid verification string.' })
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.status(HttpStatusCode.FORBIDDEN_403)
458 .json({ error: 'Only owner can access ratings list.' })
459 }
460
461 return next()
462 }
463 ]
464
465 const ensureCanManageUser = [
466 (req: express.Request, res: express.Response, next: express.NextFunction) => {
467 const authUser = res.locals.oauth.token.User
468 const onUser = res.locals.user
469
470 if (authUser.role === UserRole.ADMINISTRATOR) return next()
471 if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next()
472
473 return res.status(HttpStatusCode.FORBIDDEN_403)
474 .json({ error: 'A moderator can only manager users.' })
475 }
476 ]
477
478 // ---------------------------------------------------------------------------
479
480 export {
481 usersListValidator,
482 usersAddValidator,
483 deleteMeValidator,
484 usersRegisterValidator,
485 usersBlockingValidator,
486 usersRemoveValidator,
487 usersUpdateValidator,
488 usersUpdateMeValidator,
489 usersVideoRatingValidator,
490 ensureUserRegistrationAllowed,
491 ensureUserRegistrationAllowedForIP,
492 usersGetValidator,
493 usersAskResetPasswordValidator,
494 usersResetPasswordValidator,
495 usersAskSendVerifyEmailValidator,
496 usersVerifyEmailValidator,
497 userAutocompleteValidator,
498 ensureAuthUserOwnsAccountValidator,
499 ensureCanManageUser
500 }
501
502 // ---------------------------------------------------------------------------
503
504 function checkUserIdExist (idArg: number | string, res: express.Response, withStats = false) {
505 const id = parseInt(idArg + '', 10)
506 return checkUserExist(() => UserModel.loadByIdWithChannels(id, withStats), res)
507 }
508
509 function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) {
510 return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse)
511 }
512
513 async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email: string, res: express.Response) {
514 const user = await UserModel.loadByUsernameOrEmail(username, email)
515
516 if (user) {
517 res.status(HttpStatusCode.CONFLICT_409)
518 .json({ error: 'User with this username or email already exists.' })
519 return false
520 }
521
522 const actor = await ActorModel.loadLocalByName(username)
523 if (actor) {
524 res.status(HttpStatusCode.CONFLICT_409)
525 .json({ error: 'Another actor (account/channel) with this name on this instance already exists or has already existed.' })
526 return false
527 }
528
529 return true
530 }
531
532 async function checkUserExist (finder: () => Promise<MUserDefault>, res: express.Response, abortResponse = true) {
533 const user = await finder()
534
535 if (!user) {
536 if (abortResponse === true) {
537 res.status(HttpStatusCode.NOT_FOUND_404)
538 .json({ error: 'User not found' })
539 }
540
541 return false
542 }
543
544 res.locals.user = user
545
546 return true
547 }