aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/middlewares
diff options
context:
space:
mode:
Diffstat (limited to 'server/middlewares')
-rw-r--r--server/middlewares/activitypub.ts97
-rw-r--r--server/middlewares/cache.ts7
-rw-r--r--server/middlewares/csp.ts44
-rw-r--r--server/middlewares/dnt.ts2
-rw-r--r--server/middlewares/index.ts2
-rw-r--r--server/middlewares/oauth.ts38
-rw-r--r--server/middlewares/validators/activitypub/signature.ts16
-rw-r--r--server/middlewares/validators/blocklist.ts172
-rw-r--r--server/middlewares/validators/config.ts19
-rw-r--r--server/middlewares/validators/index.ts3
-rw-r--r--server/middlewares/validators/redundancy.ts33
-rw-r--r--server/middlewares/validators/search.ts38
-rw-r--r--server/middlewares/validators/server.ts78
-rw-r--r--server/middlewares/validators/sort.ts11
-rw-r--r--server/middlewares/validators/user-history.ts26
-rw-r--r--server/middlewares/validators/user-notifications.ts63
-rw-r--r--server/middlewares/validators/users.ts14
-rw-r--r--server/middlewares/validators/videos/index.ts2
-rw-r--r--server/middlewares/validators/videos/video-blacklist.ts15
-rw-r--r--server/middlewares/validators/videos/video-rates.ts55
-rw-r--r--server/middlewares/validators/videos/video-shares.ts38
-rw-r--r--server/middlewares/validators/videos/video-watch.ts7
-rw-r--r--server/middlewares/validators/videos/videos.ts165
23 files changed, 808 insertions, 137 deletions
diff --git a/server/middlewares/activitypub.ts b/server/middlewares/activitypub.ts
index d7f59be8c..01e5dd24e 100644
--- a/server/middlewares/activitypub.ts
+++ b/server/middlewares/activitypub.ts
@@ -2,34 +2,32 @@ import { eachSeries } from 'async'
2import { NextFunction, Request, RequestHandler, Response } from 'express' 2import { NextFunction, Request, RequestHandler, Response } from 'express'
3import { ActivityPubSignature } from '../../shared' 3import { ActivityPubSignature } from '../../shared'
4import { logger } from '../helpers/logger' 4import { logger } from '../helpers/logger'
5import { isSignatureVerified } from '../helpers/peertube-crypto' 5import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../helpers/peertube-crypto'
6import { ACCEPT_HEADERS, ACTIVITY_PUB } from '../initializers' 6import { ACCEPT_HEADERS, ACTIVITY_PUB, HTTP_SIGNATURE } from '../initializers'
7import { getOrCreateActorAndServerAndModel } from '../lib/activitypub' 7import { getOrCreateActorAndServerAndModel } from '../lib/activitypub'
8import { ActorModel } from '../models/activitypub/actor' 8import { ActorModel } from '../models/activitypub/actor'
9import { loadActorUrlOrGetFromWebfinger } from '../helpers/webfinger'
9 10
10async function checkSignature (req: Request, res: Response, next: NextFunction) { 11async function checkSignature (req: Request, res: Response, next: NextFunction) {
11 const signatureObject: ActivityPubSignature = req.body.signature 12 try {
13 const httpSignatureChecked = await checkHttpSignature(req, res)
14 if (httpSignatureChecked !== true) return
12 15
13 const [ creator ] = signatureObject.creator.split('#') 16 const actor: ActorModel = res.locals.signature.actor
14 17
15 logger.debug('Checking signature of actor %s...', creator) 18 // Forwarded activity
19 const bodyActor = req.body.actor
20 const bodyActorId = bodyActor && bodyActor.id ? bodyActor.id : bodyActor
21 if (bodyActorId && bodyActorId !== actor.url) {
22 const jsonLDSignatureChecked = await checkJsonLDSignature(req, res)
23 if (jsonLDSignatureChecked !== true) return
24 }
16 25
17 let actor: ActorModel 26 return next()
18 try {
19 actor = await getOrCreateActorAndServerAndModel(creator)
20 } catch (err) { 27 } catch (err) {
21 logger.warn('Cannot create remote actor %s and check signature.', creator, { err }) 28 logger.error('Error in ActivityPub signature checker.', err)
22 return res.sendStatus(403) 29 return res.sendStatus(403)
23 } 30 }
24
25 const verified = await isSignatureVerified(actor, req.body)
26 if (verified === false) return res.sendStatus(403)
27
28 res.locals.signature = {
29 actor
30 }
31
32 return next()
33} 31}
34 32
35function executeIfActivityPub (fun: RequestHandler | RequestHandler[]) { 33function executeIfActivityPub (fun: RequestHandler | RequestHandler[]) {
@@ -55,5 +53,66 @@ function executeIfActivityPub (fun: RequestHandler | RequestHandler[]) {
55 53
56export { 54export {
57 checkSignature, 55 checkSignature,
58 executeIfActivityPub 56 executeIfActivityPub,
57 checkHttpSignature
58}
59
60// ---------------------------------------------------------------------------
61
62async function checkHttpSignature (req: Request, res: Response) {
63 // FIXME: mastodon does not include the Signature scheme
64 const sig = req.headers[HTTP_SIGNATURE.HEADER_NAME] as string
65 if (sig && sig.startsWith('Signature ') === false) req.headers[HTTP_SIGNATURE.HEADER_NAME] = 'Signature ' + sig
66
67 const parsed = parseHTTPSignature(req)
68
69 const keyId = parsed.keyId
70 if (!keyId) {
71 res.sendStatus(403)
72 return false
73 }
74
75 logger.debug('Checking HTTP signature of actor %s...', keyId)
76
77 let [ actorUrl ] = keyId.split('#')
78 if (actorUrl.startsWith('acct:')) {
79 actorUrl = await loadActorUrlOrGetFromWebfinger(actorUrl.replace(/^acct:/, ''))
80 }
81
82 const actor = await getOrCreateActorAndServerAndModel(actorUrl)
83
84 const verified = isHTTPSignatureVerified(parsed, actor)
85 if (verified !== true) {
86 res.sendStatus(403)
87 return false
88 }
89
90 res.locals.signature = { actor }
91
92 return true
93}
94
95async function checkJsonLDSignature (req: Request, res: Response) {
96 const signatureObject: ActivityPubSignature = req.body.signature
97
98 if (!signatureObject || !signatureObject.creator) {
99 res.sendStatus(403)
100 return false
101 }
102
103 const [ creator ] = signatureObject.creator.split('#')
104
105 logger.debug('Checking JsonLD signature of actor %s...', creator)
106
107 const actor = await getOrCreateActorAndServerAndModel(creator)
108 const verified = await isJsonLDSignatureVerified(actor, req.body)
109
110 if (verified !== true) {
111 res.sendStatus(403)
112 return false
113 }
114
115 res.locals.signature = { actor }
116
117 return true
59} 118}
diff --git a/server/middlewares/cache.ts b/server/middlewares/cache.ts
index 1e00fc731..8ffe75700 100644
--- a/server/middlewares/cache.ts
+++ b/server/middlewares/cache.ts
@@ -19,6 +19,7 @@ function cacheRoute (lifetimeArg: string | number) {
19 logger.debug('No cached results for route %s.', req.originalUrl) 19 logger.debug('No cached results for route %s.', req.originalUrl)
20 20
21 const sendSave = res.send.bind(res) 21 const sendSave = res.send.bind(res)
22 const redirectSave = res.redirect.bind(res)
22 23
23 res.send = (body) => { 24 res.send = (body) => {
24 if (res.statusCode >= 200 && res.statusCode < 400) { 25 if (res.statusCode >= 200 && res.statusCode < 400) {
@@ -38,6 +39,12 @@ function cacheRoute (lifetimeArg: string | number) {
38 return sendSave(body) 39 return sendSave(body)
39 } 40 }
40 41
42 res.redirect = url => {
43 done()
44
45 return redirectSave(url)
46 }
47
41 return next() 48 return next()
42 } 49 }
43 50
diff --git a/server/middlewares/csp.ts b/server/middlewares/csp.ts
new file mode 100644
index 000000000..5fa9d1ab5
--- /dev/null
+++ b/server/middlewares/csp.ts
@@ -0,0 +1,44 @@
1import * as helmet from 'helmet'
2import { CONFIG } from '../initializers/constants'
3
4const baseDirectives = Object.assign({},
5 {
6 defaultSrc: ["'none'"], // by default, not specifying default-src = '*'
7 connectSrc: ['*', 'data:'],
8 mediaSrc: ["'self'", 'https:', 'blob:'],
9 fontSrc: ["'self'", 'data:'],
10 imgSrc: ["'self'", 'data:'],
11 scriptSrc: ["'self' 'unsafe-inline' 'unsafe-eval'"],
12 styleSrc: ["'self' 'unsafe-inline'"],
13 objectSrc: ["'none'"], // only define to allow plugins, else let defaultSrc 'none' block it
14 formAction: ["'self'"],
15 frameAncestors: ["'none'"],
16 baseUri: ["'self'"],
17 manifestSrc: ["'self'"],
18 frameSrc: ["'self'"], // instead of deprecated child-src / self because of test-embed
19 workerSrc: ["'self'", 'blob:'] // instead of deprecated child-src
20 },
21 CONFIG.SERVICES['CSP-LOGGER'] ? { reportUri: CONFIG.SERVICES['CSP-LOGGER'] } : {},
22 CONFIG.WEBSERVER.SCHEME === 'https' ? { upgradeInsecureRequests: true } : {}
23)
24
25const baseCSP = helmet.contentSecurityPolicy({
26 directives: baseDirectives,
27 browserSniff: false,
28 reportOnly: true
29})
30
31const embedCSP = helmet.contentSecurityPolicy({
32 directives: Object.assign(baseDirectives, {
33 frameAncestors: ['*']
34 }),
35 browserSniff: false, // assumes a modern browser, but allows CDN in front
36 reportOnly: true
37})
38
39// ---------------------------------------------------------------------------
40
41export {
42 baseCSP,
43 embedCSP
44}
diff --git a/server/middlewares/dnt.ts b/server/middlewares/dnt.ts
index cabad39c6..607def855 100644
--- a/server/middlewares/dnt.ts
+++ b/server/middlewares/dnt.ts
@@ -10,4 +10,4 @@ const advertiseDoNotTrack = (_, res, next) => {
10 10
11export { 11export {
12 advertiseDoNotTrack 12 advertiseDoNotTrack
13 } 13}
diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts
index 0cef26953..b758a8586 100644
--- a/server/middlewares/index.ts
+++ b/server/middlewares/index.ts
@@ -6,3 +6,5 @@ export * from './pagination'
6export * from './servers' 6export * from './servers'
7export * from './sort' 7export * from './sort'
8export * from './user-right' 8export * from './user-right'
9export * from './dnt'
10export * from './csp'
diff --git a/server/middlewares/oauth.ts b/server/middlewares/oauth.ts
index 5233b66bd..1d193d467 100644
--- a/server/middlewares/oauth.ts
+++ b/server/middlewares/oauth.ts
@@ -3,6 +3,8 @@ import * as OAuthServer from 'express-oauth-server'
3import 'express-validator' 3import 'express-validator'
4import { OAUTH_LIFETIME } from '../initializers' 4import { OAUTH_LIFETIME } from '../initializers'
5import { logger } from '../helpers/logger' 5import { logger } from '../helpers/logger'
6import { Socket } from 'socket.io'
7import { getAccessToken } from '../lib/oauth-model'
6 8
7const oAuthServer = new OAuthServer({ 9const oAuthServer = new OAuthServer({
8 useErrorHandler: true, 10 useErrorHandler: true,
@@ -28,9 +30,43 @@ function authenticate (req: express.Request, res: express.Response, next: expres
28 }) 30 })
29} 31}
30 32
33function authenticateSocket (socket: Socket, next: (err?: any) => void) {
34 const accessToken = socket.handshake.query.accessToken
35
36 logger.debug('Checking socket access token %s.', accessToken)
37
38 getAccessToken(accessToken)
39 .then(tokenDB => {
40 const now = new Date()
41
42 if (!tokenDB || tokenDB.accessTokenExpiresAt < now || tokenDB.refreshTokenExpiresAt < now) {
43 return next(new Error('Invalid access token.'))
44 }
45
46 socket.handshake.query.user = tokenDB.User
47
48 return next()
49 })
50}
51
52function authenticatePromiseIfNeeded (req: express.Request, res: express.Response) {
53 return new Promise(resolve => {
54 // Already authenticated? (or tried to)
55 if (res.locals.oauth && res.locals.oauth.token.User) return resolve()
56
57 if (res.locals.authenticated === false) return res.sendStatus(401)
58
59 authenticate(req, res, () => {
60 return resolve()
61 })
62 })
63}
64
31function optionalAuthenticate (req: express.Request, res: express.Response, next: express.NextFunction) { 65function optionalAuthenticate (req: express.Request, res: express.Response, next: express.NextFunction) {
32 if (req.header('authorization')) return authenticate(req, res, next) 66 if (req.header('authorization')) return authenticate(req, res, next)
33 67
68 res.locals.authenticated = false
69
34 return next() 70 return next()
35} 71}
36 72
@@ -53,6 +89,8 @@ function token (req: express.Request, res: express.Response, next: express.NextF
53 89
54export { 90export {
55 authenticate, 91 authenticate,
92 authenticateSocket,
93 authenticatePromiseIfNeeded,
56 optionalAuthenticate, 94 optionalAuthenticate,
57 token 95 token
58} 96}
diff --git a/server/middlewares/validators/activitypub/signature.ts b/server/middlewares/validators/activitypub/signature.ts
index 4efe9aafa..be14e92ea 100644
--- a/server/middlewares/validators/activitypub/signature.ts
+++ b/server/middlewares/validators/activitypub/signature.ts
@@ -9,10 +9,18 @@ import { logger } from '../../../helpers/logger'
9import { areValidationErrors } from '../utils' 9import { areValidationErrors } from '../utils'
10 10
11const signatureValidator = [ 11const signatureValidator = [
12 body('signature.type').custom(isSignatureTypeValid).withMessage('Should have a valid signature type'), 12 body('signature.type')
13 body('signature.created').custom(isDateValid).withMessage('Should have a valid signature created date'), 13 .optional()
14 body('signature.creator').custom(isSignatureCreatorValid).withMessage('Should have a valid signature creator'), 14 .custom(isSignatureTypeValid).withMessage('Should have a valid signature type'),
15 body('signature.signatureValue').custom(isSignatureValueValid).withMessage('Should have a valid signature value'), 15 body('signature.created')
16 .optional()
17 .custom(isDateValid).withMessage('Should have a valid signature created date'),
18 body('signature.creator')
19 .optional()
20 .custom(isSignatureCreatorValid).withMessage('Should have a valid signature creator'),
21 body('signature.signatureValue')
22 .optional()
23 .custom(isSignatureValueValid).withMessage('Should have a valid signature value'),
16 24
17 (req: express.Request, res: express.Response, next: express.NextFunction) => { 25 (req: express.Request, res: express.Response, next: express.NextFunction) => {
18 logger.debug('Checking activitypub signature parameter', { parameters: { signature: req.body.signature } }) 26 logger.debug('Checking activitypub signature parameter', { parameters: { signature: req.body.signature } })
diff --git a/server/middlewares/validators/blocklist.ts b/server/middlewares/validators/blocklist.ts
new file mode 100644
index 000000000..109276c63
--- /dev/null
+++ b/server/middlewares/validators/blocklist.ts
@@ -0,0 +1,172 @@
1import { body, param } from 'express-validator/check'
2import * as express from 'express'
3import { logger } from '../../helpers/logger'
4import { areValidationErrors } from './utils'
5import { isAccountNameWithHostExist } from '../../helpers/custom-validators/accounts'
6import { UserModel } from '../../models/account/user'
7import { AccountBlocklistModel } from '../../models/account/account-blocklist'
8import { isHostValid } from '../../helpers/custom-validators/servers'
9import { ServerBlocklistModel } from '../../models/server/server-blocklist'
10import { ServerModel } from '../../models/server/server'
11import { CONFIG } from '../../initializers'
12import { getServerActor } from '../../helpers/utils'
13
14const blockAccountValidator = [
15 body('accountName').exists().withMessage('Should have an account name with host'),
16
17 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
18 logger.debug('Checking blockAccountByAccountValidator parameters', { parameters: req.body })
19
20 if (areValidationErrors(req, res)) return
21 if (!await isAccountNameWithHostExist(req.body.accountName, res)) return
22
23 const user = res.locals.oauth.token.User as UserModel
24 const accountToBlock = res.locals.account
25
26 if (user.Account.id === accountToBlock.id) {
27 res.status(409)
28 .send({ error: 'You cannot block yourself.' })
29 .end()
30
31 return
32 }
33
34 return next()
35 }
36]
37
38const unblockAccountByAccountValidator = [
39 param('accountName').exists().withMessage('Should have an account name with host'),
40
41 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
42 logger.debug('Checking unblockAccountByAccountValidator parameters', { parameters: req.params })
43
44 if (areValidationErrors(req, res)) return
45 if (!await isAccountNameWithHostExist(req.params.accountName, res)) return
46
47 const user = res.locals.oauth.token.User as UserModel
48 const targetAccount = res.locals.account
49 if (!await isUnblockAccountExists(user.Account.id, targetAccount.id, res)) return
50
51 return next()
52 }
53]
54
55const unblockAccountByServerValidator = [
56 param('accountName').exists().withMessage('Should have an account name with host'),
57
58 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
59 logger.debug('Checking unblockAccountByServerValidator parameters', { parameters: req.params })
60
61 if (areValidationErrors(req, res)) return
62 if (!await isAccountNameWithHostExist(req.params.accountName, res)) return
63
64 const serverActor = await getServerActor()
65 const targetAccount = res.locals.account
66 if (!await isUnblockAccountExists(serverActor.Account.id, targetAccount.id, res)) return
67
68 return next()
69 }
70]
71
72const blockServerValidator = [
73 body('host').custom(isHostValid).withMessage('Should have a valid host'),
74
75 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
76 logger.debug('Checking serverGetValidator parameters', { parameters: req.body })
77
78 if (areValidationErrors(req, res)) return
79
80 const host: string = req.body.host
81
82 if (host === CONFIG.WEBSERVER.HOST) {
83 return res.status(409)
84 .send({ error: 'You cannot block your own server.' })
85 .end()
86 }
87
88 const server = await ServerModel.loadByHost(host)
89 if (!server) {
90 return res.status(404)
91 .send({ error: 'Server host not found.' })
92 .end()
93 }
94
95 res.locals.server = server
96
97 return next()
98 }
99]
100
101const unblockServerByAccountValidator = [
102 param('host').custom(isHostValid).withMessage('Should have an account name with host'),
103
104 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
105 logger.debug('Checking unblockServerByAccountValidator parameters', { parameters: req.params })
106
107 if (areValidationErrors(req, res)) return
108
109 const user = res.locals.oauth.token.User as UserModel
110 if (!await isUnblockServerExists(user.Account.id, req.params.host, res)) return
111
112 return next()
113 }
114]
115
116const unblockServerByServerValidator = [
117 param('host').custom(isHostValid).withMessage('Should have an account name with host'),
118
119 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
120 logger.debug('Checking unblockServerByServerValidator parameters', { parameters: req.params })
121
122 if (areValidationErrors(req, res)) return
123
124 const serverActor = await getServerActor()
125 if (!await isUnblockServerExists(serverActor.Account.id, req.params.host, res)) return
126
127 return next()
128 }
129]
130
131// ---------------------------------------------------------------------------
132
133export {
134 blockServerValidator,
135 blockAccountValidator,
136 unblockAccountByAccountValidator,
137 unblockServerByAccountValidator,
138 unblockAccountByServerValidator,
139 unblockServerByServerValidator
140}
141
142// ---------------------------------------------------------------------------
143
144async function isUnblockAccountExists (accountId: number, targetAccountId: number, res: express.Response) {
145 const accountBlock = await AccountBlocklistModel.loadByAccountAndTarget(accountId, targetAccountId)
146 if (!accountBlock) {
147 res.status(404)
148 .send({ error: 'Account block entry not found.' })
149 .end()
150
151 return false
152 }
153
154 res.locals.accountBlock = accountBlock
155
156 return true
157}
158
159async function isUnblockServerExists (accountId: number, host: string, res: express.Response) {
160 const serverBlock = await ServerBlocklistModel.loadByAccountAndHost(accountId, host)
161 if (!serverBlock) {
162 res.status(404)
163 .send({ error: 'Server block entry not found.' })
164 .end()
165
166 return false
167 }
168
169 res.locals.serverBlock = serverBlock
170
171 return true
172}
diff --git a/server/middlewares/validators/config.ts b/server/middlewares/validators/config.ts
index f3f257d57..90108fa82 100644
--- a/server/middlewares/validators/config.ts
+++ b/server/middlewares/validators/config.ts
@@ -1,29 +1,44 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body } from 'express-validator/check' 2import { body } from 'express-validator/check'
3import { isUserNSFWPolicyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users' 3import { isUserNSFWPolicyValid, isUserVideoQuotaValid, isUserVideoQuotaDailyValid } from '../../helpers/custom-validators/users'
4import { logger } from '../../helpers/logger' 4import { logger } from '../../helpers/logger'
5import { areValidationErrors } from './utils' 5import { areValidationErrors } from './utils'
6 6
7const customConfigUpdateValidator = [ 7const customConfigUpdateValidator = [
8 body('instance.name').exists().withMessage('Should have a valid instance name'), 8 body('instance.name').exists().withMessage('Should have a valid instance name'),
9 body('instance.shortDescription').exists().withMessage('Should have a valid instance short description'),
9 body('instance.description').exists().withMessage('Should have a valid instance description'), 10 body('instance.description').exists().withMessage('Should have a valid instance description'),
10 body('instance.terms').exists().withMessage('Should have a valid instance terms'), 11 body('instance.terms').exists().withMessage('Should have a valid instance terms'),
11 body('instance.defaultClientRoute').exists().withMessage('Should have a valid instance default client route'), 12 body('instance.defaultClientRoute').exists().withMessage('Should have a valid instance default client route'),
12 body('instance.defaultNSFWPolicy').custom(isUserNSFWPolicyValid).withMessage('Should have a valid NSFW policy'), 13 body('instance.defaultNSFWPolicy').custom(isUserNSFWPolicyValid).withMessage('Should have a valid NSFW policy'),
13 body('instance.customizations.css').exists().withMessage('Should have a valid instance CSS customization'), 14 body('instance.customizations.css').exists().withMessage('Should have a valid instance CSS customization'),
14 body('instance.customizations.javascript').exists().withMessage('Should have a valid instance JavaScript customization'), 15 body('instance.customizations.javascript').exists().withMessage('Should have a valid instance JavaScript customization'),
15 body('cache.previews.size').isInt().withMessage('Should have a valid previews size'), 16
17 body('services.twitter.username').exists().withMessage('Should have a valid twitter username'),
18 body('services.twitter.whitelisted').isBoolean().withMessage('Should have a valid twitter whitelisted boolean'),
19
20 body('cache.previews.size').isInt().withMessage('Should have a valid previews cache size'),
21 body('cache.captions.size').isInt().withMessage('Should have a valid captions cache size'),
22
16 body('signup.enabled').isBoolean().withMessage('Should have a valid signup enabled boolean'), 23 body('signup.enabled').isBoolean().withMessage('Should have a valid signup enabled boolean'),
17 body('signup.limit').isInt().withMessage('Should have a valid signup limit'), 24 body('signup.limit').isInt().withMessage('Should have a valid signup limit'),
25 body('signup.requiresEmailVerification').isBoolean().withMessage('Should have a valid requiresEmailVerification boolean'),
26
18 body('admin.email').isEmail().withMessage('Should have a valid administrator email'), 27 body('admin.email').isEmail().withMessage('Should have a valid administrator email'),
28 body('contactForm.enabled').isBoolean().withMessage('Should have a valid contact form enabled boolean'),
29
19 body('user.videoQuota').custom(isUserVideoQuotaValid).withMessage('Should have a valid video quota'), 30 body('user.videoQuota').custom(isUserVideoQuotaValid).withMessage('Should have a valid video quota'),
31 body('user.videoQuotaDaily').custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily video quota'),
32
20 body('transcoding.enabled').isBoolean().withMessage('Should have a valid transcoding enabled boolean'), 33 body('transcoding.enabled').isBoolean().withMessage('Should have a valid transcoding enabled boolean'),
34 body('transcoding.allowAdditionalExtensions').isBoolean().withMessage('Should have a valid additional extensions boolean'),
21 body('transcoding.threads').isInt().withMessage('Should have a valid transcoding threads number'), 35 body('transcoding.threads').isInt().withMessage('Should have a valid transcoding threads number'),
22 body('transcoding.resolutions.240p').isBoolean().withMessage('Should have a valid transcoding 240p resolution enabled boolean'), 36 body('transcoding.resolutions.240p').isBoolean().withMessage('Should have a valid transcoding 240p resolution enabled boolean'),
23 body('transcoding.resolutions.360p').isBoolean().withMessage('Should have a valid transcoding 360p resolution enabled boolean'), 37 body('transcoding.resolutions.360p').isBoolean().withMessage('Should have a valid transcoding 360p resolution enabled boolean'),
24 body('transcoding.resolutions.480p').isBoolean().withMessage('Should have a valid transcoding 480p resolution enabled boolean'), 38 body('transcoding.resolutions.480p').isBoolean().withMessage('Should have a valid transcoding 480p resolution enabled boolean'),
25 body('transcoding.resolutions.720p').isBoolean().withMessage('Should have a valid transcoding 720p resolution enabled boolean'), 39 body('transcoding.resolutions.720p').isBoolean().withMessage('Should have a valid transcoding 720p resolution enabled boolean'),
26 body('transcoding.resolutions.1080p').isBoolean().withMessage('Should have a valid transcoding 1080p resolution enabled boolean'), 40 body('transcoding.resolutions.1080p').isBoolean().withMessage('Should have a valid transcoding 1080p resolution enabled boolean'),
41
27 body('import.videos.http.enabled').isBoolean().withMessage('Should have a valid import video http enabled boolean'), 42 body('import.videos.http.enabled').isBoolean().withMessage('Should have a valid import video http enabled boolean'),
28 body('import.videos.torrent.enabled').isBoolean().withMessage('Should have a valid import video torrent enabled boolean'), 43 body('import.videos.torrent.enabled').isBoolean().withMessage('Should have a valid import video torrent enabled boolean'),
29 44
diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts
index 17226614c..65dd00335 100644
--- a/server/middlewares/validators/index.ts
+++ b/server/middlewares/validators/index.ts
@@ -1,4 +1,5 @@
1export * from './account' 1export * from './account'
2export * from './blocklist'
2export * from './oembed' 3export * from './oembed'
3export * from './activitypub' 4export * from './activitypub'
4export * from './pagination' 5export * from './pagination'
@@ -10,3 +11,5 @@ export * from './user-subscriptions'
10export * from './videos' 11export * from './videos'
11export * from './webfinger' 12export * from './webfinger'
12export * from './search' 13export * from './search'
14export * from './server'
15export * from './user-history'
diff --git a/server/middlewares/validators/redundancy.ts b/server/middlewares/validators/redundancy.ts
index c72ab78b2..329322509 100644
--- a/server/middlewares/validators/redundancy.ts
+++ b/server/middlewares/validators/redundancy.ts
@@ -13,7 +13,7 @@ import { ActorFollowModel } from '../../models/activitypub/actor-follow'
13import { SERVER_ACTOR_NAME } from '../../initializers' 13import { SERVER_ACTOR_NAME } from '../../initializers'
14import { ServerModel } from '../../models/server/server' 14import { ServerModel } from '../../models/server/server'
15 15
16const videoRedundancyGetValidator = [ 16const videoFileRedundancyGetValidator = [
17 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), 17 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'),
18 param('resolution') 18 param('resolution')
19 .customSanitizer(toIntOrNull) 19 .customSanitizer(toIntOrNull)
@@ -24,7 +24,7 @@ const videoRedundancyGetValidator = [
24 .custom(exists).withMessage('Should have a valid fps'), 24 .custom(exists).withMessage('Should have a valid fps'),
25 25
26 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 26 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
27 logger.debug('Checking videoRedundancyGetValidator parameters', { parameters: req.params }) 27 logger.debug('Checking videoFileRedundancyGetValidator parameters', { parameters: req.params })
28 28
29 if (areValidationErrors(req, res)) return 29 if (areValidationErrors(req, res)) return
30 if (!await isVideoExist(req.params.videoId, res)) return 30 if (!await isVideoExist(req.params.videoId, res)) return
@@ -38,7 +38,31 @@ const videoRedundancyGetValidator = [
38 res.locals.videoFile = videoFile 38 res.locals.videoFile = videoFile
39 39
40 const videoRedundancy = await VideoRedundancyModel.loadLocalByFileId(videoFile.id) 40 const videoRedundancy = await VideoRedundancyModel.loadLocalByFileId(videoFile.id)
41 if (!videoRedundancy)return res.status(404).json({ error: 'Video redundancy not found.' }) 41 if (!videoRedundancy) return res.status(404).json({ error: 'Video redundancy not found.' })
42 res.locals.videoRedundancy = videoRedundancy
43
44 return next()
45 }
46]
47
48const videoPlaylistRedundancyGetValidator = [
49 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'),
50 param('streamingPlaylistType').custom(exists).withMessage('Should have a valid streaming playlist type'),
51
52 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
53 logger.debug('Checking videoPlaylistRedundancyGetValidator parameters', { parameters: req.params })
54
55 if (areValidationErrors(req, res)) return
56 if (!await isVideoExist(req.params.videoId, res)) return
57
58 const video: VideoModel = res.locals.video
59 const videoStreamingPlaylist = video.VideoStreamingPlaylists.find(p => p === req.params.streamingPlaylistType)
60
61 if (!videoStreamingPlaylist) return res.status(404).json({ error: 'Video playlist not found.' })
62 res.locals.videoStreamingPlaylist = videoStreamingPlaylist
63
64 const videoRedundancy = await VideoRedundancyModel.loadLocalByStreamingPlaylistId(videoStreamingPlaylist.id)
65 if (!videoRedundancy) return res.status(404).json({ error: 'Video redundancy not found.' })
42 res.locals.videoRedundancy = videoRedundancy 66 res.locals.videoRedundancy = videoRedundancy
43 67
44 return next() 68 return next()
@@ -75,6 +99,7 @@ const updateServerRedundancyValidator = [
75// --------------------------------------------------------------------------- 99// ---------------------------------------------------------------------------
76 100
77export { 101export {
78 videoRedundancyGetValidator, 102 videoFileRedundancyGetValidator,
103 videoPlaylistRedundancyGetValidator,
79 updateServerRedundancyValidator 104 updateServerRedundancyValidator
80} 105}
diff --git a/server/middlewares/validators/search.ts b/server/middlewares/validators/search.ts
index 8baf643a5..6a95d6095 100644
--- a/server/middlewares/validators/search.ts
+++ b/server/middlewares/validators/search.ts
@@ -2,8 +2,7 @@ import * as express from 'express'
2import { areValidationErrors } from './utils' 2import { areValidationErrors } from './utils'
3import { logger } from '../../helpers/logger' 3import { logger } from '../../helpers/logger'
4import { query } from 'express-validator/check' 4import { query } from 'express-validator/check'
5import { isNumberArray, isStringArray, isNSFWQueryValid } from '../../helpers/custom-validators/search' 5import { isDateValid } from '../../helpers/custom-validators/misc'
6import { isBooleanValid, isDateValid, toArray } from '../../helpers/custom-validators/misc'
7 6
8const videosSearchValidator = [ 7const videosSearchValidator = [
9 query('search').optional().not().isEmpty().withMessage('Should have a valid search'), 8 query('search').optional().not().isEmpty().withMessage('Should have a valid search'),
@@ -35,44 +34,9 @@ const videoChannelsSearchValidator = [
35 } 34 }
36] 35]
37 36
38const commonVideosFiltersValidator = [
39 query('categoryOneOf')
40 .optional()
41 .customSanitizer(toArray)
42 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
43 query('licenceOneOf')
44 .optional()
45 .customSanitizer(toArray)
46 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
47 query('languageOneOf')
48 .optional()
49 .customSanitizer(toArray)
50 .custom(isStringArray).withMessage('Should have a valid one of language array'),
51 query('tagsOneOf')
52 .optional()
53 .customSanitizer(toArray)
54 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
55 query('tagsAllOf')
56 .optional()
57 .customSanitizer(toArray)
58 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
59 query('nsfw')
60 .optional()
61 .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
62
63 (req: express.Request, res: express.Response, next: express.NextFunction) => {
64 logger.debug('Checking commons video filters query', { parameters: req.query })
65
66 if (areValidationErrors(req, res)) return
67
68 return next()
69 }
70]
71
72// --------------------------------------------------------------------------- 37// ---------------------------------------------------------------------------
73 38
74export { 39export {
75 commonVideosFiltersValidator,
76 videoChannelsSearchValidator, 40 videoChannelsSearchValidator,
77 videosSearchValidator 41 videosSearchValidator
78} 42}
diff --git a/server/middlewares/validators/server.ts b/server/middlewares/validators/server.ts
new file mode 100644
index 000000000..d85afc2ff
--- /dev/null
+++ b/server/middlewares/validators/server.ts
@@ -0,0 +1,78 @@
1import * as express from 'express'
2import { logger } from '../../helpers/logger'
3import { areValidationErrors } from './utils'
4import { isHostValid, isValidContactBody } from '../../helpers/custom-validators/servers'
5import { ServerModel } from '../../models/server/server'
6import { body } from 'express-validator/check'
7import { isUserDisplayNameValid } from '../../helpers/custom-validators/users'
8import { Emailer } from '../../lib/emailer'
9import { Redis } from '../../lib/redis'
10import { CONFIG } from '../../initializers/constants'
11
12const serverGetValidator = [
13 body('host').custom(isHostValid).withMessage('Should have a valid host'),
14
15 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
16 logger.debug('Checking serverGetValidator parameters', { parameters: req.body })
17
18 if (areValidationErrors(req, res)) return
19
20 const server = await ServerModel.loadByHost(req.body.host)
21 if (!server) {
22 return res.status(404)
23 .send({ error: 'Server host not found.' })
24 .end()
25 }
26
27 res.locals.server = server
28
29 return next()
30 }
31]
32
33const contactAdministratorValidator = [
34 body('fromName')
35 .custom(isUserDisplayNameValid).withMessage('Should have a valid name'),
36 body('fromEmail')
37 .isEmail().withMessage('Should have a valid email'),
38 body('body')
39 .custom(isValidContactBody).withMessage('Should have a valid body'),
40
41 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
42 logger.debug('Checking contactAdministratorValidator parameters', { parameters: req.body })
43
44 if (areValidationErrors(req, res)) return
45
46 if (CONFIG.CONTACT_FORM.ENABLED === false) {
47 return res
48 .status(409)
49 .send({ error: 'Contact form is not enabled on this instance.' })
50 .end()
51 }
52
53 if (Emailer.isEnabled() === false) {
54 return res
55 .status(409)
56 .send({ error: 'Emailer is not enabled on this instance.' })
57 .end()
58 }
59
60 if (await Redis.Instance.isContactFormIpExists(req.ip)) {
61 logger.info('Refusing a contact form by %s: already sent one recently.', req.ip)
62
63 return res
64 .status(403)
65 .send({ error: 'You already sent a contact form recently.' })
66 .end()
67 }
68
69 return next()
70 }
71]
72
73// ---------------------------------------------------------------------------
74
75export {
76 serverGetValidator,
77 contactAdministratorValidator
78}
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts
index 08dcc2680..5ceda845f 100644
--- a/server/middlewares/validators/sort.ts
+++ b/server/middlewares/validators/sort.ts
@@ -16,6 +16,9 @@ const SORTABLE_VIDEO_CHANNELS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.V
16const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWERS) 16const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWERS)
17const SORTABLE_FOLLOWING_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWING) 17const SORTABLE_FOLLOWING_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWING)
18const SORTABLE_USER_SUBSCRIPTIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_SUBSCRIPTIONS) 18const SORTABLE_USER_SUBSCRIPTIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_SUBSCRIPTIONS)
19const SORTABLE_ACCOUNTS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ACCOUNTS_BLOCKLIST)
20const SORTABLE_SERVERS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.SERVERS_BLOCKLIST)
21const SORTABLE_USER_NOTIFICATIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_NOTIFICATIONS)
19 22
20const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) 23const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS)
21const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) 24const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS)
@@ -31,6 +34,9 @@ const videoChannelsSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_COLUMNS)
31const followersSortValidator = checkSort(SORTABLE_FOLLOWERS_COLUMNS) 34const followersSortValidator = checkSort(SORTABLE_FOLLOWERS_COLUMNS)
32const followingSortValidator = checkSort(SORTABLE_FOLLOWING_COLUMNS) 35const followingSortValidator = checkSort(SORTABLE_FOLLOWING_COLUMNS)
33const userSubscriptionsSortValidator = checkSort(SORTABLE_USER_SUBSCRIPTIONS_COLUMNS) 36const userSubscriptionsSortValidator = checkSort(SORTABLE_USER_SUBSCRIPTIONS_COLUMNS)
37const accountsBlocklistSortValidator = checkSort(SORTABLE_ACCOUNTS_BLOCKLIST_COLUMNS)
38const serversBlocklistSortValidator = checkSort(SORTABLE_SERVERS_BLOCKLIST_COLUMNS)
39const userNotificationsSortValidator = checkSort(SORTABLE_USER_NOTIFICATIONS_COLUMNS)
34 40
35// --------------------------------------------------------------------------- 41// ---------------------------------------------------------------------------
36 42
@@ -48,5 +54,8 @@ export {
48 jobsSortValidator, 54 jobsSortValidator,
49 videoCommentThreadsSortValidator, 55 videoCommentThreadsSortValidator,
50 userSubscriptionsSortValidator, 56 userSubscriptionsSortValidator,
51 videoChannelsSearchSortValidator 57 videoChannelsSearchSortValidator,
58 accountsBlocklistSortValidator,
59 serversBlocklistSortValidator,
60 userNotificationsSortValidator
52} 61}
diff --git a/server/middlewares/validators/user-history.ts b/server/middlewares/validators/user-history.ts
new file mode 100644
index 000000000..418313d09
--- /dev/null
+++ b/server/middlewares/validators/user-history.ts
@@ -0,0 +1,26 @@
1import * as express from 'express'
2import 'express-validator'
3import { body } from 'express-validator/check'
4import { logger } from '../../helpers/logger'
5import { areValidationErrors } from './utils'
6import { isDateValid } from '../../helpers/custom-validators/misc'
7
8const userHistoryRemoveValidator = [
9 body('beforeDate')
10 .optional()
11 .custom(isDateValid).withMessage('Should have a valid before date'),
12
13 (req: express.Request, res: express.Response, next: express.NextFunction) => {
14 logger.debug('Checking userHistoryRemoveValidator parameters', { parameters: req.body })
15
16 if (areValidationErrors(req, res)) return
17
18 return next()
19 }
20]
21
22// ---------------------------------------------------------------------------
23
24export {
25 userHistoryRemoveValidator
26}
diff --git a/server/middlewares/validators/user-notifications.ts b/server/middlewares/validators/user-notifications.ts
new file mode 100644
index 000000000..46486e081
--- /dev/null
+++ b/server/middlewares/validators/user-notifications.ts
@@ -0,0 +1,63 @@
1import * as express from 'express'
2import 'express-validator'
3import { body, query } from 'express-validator/check'
4import { logger } from '../../helpers/logger'
5import { areValidationErrors } from './utils'
6import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications'
7import { isNotEmptyIntArray } from '../../helpers/custom-validators/misc'
8
9const listUserNotificationsValidator = [
10 query('unread')
11 .optional()
12 .toBoolean()
13 .isBoolean().withMessage('Should have a valid unread boolean'),
14
15 (req: express.Request, res: express.Response, next: express.NextFunction) => {
16 logger.debug('Checking listUserNotificationsValidator parameters', { parameters: req.query })
17
18 if (areValidationErrors(req, res)) return
19
20 return next()
21 }
22]
23
24const updateNotificationSettingsValidator = [
25 body('newVideoFromSubscription')
26 .custom(isUserNotificationSettingValid).withMessage('Should have a valid new video from subscription notification setting'),
27 body('newCommentOnMyVideo')
28 .custom(isUserNotificationSettingValid).withMessage('Should have a valid new comment on my video notification setting'),
29 body('videoAbuseAsModerator')
30 .custom(isUserNotificationSettingValid).withMessage('Should have a valid new video abuse as moderator notification setting'),
31 body('blacklistOnMyVideo')
32 .custom(isUserNotificationSettingValid).withMessage('Should have a valid new blacklist on my video notification setting'),
33
34 (req: express.Request, res: express.Response, next: express.NextFunction) => {
35 logger.debug('Checking updateNotificationSettingsValidator parameters', { parameters: req.body })
36
37 if (areValidationErrors(req, res)) return
38
39 return next()
40 }
41]
42
43const markAsReadUserNotificationsValidator = [
44 body('ids')
45 .optional()
46 .custom(isNotEmptyIntArray).withMessage('Should have a valid notification ids to mark as read'),
47
48 (req: express.Request, res: express.Response, next: express.NextFunction) => {
49 logger.debug('Checking markAsReadUserNotificationsValidator parameters', { parameters: req.body })
50
51 if (areValidationErrors(req, res)) return
52
53 return next()
54 }
55]
56
57// ---------------------------------------------------------------------------
58
59export {
60 listUserNotificationsValidator,
61 updateNotificationSettingsValidator,
62 markAsReadUserNotificationsValidator
63}
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts
index 61297120a..a52e3060a 100644
--- a/server/middlewares/validators/users.ts
+++ b/server/middlewares/validators/users.ts
@@ -5,15 +5,16 @@ import { body, param } from 'express-validator/check'
5import { omit } from 'lodash' 5import { omit } from 'lodash'
6import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' 6import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc'
7import { 7import {
8 isUserAutoPlayVideoValid, isUserBlockedReasonValid, 8 isUserAutoPlayVideoValid,
9 isUserBlockedReasonValid,
9 isUserDescriptionValid, 10 isUserDescriptionValid,
10 isUserDisplayNameValid, 11 isUserDisplayNameValid,
11 isUserNSFWPolicyValid, 12 isUserNSFWPolicyValid,
12 isUserPasswordValid, 13 isUserPasswordValid,
13 isUserRoleValid, 14 isUserRoleValid,
14 isUserUsernameValid, 15 isUserUsernameValid,
15 isUserVideoQuotaValid, 16 isUserVideoQuotaDailyValid,
16 isUserVideoQuotaDailyValid 17 isUserVideoQuotaValid, isUserVideosHistoryEnabledValid
17} from '../../helpers/custom-validators/users' 18} from '../../helpers/custom-validators/users'
18import { isVideoExist } from '../../helpers/custom-validators/videos' 19import { isVideoExist } from '../../helpers/custom-validators/videos'
19import { logger } from '../../helpers/logger' 20import { logger } from '../../helpers/logger'
@@ -22,7 +23,6 @@ import { Redis } from '../../lib/redis'
22import { UserModel } from '../../models/account/user' 23import { UserModel } from '../../models/account/user'
23import { areValidationErrors } from './utils' 24import { areValidationErrors } from './utils'
24import { ActorModel } from '../../models/activitypub/actor' 25import { ActorModel } from '../../models/activitypub/actor'
25import { comparePassword } from '../../helpers/peertube-crypto'
26 26
27const usersAddValidator = [ 27const usersAddValidator = [
28 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'), 28 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'),
@@ -113,7 +113,9 @@ const deleteMeValidator = [
113 113
114const usersUpdateValidator = [ 114const usersUpdateValidator = [
115 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'), 115 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
116 body('password').optional().custom(isUserPasswordValid).withMessage('Should have a valid password'),
116 body('email').optional().isEmail().withMessage('Should have a valid email attribute'), 117 body('email').optional().isEmail().withMessage('Should have a valid email attribute'),
118 body('emailVerified').optional().isBoolean().withMessage('Should have a valid email verified attribute'),
117 body('videoQuota').optional().custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'), 119 body('videoQuota').optional().custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'),
118 body('videoQuotaDaily').optional().custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'), 120 body('videoQuotaDaily').optional().custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'),
119 body('role').optional().custom(isUserRoleValid).withMessage('Should have a valid role'), 121 body('role').optional().custom(isUserRoleValid).withMessage('Should have a valid role'),
@@ -143,6 +145,9 @@ const usersUpdateMeValidator = [
143 body('email').optional().isEmail().withMessage('Should have a valid email attribute'), 145 body('email').optional().isEmail().withMessage('Should have a valid email attribute'),
144 body('nsfwPolicy').optional().custom(isUserNSFWPolicyValid).withMessage('Should have a valid display Not Safe For Work policy'), 146 body('nsfwPolicy').optional().custom(isUserNSFWPolicyValid).withMessage('Should have a valid display Not Safe For Work policy'),
145 body('autoPlayVideo').optional().custom(isUserAutoPlayVideoValid).withMessage('Should have a valid automatically plays video attribute'), 147 body('autoPlayVideo').optional().custom(isUserAutoPlayVideoValid).withMessage('Should have a valid automatically plays video attribute'),
148 body('videosHistoryEnabled')
149 .optional()
150 .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled attribute'),
146 151
147 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 152 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
148 logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') }) 153 logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') })
@@ -229,6 +234,7 @@ const usersAskResetPasswordValidator = [
229 logger.debug('Checking usersAskResetPassword parameters', { parameters: req.body }) 234 logger.debug('Checking usersAskResetPassword parameters', { parameters: req.body })
230 235
231 if (areValidationErrors(req, res)) return 236 if (areValidationErrors(req, res)) return
237
232 const exists = await checkUserEmailExist(req.body.email, res, false) 238 const exists = await checkUserEmailExist(req.body.email, res, false)
233 if (!exists) { 239 if (!exists) {
234 logger.debug('User with email %s does not exist (asking reset password).', req.body.email) 240 logger.debug('User with email %s does not exist (asking reset password).', req.body.email)
diff --git a/server/middlewares/validators/videos/index.ts b/server/middlewares/validators/videos/index.ts
index 294783d85..a0d585b93 100644
--- a/server/middlewares/validators/videos/index.ts
+++ b/server/middlewares/validators/videos/index.ts
@@ -5,4 +5,6 @@ export * from './video-channels'
5export * from './video-comments' 5export * from './video-comments'
6export * from './video-imports' 6export * from './video-imports'
7export * from './video-watch' 7export * from './video-watch'
8export * from './video-rates'
9export * from './video-shares'
8export * from './videos' 10export * from './videos'
diff --git a/server/middlewares/validators/videos/video-blacklist.ts b/server/middlewares/validators/videos/video-blacklist.ts
index 13da7acff..2688f63ae 100644
--- a/server/middlewares/validators/videos/video-blacklist.ts
+++ b/server/middlewares/validators/videos/video-blacklist.ts
@@ -1,10 +1,11 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body, param } from 'express-validator/check' 2import { body, param } from 'express-validator/check'
3import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc' 3import { isBooleanValid, isIdOrUUIDValid } from '../../../helpers/custom-validators/misc'
4import { isVideoExist } from '../../../helpers/custom-validators/videos' 4import { isVideoExist } from '../../../helpers/custom-validators/videos'
5import { logger } from '../../../helpers/logger' 5import { logger } from '../../../helpers/logger'
6import { areValidationErrors } from '../utils' 6import { areValidationErrors } from '../utils'
7import { isVideoBlacklistExist, isVideoBlacklistReasonValid } from '../../../helpers/custom-validators/video-blacklist' 7import { isVideoBlacklistExist, isVideoBlacklistReasonValid } from '../../../helpers/custom-validators/video-blacklist'
8import { VideoModel } from '../../../models/video/video'
8 9
9const videosBlacklistRemoveValidator = [ 10const videosBlacklistRemoveValidator = [
10 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 11 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
@@ -22,6 +23,10 @@ const videosBlacklistRemoveValidator = [
22 23
23const videosBlacklistAddValidator = [ 24const videosBlacklistAddValidator = [
24 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 25 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
26 body('unfederate')
27 .optional()
28 .toBoolean()
29 .custom(isBooleanValid).withMessage('Should have a valid unfederate boolean'),
25 body('reason') 30 body('reason')
26 .optional() 31 .optional()
27 .custom(isVideoBlacklistReasonValid).withMessage('Should have a valid reason'), 32 .custom(isVideoBlacklistReasonValid).withMessage('Should have a valid reason'),
@@ -32,6 +37,14 @@ const videosBlacklistAddValidator = [
32 if (areValidationErrors(req, res)) return 37 if (areValidationErrors(req, res)) return
33 if (!await isVideoExist(req.params.videoId, res)) return 38 if (!await isVideoExist(req.params.videoId, res)) return
34 39
40 const video: VideoModel = res.locals.video
41 if (req.body.unfederate === true && video.remote === true) {
42 return res
43 .status(409)
44 .send({ error: 'You cannot unfederate a remote video.' })
45 .end()
46 }
47
35 return next() 48 return next()
36 } 49 }
37] 50]
diff --git a/server/middlewares/validators/videos/video-rates.ts b/server/middlewares/validators/videos/video-rates.ts
new file mode 100644
index 000000000..793354520
--- /dev/null
+++ b/server/middlewares/validators/videos/video-rates.ts
@@ -0,0 +1,55 @@
1import * as express from 'express'
2import 'express-validator'
3import { body, param } from 'express-validator/check'
4import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
5import { isVideoExist, isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos'
6import { logger } from '../../../helpers/logger'
7import { areValidationErrors } from '../utils'
8import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
9import { VideoRateType } from '../../../../shared/models/videos'
10import { isAccountNameValid } from '../../../helpers/custom-validators/accounts'
11
12const videoUpdateRateValidator = [
13 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
14 body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
15
16 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
17 logger.debug('Checking videoRate parameters', { parameters: req.body })
18
19 if (areValidationErrors(req, res)) return
20 if (!await isVideoExist(req.params.id, res)) return
21
22 return next()
23 }
24]
25
26const getAccountVideoRateValidator = function (rateType: VideoRateType) {
27 return [
28 param('name').custom(isAccountNameValid).withMessage('Should have a valid account name'),
29 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
30
31 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
32 logger.debug('Checking videoCommentGetValidator parameters.', { parameters: req.params })
33
34 if (areValidationErrors(req, res)) return
35
36 const rate = await AccountVideoRateModel.loadLocalAndPopulateVideo(rateType, req.params.name, req.params.videoId)
37 if (!rate) {
38 return res.status(404)
39 .json({ error: 'Video rate not found' })
40 .end()
41 }
42
43 res.locals.accountVideoRate = rate
44
45 return next()
46 }
47 ]
48}
49
50// ---------------------------------------------------------------------------
51
52export {
53 videoUpdateRateValidator,
54 getAccountVideoRateValidator
55}
diff --git a/server/middlewares/validators/videos/video-shares.ts b/server/middlewares/validators/videos/video-shares.ts
new file mode 100644
index 000000000..646d7acb1
--- /dev/null
+++ b/server/middlewares/validators/videos/video-shares.ts
@@ -0,0 +1,38 @@
1import * as express from 'express'
2import 'express-validator'
3import { param } from 'express-validator/check'
4import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
5import { isVideoExist } from '../../../helpers/custom-validators/videos'
6import { logger } from '../../../helpers/logger'
7import { VideoShareModel } from '../../../models/video/video-share'
8import { areValidationErrors } from '../utils'
9import { VideoModel } from '../../../models/video/video'
10
11const videosShareValidator = [
12 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
13 param('actorId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid actor id'),
14
15 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
16 logger.debug('Checking videoShare parameters', { parameters: req.params })
17
18 if (areValidationErrors(req, res)) return
19 if (!await isVideoExist(req.params.id, res)) return
20
21 const video: VideoModel = res.locals.video
22
23 const share = await VideoShareModel.load(req.params.actorId, video.id)
24 if (!share) {
25 return res.status(404)
26 .end()
27 }
28
29 res.locals.videoShare = share
30 return next()
31 }
32]
33
34// ---------------------------------------------------------------------------
35
36export {
37 videosShareValidator
38}
diff --git a/server/middlewares/validators/videos/video-watch.ts b/server/middlewares/validators/videos/video-watch.ts
index bca64662f..c38ad8a10 100644
--- a/server/middlewares/validators/videos/video-watch.ts
+++ b/server/middlewares/validators/videos/video-watch.ts
@@ -4,6 +4,7 @@ import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc'
4import { isVideoExist } from '../../../helpers/custom-validators/videos' 4import { isVideoExist } from '../../../helpers/custom-validators/videos'
5import { areValidationErrors } from '../utils' 5import { areValidationErrors } from '../utils'
6import { logger } from '../../../helpers/logger' 6import { logger } from '../../../helpers/logger'
7import { UserModel } from '../../../models/account/user'
7 8
8const videoWatchingValidator = [ 9const videoWatchingValidator = [
9 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), 10 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
@@ -17,6 +18,12 @@ const videoWatchingValidator = [
17 if (areValidationErrors(req, res)) return 18 if (areValidationErrors(req, res)) return
18 if (!await isVideoExist(req.params.videoId, res, 'id')) return 19 if (!await isVideoExist(req.params.videoId, res, 'id')) return
19 20
21 const user = res.locals.oauth.token.User as UserModel
22 if (user.videosHistoryEnabled === false) {
23 logger.warn('Cannot set videos to watch by user %d: videos history is disabled.', user.id)
24 return res.status(409).end()
25 }
26
20 return next() 27 return next()
21 } 28 }
22] 29]
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts
index 27e8a7449..d9626929c 100644
--- a/server/middlewares/validators/videos/videos.ts
+++ b/server/middlewares/validators/videos/videos.ts
@@ -1,6 +1,6 @@
1import * as express from 'express' 1import * as express from 'express'
2import 'express-validator' 2import 'express-validator'
3import { body, param, ValidationChain } from 'express-validator/check' 3import { body, param, query, ValidationChain } from 'express-validator/check'
4import { UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared' 4import { UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared'
5import { 5import {
6 isBooleanValid, 6 isBooleanValid,
@@ -8,6 +8,7 @@ import {
8 isIdOrUUIDValid, 8 isIdOrUUIDValid,
9 isIdValid, 9 isIdValid,
10 isUUIDValid, 10 isUUIDValid,
11 toArray,
11 toIntOrNull, 12 toIntOrNull,
12 toValueOrNull 13 toValueOrNull
13} from '../../../helpers/custom-validators/misc' 14} from '../../../helpers/custom-validators/misc'
@@ -19,20 +20,19 @@ import {
19 isVideoDescriptionValid, 20 isVideoDescriptionValid,
20 isVideoExist, 21 isVideoExist,
21 isVideoFile, 22 isVideoFile,
23 isVideoFilterValid,
22 isVideoImage, 24 isVideoImage,
23 isVideoLanguageValid, 25 isVideoLanguageValid,
24 isVideoLicenceValid, 26 isVideoLicenceValid,
25 isVideoNameValid, 27 isVideoNameValid,
26 isVideoPrivacyValid, 28 isVideoPrivacyValid,
27 isVideoRatingTypeValid,
28 isVideoSupportValid, 29 isVideoSupportValid,
29 isVideoTagsValid 30 isVideoTagsValid
30} from '../../../helpers/custom-validators/videos' 31} from '../../../helpers/custom-validators/videos'
31import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils' 32import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
32import { logger } from '../../../helpers/logger' 33import { logger } from '../../../helpers/logger'
33import { CONSTRAINTS_FIELDS } from '../../../initializers' 34import { CONFIG, CONSTRAINTS_FIELDS } from '../../../initializers'
34import { VideoShareModel } from '../../../models/video/video-share' 35import { authenticatePromiseIfNeeded } from '../../oauth'
35import { authenticate } from '../../oauth'
36import { areValidationErrors } from '../utils' 36import { areValidationErrors } from '../utils'
37import { cleanUpReqFiles } from '../../../helpers/express-utils' 37import { cleanUpReqFiles } from '../../../helpers/express-utils'
38import { VideoModel } from '../../../models/video/video' 38import { VideoModel } from '../../../models/video/video'
@@ -42,6 +42,8 @@ import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/vid
42import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership' 42import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership'
43import { AccountModel } from '../../../models/account/account' 43import { AccountModel } from '../../../models/account/account'
44import { VideoFetchType } from '../../../helpers/video' 44import { VideoFetchType } from '../../../helpers/video'
45import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
46import { getServerActor } from '../../../helpers/utils'
45 47
46const videosAddValidator = getCommonVideoAttributes().concat([ 48const videosAddValidator = getCommonVideoAttributes().concat([
47 body('videofile') 49 body('videofile')
@@ -69,7 +71,6 @@ const videosAddValidator = getCommonVideoAttributes().concat([
69 if (isAble === false) { 71 if (isAble === false) {
70 res.status(403) 72 res.status(403)
71 .json({ error: 'The user video quota is exceeded with this video.' }) 73 .json({ error: 'The user video quota is exceeded with this video.' })
72 .end()
73 74
74 return cleanUpReqFiles(req) 75 return cleanUpReqFiles(req)
75 } 76 }
@@ -82,7 +83,6 @@ const videosAddValidator = getCommonVideoAttributes().concat([
82 logger.error('Invalid input file in videosAddValidator.', { err }) 83 logger.error('Invalid input file in videosAddValidator.', { err })
83 res.status(400) 84 res.status(400)
84 .json({ error: 'Invalid input file.' }) 85 .json({ error: 'Invalid input file.' })
85 .end()
86 86
87 return cleanUpReqFiles(req) 87 return cleanUpReqFiles(req)
88 } 88 }
@@ -120,7 +120,6 @@ const videosUpdateValidator = getCommonVideoAttributes().concat([
120 cleanUpReqFiles(req) 120 cleanUpReqFiles(req)
121 return res.status(409) 121 return res.status(409)
122 .json({ error: 'Cannot set "private" a video that was not private.' }) 122 .json({ error: 'Cannot set "private" a video that was not private.' })
123 .end()
124 } 123 }
125 124
126 if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req) 125 if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
@@ -129,6 +128,31 @@ const videosUpdateValidator = getCommonVideoAttributes().concat([
129 } 128 }
130]) 129])
131 130
131async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
132 const video: VideoModel = res.locals.video
133
134 // Anybody can watch local videos
135 if (video.isOwned() === true) return next()
136
137 // Logged user
138 if (res.locals.oauth) {
139 // Users can search or watch remote videos
140 if (CONFIG.SEARCH.REMOTE_URI.USERS === true) return next()
141 }
142
143 // Anybody can search or watch remote videos
144 if (CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true) return next()
145
146 // Check our instance follows an actor that shared this video
147 const serverActor = await getServerActor()
148 if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next()
149
150 return res.status(403)
151 .json({
152 error: 'Cannot get this video regarding follow constraints.'
153 })
154}
155
132const videosCustomGetValidator = (fetchType: VideoFetchType) => { 156const videosCustomGetValidator = (fetchType: VideoFetchType) => {
133 return [ 157 return [
134 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), 158 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
@@ -143,18 +167,20 @@ const videosCustomGetValidator = (fetchType: VideoFetchType) => {
143 167
144 // Video private or blacklisted 168 // Video private or blacklisted
145 if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) { 169 if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) {
146 return authenticate(req, res, () => { 170 await authenticatePromiseIfNeeded(req, res)
147 const user: UserModel = res.locals.oauth.token.User 171
148 172 const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : null
149 // Only the owner or a user that have blacklist rights can see the video 173
150 if (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) { 174 // Only the owner or a user that have blacklist rights can see the video
151 return res.status(403) 175 if (
152 .json({ error: 'Cannot get this private or blacklisted video.' }) 176 !user ||
153 .end() 177 (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST))
154 } 178 ) {
155 179 return res.status(403)
156 return next() 180 .json({ error: 'Cannot get this private or blacklisted video.' })
157 }) 181 }
182
183 return next()
158 } 184 }
159 185
160 // Video is public, anyone can access it 186 // Video is public, anyone can access it
@@ -189,41 +215,6 @@ const videosRemoveValidator = [
189 } 215 }
190] 216]
191 217
192const videoRateValidator = [
193 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
194 body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'),
195
196 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
197 logger.debug('Checking videoRate parameters', { parameters: req.body })
198
199 if (areValidationErrors(req, res)) return
200 if (!await isVideoExist(req.params.id, res)) return
201
202 return next()
203 }
204]
205
206const videosShareValidator = [
207 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
208 param('accountId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid account id'),
209
210 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
211 logger.debug('Checking videoShare parameters', { parameters: req.params })
212
213 if (areValidationErrors(req, res)) return
214 if (!await isVideoExist(req.params.id, res)) return
215
216 const share = await VideoShareModel.load(req.params.accountId, res.locals.video.id, undefined)
217 if (!share) {
218 return res.status(404)
219 .end()
220 }
221
222 res.locals.videoShare = share
223 return next()
224 }
225]
226
227const videosChangeOwnershipValidator = [ 218const videosChangeOwnershipValidator = [
228 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), 219 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
229 220
@@ -239,8 +230,8 @@ const videosChangeOwnershipValidator = [
239 const nextOwner = await AccountModel.loadLocalByName(req.body.username) 230 const nextOwner = await AccountModel.loadLocalByName(req.body.username)
240 if (!nextOwner) { 231 if (!nextOwner) {
241 res.status(400) 232 res.status(400)
242 .type('json') 233 .json({ error: 'Changing video ownership to a remote account is not supported yet' })
243 .end() 234
244 return 235 return
245 } 236 }
246 res.locals.nextOwner = nextOwner 237 res.locals.nextOwner = nextOwner
@@ -271,7 +262,7 @@ const videosTerminateChangeOwnershipValidator = [
271 } else { 262 } else {
272 res.status(403) 263 res.status(403)
273 .json({ error: 'Ownership already accepted or refused' }) 264 .json({ error: 'Ownership already accepted or refused' })
274 .end() 265
275 return 266 return
276 } 267 }
277 } 268 }
@@ -288,7 +279,7 @@ const videosAcceptChangeOwnershipValidator = [
288 if (isAble === false) { 279 if (isAble === false) {
289 res.status(403) 280 res.status(403)
290 .json({ error: 'The user video quota is exceeded with this video.' }) 281 .json({ error: 'The user video quota is exceeded with this video.' })
291 .end() 282
292 return 283 return
293 } 284 }
294 285
@@ -367,23 +358,68 @@ function getCommonVideoAttributes () {
367 ] as (ValidationChain | express.Handler)[] 358 ] as (ValidationChain | express.Handler)[]
368} 359}
369 360
361const commonVideosFiltersValidator = [
362 query('categoryOneOf')
363 .optional()
364 .customSanitizer(toArray)
365 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
366 query('licenceOneOf')
367 .optional()
368 .customSanitizer(toArray)
369 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
370 query('languageOneOf')
371 .optional()
372 .customSanitizer(toArray)
373 .custom(isStringArray).withMessage('Should have a valid one of language array'),
374 query('tagsOneOf')
375 .optional()
376 .customSanitizer(toArray)
377 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
378 query('tagsAllOf')
379 .optional()
380 .customSanitizer(toArray)
381 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
382 query('nsfw')
383 .optional()
384 .custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
385 query('filter')
386 .optional()
387 .custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
388
389 (req: express.Request, res: express.Response, next: express.NextFunction) => {
390 logger.debug('Checking commons video filters query', { parameters: req.query })
391
392 if (areValidationErrors(req, res)) return
393
394 const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined
395 if (req.query.filter === 'all-local' && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)) {
396 res.status(401)
397 .json({ error: 'You are not allowed to see all local videos.' })
398
399 return
400 }
401
402 return next()
403 }
404]
405
370// --------------------------------------------------------------------------- 406// ---------------------------------------------------------------------------
371 407
372export { 408export {
373 videosAddValidator, 409 videosAddValidator,
374 videosUpdateValidator, 410 videosUpdateValidator,
375 videosGetValidator, 411 videosGetValidator,
412 checkVideoFollowConstraints,
376 videosCustomGetValidator, 413 videosCustomGetValidator,
377 videosRemoveValidator, 414 videosRemoveValidator,
378 videosShareValidator,
379
380 videoRateValidator,
381 415
382 videosChangeOwnershipValidator, 416 videosChangeOwnershipValidator,
383 videosTerminateChangeOwnershipValidator, 417 videosTerminateChangeOwnershipValidator,
384 videosAcceptChangeOwnershipValidator, 418 videosAcceptChangeOwnershipValidator,
385 419
386 getCommonVideoAttributes 420 getCommonVideoAttributes,
421
422 commonVideosFiltersValidator
387} 423}
388 424
389// --------------------------------------------------------------------------- 425// ---------------------------------------------------------------------------
@@ -391,9 +427,10 @@ export {
391function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) { 427function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
392 if (req.body.scheduleUpdate) { 428 if (req.body.scheduleUpdate) {
393 if (!req.body.scheduleUpdate.updateAt) { 429 if (!req.body.scheduleUpdate.updateAt) {
430 logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
431
394 res.status(400) 432 res.status(400)
395 .json({ error: 'Schedule update at is mandatory.' }) 433 .json({ error: 'Schedule update at is mandatory.' })
396 .end()
397 434
398 return true 435 return true
399 } 436 }