]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/middlewares/validators/users.ts
Merge branch 'release/5.0.0' into develop
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / users.ts
CommitLineData
41fb13c3 1import express from 'express'
76314386 2import { body, param, query } from 'express-validator'
4638cd71 3import { forceNumber } from '@shared/core-utils'
e364e31e 4import { HttpStatusCode, UserRight, UserRole } from '@shared/models'
56f47830 5import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
b49f22d8 6import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
b60e5f38 7import {
1eddc9a7 8 isUserAdminFlagsValid,
c1e5bd23 9 isUserAutoPlayNextVideoValid,
1a12adcd
C
10 isUserAutoPlayVideoValid,
11 isUserBlockedReasonValid,
4bbfc6c6
C
12 isUserDescriptionValid,
13 isUserDisplayNameValid,
8f581725 14 isUserNoModal,
0883b324 15 isUserNSFWPolicyValid,
a9bfa85d 16 isUserP2PEnabledValid,
ecb4e35f 17 isUserPasswordValid,
45f1bd72 18 isUserPasswordValidOrEmpty,
ecb4e35f 19 isUserRoleValid,
3e753302
C
20 isUserUsernameValid,
21 isUserVideoLanguages,
1a12adcd 22 isUserVideoQuotaDailyValid,
dae86118
C
23 isUserVideoQuotaValid,
24 isUserVideosHistoryEnabledValid
3fd3ab2d 25} from '../../helpers/custom-validators/users'
e364e31e 26import { isVideoChannelUsernameValid } from '../../helpers/custom-validators/video-channels'
da854ddd 27import { logger } from '../../helpers/logger'
b49f22d8 28import { isThemeRegistered } from '../../lib/plugins/theme-utils'
ecb4e35f 29import { Redis } from '../../lib/redis'
7d9ba5c0 30import { ActorModel } from '../../models/actor/actor'
56f47830
C
31import {
32 areValidationErrors,
33 checkUserEmailExist,
34 checkUserIdExist,
e364e31e 35 checkUserNameOrEmailDoNotAlreadyExist,
56f47830
C
36 doesVideoChannelIdExist,
37 doesVideoExist,
38 isValidVideoIdParam
39} from './shared'
9bd26629 40
8491293b
RK
41const usersListValidator = [
42 query('blocked')
43 .optional()
f1273314 44 .customSanitizer(toBooleanOrNull)
71e3e879 45 .isBoolean().withMessage('Should be a valid blocked boolean'),
8491293b 46
ea7337cf 47 (req: express.Request, res: express.Response, next: express.NextFunction) => {
8491293b
RK
48 if (areValidationErrors(req, res)) return
49
50 return next()
51 }
52]
53
b60e5f38 54const usersAddValidator = [
396f6f01
C
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(),
27db7840 62
396f6f01
C
63 body('channelName')
64 .optional()
65 .custom(isVideoChannelUsernameValid),
27db7840 66
396f6f01
C
67 body('videoQuota')
68 .custom(isUserVideoQuotaValid),
69 body('videoQuotaDaily')
70 .custom(isUserVideoQuotaDailyValid),
27db7840 71
dea16773
C
72 body('role')
73 .customSanitizer(toIntOrNull)
396f6f01
C
74 .custom(isUserRoleValid),
75
76 body('adminFlags')
77 .optional()
78 .custom(isUserAdminFlagsValid),
9bd26629 79
a2431b7d 80 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
a85d5303 81 if (areValidationErrors(req, res, { omitBodyLog: true })) return
e364e31e 82 if (!await checkUserNameOrEmailDoNotAlreadyExist(req.body.username, req.body.email, res)) return
a2431b7d 83
a95a4cc8
C
84 const authUser = res.locals.oauth.token.User
85 if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) {
76148b27
RK
86 return res.fail({
87 status: HttpStatusCode.FORBIDDEN_403,
88 message: 'You can only create users (and not administrators or moderators)'
89 })
a95a4cc8
C
90 }
91
766d13b4 92 if (req.body.channelName) {
93 if (req.body.channelName === req.body.username) {
76148b27 94 return res.fail({ message: 'Channel name cannot be the same as user username.' })
766d13b4 95 }
4e68fc86 96
766d13b4 97 const existing = await ActorModel.loadLocalByName(req.body.channelName)
98 if (existing) {
76148b27
RK
99 return res.fail({
100 status: HttpStatusCode.CONFLICT_409,
101 message: `Channel with name ${req.body.channelName} already exists.`
102 })
766d13b4 103 }
4e68fc86 104 }
105
a2431b7d 106 return next()
b60e5f38
C
107 }
108]
6fcd19ba 109
b60e5f38 110const usersRemoveValidator = [
396f6f01
C
111 param('id')
112 .custom(isIdValid),
9bd26629 113
a2431b7d 114 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
a2431b7d
C
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') {
76148b27 120 return res.fail({ message: 'Cannot remove the root user' })
a2431b7d
C
121 }
122
123 return next()
b60e5f38
C
124 }
125]
8094a898 126
e6921918 127const usersBlockingValidator = [
396f6f01
C
128 param('id')
129 .custom(isIdValid),
130 body('reason')
131 .optional()
132 .custom(isUserBlockedReasonValid),
e6921918
C
133
134 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
e6921918
C
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') {
76148b27 140 return res.fail({ message: 'Cannot block the root user' })
e6921918
C
141 }
142
143 return next()
144 }
145]
146
92b9d60c 147const deleteMeValidator = [
a1587156 148 (req: express.Request, res: express.Response, next: express.NextFunction) => {
dae86118 149 const user = res.locals.oauth.token.User
92b9d60c 150 if (user.username === 'root') {
76148b27 151 return res.fail({ message: 'You cannot delete your root account.' })
92b9d60c
C
152 }
153
154 return next()
155 }
156]
157
b60e5f38 158const usersUpdateValidator = [
396f6f01
C
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(),
dea16773
C
179 body('role')
180 .optional()
181 .customSanitizer(toIntOrNull)
396f6f01
C
182 .custom(isUserRoleValid),
183 body('adminFlags')
184 .optional()
185 .custom(isUserAdminFlagsValid),
8094a898 186
a2431b7d 187 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
a85d5303 188 if (areValidationErrors(req, res, { omitBodyLog: true })) return
a2431b7d
C
189 if (!await checkUserIdExist(req.params.id, res)) return
190
f8b8c36b
C
191 const user = res.locals.user
192 if (user.username === 'root' && req.body.role !== undefined && user.role !== req.body.role) {
76148b27 193 return res.fail({ message: 'Cannot change root role.' })
f8b8c36b
C
194 }
195
a2431b7d 196 return next()
b60e5f38
C
197 }
198]
9bd26629 199
b60e5f38 200const usersUpdateMeValidator = [
d1ab89de
C
201 body('displayName')
202 .optional()
396f6f01 203 .custom(isUserDisplayNameValid),
d1ab89de
C
204 body('description')
205 .optional()
396f6f01 206 .custom(isUserDescriptionValid),
d1ab89de
C
207 body('currentPassword')
208 .optional()
396f6f01 209 .custom(isUserPasswordValid),
d1ab89de
C
210 body('password')
211 .optional()
396f6f01 212 .custom(isUserPasswordValid),
d1ab89de
C
213 body('email')
214 .optional()
396f6f01 215 .isEmail(),
d1ab89de
C
216 body('nsfwPolicy')
217 .optional()
396f6f01 218 .custom(isUserNSFWPolicyValid),
d1ab89de
C
219 body('autoPlayVideo')
220 .optional()
396f6f01 221 .custom(isUserAutoPlayVideoValid),
a9bfa85d
C
222 body('p2pEnabled')
223 .optional()
224 .custom(isUserP2PEnabledValid).withMessage('Should have a valid p2p enabled boolean'),
3caf77d3
C
225 body('videoLanguages')
226 .optional()
396f6f01 227 .custom(isUserVideoLanguages),
1a12adcd
C
228 body('videosHistoryEnabled')
229 .optional()
396f6f01 230 .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled boolean'),
7cd4d2ba
C
231 body('theme')
232 .optional()
396f6f01 233 .custom(v => isThemeNameValid(v) && isThemeRegistered(v)),
8f581725 234
43d0ea7f
C
235 body('noInstanceConfigWarningModal')
236 .optional()
8f581725 237 .custom(v => isUserNoModal(v)).withMessage('Should have a valid noInstanceConfigWarningModal boolean'),
43d0ea7f
C
238 body('noWelcomeModal')
239 .optional()
8f581725
C
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
c1e5bd23
C
245 body('autoPlayNextVideo')
246 .optional()
247 .custom(v => isUserAutoPlayNextVideoValid(v)).withMessage('Should have a valid autoPlayNextVideo boolean'),
9bd26629 248
a890d1e0 249 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
9a7fd960
C
250 const user = res.locals.oauth.token.User
251
0ba5f5ba 252 if (req.body.password || req.body.email) {
9a7fd960 253 if (user.pluginAuth !== null) {
76148b27 254 return res.fail({ message: 'You cannot update your email or password that is associated with an external auth system.' })
9a7fd960
C
255 }
256
a890d1e0 257 if (!req.body.currentPassword) {
76148b27 258 return res.fail({ message: 'currentPassword parameter is missing.' })
a890d1e0
C
259 }
260
a890d1e0 261 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
76148b27
RK
262 return res.fail({
263 status: HttpStatusCode.UNAUTHORIZED_401,
264 message: 'currentPassword is invalid.'
265 })
a890d1e0
C
266 }
267 }
268
a85d5303 269 if (areValidationErrors(req, res, { omitBodyLog: true })) return
a2431b7d
C
270
271 return next()
b60e5f38
C
272 }
273]
8094a898 274
b60e5f38 275const usersGetValidator = [
396f6f01
C
276 param('id')
277 .custom(isIdValid),
278 query('withStats')
279 .optional()
280 .isBoolean().withMessage('Should have a valid withStats boolean'),
d38b8281 281
a2431b7d 282 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
a2431b7d 283 if (areValidationErrors(req, res)) return
76314386 284 if (!await checkUserIdExist(req.params.id, res, req.query.withStats)) return
a2431b7d
C
285
286 return next()
b60e5f38
C
287 }
288]
d38b8281 289
b60e5f38 290const usersVideoRatingValidator = [
d4a8e7a6 291 isValidVideoIdParam('videoId'),
0a6658fd 292
a2431b7d 293 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
a2431b7d 294 if (areValidationErrors(req, res)) return
0f6acda1 295 if (!await doesVideoExist(req.params.videoId, res, 'id')) return
a2431b7d
C
296
297 return next()
b60e5f38
C
298 }
299]
300
978c87e7
C
301const usersVideosValidator = [
302 query('isLive')
303 .optional()
304 .customSanitizer(toBooleanOrNull)
396f6f01 305 .custom(isBooleanValid).withMessage('Should have a valid isLive boolean'),
978c87e7
C
306
307 query('channelId')
308 .optional()
309 .customSanitizer(toIntOrNull)
396f6f01 310 .custom(isIdValid),
978c87e7
C
311
312 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
978c87e7
C
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
ecb4e35f 321const usersAskResetPasswordValidator = [
396f6f01
C
322 body('email')
323 .isEmail(),
ecb4e35f
C
324
325 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
ecb4e35f 326 if (areValidationErrors(req, res)) return
b426edd4 327
ecb4e35f
C
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
2d53be02 332 return res.status(HttpStatusCode.NO_CONTENT_204).end()
ecb4e35f
C
333 }
334
c5f3ff39
C
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
ecb4e35f
C
342 return next()
343 }
344]
345
346const usersResetPasswordValidator = [
396f6f01
C
347 param('id')
348 .custom(isIdValid),
349 body('verificationString')
350 .not().isEmpty(),
351 body('password')
352 .custom(isUserPasswordValid),
ecb4e35f
C
353
354 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
ecb4e35f
C
355 if (areValidationErrors(req, res)) return
356 if (!await checkUserIdExist(req.params.id, res)) return
357
dae86118 358 const user = res.locals.user
56f47830 359 const redisVerificationString = await Redis.Instance.getResetPasswordVerificationString(user.id)
ecb4e35f
C
360
361 if (redisVerificationString !== req.body.verificationString) {
76148b27
RK
362 return res.fail({
363 status: HttpStatusCode.FORBIDDEN_403,
364 message: 'Invalid verification string.'
365 })
ecb4e35f
C
366 }
367
368 return next()
369 }
370]
371
2166c058
C
372const usersCheckCurrentPasswordFactory = (targetUserIdGetter: (req: express.Request) => number | string) => {
373 return [
374 body('currentPassword').optional().custom(exists),
56f47830 375
2166c058
C
376 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
377 if (areValidationErrors(req, res)) return
56f47830 378
2166c058
C
379 const user = res.locals.oauth.token.User
380 const isAdminOrModerator = user.role === UserRole.ADMINISTRATOR || user.role === UserRole.MODERATOR
4638cd71 381 const targetUserId = forceNumber(targetUserIdGetter(req))
56f47830 382
2166c058
C
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}
56f47830 406
74d63469 407const userAutocompleteValidator = [
a85d5303
C
408 param('search')
409 .isString()
410 .not().isEmpty()
74d63469
GR
411]
412
c100a614 413const ensureAuthUserOwnsAccountValidator = [
a1587156 414 (req: express.Request, res: express.Response, next: express.NextFunction) => {
c100a614
YB
415 const user = res.locals.oauth.token.User
416
417 if (res.locals.account.id !== user.Account.id) {
76148b27
RK
418 return res.fail({
419 status: HttpStatusCode.FORBIDDEN_403,
7a4fd56c 420 message: 'Only owner of this account can access this resource.'
4beda9e1
C
421 })
422 }
423
424 return next()
425 }
426]
427
d4d9bbc6 428const ensureCanManageChannelOrAccount = [
4beda9e1 429 (req: express.Request, res: express.Response, next: express.NextFunction) => {
a37e9e74 430 const user = res.locals.oauth.token.user
d4d9bbc6
C
431 const account = res.locals.videoChannel?.Account ?? res.locals.account
432 const isUserOwner = account.userId === user.id
a37e9e74 433
434 if (!isUserOwner && user.hasRight(UserRight.MANAGE_ANY_VIDEO_CHANNEL) === false) {
d4d9bbc6 435 const message = `User ${user.username} does not have right this channel or account.`
4beda9e1 436
4beda9e1
C
437 return res.fail({
438 status: HttpStatusCode.FORBIDDEN_403,
a37e9e74 439 message
76148b27 440 })
c100a614
YB
441 }
442
443 return next()
444 }
445]
446
d4d9bbc6 447const ensureCanModerateUser = [
a95a4cc8
C
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
76148b27
RK
455 return res.fail({
456 status: HttpStatusCode.FORBIDDEN_403,
d4d9bbc6 457 message: 'A moderator can only manage users.'
76148b27 458 })
a95a4cc8
C
459 }
460]
461
9bd26629
C
462// ---------------------------------------------------------------------------
463
65fcc311 464export {
8491293b 465 usersListValidator,
65fcc311 466 usersAddValidator,
92b9d60c 467 deleteMeValidator,
e6921918 468 usersBlockingValidator,
65fcc311
C
469 usersRemoveValidator,
470 usersUpdateValidator,
8094a898 471 usersUpdateMeValidator,
291e8d3e 472 usersVideoRatingValidator,
2166c058 473 usersCheckCurrentPasswordFactory,
c5911fd3 474 usersGetValidator,
978c87e7 475 usersVideosValidator,
ecb4e35f 476 usersAskResetPasswordValidator,
d9eaee39 477 usersResetPasswordValidator,
c100a614 478 userAutocompleteValidator,
a95a4cc8 479 ensureAuthUserOwnsAccountValidator,
d4d9bbc6
C
480 ensureCanModerateUser,
481 ensureCanManageChannelOrAccount
8094a898 482}