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