]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/middlewares/validators/users.ts
Merge remote-tracking branch 'weblate/develop' into develop
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / users.ts
CommitLineData
ecb4e35f 1import * as Bluebird from 'bluebird'
69818c93 2import * as express from 'express'
c8861d5d 3import { body, param } from 'express-validator'
ecb4e35f 4import { omit } from 'lodash'
d11c28a3 5import { isIdOrUUIDValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
b60e5f38 6import {
c1e5bd23
C
7 isNoInstanceConfigWarningModal,
8 isNoWelcomeModal,
1eddc9a7 9 isUserAdminFlagsValid,
c1e5bd23 10 isUserAutoPlayNextVideoValid,
1a12adcd
C
11 isUserAutoPlayVideoValid,
12 isUserBlockedReasonValid,
4bbfc6c6
C
13 isUserDescriptionValid,
14 isUserDisplayNameValid,
0883b324 15 isUserNSFWPolicyValid,
ecb4e35f
C
16 isUserPasswordValid,
17 isUserRoleValid,
3e753302
C
18 isUserUsernameValid,
19 isUserVideoLanguages,
1a12adcd 20 isUserVideoQuotaDailyValid,
dae86118
C
21 isUserVideoQuotaValid,
22 isUserVideosHistoryEnabledValid
3fd3ab2d 23} from '../../helpers/custom-validators/users'
da854ddd 24import { logger } from '../../helpers/logger'
06215f15 25import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup'
ecb4e35f 26import { Redis } from '../../lib/redis'
3fd3ab2d 27import { UserModel } from '../../models/account/user'
a2431b7d 28import { areValidationErrors } from './utils'
2ef6a063 29import { ActorModel } from '../../models/activitypub/actor'
e590b4a5
C
30import { isActorPreferredUsernameValid } from '../../helpers/custom-validators/activitypub/actor'
31import { isVideoChannelNameValid } from '../../helpers/custom-validators/video-channels'
e590b4a5 32import { UserRegister } from '../../../shared/models/users/user-register.model'
503c6f44
C
33import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
34import { isThemeRegistered } from '../../lib/plugins/theme-utils'
3e753302 35import { doesVideoExist } from '../../helpers/middlewares'
a95a4cc8 36import { UserRole } from '../../../shared/models/users'
453e83ea 37import { MUserDefault } from '@server/typings/models'
4ce7eb71 38import { Hooks } from '@server/lib/plugins/hooks'
9bd26629 39
b60e5f38 40const usersAddValidator = [
563d032e 41 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'),
b60e5f38
C
42 body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'),
43 body('email').isEmail().withMessage('Should have a valid email'),
44 body('videoQuota').custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'),
bee0abff 45 body('videoQuotaDaily').custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'),
dea16773
C
46 body('role')
47 .customSanitizer(toIntOrNull)
48 .custom(isUserRoleValid).withMessage('Should have a valid role'),
1eddc9a7 49 body('adminFlags').optional().custom(isUserAdminFlagsValid).withMessage('Should have a valid admin flags'),
9bd26629 50
a2431b7d 51 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
ce97fe36 52 logger.debug('Checking usersAdd parameters', { parameters: omit(req.body, 'password') })
9bd26629 53
a2431b7d
C
54 if (areValidationErrors(req, res)) return
55 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
56
a95a4cc8
C
57 const authUser = res.locals.oauth.token.User
58 if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) {
59 return res.status(403)
dea16773 60 .json({ error: 'You can only create users (and not administrators or moderators)' })
a95a4cc8
C
61 }
62
a2431b7d 63 return next()
b60e5f38
C
64 }
65]
6fcd19ba 66
b60e5f38
C
67const usersRegisterValidator = [
68 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username'),
69 body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'),
70 body('email').isEmail().withMessage('Should have a valid email'),
1f20622f
C
71 body('displayName')
72 .optional()
73 .custom(isUserDisplayNameValid).withMessage('Should have a valid display name'),
74
75 body('channel.name')
76 .optional()
77 .custom(isActorPreferredUsernameValid).withMessage('Should have a valid channel name'),
78 body('channel.displayName')
79 .optional()
80 .custom(isVideoChannelNameValid).withMessage('Should have a valid display name'),
77a5501f 81
a2431b7d 82 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
ce97fe36 83 logger.debug('Checking usersRegister parameters', { parameters: omit(req.body, 'password') })
77a5501f 84
a2431b7d
C
85 if (areValidationErrors(req, res)) return
86 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
87
e590b4a5
C
88 const body: UserRegister = req.body
89 if (body.channel) {
90 if (!body.channel.name || !body.channel.displayName) {
91 return res.status(400)
a95a4cc8 92 .json({ error: 'Channel is optional but if you specify it, channel.name and channel.displayName are required.' })
e590b4a5
C
93 }
94
1d5342ab
C
95 if (body.channel.name === body.username) {
96 return res.status(400)
32d7f2b7 97 .json({ error: 'Channel name cannot be the same as user username.' })
1d5342ab
C
98 }
99
e590b4a5
C
100 const existing = await ActorModel.loadLocalByName(body.channel.name)
101 if (existing) {
102 return res.status(409)
a95a4cc8 103 .json({ error: `Channel with name ${body.channel.name} already exists.` })
e590b4a5
C
104 }
105 }
106
a2431b7d 107 return next()
b60e5f38
C
108 }
109]
9bd26629 110
b60e5f38
C
111const usersRemoveValidator = [
112 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
9bd26629 113
a2431b7d 114 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 115 logger.debug('Checking usersRemove parameters', { parameters: req.params })
9bd26629 116
a2431b7d
C
117 if (areValidationErrors(req, res)) return
118 if (!await checkUserIdExist(req.params.id, res)) return
119
120 const user = res.locals.user
121 if (user.username === 'root') {
122 return res.status(400)
a95a4cc8 123 .json({ error: 'Cannot remove the root user' })
a2431b7d
C
124 }
125
126 return next()
b60e5f38
C
127 }
128]
8094a898 129
e6921918
C
130const usersBlockingValidator = [
131 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
eacb25c4 132 body('reason').optional().custom(isUserBlockedReasonValid).withMessage('Should have a valid blocking reason'),
e6921918
C
133
134 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
eacb25c4 135 logger.debug('Checking usersBlocking parameters', { parameters: req.params })
e6921918
C
136
137 if (areValidationErrors(req, res)) return
138 if (!await checkUserIdExist(req.params.id, res)) return
139
140 const user = res.locals.user
141 if (user.username === 'root') {
142 return res.status(400)
a95a4cc8 143 .json({ error: '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
C
153 if (user.username === 'root') {
154 return res.status(400)
a95a4cc8 155 .json({ error: 'You cannot delete your root account.' })
92b9d60c
C
156 .end()
157 }
158
159 return next()
160 }
161]
162
b60e5f38
C
163const usersUpdateValidator = [
164 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
b426edd4 165 body('password').optional().custom(isUserPasswordValid).withMessage('Should have a valid password'),
b60e5f38 166 body('email').optional().isEmail().withMessage('Should have a valid email attribute'),
fc2ec87a 167 body('emailVerified').optional().isBoolean().withMessage('Should have a valid email verified attribute'),
b60e5f38 168 body('videoQuota').optional().custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'),
bee0abff 169 body('videoQuotaDaily').optional().custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'),
dea16773
C
170 body('role')
171 .optional()
172 .customSanitizer(toIntOrNull)
173 .custom(isUserRoleValid).withMessage('Should have a valid role'),
1eddc9a7 174 body('adminFlags').optional().custom(isUserAdminFlagsValid).withMessage('Should have a valid admin flags'),
8094a898 175
a2431b7d 176 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 177 logger.debug('Checking usersUpdate parameters', { parameters: req.body })
9bd26629 178
a2431b7d
C
179 if (areValidationErrors(req, res)) return
180 if (!await checkUserIdExist(req.params.id, res)) return
181
f8b8c36b
C
182 const user = res.locals.user
183 if (user.username === 'root' && req.body.role !== undefined && user.role !== req.body.role) {
184 return res.status(400)
a95a4cc8 185 .json({ error: 'Cannot change root role.' })
f8b8c36b
C
186 }
187
a2431b7d 188 return next()
b60e5f38
C
189 }
190]
9bd26629 191
b60e5f38 192const usersUpdateMeValidator = [
d1ab89de
C
193 body('displayName')
194 .optional()
195 .custom(isUserDisplayNameValid).withMessage('Should have a valid display name'),
196 body('description')
197 .optional()
198 .custom(isUserDescriptionValid).withMessage('Should have a valid description'),
199 body('currentPassword')
200 .optional()
201 .custom(isUserPasswordValid).withMessage('Should have a valid current password'),
202 body('password')
203 .optional()
204 .custom(isUserPasswordValid).withMessage('Should have a valid password'),
205 body('email')
206 .optional()
207 .isEmail().withMessage('Should have a valid email attribute'),
208 body('nsfwPolicy')
209 .optional()
210 .custom(isUserNSFWPolicyValid).withMessage('Should have a valid display Not Safe For Work policy'),
211 body('autoPlayVideo')
212 .optional()
213 .custom(isUserAutoPlayVideoValid).withMessage('Should have a valid automatically plays video attribute'),
3caf77d3
C
214 body('videoLanguages')
215 .optional()
216 .custom(isUserVideoLanguages).withMessage('Should have a valid video languages attribute'),
1a12adcd
C
217 body('videosHistoryEnabled')
218 .optional()
219 .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled attribute'),
7cd4d2ba
C
220 body('theme')
221 .optional()
503c6f44 222 .custom(v => isThemeNameValid(v) && isThemeRegistered(v)).withMessage('Should have a valid theme'),
43d0ea7f
C
223 body('noInstanceConfigWarningModal')
224 .optional()
225 .custom(v => isNoInstanceConfigWarningModal(v)).withMessage('Should have a valid noInstanceConfigWarningModal boolean'),
226 body('noWelcomeModal')
227 .optional()
228 .custom(v => isNoWelcomeModal(v)).withMessage('Should have a valid noWelcomeModal boolean'),
c1e5bd23
C
229 body('autoPlayNextVideo')
230 .optional()
231 .custom(v => isUserAutoPlayNextVideoValid(v)).withMessage('Should have a valid autoPlayNextVideo boolean'),
9bd26629 232
a890d1e0 233 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
ce97fe36 234 logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') })
8094a898 235
0ba5f5ba 236 if (req.body.password || req.body.email) {
a890d1e0
C
237 if (!req.body.currentPassword) {
238 return res.status(400)
a95a4cc8 239 .json({ error: 'currentPassword parameter is missing.' })
a890d1e0
C
240 .end()
241 }
242
2ba92871 243 const user = res.locals.oauth.token.User
a890d1e0
C
244 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
245 return res.status(401)
a95a4cc8 246 .json({ error: 'currentPassword is invalid.' })
a890d1e0
C
247 }
248 }
249
a2431b7d
C
250 if (areValidationErrors(req, res)) return
251
252 return next()
b60e5f38
C
253 }
254]
8094a898 255
b60e5f38
C
256const usersGetValidator = [
257 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
d38b8281 258
a2431b7d 259 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
ce97fe36 260 logger.debug('Checking usersGet parameters', { parameters: req.params })
a2431b7d
C
261
262 if (areValidationErrors(req, res)) return
263 if (!await checkUserIdExist(req.params.id, res)) return
264
265 return next()
b60e5f38
C
266 }
267]
d38b8281 268
b60e5f38 269const usersVideoRatingValidator = [
72c7248b 270 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'),
0a6658fd 271
a2431b7d 272 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 273 logger.debug('Checking usersVideoRating parameters', { parameters: req.params })
0a6658fd 274
a2431b7d 275 if (areValidationErrors(req, res)) return
0f6acda1 276 if (!await doesVideoExist(req.params.videoId, res, 'id')) return
a2431b7d
C
277
278 return next()
b60e5f38
C
279 }
280]
281
282const ensureUserRegistrationAllowed = [
a2431b7d 283 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
4ce7eb71 284 const allowedParams = {
ba7b7e57
RK
285 body: req.body,
286 ip: req.ip
4ce7eb71
C
287 }
288
289 const allowedResult = await Hooks.wrapPromiseFun(
290 isSignupAllowed,
291 allowedParams,
292 'filter:api.user.signup.allowed.result'
293 )
294
295 if (allowedResult.allowed === false) {
a2431b7d 296 return res.status(403)
4ce7eb71 297 .json({ error: allowedResult.errorMessage || 'User registration is not enabled or user limit is reached.' })
a2431b7d
C
298 }
299
300 return next()
b60e5f38
C
301 }
302]
291e8d3e 303
ff2c1fe8 304const ensureUserRegistrationAllowedForIP = [
a1587156 305 (req: express.Request, res: express.Response, next: express.NextFunction) => {
ff2c1fe8
RK
306 const allowed = isSignupAllowedForCurrentIP(req.ip)
307
308 if (allowed === false) {
309 return res.status(403)
a95a4cc8 310 .json({ error: 'You are not on a network authorized for registration.' })
ff2c1fe8
RK
311 }
312
313 return next()
314 }
315]
316
ecb4e35f
C
317const usersAskResetPasswordValidator = [
318 body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
319
320 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
321 logger.debug('Checking usersAskResetPassword parameters', { parameters: req.body })
322
323 if (areValidationErrors(req, res)) return
b426edd4 324
ecb4e35f
C
325 const exists = await checkUserEmailExist(req.body.email, res, false)
326 if (!exists) {
327 logger.debug('User with email %s does not exist (asking reset password).', req.body.email)
328 // Do not leak our emails
329 return res.status(204).end()
330 }
331
332 return next()
333 }
334]
335
336const usersResetPasswordValidator = [
337 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
338 body('verificationString').not().isEmpty().withMessage('Should have a valid verification string'),
339 body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'),
340
341 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
342 logger.debug('Checking usersResetPassword parameters', { parameters: req.params })
343
344 if (areValidationErrors(req, res)) return
345 if (!await checkUserIdExist(req.params.id, res)) return
346
dae86118 347 const user = res.locals.user
ecb4e35f
C
348 const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id)
349
350 if (redisVerificationString !== req.body.verificationString) {
351 return res
352 .status(403)
a95a4cc8 353 .json({ error: 'Invalid verification string.' })
ecb4e35f
C
354 }
355
356 return next()
357 }
358]
359
d9eaee39
JM
360const usersAskSendVerifyEmailValidator = [
361 body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
362
363 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
364 logger.debug('Checking askUsersSendVerifyEmail parameters', { parameters: req.body })
365
366 if (areValidationErrors(req, res)) return
367 const exists = await checkUserEmailExist(req.body.email, res, false)
368 if (!exists) {
369 logger.debug('User with email %s does not exist (asking verify email).', req.body.email)
370 // Do not leak our emails
371 return res.status(204).end()
372 }
373
374 return next()
375 }
376]
377
378const usersVerifyEmailValidator = [
d1ab89de
C
379 param('id')
380 .isInt().not().isEmpty().withMessage('Should have a valid id'),
381
382 body('verificationString')
383 .not().isEmpty().withMessage('Should have a valid verification string'),
384 body('isPendingEmail')
385 .optional()
2b65c4e5 386 .customSanitizer(toBooleanOrNull),
d9eaee39
JM
387
388 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
389 logger.debug('Checking usersVerifyEmail parameters', { parameters: req.params })
390
391 if (areValidationErrors(req, res)) return
392 if (!await checkUserIdExist(req.params.id, res)) return
393
dae86118 394 const user = res.locals.user
d9eaee39
JM
395 const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id)
396
397 if (redisVerificationString !== req.body.verificationString) {
398 return res
399 .status(403)
a95a4cc8 400 .json({ error: 'Invalid verification string.' })
d9eaee39
JM
401 }
402
403 return next()
404 }
405]
406
74d63469
GR
407const userAutocompleteValidator = [
408 param('search').isString().not().isEmpty().withMessage('Should have a search parameter')
409]
410
c100a614 411const ensureAuthUserOwnsAccountValidator = [
a1587156 412 (req: express.Request, res: express.Response, next: express.NextFunction) => {
c100a614
YB
413 const user = res.locals.oauth.token.User
414
415 if (res.locals.account.id !== user.Account.id) {
416 return res.status(403)
a95a4cc8 417 .json({ error: 'Only owner can access ratings list.' })
c100a614
YB
418 }
419
420 return next()
421 }
422]
423
a95a4cc8
C
424const ensureCanManageUser = [
425 (req: express.Request, res: express.Response, next: express.NextFunction) => {
426 const authUser = res.locals.oauth.token.User
427 const onUser = res.locals.user
428
429 if (authUser.role === UserRole.ADMINISTRATOR) return next()
430 if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next()
431
432 return res.status(403)
433 .json({ error: 'A moderator can only manager users.' })
434 }
435]
436
9bd26629
C
437// ---------------------------------------------------------------------------
438
65fcc311
C
439export {
440 usersAddValidator,
92b9d60c 441 deleteMeValidator,
77a5501f 442 usersRegisterValidator,
e6921918 443 usersBlockingValidator,
65fcc311
C
444 usersRemoveValidator,
445 usersUpdateValidator,
8094a898 446 usersUpdateMeValidator,
291e8d3e 447 usersVideoRatingValidator,
8094a898 448 ensureUserRegistrationAllowed,
ff2c1fe8 449 ensureUserRegistrationAllowedForIP,
c5911fd3 450 usersGetValidator,
ecb4e35f 451 usersAskResetPasswordValidator,
d9eaee39
JM
452 usersResetPasswordValidator,
453 usersAskSendVerifyEmailValidator,
74d63469 454 usersVerifyEmailValidator,
c100a614 455 userAutocompleteValidator,
a95a4cc8
C
456 ensureAuthUserOwnsAccountValidator,
457 ensureCanManageUser
8094a898
C
458}
459
460// ---------------------------------------------------------------------------
461
d5d9b6d7
C
462function checkUserIdExist (idArg: number | string, res: express.Response) {
463 const id = parseInt(idArg + '', 10)
ecb4e35f
C
464 return checkUserExist(() => UserModel.loadById(id), res)
465}
a2431b7d 466
ecb4e35f
C
467function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) {
468 return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse)
65fcc311 469}
77a5501f 470
a2431b7d 471async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email: string, res: express.Response) {
3fd3ab2d 472 const user = await UserModel.loadByUsernameOrEmail(username, email)
a2431b7d
C
473
474 if (user) {
475 res.status(409)
a95a4cc8 476 .json({ error: 'User with this username or email already exists.' })
a2431b7d
C
477 return false
478 }
479
2ef6a063
C
480 const actor = await ActorModel.loadLocalByName(username)
481 if (actor) {
482 res.status(409)
a95a4cc8 483 .json({ error: 'Another actor (account/channel) with this name on this instance already exists or has already existed.' })
2ef6a063
C
484 return false
485 }
486
a2431b7d 487 return true
77a5501f 488}
ecb4e35f 489
453e83ea 490async function checkUserExist (finder: () => Bluebird<MUserDefault>, res: express.Response, abortResponse = true) {
ecb4e35f
C
491 const user = await finder()
492
493 if (!user) {
494 if (abortResponse === true) {
495 res.status(404)
a95a4cc8 496 .json({ error: 'User not found' })
ecb4e35f
C
497 }
498
499 return false
500 }
501
502 res.locals.user = user
503
504 return true
505}