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