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