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