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