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