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