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