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