]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/middlewares/validators/users.ts
add user account email verificiation (#977)
[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 isUserAutoPlayVideoValid, isUserBlockedReasonValid,
9 isUserDescriptionValid,
10 isUserDisplayNameValid,
11 isUserNSFWPolicyValid,
12 isUserPasswordValid,
13 isUserRoleValid,
14 isUserUsernameValid,
15 isUserVideoQuotaValid,
16 isUserVideoQuotaDailyValid
17 } from '../../helpers/custom-validators/users'
18 import { isVideoExist } from '../../helpers/custom-validators/videos'
19 import { logger } from '../../helpers/logger'
20 import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup'
21 import { Redis } from '../../lib/redis'
22 import { UserModel } from '../../models/account/user'
23 import { areValidationErrors } from './utils'
24 import { ActorModel } from '../../models/activitypub/actor'
25
26 const usersAddValidator = [
27 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'),
28 body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'),
29 body('email').isEmail().withMessage('Should have a valid email'),
30 body('videoQuota').custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'),
31 body('videoQuotaDaily').custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'),
32 body('role').custom(isUserRoleValid).withMessage('Should have a valid role'),
33
34 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
35 logger.debug('Checking usersAdd parameters', { parameters: omit(req.body, 'password') })
36
37 if (areValidationErrors(req, res)) return
38 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
39
40 return next()
41 }
42 ]
43
44 const usersRegisterValidator = [
45 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username'),
46 body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'),
47 body('email').isEmail().withMessage('Should have a valid email'),
48
49 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
50 logger.debug('Checking usersRegister parameters', { parameters: omit(req.body, 'password') })
51
52 if (areValidationErrors(req, res)) return
53 if (!await checkUserNameOrEmailDoesNotAlreadyExist(req.body.username, req.body.email, res)) return
54
55 return next()
56 }
57 ]
58
59 const usersRemoveValidator = [
60 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
61
62 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
63 logger.debug('Checking usersRemove parameters', { parameters: req.params })
64
65 if (areValidationErrors(req, res)) return
66 if (!await checkUserIdExist(req.params.id, res)) return
67
68 const user = res.locals.user
69 if (user.username === 'root') {
70 return res.status(400)
71 .send({ error: 'Cannot remove the root user' })
72 .end()
73 }
74
75 return next()
76 }
77 ]
78
79 const usersBlockingValidator = [
80 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
81 body('reason').optional().custom(isUserBlockedReasonValid).withMessage('Should have a valid blocking reason'),
82
83 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
84 logger.debug('Checking usersBlocking parameters', { parameters: req.params })
85
86 if (areValidationErrors(req, res)) return
87 if (!await checkUserIdExist(req.params.id, res)) return
88
89 const user = res.locals.user
90 if (user.username === 'root') {
91 return res.status(400)
92 .send({ error: 'Cannot block the root user' })
93 .end()
94 }
95
96 return next()
97 }
98 ]
99
100 const deleteMeValidator = [
101 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
102 const user: UserModel = res.locals.oauth.token.User
103 if (user.username === 'root') {
104 return res.status(400)
105 .send({ error: 'You cannot delete your root account.' })
106 .end()
107 }
108
109 return next()
110 }
111 ]
112
113 const usersUpdateValidator = [
114 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
115 body('email').optional().isEmail().withMessage('Should have a valid email attribute'),
116 body('videoQuota').optional().custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'),
117 body('videoQuotaDaily').optional().custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'),
118 body('role').optional().custom(isUserRoleValid).withMessage('Should have a valid role'),
119
120 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
121 logger.debug('Checking usersUpdate parameters', { parameters: req.body })
122
123 if (areValidationErrors(req, res)) return
124 if (!await checkUserIdExist(req.params.id, res)) return
125
126 const user = res.locals.user
127 if (user.username === 'root' && req.body.role !== undefined && user.role !== req.body.role) {
128 return res.status(400)
129 .send({ error: 'Cannot change root role.' })
130 .end()
131 }
132
133 return next()
134 }
135 ]
136
137 const usersUpdateMeValidator = [
138 body('displayName').optional().custom(isUserDisplayNameValid).withMessage('Should have a valid display name'),
139 body('description').optional().custom(isUserDescriptionValid).withMessage('Should have a valid description'),
140 body('password').optional().custom(isUserPasswordValid).withMessage('Should have a valid password'),
141 body('email').optional().isEmail().withMessage('Should have a valid email attribute'),
142 body('nsfwPolicy').optional().custom(isUserNSFWPolicyValid).withMessage('Should have a valid display Not Safe For Work policy'),
143 body('autoPlayVideo').optional().custom(isUserAutoPlayVideoValid).withMessage('Should have a valid automatically plays video attribute'),
144
145 (req: express.Request, res: express.Response, next: express.NextFunction) => {
146 // TODO: Add old password verification
147 logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') })
148
149 if (areValidationErrors(req, res)) return
150
151 return next()
152 }
153 ]
154
155 const usersGetValidator = [
156 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
157
158 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
159 logger.debug('Checking usersGet parameters', { parameters: req.params })
160
161 if (areValidationErrors(req, res)) return
162 if (!await checkUserIdExist(req.params.id, res)) return
163
164 return next()
165 }
166 ]
167
168 const usersVideoRatingValidator = [
169 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'),
170
171 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
172 logger.debug('Checking usersVideoRating parameters', { parameters: req.params })
173
174 if (areValidationErrors(req, res)) return
175 if (!await isVideoExist(req.params.videoId, res)) return
176
177 return next()
178 }
179 ]
180
181 const ensureUserRegistrationAllowed = [
182 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
183 const allowed = await isSignupAllowed()
184 if (allowed === false) {
185 return res.status(403)
186 .send({ error: 'User registration is not enabled or user limit is reached.' })
187 .end()
188 }
189
190 return next()
191 }
192 ]
193
194 const ensureUserRegistrationAllowedForIP = [
195 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
196 const allowed = isSignupAllowedForCurrentIP(req.ip)
197
198 if (allowed === false) {
199 return res.status(403)
200 .send({ error: 'You are not on a network authorized for registration.' })
201 .end()
202 }
203
204 return next()
205 }
206 ]
207
208 const usersAskResetPasswordValidator = [
209 body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
210
211 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
212 logger.debug('Checking usersAskResetPassword parameters', { parameters: req.body })
213
214 if (areValidationErrors(req, res)) return
215 const exists = await checkUserEmailExist(req.body.email, res, false)
216 if (!exists) {
217 logger.debug('User with email %s does not exist (asking reset password).', req.body.email)
218 // Do not leak our emails
219 return res.status(204).end()
220 }
221
222 return next()
223 }
224 ]
225
226 const usersResetPasswordValidator = [
227 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
228 body('verificationString').not().isEmpty().withMessage('Should have a valid verification string'),
229 body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'),
230
231 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
232 logger.debug('Checking usersResetPassword parameters', { parameters: req.params })
233
234 if (areValidationErrors(req, res)) return
235 if (!await checkUserIdExist(req.params.id, res)) return
236
237 const user = res.locals.user as UserModel
238 const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id)
239
240 if (redisVerificationString !== req.body.verificationString) {
241 return res
242 .status(403)
243 .send({ error: 'Invalid verification string.' })
244 .end()
245 }
246
247 return next()
248 }
249 ]
250
251 const usersAskSendVerifyEmailValidator = [
252 body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
253
254 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
255 logger.debug('Checking askUsersSendVerifyEmail parameters', { parameters: req.body })
256
257 if (areValidationErrors(req, res)) return
258 const exists = await checkUserEmailExist(req.body.email, res, false)
259 if (!exists) {
260 logger.debug('User with email %s does not exist (asking verify email).', req.body.email)
261 // Do not leak our emails
262 return res.status(204).end()
263 }
264
265 return next()
266 }
267 ]
268
269 const usersVerifyEmailValidator = [
270 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
271 body('verificationString').not().isEmpty().withMessage('Should have a valid verification string'),
272
273 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
274 logger.debug('Checking usersVerifyEmail parameters', { parameters: req.params })
275
276 if (areValidationErrors(req, res)) return
277 if (!await checkUserIdExist(req.params.id, res)) return
278
279 const user = res.locals.user as UserModel
280 const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id)
281
282 if (redisVerificationString !== req.body.verificationString) {
283 return res
284 .status(403)
285 .send({ error: 'Invalid verification string.' })
286 .end()
287 }
288
289 return next()
290 }
291 ]
292
293 // ---------------------------------------------------------------------------
294
295 export {
296 usersAddValidator,
297 deleteMeValidator,
298 usersRegisterValidator,
299 usersBlockingValidator,
300 usersRemoveValidator,
301 usersUpdateValidator,
302 usersUpdateMeValidator,
303 usersVideoRatingValidator,
304 ensureUserRegistrationAllowed,
305 ensureUserRegistrationAllowedForIP,
306 usersGetValidator,
307 usersAskResetPasswordValidator,
308 usersResetPasswordValidator,
309 usersAskSendVerifyEmailValidator,
310 usersVerifyEmailValidator
311 }
312
313 // ---------------------------------------------------------------------------
314
315 function checkUserIdExist (id: number, res: express.Response) {
316 return checkUserExist(() => UserModel.loadById(id), res)
317 }
318
319 function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) {
320 return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse)
321 }
322
323 async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email: string, res: express.Response) {
324 const user = await UserModel.loadByUsernameOrEmail(username, email)
325
326 if (user) {
327 res.status(409)
328 .send({ error: 'User with this username or email already exists.' })
329 .end()
330 return false
331 }
332
333 const actor = await ActorModel.loadLocalByName(username)
334 if (actor) {
335 res.status(409)
336 .send({ error: 'Another actor (account/channel) with this name on this instance already exists or has already existed.' })
337 .end()
338 return false
339 }
340
341 return true
342 }
343
344 async function checkUserExist (finder: () => Bluebird<UserModel>, res: express.Response, abortResponse = true) {
345 const user = await finder()
346
347 if (!user) {
348 if (abortResponse === true) {
349 res.status(404)
350 .send({ error: 'User not found' })
351 .end()
352 }
353
354 return false
355 }
356
357 res.locals.user = user
358
359 return true
360 }