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