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