]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/middlewares/validators/users.ts
Merge branch 'signup-hooks' 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
C
38import { Hooks } from '@server/lib/plugins/hooks'
39import { isLocalVideoAccepted } from '@server/lib/moderation'
9bd26629 40
b60e5f38 41const usersAddValidator = [
563d032e 42 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'),
b60e5f38
C
43 body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'),
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
C
151const deleteMeValidator = [
152 async (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'),
d38b8281 259
a2431b7d 260 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
ce97fe36 261 logger.debug('Checking usersGet parameters', { parameters: req.params })
a2431b7d
C
262
263 if (areValidationErrors(req, res)) return
264 if (!await checkUserIdExist(req.params.id, res)) return
265
266 return next()
b60e5f38
C
267 }
268]
d38b8281 269
b60e5f38 270const usersVideoRatingValidator = [
72c7248b 271 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'),
0a6658fd 272
a2431b7d 273 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 274 logger.debug('Checking usersVideoRating parameters', { parameters: req.params })
0a6658fd 275
a2431b7d 276 if (areValidationErrors(req, res)) return
0f6acda1 277 if (!await doesVideoExist(req.params.videoId, res, 'id')) return
a2431b7d
C
278
279 return next()
b60e5f38
C
280 }
281]
282
283const ensureUserRegistrationAllowed = [
a2431b7d 284 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
4ce7eb71 285 const allowedParams = {
ba7b7e57
RK
286 body: req.body,
287 ip: req.ip
4ce7eb71
C
288 }
289
290 const allowedResult = await Hooks.wrapPromiseFun(
291 isSignupAllowed,
292 allowedParams,
293 'filter:api.user.signup.allowed.result'
294 )
295
296 if (allowedResult.allowed === false) {
a2431b7d 297 return res.status(403)
4ce7eb71 298 .json({ error: allowedResult.errorMessage || 'User registration is not enabled or user limit is reached.' })
a2431b7d
C
299 }
300
301 return next()
b60e5f38
C
302 }
303]
291e8d3e 304
ff2c1fe8
RK
305const ensureUserRegistrationAllowedForIP = [
306 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
307 const allowed = isSignupAllowedForCurrentIP(req.ip)
308
309 if (allowed === false) {
310 return res.status(403)
a95a4cc8 311 .json({ error: 'You are not on a network authorized for registration.' })
ff2c1fe8
RK
312 }
313
314 return next()
315 }
316]
317
ecb4e35f
C
318const usersAskResetPasswordValidator = [
319 body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
320
321 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
322 logger.debug('Checking usersAskResetPassword parameters', { parameters: req.body })
323
324 if (areValidationErrors(req, res)) return
b426edd4 325
ecb4e35f
C
326 const exists = await checkUserEmailExist(req.body.email, res, false)
327 if (!exists) {
328 logger.debug('User with email %s does not exist (asking reset password).', req.body.email)
329 // Do not leak our emails
330 return res.status(204).end()
331 }
332
333 return next()
334 }
335]
336
337const usersResetPasswordValidator = [
338 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
339 body('verificationString').not().isEmpty().withMessage('Should have a valid verification string'),
340 body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'),
341
342 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
343 logger.debug('Checking usersResetPassword parameters', { parameters: req.params })
344
345 if (areValidationErrors(req, res)) return
346 if (!await checkUserIdExist(req.params.id, res)) return
347
dae86118 348 const user = res.locals.user
ecb4e35f
C
349 const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id)
350
351 if (redisVerificationString !== req.body.verificationString) {
352 return res
353 .status(403)
a95a4cc8 354 .json({ error: 'Invalid verification string.' })
ecb4e35f
C
355 }
356
357 return next()
358 }
359]
360
d9eaee39
JM
361const usersAskSendVerifyEmailValidator = [
362 body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
363
364 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
365 logger.debug('Checking askUsersSendVerifyEmail parameters', { parameters: req.body })
366
367 if (areValidationErrors(req, res)) return
368 const exists = await checkUserEmailExist(req.body.email, res, false)
369 if (!exists) {
370 logger.debug('User with email %s does not exist (asking verify email).', req.body.email)
371 // Do not leak our emails
372 return res.status(204).end()
373 }
374
375 return next()
376 }
377]
378
379const usersVerifyEmailValidator = [
d1ab89de
C
380 param('id')
381 .isInt().not().isEmpty().withMessage('Should have a valid id'),
382
383 body('verificationString')
384 .not().isEmpty().withMessage('Should have a valid verification string'),
385 body('isPendingEmail')
386 .optional()
2b65c4e5 387 .customSanitizer(toBooleanOrNull),
d9eaee39
JM
388
389 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
390 logger.debug('Checking usersVerifyEmail parameters', { parameters: req.params })
391
392 if (areValidationErrors(req, res)) return
393 if (!await checkUserIdExist(req.params.id, res)) return
394
dae86118 395 const user = res.locals.user
d9eaee39
JM
396 const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id)
397
398 if (redisVerificationString !== req.body.verificationString) {
399 return res
400 .status(403)
a95a4cc8 401 .json({ error: 'Invalid verification string.' })
d9eaee39
JM
402 }
403
404 return next()
405 }
406]
407
74d63469
GR
408const userAutocompleteValidator = [
409 param('search').isString().not().isEmpty().withMessage('Should have a search parameter')
410]
411
c100a614
YB
412const ensureAuthUserOwnsAccountValidator = [
413 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
414 const user = res.locals.oauth.token.User
415
416 if (res.locals.account.id !== user.Account.id) {
417 return res.status(403)
a95a4cc8 418 .json({ error: 'Only owner can access ratings list.' })
c100a614
YB
419 }
420
421 return next()
422 }
423]
424
a95a4cc8
C
425const ensureCanManageUser = [
426 (req: express.Request, res: express.Response, next: express.NextFunction) => {
427 const authUser = res.locals.oauth.token.User
428 const onUser = res.locals.user
429
430 if (authUser.role === UserRole.ADMINISTRATOR) return next()
431 if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next()
432
433 return res.status(403)
434 .json({ error: 'A moderator can only manager users.' })
435 }
436]
437
9bd26629
C
438// ---------------------------------------------------------------------------
439
65fcc311
C
440export {
441 usersAddValidator,
92b9d60c 442 deleteMeValidator,
77a5501f 443 usersRegisterValidator,
e6921918 444 usersBlockingValidator,
65fcc311
C
445 usersRemoveValidator,
446 usersUpdateValidator,
8094a898 447 usersUpdateMeValidator,
291e8d3e 448 usersVideoRatingValidator,
8094a898 449 ensureUserRegistrationAllowed,
ff2c1fe8 450 ensureUserRegistrationAllowedForIP,
c5911fd3 451 usersGetValidator,
ecb4e35f 452 usersAskResetPasswordValidator,
d9eaee39
JM
453 usersResetPasswordValidator,
454 usersAskSendVerifyEmailValidator,
74d63469 455 usersVerifyEmailValidator,
c100a614 456 userAutocompleteValidator,
a95a4cc8
C
457 ensureAuthUserOwnsAccountValidator,
458 ensureCanManageUser
8094a898
C
459}
460
461// ---------------------------------------------------------------------------
462
d5d9b6d7
C
463function checkUserIdExist (idArg: number | string, res: express.Response) {
464 const id = parseInt(idArg + '', 10)
ecb4e35f
C
465 return checkUserExist(() => UserModel.loadById(id), res)
466}
a2431b7d 467
ecb4e35f
C
468function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) {
469 return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse)
65fcc311 470}
77a5501f 471
a2431b7d 472async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email: string, res: express.Response) {
3fd3ab2d 473 const user = await UserModel.loadByUsernameOrEmail(username, email)
a2431b7d
C
474
475 if (user) {
476 res.status(409)
a95a4cc8 477 .json({ error: 'User with this username or email already exists.' })
a2431b7d
C
478 return false
479 }
480
2ef6a063
C
481 const actor = await ActorModel.loadLocalByName(username)
482 if (actor) {
483 res.status(409)
a95a4cc8 484 .json({ error: 'Another actor (account/channel) with this name on this instance already exists or has already existed.' })
2ef6a063
C
485 return false
486 }
487
a2431b7d 488 return true
77a5501f 489}
ecb4e35f 490
453e83ea 491async function checkUserExist (finder: () => Bluebird<MUserDefault>, res: express.Response, abortResponse = true) {
ecb4e35f
C
492 const user = await finder()
493
494 if (!user) {
495 if (abortResponse === true) {
496 res.status(404)
a95a4cc8 497 .json({ error: 'User not found' })
ecb4e35f
C
498 }
499
500 return false
501 }
502
503 res.locals.user = user
504
505 return true
506}