]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/middlewares/validators/users.ts
840b9fc744379e2a13039efafeb5d31b98824e30
[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/typings/models'
39 import { Hooks } from '@server/lib/plugins/hooks'
40
41 const usersAddValidator = [
42 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'),
43 body('password').custom(isUserPasswordValidOrEmpty).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'),
46 body('videoQuotaDaily').custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'),
47 body('role')
48 .customSanitizer(toIntOrNull)
49 .custom(isUserRoleValid).withMessage('Should have a valid role'),
50 body('adminFlags').optional().custom(isUserAdminFlagsValid).withMessage('Should have a valid admin flags'),
51
52 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
53 logger.debug('Checking usersAdd parameters', { parameters: omit(req.body, 'password') })
54
55 if (areValidationErrors(req, res)) return
56 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
57
58 const authUser = res.locals.oauth.token.User
59 if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) {
60 return res.status(403)
61 .json({ error: 'You can only create users (and not administrators or moderators)' })
62 }
63
64 return next()
65 }
66 ]
67
68 const 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'),
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'),
82
83 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
84 logger.debug('Checking usersRegister parameters', { parameters: omit(req.body, 'password') })
85
86 if (areValidationErrors(req, res)) return
87 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
88
89 const body: UserRegister = req.body
90 if (body.channel) {
91 if (!body.channel.name || !body.channel.displayName) {
92 return res.status(400)
93 .json({ error: 'Channel is optional but if you specify it, channel.name and channel.displayName are required.' })
94 }
95
96 if (body.channel.name === body.username) {
97 return res.status(400)
98 .json({ error: 'Channel name cannot be the same as user username.' })
99 }
100
101 const existing = await ActorModel.loadLocalByName(body.channel.name)
102 if (existing) {
103 return res.status(409)
104 .json({ error: `Channel with name ${body.channel.name} already exists.` })
105 }
106 }
107
108 return next()
109 }
110 ]
111
112 const usersRemoveValidator = [
113 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
114
115 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
116 logger.debug('Checking usersRemove parameters', { parameters: req.params })
117
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)
124 .json({ error: 'Cannot remove the root user' })
125 }
126
127 return next()
128 }
129 ]
130
131 const usersBlockingValidator = [
132 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
133 body('reason').optional().custom(isUserBlockedReasonValid).withMessage('Should have a valid blocking reason'),
134
135 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
136 logger.debug('Checking usersBlocking parameters', { parameters: req.params })
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)
144 .json({ error: 'Cannot block the root user' })
145 }
146
147 return next()
148 }
149 ]
150
151 const deleteMeValidator = [
152 (req: express.Request, res: express.Response, next: express.NextFunction) => {
153 const user = res.locals.oauth.token.User
154 if (user.username === 'root') {
155 return res.status(400)
156 .json({ error: 'You cannot delete your root account.' })
157 .end()
158 }
159
160 return next()
161 }
162 ]
163
164 const usersUpdateValidator = [
165 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
166 body('password').optional().custom(isUserPasswordValid).withMessage('Should have a valid password'),
167 body('email').optional().isEmail().withMessage('Should have a valid email attribute'),
168 body('emailVerified').optional().isBoolean().withMessage('Should have a valid email verified attribute'),
169 body('videoQuota').optional().custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'),
170 body('videoQuotaDaily').optional().custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'),
171 body('role')
172 .optional()
173 .customSanitizer(toIntOrNull)
174 .custom(isUserRoleValid).withMessage('Should have a valid role'),
175 body('adminFlags').optional().custom(isUserAdminFlagsValid).withMessage('Should have a valid admin flags'),
176
177 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
178 logger.debug('Checking usersUpdate parameters', { parameters: req.body })
179
180 if (areValidationErrors(req, res)) return
181 if (!await checkUserIdExist(req.params.id, res)) return
182
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)
186 .json({ error: 'Cannot change root role.' })
187 }
188
189 return next()
190 }
191 ]
192
193 const usersUpdateMeValidator = [
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'),
215 body('videoLanguages')
216 .optional()
217 .custom(isUserVideoLanguages).withMessage('Should have a valid video languages attribute'),
218 body('videosHistoryEnabled')
219 .optional()
220 .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled attribute'),
221 body('theme')
222 .optional()
223 .custom(v => isThemeNameValid(v) && isThemeRegistered(v)).withMessage('Should have a valid theme'),
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'),
230 body('autoPlayNextVideo')
231 .optional()
232 .custom(v => isUserAutoPlayNextVideoValid(v)).withMessage('Should have a valid autoPlayNextVideo boolean'),
233
234 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
235 logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') })
236
237 if (req.body.password || req.body.email) {
238 if (!req.body.currentPassword) {
239 return res.status(400)
240 .json({ error: 'currentPassword parameter is missing.' })
241 .end()
242 }
243
244 const user = res.locals.oauth.token.User
245 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
246 return res.status(401)
247 .json({ error: 'currentPassword is invalid.' })
248 }
249 }
250
251 if (areValidationErrors(req, res)) return
252
253 return next()
254 }
255 ]
256
257 const usersGetValidator = [
258 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
259 query('withStats').optional().isBoolean().withMessage('Should have a valid stats flag'),
260
261 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
262 logger.debug('Checking usersGet parameters', { parameters: req.params })
263
264 if (areValidationErrors(req, res)) return
265 if (!await checkUserIdExist(req.params.id, res, req.query.withStats)) return
266
267 return next()
268 }
269 ]
270
271 const usersVideoRatingValidator = [
272 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'),
273
274 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
275 logger.debug('Checking usersVideoRating parameters', { parameters: req.params })
276
277 if (areValidationErrors(req, res)) return
278 if (!await doesVideoExist(req.params.videoId, res, 'id')) return
279
280 return next()
281 }
282 ]
283
284 const ensureUserRegistrationAllowed = [
285 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
286 const allowedParams = {
287 body: req.body,
288 ip: req.ip
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) {
298 return res.status(403)
299 .json({ error: allowedResult.errorMessage || 'User registration is not enabled or user limit is reached.' })
300 }
301
302 return next()
303 }
304 ]
305
306 const ensureUserRegistrationAllowedForIP = [
307 (req: express.Request, res: express.Response, next: express.NextFunction) => {
308 const allowed = isSignupAllowedForCurrentIP(req.ip)
309
310 if (allowed === false) {
311 return res.status(403)
312 .json({ error: 'You are not on a network authorized for registration.' })
313 }
314
315 return next()
316 }
317 ]
318
319 const 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
326
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
338 const 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
349 const user = res.locals.user
350 const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id)
351
352 if (redisVerificationString !== req.body.verificationString) {
353 return res
354 .status(403)
355 .json({ error: 'Invalid verification string.' })
356 }
357
358 return next()
359 }
360 ]
361
362 const 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
380 const usersVerifyEmailValidator = [
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()
388 .customSanitizer(toBooleanOrNull),
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
396 const user = res.locals.user
397 const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id)
398
399 if (redisVerificationString !== req.body.verificationString) {
400 return res
401 .status(403)
402 .json({ error: 'Invalid verification string.' })
403 }
404
405 return next()
406 }
407 ]
408
409 const userAutocompleteValidator = [
410 param('search').isString().not().isEmpty().withMessage('Should have a search parameter')
411 ]
412
413 const ensureAuthUserOwnsAccountValidator = [
414 (req: express.Request, res: express.Response, next: express.NextFunction) => {
415 const user = res.locals.oauth.token.User
416
417 if (res.locals.account.id !== user.Account.id) {
418 return res.status(403)
419 .json({ error: 'Only owner can access ratings list.' })
420 }
421
422 return next()
423 }
424 ]
425
426 const 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
439 // ---------------------------------------------------------------------------
440
441 export {
442 usersAddValidator,
443 deleteMeValidator,
444 usersRegisterValidator,
445 usersBlockingValidator,
446 usersRemoveValidator,
447 usersUpdateValidator,
448 usersUpdateMeValidator,
449 usersVideoRatingValidator,
450 ensureUserRegistrationAllowed,
451 ensureUserRegistrationAllowedForIP,
452 usersGetValidator,
453 usersAskResetPasswordValidator,
454 usersResetPasswordValidator,
455 usersAskSendVerifyEmailValidator,
456 usersVerifyEmailValidator,
457 userAutocompleteValidator,
458 ensureAuthUserOwnsAccountValidator,
459 ensureCanManageUser
460 }
461
462 // ---------------------------------------------------------------------------
463
464 function checkUserIdExist (idArg: number | string, res: express.Response, withStats = false) {
465 const id = parseInt(idArg + '', 10)
466 return checkUserExist(() => UserModel.loadById(id, withStats), res)
467 }
468
469 function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) {
470 return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse)
471 }
472
473 async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email: string, res: express.Response) {
474 const user = await UserModel.loadByUsernameOrEmail(username, email)
475
476 if (user) {
477 res.status(409)
478 .json({ error: 'User with this username or email already exists.' })
479 return false
480 }
481
482 const actor = await ActorModel.loadLocalByName(username)
483 if (actor) {
484 res.status(409)
485 .json({ error: 'Another actor (account/channel) with this name on this instance already exists or has already existed.' })
486 return false
487 }
488
489 return true
490 }
491
492 async function checkUserExist (finder: () => Bluebird<MUserDefault>, res: express.Response, abortResponse = true) {
493 const user = await finder()
494
495 if (!user) {
496 if (abortResponse === true) {
497 res.status(404)
498 .json({ error: 'User not found' })
499 }
500
501 return false
502 }
503
504 res.locals.user = user
505
506 return true
507 }