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