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