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