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