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