]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/middlewares/validators/users.ts
Merge branch 'release/1.4.0' into develop
[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'
453e83ea 5import { isIdOrUUIDValid, toBooleanOrNull } from '../../helpers/custom-validators/misc'
b60e5f38 6import {
1eddc9a7 7 isUserAdminFlagsValid,
1a12adcd
C
8 isUserAutoPlayVideoValid,
9 isUserBlockedReasonValid,
4bbfc6c6
C
10 isUserDescriptionValid,
11 isUserDisplayNameValid,
0883b324 12 isUserNSFWPolicyValid,
ecb4e35f
C
13 isUserPasswordValid,
14 isUserRoleValid,
3e753302
C
15 isUserUsernameValid,
16 isUserVideoLanguages,
1a12adcd 17 isUserVideoQuotaDailyValid,
dae86118
C
18 isUserVideoQuotaValid,
19 isUserVideosHistoryEnabledValid
3fd3ab2d 20} from '../../helpers/custom-validators/users'
da854ddd 21import { logger } from '../../helpers/logger'
06215f15 22import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup'
ecb4e35f 23import { Redis } from '../../lib/redis'
3fd3ab2d 24import { UserModel } from '../../models/account/user'
a2431b7d 25import { areValidationErrors } from './utils'
2ef6a063 26import { ActorModel } from '../../models/activitypub/actor'
e590b4a5
C
27import { isActorPreferredUsernameValid } from '../../helpers/custom-validators/activitypub/actor'
28import { isVideoChannelNameValid } from '../../helpers/custom-validators/video-channels'
e590b4a5 29import { UserRegister } from '../../../shared/models/users/user-register.model'
503c6f44
C
30import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
31import { isThemeRegistered } from '../../lib/plugins/theme-utils'
3e753302 32import { doesVideoExist } from '../../helpers/middlewares'
a95a4cc8 33import { UserRole } from '../../../shared/models/users'
453e83ea 34import { MUserDefault } from '@server/typings/models'
9bd26629 35
b60e5f38 36const usersAddValidator = [
563d032e 37 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'),
b60e5f38
C
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'),
bee0abff 41 body('videoQuotaDaily').custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'),
dea16773
C
42 body('role')
43 .customSanitizer(toIntOrNull)
44 .custom(isUserRoleValid).withMessage('Should have a valid role'),
1eddc9a7 45 body('adminFlags').optional().custom(isUserAdminFlagsValid).withMessage('Should have a valid admin flags'),
9bd26629 46
a2431b7d 47 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
ce97fe36 48 logger.debug('Checking usersAdd parameters', { parameters: omit(req.body, 'password') })
9bd26629 49
a2431b7d
C
50 if (areValidationErrors(req, res)) return
51 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
52
a95a4cc8
C
53 const authUser = res.locals.oauth.token.User
54 if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) {
55 return res.status(403)
dea16773 56 .json({ error: 'You can only create users (and not administrators or moderators)' })
a95a4cc8
C
57 }
58
a2431b7d 59 return next()
b60e5f38
C
60 }
61]
6fcd19ba 62
b60e5f38
C
63const 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'),
1f20622f
C
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'),
77a5501f 77
a2431b7d 78 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
ce97fe36 79 logger.debug('Checking usersRegister parameters', { parameters: omit(req.body, 'password') })
77a5501f 80
a2431b7d
C
81 if (areValidationErrors(req, res)) return
82 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
83
e590b4a5
C
84 const body: UserRegister = req.body
85 if (body.channel) {
86 if (!body.channel.name || !body.channel.displayName) {
87 return res.status(400)
a95a4cc8 88 .json({ error: 'Channel is optional but if you specify it, channel.name and channel.displayName are required.' })
e590b4a5
C
89 }
90
1d5342ab
C
91 if (body.channel.name === body.username) {
92 return res.status(400)
a95a4cc8 93 .json({ error: 'Channel name cannot be the same than user username.' })
1d5342ab
C
94 }
95
e590b4a5
C
96 const existing = await ActorModel.loadLocalByName(body.channel.name)
97 if (existing) {
98 return res.status(409)
a95a4cc8 99 .json({ error: `Channel with name ${body.channel.name} already exists.` })
e590b4a5
C
100 }
101 }
102
a2431b7d 103 return next()
b60e5f38
C
104 }
105]
9bd26629 106
b60e5f38
C
107const usersRemoveValidator = [
108 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
9bd26629 109
a2431b7d 110 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 111 logger.debug('Checking usersRemove parameters', { parameters: req.params })
9bd26629 112
a2431b7d
C
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)
a95a4cc8 119 .json({ error: 'Cannot remove the root user' })
a2431b7d
C
120 }
121
122 return next()
b60e5f38
C
123 }
124]
8094a898 125
e6921918
C
126const usersBlockingValidator = [
127 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
eacb25c4 128 body('reason').optional().custom(isUserBlockedReasonValid).withMessage('Should have a valid blocking reason'),
e6921918
C
129
130 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
eacb25c4 131 logger.debug('Checking usersBlocking parameters', { parameters: req.params })
e6921918
C
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)
a95a4cc8 139 .json({ error: 'Cannot block the root user' })
e6921918
C
140 }
141
142 return next()
143 }
144]
145
92b9d60c
C
146const deleteMeValidator = [
147 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
dae86118 148 const user = res.locals.oauth.token.User
92b9d60c
C
149 if (user.username === 'root') {
150 return res.status(400)
a95a4cc8 151 .json({ error: 'You cannot delete your root account.' })
92b9d60c
C
152 .end()
153 }
154
155 return next()
156 }
157]
158
b60e5f38
C
159const usersUpdateValidator = [
160 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
b426edd4 161 body('password').optional().custom(isUserPasswordValid).withMessage('Should have a valid password'),
b60e5f38 162 body('email').optional().isEmail().withMessage('Should have a valid email attribute'),
fc2ec87a 163 body('emailVerified').optional().isBoolean().withMessage('Should have a valid email verified attribute'),
b60e5f38 164 body('videoQuota').optional().custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'),
bee0abff 165 body('videoQuotaDaily').optional().custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'),
dea16773
C
166 body('role')
167 .optional()
168 .customSanitizer(toIntOrNull)
169 .custom(isUserRoleValid).withMessage('Should have a valid role'),
1eddc9a7 170 body('adminFlags').optional().custom(isUserAdminFlagsValid).withMessage('Should have a valid admin flags'),
8094a898 171
a2431b7d 172 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 173 logger.debug('Checking usersUpdate parameters', { parameters: req.body })
9bd26629 174
a2431b7d
C
175 if (areValidationErrors(req, res)) return
176 if (!await checkUserIdExist(req.params.id, res)) return
177
f8b8c36b
C
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)
a95a4cc8 181 .json({ error: 'Cannot change root role.' })
f8b8c36b
C
182 }
183
a2431b7d 184 return next()
b60e5f38
C
185 }
186]
9bd26629 187
b60e5f38 188const usersUpdateMeValidator = [
d1ab89de
C
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'),
3caf77d3
C
210 body('videoLanguages')
211 .optional()
212 .custom(isUserVideoLanguages).withMessage('Should have a valid video languages attribute'),
1a12adcd
C
213 body('videosHistoryEnabled')
214 .optional()
215 .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled attribute'),
7cd4d2ba
C
216 body('theme')
217 .optional()
503c6f44 218 .custom(v => isThemeNameValid(v) && isThemeRegistered(v)).withMessage('Should have a valid theme'),
9bd26629 219
a890d1e0 220 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
ce97fe36 221 logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') })
8094a898 222
0ba5f5ba 223 if (req.body.password || req.body.email) {
a890d1e0
C
224 if (!req.body.currentPassword) {
225 return res.status(400)
a95a4cc8 226 .json({ error: 'currentPassword parameter is missing.' })
a890d1e0
C
227 .end()
228 }
229
2ba92871 230 const user = res.locals.oauth.token.User
a890d1e0
C
231 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
232 return res.status(401)
a95a4cc8 233 .json({ error: 'currentPassword is invalid.' })
a890d1e0
C
234 }
235 }
236
a2431b7d
C
237 if (areValidationErrors(req, res)) return
238
239 return next()
b60e5f38
C
240 }
241]
8094a898 242
b60e5f38
C
243const usersGetValidator = [
244 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
d38b8281 245
a2431b7d 246 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
ce97fe36 247 logger.debug('Checking usersGet parameters', { parameters: req.params })
a2431b7d
C
248
249 if (areValidationErrors(req, res)) return
250 if (!await checkUserIdExist(req.params.id, res)) return
251
252 return next()
b60e5f38
C
253 }
254]
d38b8281 255
b60e5f38 256const usersVideoRatingValidator = [
72c7248b 257 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'),
0a6658fd 258
a2431b7d 259 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
b60e5f38 260 logger.debug('Checking usersVideoRating parameters', { parameters: req.params })
0a6658fd 261
a2431b7d 262 if (areValidationErrors(req, res)) return
0f6acda1 263 if (!await doesVideoExist(req.params.videoId, res, 'id')) return
a2431b7d
C
264
265 return next()
b60e5f38
C
266 }
267]
268
269const ensureUserRegistrationAllowed = [
a2431b7d
C
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)
a95a4cc8 274 .json({ error: 'User registration is not enabled or user limit is reached.' })
a2431b7d
C
275 }
276
277 return next()
b60e5f38
C
278 }
279]
291e8d3e 280
ff2c1fe8
RK
281const 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)
a95a4cc8 287 .json({ error: 'You are not on a network authorized for registration.' })
ff2c1fe8
RK
288 }
289
290 return next()
291 }
292]
293
ecb4e35f
C
294const 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
b426edd4 301
ecb4e35f
C
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
313const 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
dae86118 324 const user = res.locals.user
ecb4e35f
C
325 const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id)
326
327 if (redisVerificationString !== req.body.verificationString) {
328 return res
329 .status(403)
a95a4cc8 330 .json({ error: 'Invalid verification string.' })
ecb4e35f
C
331 }
332
333 return next()
334 }
335]
336
d9eaee39
JM
337const 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
355const usersVerifyEmailValidator = [
d1ab89de
C
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()
2b65c4e5 363 .customSanitizer(toBooleanOrNull),
d9eaee39
JM
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
dae86118 371 const user = res.locals.user
d9eaee39
JM
372 const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id)
373
374 if (redisVerificationString !== req.body.verificationString) {
375 return res
376 .status(403)
a95a4cc8 377 .json({ error: 'Invalid verification string.' })
d9eaee39
JM
378 }
379
380 return next()
381 }
382]
383
74d63469
GR
384const userAutocompleteValidator = [
385 param('search').isString().not().isEmpty().withMessage('Should have a search parameter')
386]
387
c100a614
YB
388const 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)
a95a4cc8 394 .json({ error: 'Only owner can access ratings list.' })
c100a614
YB
395 }
396
397 return next()
398 }
399]
400
a95a4cc8
C
401const 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
9bd26629
C
414// ---------------------------------------------------------------------------
415
65fcc311
C
416export {
417 usersAddValidator,
92b9d60c 418 deleteMeValidator,
77a5501f 419 usersRegisterValidator,
e6921918 420 usersBlockingValidator,
65fcc311
C
421 usersRemoveValidator,
422 usersUpdateValidator,
8094a898 423 usersUpdateMeValidator,
291e8d3e 424 usersVideoRatingValidator,
8094a898 425 ensureUserRegistrationAllowed,
ff2c1fe8 426 ensureUserRegistrationAllowedForIP,
c5911fd3 427 usersGetValidator,
ecb4e35f 428 usersAskResetPasswordValidator,
d9eaee39
JM
429 usersResetPasswordValidator,
430 usersAskSendVerifyEmailValidator,
74d63469 431 usersVerifyEmailValidator,
c100a614 432 userAutocompleteValidator,
a95a4cc8
C
433 ensureAuthUserOwnsAccountValidator,
434 ensureCanManageUser
8094a898
C
435}
436
437// ---------------------------------------------------------------------------
438
ecb4e35f
C
439function checkUserIdExist (id: number, res: express.Response) {
440 return checkUserExist(() => UserModel.loadById(id), res)
441}
a2431b7d 442
ecb4e35f
C
443function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) {
444 return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse)
65fcc311 445}
77a5501f 446
a2431b7d 447async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email: string, res: express.Response) {
3fd3ab2d 448 const user = await UserModel.loadByUsernameOrEmail(username, email)
a2431b7d
C
449
450 if (user) {
451 res.status(409)
a95a4cc8 452 .json({ error: 'User with this username or email already exists.' })
a2431b7d
C
453 return false
454 }
455
2ef6a063
C
456 const actor = await ActorModel.loadLocalByName(username)
457 if (actor) {
458 res.status(409)
a95a4cc8 459 .json({ error: 'Another actor (account/channel) with this name on this instance already exists or has already existed.' })
2ef6a063
C
460 return false
461 }
462
a2431b7d 463 return true
77a5501f 464}
ecb4e35f 465
453e83ea 466async function checkUserExist (finder: () => Bluebird<MUserDefault>, res: express.Response, abortResponse = true) {
ecb4e35f
C
467 const user = await finder()
468
469 if (!user) {
470 if (abortResponse === true) {
471 res.status(404)
a95a4cc8 472 .json({ error: 'User not found' })
ecb4e35f
C
473 }
474
475 return false
476 }
477
478 res.locals.user = user
479
480 return true
481}