]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/middlewares/validators/users.ts
Merge branch 'signup-hooks' into 'develop'
[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 } 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 isUserRoleValid,
18 isUserUsernameValid,
19 isUserVideoLanguages,
20 isUserVideoQuotaDailyValid,
21 isUserVideoQuotaValid,
22 isUserVideosHistoryEnabledValid
23 } from '../../helpers/custom-validators/users'
24 import { logger } from '../../helpers/logger'
25 import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup'
26 import { Redis } from '../../lib/redis'
27 import { UserModel } from '../../models/account/user'
28 import { areValidationErrors } from './utils'
29 import { ActorModel } from '../../models/activitypub/actor'
30 import { isActorPreferredUsernameValid } from '../../helpers/custom-validators/activitypub/actor'
31 import { isVideoChannelNameValid } from '../../helpers/custom-validators/video-channels'
32 import { UserRegister } from '../../../shared/models/users/user-register.model'
33 import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
34 import { isThemeRegistered } from '../../lib/plugins/theme-utils'
35 import { doesVideoExist } from '../../helpers/middlewares'
36 import { UserRole } from '../../../shared/models/users'
37 import { MUserDefault } from '@server/typings/models'
38 import { Hooks } from '@server/lib/plugins/hooks'
39 import { isLocalVideoAccepted } from '@server/lib/moderation'
40
41 const usersAddValidator = [
42 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'),
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'),
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 async (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
260 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
261 logger.debug('Checking usersGet parameters', { parameters: req.params })
262
263 if (areValidationErrors(req, res)) return
264 if (!await checkUserIdExist(req.params.id, res)) return
265
266 return next()
267 }
268 ]
269
270 const usersVideoRatingValidator = [
271 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'),
272
273 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
274 logger.debug('Checking usersVideoRating parameters', { parameters: req.params })
275
276 if (areValidationErrors(req, res)) return
277 if (!await doesVideoExist(req.params.videoId, res, 'id')) return
278
279 return next()
280 }
281 ]
282
283 const ensureUserRegistrationAllowed = [
284 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
285 const allowedParams = {
286 body: req.body,
287 ip: req.ip
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) {
297 return res.status(403)
298 .json({ error: allowedResult.errorMessage || 'User registration is not enabled or user limit is reached.' })
299 }
300
301 return next()
302 }
303 ]
304
305 const 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)
311 .json({ error: 'You are not on a network authorized for registration.' })
312 }
313
314 return next()
315 }
316 ]
317
318 const 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
325
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
337 const 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
348 const user = res.locals.user
349 const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id)
350
351 if (redisVerificationString !== req.body.verificationString) {
352 return res
353 .status(403)
354 .json({ error: 'Invalid verification string.' })
355 }
356
357 return next()
358 }
359 ]
360
361 const 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
379 const usersVerifyEmailValidator = [
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()
387 .customSanitizer(toBooleanOrNull),
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
395 const user = res.locals.user
396 const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id)
397
398 if (redisVerificationString !== req.body.verificationString) {
399 return res
400 .status(403)
401 .json({ error: 'Invalid verification string.' })
402 }
403
404 return next()
405 }
406 ]
407
408 const userAutocompleteValidator = [
409 param('search').isString().not().isEmpty().withMessage('Should have a search parameter')
410 ]
411
412 const 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)
418 .json({ error: 'Only owner can access ratings list.' })
419 }
420
421 return next()
422 }
423 ]
424
425 const 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
438 // ---------------------------------------------------------------------------
439
440 export {
441 usersAddValidator,
442 deleteMeValidator,
443 usersRegisterValidator,
444 usersBlockingValidator,
445 usersRemoveValidator,
446 usersUpdateValidator,
447 usersUpdateMeValidator,
448 usersVideoRatingValidator,
449 ensureUserRegistrationAllowed,
450 ensureUserRegistrationAllowedForIP,
451 usersGetValidator,
452 usersAskResetPasswordValidator,
453 usersResetPasswordValidator,
454 usersAskSendVerifyEmailValidator,
455 usersVerifyEmailValidator,
456 userAutocompleteValidator,
457 ensureAuthUserOwnsAccountValidator,
458 ensureCanManageUser
459 }
460
461 // ---------------------------------------------------------------------------
462
463 function checkUserIdExist (idArg: number | string, res: express.Response) {
464 const id = parseInt(idArg + '', 10)
465 return checkUserExist(() => UserModel.loadById(id), res)
466 }
467
468 function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) {
469 return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse)
470 }
471
472 async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email: string, res: express.Response) {
473 const user = await UserModel.loadByUsernameOrEmail(username, email)
474
475 if (user) {
476 res.status(409)
477 .json({ error: 'User with this username or email already exists.' })
478 return false
479 }
480
481 const actor = await ActorModel.loadLocalByName(username)
482 if (actor) {
483 res.status(409)
484 .json({ error: 'Another actor (account/channel) with this name on this instance already exists or has already existed.' })
485 return false
486 }
487
488 return true
489 }
490
491 async function checkUserExist (finder: () => Bluebird<MUserDefault>, res: express.Response, abortResponse = true) {
492 const user = await finder()
493
494 if (!user) {
495 if (abortResponse === true) {
496 res.status(404)
497 .json({ error: 'User not found' })
498 }
499
500 return false
501 }
502
503 res.locals.user = user
504
505 return true
506 }