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