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