]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/middlewares/validators/users.ts
Add ability to view my followers
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / users.ts
1 import 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, UserRegister, UserRole } from '@shared/models'
7 import { toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
8 import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
9 import {
10 isUserAdminFlagsValid,
11 isUserAutoPlayNextVideoValid,
12 isUserAutoPlayVideoValid,
13 isUserBlockedReasonValid,
14 isUserDescriptionValid,
15 isUserDisplayNameValid,
16 isUserNoModal,
17 isUserNSFWPolicyValid,
18 isUserPasswordValid,
19 isUserPasswordValidOrEmpty,
20 isUserRoleValid,
21 isUserUsernameValid,
22 isUserVideoLanguages,
23 isUserVideoQuotaDailyValid,
24 isUserVideoQuotaValid,
25 isUserVideosHistoryEnabledValid
26 } from '../../helpers/custom-validators/users'
27 import { isVideoChannelDisplayNameValid, isVideoChannelUsernameValid } from '../../helpers/custom-validators/video-channels'
28 import { logger } from '../../helpers/logger'
29 import { isThemeRegistered } from '../../lib/plugins/theme-utils'
30 import { Redis } from '../../lib/redis'
31 import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../lib/signup'
32 import { ActorModel } from '../../models/actor/actor'
33 import { UserModel } from '../../models/user/user'
34 import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from './shared'
35
36 const usersListValidator = [
37 query('blocked')
38 .optional()
39 .customSanitizer(toBooleanOrNull)
40 .isBoolean().withMessage('Should be a valid boolean banned state'),
41
42 (req: express.Request, res: express.Response, next: express.NextFunction) => {
43 logger.debug('Checking usersList parameters', { parameters: req.query })
44
45 if (areValidationErrors(req, res)) return
46
47 return next()
48 }
49 ]
50
51 const usersAddValidator = [
52 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'),
53 body('password').custom(isUserPasswordValidOrEmpty).withMessage('Should have a valid password'),
54 body('email').isEmail().withMessage('Should have a valid email'),
55
56 body('channelName').optional().custom(isVideoChannelUsernameValid).withMessage('Should have a valid channel name'),
57
58 body('videoQuota').custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'),
59 body('videoQuotaDaily').custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'),
60
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.fail({
75 status: HttpStatusCode.FORBIDDEN_403,
76 message: 'You can only create users (and not administrators or moderators)'
77 })
78 }
79
80 if (req.body.channelName) {
81 if (req.body.channelName === req.body.username) {
82 return res.fail({ message: '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.fail({
88 status: HttpStatusCode.CONFLICT_409,
89 message: `Channel with name ${req.body.channelName} already exists.`
90 })
91 }
92 }
93
94 return next()
95 }
96 ]
97
98 const usersRegisterValidator = [
99 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username'),
100 body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'),
101 body('email').isEmail().withMessage('Should have a valid email'),
102 body('displayName')
103 .optional()
104 .custom(isUserDisplayNameValid).withMessage('Should have a valid display name'),
105
106 body('channel.name')
107 .optional()
108 .custom(isVideoChannelUsernameValid).withMessage('Should have a valid channel name'),
109 body('channel.displayName')
110 .optional()
111 .custom(isVideoChannelDisplayNameValid).withMessage('Should have a valid display name'),
112
113 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
114 logger.debug('Checking usersRegister parameters', { parameters: omit(req.body, 'password') })
115
116 if (areValidationErrors(req, res)) return
117 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
118
119 const body: UserRegister = req.body
120 if (body.channel) {
121 if (!body.channel.name || !body.channel.displayName) {
122 return res.fail({ message: '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.fail({ message: '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.fail({
132 status: HttpStatusCode.CONFLICT_409,
133 message: `Channel with name ${body.channel.name} already exists.`
134 })
135 }
136 }
137
138 return next()
139 }
140 ]
141
142 const usersRemoveValidator = [
143 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
144
145 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
146 logger.debug('Checking usersRemove parameters', { parameters: req.params })
147
148 if (areValidationErrors(req, res)) return
149 if (!await checkUserIdExist(req.params.id, res)) return
150
151 const user = res.locals.user
152 if (user.username === 'root') {
153 return res.fail({ message: '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.fail({ message: '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.fail({ message: 'You cannot delete your root account.' })
184 }
185
186 return next()
187 }
188 ]
189
190 const usersUpdateValidator = [
191 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
192
193 body('password').optional().custom(isUserPasswordValid).withMessage('Should have a valid password'),
194 body('email').optional().isEmail().withMessage('Should have a valid email attribute'),
195 body('emailVerified').optional().isBoolean().withMessage('Should have a valid email verified attribute'),
196 body('videoQuota').optional().custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'),
197 body('videoQuotaDaily').optional().custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'),
198 body('pluginAuth').optional(),
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.fail({ message: 'Cannot change root role.' })
214 }
215
216 return next()
217 }
218 ]
219
220 const usersUpdateMeValidator = [
221 body('displayName')
222 .optional()
223 .custom(isUserDisplayNameValid).withMessage('Should have a valid display name'),
224 body('description')
225 .optional()
226 .custom(isUserDescriptionValid).withMessage('Should have a valid description'),
227 body('currentPassword')
228 .optional()
229 .custom(isUserPasswordValid).withMessage('Should have a valid current password'),
230 body('password')
231 .optional()
232 .custom(isUserPasswordValid).withMessage('Should have a valid password'),
233 body('email')
234 .optional()
235 .isEmail().withMessage('Should have a valid email attribute'),
236 body('nsfwPolicy')
237 .optional()
238 .custom(isUserNSFWPolicyValid).withMessage('Should have a valid display Not Safe For Work policy'),
239 body('autoPlayVideo')
240 .optional()
241 .custom(isUserAutoPlayVideoValid).withMessage('Should have a valid automatically plays video attribute'),
242 body('videoLanguages')
243 .optional()
244 .custom(isUserVideoLanguages).withMessage('Should have a valid video languages attribute'),
245 body('videosHistoryEnabled')
246 .optional()
247 .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled attribute'),
248 body('theme')
249 .optional()
250 .custom(v => isThemeNameValid(v) && isThemeRegistered(v)).withMessage('Should have a valid theme'),
251
252 body('noInstanceConfigWarningModal')
253 .optional()
254 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noInstanceConfigWarningModal boolean'),
255 body('noWelcomeModal')
256 .optional()
257 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noWelcomeModal boolean'),
258 body('noAccountSetupWarningModal')
259 .optional()
260 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noAccountSetupWarningModal boolean'),
261
262 body('autoPlayNextVideo')
263 .optional()
264 .custom(v => isUserAutoPlayNextVideoValid(v)).withMessage('Should have a valid autoPlayNextVideo boolean'),
265
266 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
267 logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') })
268
269 const user = res.locals.oauth.token.User
270
271 if (req.body.password || req.body.email) {
272 if (user.pluginAuth !== null) {
273 return res.fail({ message: 'You cannot update your email or password that is associated with an external auth system.' })
274 }
275
276 if (!req.body.currentPassword) {
277 return res.fail({ message: 'currentPassword parameter is missing.' })
278 }
279
280 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
281 return res.fail({
282 status: HttpStatusCode.UNAUTHORIZED_401,
283 message: 'currentPassword is invalid.'
284 })
285 }
286 }
287
288 if (areValidationErrors(req, res)) return
289
290 return next()
291 }
292 ]
293
294 const usersGetValidator = [
295 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
296 query('withStats').optional().isBoolean().withMessage('Should have a valid stats flag'),
297
298 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
299 logger.debug('Checking usersGet parameters', { parameters: req.params })
300
301 if (areValidationErrors(req, res)) return
302 if (!await checkUserIdExist(req.params.id, res, req.query.withStats)) return
303
304 return next()
305 }
306 ]
307
308 const usersVideoRatingValidator = [
309 isValidVideoIdParam('videoId'),
310
311 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
312 logger.debug('Checking usersVideoRating parameters', { parameters: req.params })
313
314 if (areValidationErrors(req, res)) return
315 if (!await doesVideoExist(req.params.videoId, res, 'id')) return
316
317 return next()
318 }
319 ]
320
321 const ensureUserRegistrationAllowed = [
322 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
323 const allowedParams = {
324 body: req.body,
325 ip: req.ip
326 }
327
328 const allowedResult = await Hooks.wrapPromiseFun(
329 isSignupAllowed,
330 allowedParams,
331 'filter:api.user.signup.allowed.result'
332 )
333
334 if (allowedResult.allowed === false) {
335 return res.fail({
336 status: HttpStatusCode.FORBIDDEN_403,
337 message: allowedResult.errorMessage || 'User registration is not enabled or user limit is reached.'
338 })
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.fail({
351 status: HttpStatusCode.FORBIDDEN_403,
352 message: 'You are not on a network authorized for registration.'
353 })
354 }
355
356 return next()
357 }
358 ]
359
360 const usersAskResetPasswordValidator = [
361 body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
362
363 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
364 logger.debug('Checking usersAskResetPassword parameters', { parameters: req.body })
365
366 if (areValidationErrors(req, res)) return
367
368 const exists = await checkUserEmailExist(req.body.email, res, false)
369 if (!exists) {
370 logger.debug('User with email %s does not exist (asking reset password).', req.body.email)
371 // Do not leak our emails
372 return res.status(HttpStatusCode.NO_CONTENT_204).end()
373 }
374
375 return next()
376 }
377 ]
378
379 const usersResetPasswordValidator = [
380 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
381 body('verificationString').not().isEmpty().withMessage('Should have a valid verification string'),
382 body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'),
383
384 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
385 logger.debug('Checking usersResetPassword parameters', { parameters: req.params })
386
387 if (areValidationErrors(req, res)) return
388 if (!await checkUserIdExist(req.params.id, res)) return
389
390 const user = res.locals.user
391 const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id)
392
393 if (redisVerificationString !== req.body.verificationString) {
394 return res.fail({
395 status: HttpStatusCode.FORBIDDEN_403,
396 message: 'Invalid verification string.'
397 })
398 }
399
400 return next()
401 }
402 ]
403
404 const usersAskSendVerifyEmailValidator = [
405 body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
406
407 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
408 logger.debug('Checking askUsersSendVerifyEmail parameters', { parameters: req.body })
409
410 if (areValidationErrors(req, res)) return
411 const exists = await checkUserEmailExist(req.body.email, res, false)
412 if (!exists) {
413 logger.debug('User with email %s does not exist (asking verify email).', req.body.email)
414 // Do not leak our emails
415 return res.status(HttpStatusCode.NO_CONTENT_204).end()
416 }
417
418 return next()
419 }
420 ]
421
422 const usersVerifyEmailValidator = [
423 param('id')
424 .isInt().not().isEmpty().withMessage('Should have a valid id'),
425
426 body('verificationString')
427 .not().isEmpty().withMessage('Should have a valid verification string'),
428 body('isPendingEmail')
429 .optional()
430 .customSanitizer(toBooleanOrNull),
431
432 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
433 logger.debug('Checking usersVerifyEmail parameters', { parameters: req.params })
434
435 if (areValidationErrors(req, res)) return
436 if (!await checkUserIdExist(req.params.id, res)) return
437
438 const user = res.locals.user
439 const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id)
440
441 if (redisVerificationString !== req.body.verificationString) {
442 return res.fail({
443 status: HttpStatusCode.FORBIDDEN_403,
444 message: 'Invalid verification string.'
445 })
446 }
447
448 return next()
449 }
450 ]
451
452 const userAutocompleteValidator = [
453 param('search').isString().not().isEmpty().withMessage('Should have a search parameter')
454 ]
455
456 const ensureAuthUserOwnsAccountValidator = [
457 (req: express.Request, res: express.Response, next: express.NextFunction) => {
458 const user = res.locals.oauth.token.User
459
460 if (res.locals.account.id !== user.Account.id) {
461 return res.fail({
462 status: HttpStatusCode.FORBIDDEN_403,
463 message: 'Only owner of this account can access this ressource.'
464 })
465 }
466
467 return next()
468 }
469 ]
470
471 const ensureAuthUserOwnsChannelValidator = [
472 (req: express.Request, res: express.Response, next: express.NextFunction) => {
473 const user = res.locals.oauth.token.User
474
475 if (res.locals.videoChannel.Account.userId !== user.id) {
476 return res.fail({
477 status: HttpStatusCode.FORBIDDEN_403,
478 message: 'Only owner of this video channel can access this ressource'
479 })
480 }
481
482 return next()
483 }
484 ]
485
486 const ensureCanManageUser = [
487 (req: express.Request, res: express.Response, next: express.NextFunction) => {
488 const authUser = res.locals.oauth.token.User
489 const onUser = res.locals.user
490
491 if (authUser.role === UserRole.ADMINISTRATOR) return next()
492 if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next()
493
494 return res.fail({
495 status: HttpStatusCode.FORBIDDEN_403,
496 message: 'A moderator can only manager users.'
497 })
498 }
499 ]
500
501 // ---------------------------------------------------------------------------
502
503 export {
504 usersListValidator,
505 usersAddValidator,
506 deleteMeValidator,
507 usersRegisterValidator,
508 usersBlockingValidator,
509 usersRemoveValidator,
510 usersUpdateValidator,
511 usersUpdateMeValidator,
512 usersVideoRatingValidator,
513 ensureUserRegistrationAllowed,
514 ensureUserRegistrationAllowedForIP,
515 usersGetValidator,
516 usersAskResetPasswordValidator,
517 usersResetPasswordValidator,
518 usersAskSendVerifyEmailValidator,
519 usersVerifyEmailValidator,
520 userAutocompleteValidator,
521 ensureAuthUserOwnsAccountValidator,
522 ensureAuthUserOwnsChannelValidator,
523 ensureCanManageUser
524 }
525
526 // ---------------------------------------------------------------------------
527
528 function checkUserIdExist (idArg: number | string, res: express.Response, withStats = false) {
529 const id = parseInt(idArg + '', 10)
530 return checkUserExist(() => UserModel.loadByIdWithChannels(id, withStats), res)
531 }
532
533 function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) {
534 return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse)
535 }
536
537 async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email: string, res: express.Response) {
538 const user = await UserModel.loadByUsernameOrEmail(username, email)
539
540 if (user) {
541 res.fail({
542 status: HttpStatusCode.CONFLICT_409,
543 message: 'User with this username or email already exists.'
544 })
545 return false
546 }
547
548 const actor = await ActorModel.loadLocalByName(username)
549 if (actor) {
550 res.fail({
551 status: HttpStatusCode.CONFLICT_409,
552 message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.'
553 })
554 return false
555 }
556
557 return true
558 }
559
560 async function checkUserExist (finder: () => Promise<MUserDefault>, res: express.Response, abortResponse = true) {
561 const user = await finder()
562
563 if (!user) {
564 if (abortResponse === true) {
565 res.fail({
566 status: HttpStatusCode.NOT_FOUND_404,
567 message: 'User not found'
568 })
569 }
570
571 return false
572 }
573
574 res.locals.user = user
575 return true
576 }