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