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