aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/middlewares/validators/videos
diff options
context:
space:
mode:
Diffstat (limited to 'server/middlewares/validators/videos')
-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
6 files changed, 217 insertions, 65 deletions
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 }