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