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