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