1 | /**
|
2 | * Include utilities from @bowtie/sls
|
3 | */
|
4 | const AWS = require('aws-sdk');
|
5 | const qs = require('qs');
|
6 | const fs = require('fs-extra')
|
7 | const path = require('path')
|
8 | const axios = require('axios')
|
9 | const { action, parser, builder, migrator, deployer, notifier, validator, models, controllers, oauth, utils } = require('./src')
|
10 | const { BaseController, BuildsController, DeploysController, DocumentsController, SubmissionsController } = controllers
|
11 | const { Build, Deploy } = models
|
12 | const { parseServiceConfig } = utils
|
13 | const handlers = {}
|
14 |
|
15 | const CORS_HEADERS = {
|
16 | 'Access-Control-Allow-Origin': '*', // Required for CORS support to work
|
17 | 'Access-Control-Allow-Credentials': true, // Required for cookies, authorization headers with HTTPS
|
18 | };
|
19 |
|
20 | // const s3 = new AWS.S3();
|
21 |
|
22 | /**
|
23 | * Generate MVC-Style REST API routes using controllers:
|
24 | */
|
25 | // baseController = new BaseController
|
26 | // buildsController = new BuildsController()
|
27 | // deploysController = new DeploysController()
|
28 |
|
29 | // TODO: Hookup actual build/deploy logic for create/update/destroy?
|
30 | const methods = [ 'index', 'show', 'create', 'update', 'destroy', 'logs', 'deploy', 'tags', 'stacks', 'download', 'audits' ]
|
31 | // const methods = [ 'index', 'show', 'logs' ]
|
32 | const activeControllers = {
|
33 | builds: new BuildsController(),
|
34 | deploys: new DeploysController(),
|
35 | documents: new DocumentsController(),
|
36 | submissions: new SubmissionsController()
|
37 | }
|
38 |
|
39 | Object.keys(activeControllers).forEach(model => {
|
40 | const ctrl = activeControllers[model]
|
41 |
|
42 | methods.forEach(method => {
|
43 | if (typeof ctrl[method] === 'function') {
|
44 | handlers[`${model}_${method}`] = ctrl[method].bind(ctrl)
|
45 | }
|
46 | })
|
47 | })
|
48 |
|
49 | /**
|
50 | * Add info handler to report service info
|
51 | */
|
52 | if (typeof BaseController.info === 'function') {
|
53 | handlers.info = BaseController.info
|
54 | }
|
55 |
|
56 | // handlers.spec = async (event, context) => {
|
57 | // return {
|
58 | // statusCode: 200,
|
59 | // headers: BaseController.REQUIRED_RESPONSE_HEADERS,
|
60 | // body: fs.readFileSync('spec.yml').toString()
|
61 | // }
|
62 | // }
|
63 |
|
64 | handlers.oauthBitbucketAuthorize = (event, context, callback) => {
|
65 | action.init(event).then(event => {
|
66 | oauth.bitbucket.authorize(event, context, callback)
|
67 | }).catch(callback)
|
68 | }
|
69 |
|
70 | /**
|
71 | * Handle CloudFormation stack change events
|
72 | * @param {object} event
|
73 | * @param {object} context
|
74 | * @param {function} callback
|
75 | */
|
76 | handlers.stackChange = (event, context, callback) => {
|
77 | action.init(event) // Initialize the action promise chain
|
78 | .then(parser.stackChange) // Parse SNS Records into slack messages
|
79 | .then(deployer.stackChange) // Handle stack change deployments
|
80 | .then(notifier.stackChange) // Send parsed messages to slack
|
81 | .then(migrator.stackChange) // Find & run DB migration on successfull stack change
|
82 | .then(() => callback(null)) // Callback with null error successful
|
83 | .catch(callback) // Callback with error when a promise in the chain is rejected
|
84 | }
|
85 |
|
86 | /**
|
87 | * Handle Bitbucket webhook events (HTTP POST request on push)
|
88 | * @param {object} event
|
89 | * @param {object} context
|
90 | * @param {function} callback
|
91 | */
|
92 | handlers.bitbucketWebhook = (event, context, callback) => {
|
93 | action.init(event) // Initialize the action promise chain
|
94 | .then(validator.bitbucketWebhook) // Validate webhook source is bitbucket
|
95 | .then(parser.parseBody) // Parse payload body (from JSON string => object)
|
96 | .then(builder.prepareBuild) // Prepare next build (if applicable)
|
97 | .then(builder.startBuild) // Start prepared build (if exists)
|
98 | .then(builder.trackBuild) // Track started build (if started)
|
99 | .then(parser.deployments) // Parse deployments (if any, defined in service yaml file)
|
100 | .then(deployer.deployBuild) // Deploy build(s) (if any, determined by previous parse step)
|
101 | .then(notifier.deploymentNotifyAirbrake) // Notify Airbrake of deployment (if any)
|
102 | .then(e => {
|
103 | // Successful webhook, return null error with response code 200
|
104 | callback(null, {
|
105 | statusCode: '200'
|
106 | })
|
107 | })
|
108 | .catch(err => {
|
109 | // Something failed, notify error using slack integration
|
110 | notifier.actionFailureNotifySlack(err)
|
111 | // Return callback with null error to avoid lambda reporting errors
|
112 | .then(() => callback(null, { statusCode: '422' }))
|
113 | .catch(callback)
|
114 | })
|
115 | }
|
116 |
|
117 | /**
|
118 | * Handle GitHub webhook events (HTTP POST request on push)
|
119 | * @param {object} event
|
120 | * @param {object} context
|
121 | * @param {function} callback
|
122 | */
|
123 | handlers.githubWebhook = (event, context, callback) => {
|
124 | /**
|
125 | * Use headers
|
126 | * X-GitHub-Event = [ "push", "create", "pull_request" ]
|
127 | */
|
128 | action.init(event)
|
129 | .then(validator.githubWebhook)
|
130 | .then(parser.parseBody)
|
131 | .then(builder.prepareBuild)
|
132 | .then(builder.startBuild)
|
133 | .then(builder.trackBuild)
|
134 | .then(parser.deployments)
|
135 | .then(deployer.deployBuild)
|
136 | .then(notifier.deploymentNotifyAirbrake)
|
137 | .then(e => {
|
138 | // Successful webhook, return null error with response code 200
|
139 | callback(null, {
|
140 | statusCode: '200'
|
141 | })
|
142 | })
|
143 | .catch(err => {
|
144 | // Something failed, notify error using slack integration
|
145 | notifier.actionFailureNotifySlack(err)
|
146 | // Return callback with null error to avoid lambda reporting errors
|
147 | .then(() => callback(null, { statusCode: '422' }))
|
148 | .catch(callback)
|
149 | })
|
150 | }
|
151 |
|
152 | /**
|
153 | * Handle CodeBuild status change events
|
154 | * @param {object} event
|
155 | * @param {object} context
|
156 | * @param {function} callback
|
157 | */
|
158 | handlers.buildChange = (event, context, callback) => {
|
159 | action.init(event) // Initialize the action promise chain
|
160 | .then(parser.buildChange) // Parse build change message details
|
161 | .then(notifier.buildChange) // Update build from image details
|
162 | .then(notifier.buildChangeNotifyGithub) // Notify github of changes / commit statuses
|
163 | .then(notifier.buildChangeNotifyBitbucket) // Notify bitbucket of changes / commit statuses
|
164 | .then(notifier.buildChangeNotifySlack) // Notify slack of build changes
|
165 | .then(parser.deployments) // Parse deployments (from successful builds)
|
166 | .then(deployer.deployBuild) // Deploy build(s) (if any, determined by previous parse step)
|
167 | .then(notifier.deploymentNotifyAirbrake) // Notify airbrake of deployments (if any & airbrake is configured)
|
168 | .then(e => callback(null)) // Successful handler, return callback with null error
|
169 | .catch(err => {
|
170 | // Something failed, notify error using slack integration
|
171 | notifier.actionFailureNotifySlack(err)
|
172 | // Return callback with null error to avoid lambda reporting errors
|
173 | .then(() => callback(null))
|
174 | .catch(callback)
|
175 | })
|
176 | }
|
177 |
|
178 | // handlers.s3Download = (event, context, callback) => {
|
179 | // action.init(event)
|
180 | // .then(event => {
|
181 | // const Expires = 5;
|
182 | // const Bucket = process.env.ASSET_BUCKET_NAME;
|
183 | // const { Key } = event.queryStringParameters;
|
184 |
|
185 | // AWS.config.update({region: 'us-east-1'});
|
186 |
|
187 | // const s3 = new AWS.S3({
|
188 | // signatureVersion: 'v4'
|
189 | // });
|
190 |
|
191 | // const headers= {
|
192 | // 'Location': s3.getSignedUrl('getObject', { Bucket, Key, Expires })
|
193 | // };
|
194 |
|
195 | // console.log('s3Download() headers', headers);
|
196 |
|
197 | // return callback(null, {
|
198 | // statusCode: '302',
|
199 | // headers
|
200 | // })
|
201 | // })
|
202 | // .catch(err => {
|
203 | // // Something failed, notify error using slack integration
|
204 | // notifier.actionFailureNotifySlack(err)
|
205 | // // Return callback with null error to avoid lambda reporting errors
|
206 | // .then(() => callback(null))
|
207 | // .catch(callback)
|
208 | // })
|
209 | // }
|
210 |
|
211 | handlers.s3Upload = (event, context, callback) => {
|
212 | action.init(event)
|
213 | .then(parser.parseBody)
|
214 | .then(event => {
|
215 | const queryParams = event.queryStringParameters || {};
|
216 | const { ctx } = queryParams;
|
217 | const useSecureBucket = (process.env.CTX_SECURE && ctx && ctx === process.env.CTX_SECURE);
|
218 | const Bucket = useSecureBucket ? process.env.SECURE_BUCKET_NAME : process.env.ASSET_BUCKET_NAME;
|
219 | const { Key, ContentType, Data } = event.parsed.body;
|
220 |
|
221 | // const { Bucket = 'svig-testing-uploads', Key, ContentType } = event.queryStringParameters;
|
222 |
|
223 | if (!Bucket || !Key || !ContentType) {
|
224 | return callback(null, {
|
225 | statusCode: '422',
|
226 | headers: CORS_HEADERS,
|
227 | body: JSON.stringify({ message: 'Invalid parameter(s)' })
|
228 | })
|
229 | }
|
230 |
|
231 | // [HIGH] TODO: Validate incoming data with form submission(s)
|
232 | // if (useSecureBucket && !Data) {
|
233 | // return callback(null, {
|
234 | // statusCode: '422',
|
235 | // headers,
|
236 | // body: JSON.stringify({ message: 'Invalid parameter(s)' })
|
237 | // })
|
238 | // }
|
239 |
|
240 | // AWS.config.update({region: 'us-east-1'});
|
241 |
|
242 | const s3 = new AWS.S3({
|
243 | signatureVersion: 'v4'
|
244 | });
|
245 |
|
246 | // var params = {Bucket, Key};
|
247 | // s3.getSignedUrl('putObject', params, function (err, url) {
|
248 | // console.log('The URL is', url);
|
249 | // });
|
250 |
|
251 | s3.getSignedUrlPromise('putObject', { Bucket, Key, ContentType })
|
252 | .then(signedUrl => {
|
253 | let publicUrl;
|
254 | console.log("Signed URL IS: ", signedUrl)
|
255 |
|
256 | if (!useSecureBucket) {
|
257 | publicUrl = `https://${Bucket}.s3.amazonaws.com/${Key}`;
|
258 | console.log("Public URL IS: ", publicUrl)
|
259 | }
|
260 |
|
261 | callback(null, {
|
262 | statusCode: '200',
|
263 | headers: CORS_HEADERS,
|
264 | body: JSON.stringify({ signedUrl, publicUrl, location: publicUrl })
|
265 | })
|
266 | })
|
267 | .catch(err => {
|
268 | console.error(err)
|
269 | callback(null, {
|
270 | statusCode: '400',
|
271 | headers: CORS_HEADERS,
|
272 | body: JSON.stringify({ message: err.message })
|
273 | })
|
274 | })
|
275 | })
|
276 | .catch(err => {
|
277 | // Something failed, notify error using slack integration
|
278 | notifier.actionFailureNotifySlack(err)
|
279 | // Return callback with null error to avoid lambda reporting errors
|
280 | .then(() => callback(null))
|
281 | .catch(callback)
|
282 | })
|
283 | }
|
284 |
|
285 | /**
|
286 | * Verify recaptcha v3 token
|
287 | * @param {ApiEvent} event - Request event
|
288 | * @param {object} context - Handler execution context
|
289 | */
|
290 | handlers.verifyRecaptcha = async (event, context) => {
|
291 | if (event.queryStringParameters.token) {
|
292 | const resp = await axios.post('https://www.google.com/recaptcha/api/siteverify', qs.stringify({
|
293 | secret: process.env.RECAPTCHA_SECRET_KEY,
|
294 | response: event.queryStringParameters.token
|
295 | }));
|
296 |
|
297 | return {
|
298 | headers: CORS_HEADERS,
|
299 | statusCode: '200',
|
300 | body: JSON.stringify(resp.data)
|
301 | };
|
302 | } else {
|
303 | return {
|
304 | headers: CORS_HEADERS,
|
305 | statusCode: '400',
|
306 | message: 'Missing or invalid token'
|
307 | };
|
308 | }
|
309 | };
|
310 |
|
311 | // handlers.sendEmail = (event, context, callback) => {
|
312 | // action.init(event) // Initialize the action promise chain
|
313 | // .then(parser.parseBody) // Parse payload body (from JSON string => object)
|
314 | // .then(notifier.sendEmail) // Send email (using SES)
|
315 | // .then(response => {
|
316 | // // Successful webhook, return null error with response code 200
|
317 | // callback(null, {
|
318 | // statusCode: '200',
|
319 | // body: JSON.stringify(response)
|
320 | // })
|
321 | // })
|
322 | // .catch(err => {
|
323 | // // Something failed, notify error using slack integration
|
324 | // notifier.actionFailureNotifySlack(err)
|
325 | // // Return callback with null error to avoid lambda reporting errors
|
326 | // .then(() => callback(null, { statusCode: '422', body: JSON.stringify({ message: err.message || 'Something failed' }) }))
|
327 | // .catch(callback)
|
328 | // })
|
329 | // };
|
330 |
|
331 | // /**
|
332 | // * Show build logs
|
333 | // * [HIGH] TODO: Authenticate?
|
334 | // * @param {object} event
|
335 | // * @param {object} context
|
336 | // * @param {function} callback
|
337 | // */
|
338 | // handlers.buildLogs = (event, context, callback) => {
|
339 | // action.init(event) // Initialize the action promise chain
|
340 | // .then(builder.buildLogs) // Notify airbrake of deployments (if any & airbrake is configured)
|
341 | // .then(e => callback(null, e.response)) // Successful handler, return callback with null error
|
342 | // .catch(err => {
|
343 | // callback(null, {
|
344 | // statusCode: '422',
|
345 | // body: JSON.stringify({ message: err.message || err })
|
346 | // })
|
347 | // })
|
348 | // }
|
349 |
|
350 | // /**
|
351 | // * Handle Slack slash command events
|
352 | // */
|
353 | // handlers.slackCommand = (event, context, callback) => {
|
354 | // action.init(event) // Initialize the action promise chain
|
355 | // .then(parser.decodeBody) // Decode the urlencoded event.body into event.parsed.body
|
356 | // .then(validator.slackCommand) // Validate the request token from Slack
|
357 | // .then(parser.slackCommand) // Build command info from the parsed body
|
358 | // .then(manager.slackCommand) // Handle the command built in the previous step
|
359 | // .then((e) => callback(null, e.response)) // Callback with null error and constructed response if successful
|
360 | // .catch(callback); // Callback with error when a promise in the chain is rejected
|
361 | // }
|
362 |
|
363 | // /**
|
364 | // * Handle Slack response events
|
365 | // */
|
366 | // handlers.slackResponse = (event, context, callback) => {
|
367 | // action.init(event) // Initialize the action promise chain
|
368 | // .then(parser.decodeBody) // Decode the urlencoded event.body into event.parsed.body
|
369 | // .then(parser.parsePayload) // Parse payload as JSON from the decoded body
|
370 | // .then(validator.slackResponse) // Validate the request token from Slack
|
371 | // .then(parser.slackResponse) // Build response info from the parsed body
|
372 | // .then(manager.slackResponse) // Handle the response built in the previous step
|
373 | // .then((e) => callback(null, e.response)) // Callback with null error and constructed response if successful
|
374 | // .catch(callback); // Callback with error when a promise in the chain is rejected
|
375 | // }
|
376 |
|
377 | module.exports = handlers;
|