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