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