]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/middlewares/validators/users.ts
Bumped to version v5.2.1
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / users.ts
1 import express from 'express'
2 import { body, param, query } from 'express-validator'
3 import { forceNumber } from '@shared/core-utils'
4 import { HttpStatusCode, UserRight, UserRole } from '@shared/models'
5 import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
6 import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
7 import {
8 isUserAdminFlagsValid,
9 isUserAutoPlayNextVideoValid,
10 isUserAutoPlayVideoValid,
11 isUserBlockedReasonValid,
12 isUserDescriptionValid,
13 isUserDisplayNameValid,
14 isUserEmailPublicValid,
15 isUserNoModal,
16 isUserNSFWPolicyValid,
17 isUserP2PEnabledValid,
18 isUserPasswordValid,
19 isUserPasswordValidOrEmpty,
20 isUserRoleValid,
21 isUserUsernameValid,
22 isUserVideoLanguages,
23 isUserVideoQuotaDailyValid,
24 isUserVideoQuotaValid,
25 isUserVideosHistoryEnabledValid
26 } from '../../helpers/custom-validators/users'
27 import { 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 { ActorModel } from '../../models/actor/actor'
32 import {
33 areValidationErrors,
34 checkUserEmailExist,
35 checkUserIdExist,
36 checkUserNameOrEmailDoNotAlreadyExist,
37 doesVideoChannelIdExist,
38 doesVideoExist,
39 isValidVideoIdParam
40 } from './shared'
41
42 const usersListValidator = [
43 query('blocked')
44 .optional()
45 .customSanitizer(toBooleanOrNull)
46 .isBoolean().withMessage('Should be a valid blocked boolean'),
47
48 (req: express.Request, res: express.Response, next: express.NextFunction) => {
49 if (areValidationErrors(req, res)) return
50
51 return next()
52 }
53 ]
54
55 const usersAddValidator = [
56 body('username')
57 .custom(isUserUsernameValid)
58 .withMessage('Should have a valid username (lowercase alphanumeric characters)'),
59 body('password')
60 .custom(isUserPasswordValidOrEmpty),
61 body('email')
62 .isEmail(),
63
64 body('channelName')
65 .optional()
66 .custom(isVideoChannelUsernameValid),
67
68 body('videoQuota')
69 .optional()
70 .custom(isUserVideoQuotaValid),
71
72 body('videoQuotaDaily')
73 .optional()
74 .custom(isUserVideoQuotaDailyValid),
75
76 body('role')
77 .customSanitizer(toIntOrNull)
78 .custom(isUserRoleValid),
79
80 body('adminFlags')
81 .optional()
82 .custom(isUserAdminFlagsValid),
83
84 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
85 if (areValidationErrors(req, res, { omitBodyLog: true })) return
86 if (!await checkUserNameOrEmailDoNotAlreadyExist(req.body.username, req.body.email, res)) return
87
88 const authUser = res.locals.oauth.token.User
89 if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) {
90 return res.fail({
91 status: HttpStatusCode.FORBIDDEN_403,
92 message: 'You can only create users (and not administrators or moderators)'
93 })
94 }
95
96 if (req.body.channelName) {
97 if (req.body.channelName === req.body.username) {
98 return res.fail({ message: 'Channel name cannot be the same as user username.' })
99 }
100
101 const existing = await ActorModel.loadLocalByName(req.body.channelName)
102 if (existing) {
103 return res.fail({
104 status: HttpStatusCode.CONFLICT_409,
105 message: `Channel with name ${req.body.channelName} already exists.`
106 })
107 }
108 }
109
110 return next()
111 }
112 ]
113
114 const usersRemoveValidator = [
115 param('id')
116 .custom(isIdValid),
117
118 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
119 if (areValidationErrors(req, res)) return
120 if (!await checkUserIdExist(req.params.id, res)) return
121
122 const user = res.locals.user
123 if (user.username === 'root') {
124 return res.fail({ message: 'Cannot remove the root user' })
125 }
126
127 return next()
128 }
129 ]
130
131 const usersBlockingValidator = [
132 param('id')
133 .custom(isIdValid),
134 body('reason')
135 .optional()
136 .custom(isUserBlockedReasonValid),
137
138 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
139 if (areValidationErrors(req, res)) return
140 if (!await checkUserIdExist(req.params.id, res)) return
141
142 const user = res.locals.user
143 if (user.username === 'root') {
144 return res.fail({ message: 'Cannot block the root user' })
145 }
146
147 return next()
148 }
149 ]
150
151 const deleteMeValidator = [
152 (req: express.Request, res: express.Response, next: express.NextFunction) => {
153 const user = res.locals.oauth.token.User
154 if (user.username === 'root') {
155 return res.fail({ message: 'You cannot delete your root account.' })
156 }
157
158 return next()
159 }
160 ]
161
162 const usersUpdateValidator = [
163 param('id').custom(isIdValid),
164
165 body('password')
166 .optional()
167 .custom(isUserPasswordValid),
168 body('email')
169 .optional()
170 .isEmail(),
171 body('emailVerified')
172 .optional()
173 .isBoolean(),
174 body('videoQuota')
175 .optional()
176 .custom(isUserVideoQuotaValid),
177 body('videoQuotaDaily')
178 .optional()
179 .custom(isUserVideoQuotaDailyValid),
180 body('pluginAuth')
181 .optional()
182 .exists(),
183 body('role')
184 .optional()
185 .customSanitizer(toIntOrNull)
186 .custom(isUserRoleValid),
187 body('adminFlags')
188 .optional()
189 .custom(isUserAdminFlagsValid),
190
191 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
192 if (areValidationErrors(req, res, { omitBodyLog: true })) return
193 if (!await checkUserIdExist(req.params.id, res)) return
194
195 const user = res.locals.user
196 if (user.username === 'root' && req.body.role !== undefined && user.role !== req.body.role) {
197 return res.fail({ message: 'Cannot change root role.' })
198 }
199
200 return next()
201 }
202 ]
203
204 const usersUpdateMeValidator = [
205 body('displayName')
206 .optional()
207 .custom(isUserDisplayNameValid),
208 body('description')
209 .optional()
210 .custom(isUserDescriptionValid),
211 body('currentPassword')
212 .optional()
213 .custom(isUserPasswordValid),
214 body('password')
215 .optional()
216 .custom(isUserPasswordValid),
217 body('emailPublic')
218 .optional()
219 .custom(isUserEmailPublicValid),
220 body('email')
221 .optional()
222 .isEmail(),
223 body('nsfwPolicy')
224 .optional()
225 .custom(isUserNSFWPolicyValid),
226 body('autoPlayVideo')
227 .optional()
228 .custom(isUserAutoPlayVideoValid),
229 body('p2pEnabled')
230 .optional()
231 .custom(isUserP2PEnabledValid).withMessage('Should have a valid p2p enabled boolean'),
232 body('videoLanguages')
233 .optional()
234 .custom(isUserVideoLanguages),
235 body('videosHistoryEnabled')
236 .optional()
237 .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled boolean'),
238 body('theme')
239 .optional()
240 .custom(v => isThemeNameValid(v) && isThemeRegistered(v)),
241
242 body('noInstanceConfigWarningModal')
243 .optional()
244 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noInstanceConfigWarningModal boolean'),
245 body('noWelcomeModal')
246 .optional()
247 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noWelcomeModal boolean'),
248 body('noAccountSetupWarningModal')
249 .optional()
250 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noAccountSetupWarningModal boolean'),
251
252 body('autoPlayNextVideo')
253 .optional()
254 .custom(v => isUserAutoPlayNextVideoValid(v)).withMessage('Should have a valid autoPlayNextVideo boolean'),
255
256 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
257 const user = res.locals.oauth.token.User
258
259 if (req.body.password || req.body.email) {
260 if (user.pluginAuth !== null) {
261 return res.fail({ message: 'You cannot update your email or password that is associated with an external auth system.' })
262 }
263
264 if (!req.body.currentPassword) {
265 return res.fail({ message: 'currentPassword parameter is missing.' })
266 }
267
268 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
269 return res.fail({
270 status: HttpStatusCode.UNAUTHORIZED_401,
271 message: 'currentPassword is invalid.'
272 })
273 }
274 }
275
276 if (areValidationErrors(req, res, { omitBodyLog: true })) return
277
278 return next()
279 }
280 ]
281
282 const usersGetValidator = [
283 param('id')
284 .custom(isIdValid),
285 query('withStats')
286 .optional()
287 .isBoolean().withMessage('Should have a valid withStats boolean'),
288
289 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
290 if (areValidationErrors(req, res)) return
291 if (!await checkUserIdExist(req.params.id, res, req.query.withStats)) return
292
293 return next()
294 }
295 ]
296
297 const usersVideoRatingValidator = [
298 isValidVideoIdParam('videoId'),
299
300 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
301 if (areValidationErrors(req, res)) return
302 if (!await doesVideoExist(req.params.videoId, res, 'id')) return
303
304 return next()
305 }
306 ]
307
308 const usersVideosValidator = [
309 query('isLive')
310 .optional()
311 .customSanitizer(toBooleanOrNull)
312 .custom(isBooleanValid).withMessage('Should have a valid isLive boolean'),
313
314 query('channelId')
315 .optional()
316 .customSanitizer(toIntOrNull)
317 .custom(isIdValid),
318
319 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
320 if (areValidationErrors(req, res)) return
321
322 if (req.query.channelId && !await doesVideoChannelIdExist(req.query.channelId, res)) return
323
324 return next()
325 }
326 ]
327
328 const usersAskResetPasswordValidator = [
329 body('email')
330 .isEmail(),
331
332 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
333 if (areValidationErrors(req, res)) return
334
335 const exists = await checkUserEmailExist(req.body.email, res, false)
336 if (!exists) {
337 logger.debug('User with email %s does not exist (asking reset password).', req.body.email)
338 // Do not leak our emails
339 return res.status(HttpStatusCode.NO_CONTENT_204).end()
340 }
341
342 if (res.locals.user.pluginAuth) {
343 return res.fail({
344 status: HttpStatusCode.CONFLICT_409,
345 message: 'Cannot recover password of a user that uses a plugin authentication.'
346 })
347 }
348
349 return next()
350 }
351 ]
352
353 const usersResetPasswordValidator = [
354 param('id')
355 .custom(isIdValid),
356 body('verificationString')
357 .not().isEmpty(),
358 body('password')
359 .custom(isUserPasswordValid),
360
361 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
362 if (areValidationErrors(req, res)) return
363 if (!await checkUserIdExist(req.params.id, res)) return
364
365 const user = res.locals.user
366 const redisVerificationString = await Redis.Instance.getResetPasswordVerificationString(user.id)
367
368 if (redisVerificationString !== req.body.verificationString) {
369 return res.fail({
370 status: HttpStatusCode.FORBIDDEN_403,
371 message: 'Invalid verification string.'
372 })
373 }
374
375 return next()
376 }
377 ]
378
379 const usersCheckCurrentPasswordFactory = (targetUserIdGetter: (req: express.Request) => number | string) => {
380 return [
381 body('currentPassword').optional().custom(exists),
382
383 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
384 if (areValidationErrors(req, res)) return
385
386 const user = res.locals.oauth.token.User
387 const isAdminOrModerator = user.role === UserRole.ADMINISTRATOR || user.role === UserRole.MODERATOR
388 const targetUserId = forceNumber(targetUserIdGetter(req))
389
390 // Admin/moderator action on another user, skip the password check
391 if (isAdminOrModerator && targetUserId !== user.id) {
392 return next()
393 }
394
395 if (!req.body.currentPassword) {
396 return res.fail({
397 status: HttpStatusCode.BAD_REQUEST_400,
398 message: 'currentPassword is missing'
399 })
400 }
401
402 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
403 return res.fail({
404 status: HttpStatusCode.FORBIDDEN_403,
405 message: 'currentPassword is invalid.'
406 })
407 }
408
409 return next()
410 }
411 ]
412 }
413
414 const userAutocompleteValidator = [
415 param('search')
416 .isString()
417 .not().isEmpty()
418 ]
419
420 const ensureAuthUserOwnsAccountValidator = [
421 (req: express.Request, res: express.Response, next: express.NextFunction) => {
422 const user = res.locals.oauth.token.User
423
424 if (res.locals.account.id !== user.Account.id) {
425 return res.fail({
426 status: HttpStatusCode.FORBIDDEN_403,
427 message: 'Only owner of this account can access this resource.'
428 })
429 }
430
431 return next()
432 }
433 ]
434
435 const ensureCanManageChannelOrAccount = [
436 (req: express.Request, res: express.Response, next: express.NextFunction) => {
437 const user = res.locals.oauth.token.user
438 const account = res.locals.videoChannel?.Account ?? res.locals.account
439 const isUserOwner = account.userId === user.id
440
441 if (!isUserOwner && user.hasRight(UserRight.MANAGE_ANY_VIDEO_CHANNEL) === false) {
442 const message = `User ${user.username} does not have right this channel or account.`
443
444 return res.fail({
445 status: HttpStatusCode.FORBIDDEN_403,
446 message
447 })
448 }
449
450 return next()
451 }
452 ]
453
454 const ensureCanModerateUser = [
455 (req: express.Request, res: express.Response, next: express.NextFunction) => {
456 const authUser = res.locals.oauth.token.User
457 const onUser = res.locals.user
458
459 if (authUser.role === UserRole.ADMINISTRATOR) return next()
460 if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next()
461
462 return res.fail({
463 status: HttpStatusCode.FORBIDDEN_403,
464 message: 'A moderator can only manage users.'
465 })
466 }
467 ]
468
469 // ---------------------------------------------------------------------------
470
471 export {
472 usersListValidator,
473 usersAddValidator,
474 deleteMeValidator,
475 usersBlockingValidator,
476 usersRemoveValidator,
477 usersUpdateValidator,
478 usersUpdateMeValidator,
479 usersVideoRatingValidator,
480 usersCheckCurrentPasswordFactory,
481 usersGetValidator,
482 usersVideosValidator,
483 usersAskResetPasswordValidator,
484 usersResetPasswordValidator,
485 userAutocompleteValidator,
486 ensureAuthUserOwnsAccountValidator,
487 ensureCanModerateUser,
488 ensureCanManageChannelOrAccount
489 }