├── .gitignore ├── Readme.md ├── backend ├── .gitignore ├── README.md ├── functions │ ├── call-webhook │ │ ├── app.js │ │ └── package.json │ ├── create-candidate │ │ ├── app.js │ │ └── package.json │ ├── create-webhook-call │ │ ├── app.js │ │ └── package.json │ ├── fetch-resource-data │ │ ├── app.js │ │ └── package.json │ ├── fetch-webhook-history │ │ ├── app.js │ │ └── package.json │ ├── register-webhook │ │ ├── app.js │ │ └── package.json │ └── trigger-resource-created │ │ ├── app.js │ │ └── package.json ├── statemachine │ └── webhook_management.asl.json └── template.yaml └── frontend ├── .env.example ├── .gitignore ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html └── src ├── App.vue ├── assets └── logo.png ├── components ├── CreateCandidate.vue ├── CreateWebhook.vue └── WebhookCallHistory.vue └── main.js /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Manage webhooks at scale with AWS Serverless 2 | 3 | This repository contains the project source code for webhook management system built with AWS Serverless services. 4 | 5 | You can find the blog post with set up details at: 6 | #### [https://pubudu.dev/posts/manage-webhooks-at-scale-with-aws-serverless/](https://pubudu.dev/posts/manage-webhooks-at-scale-with-aws-serverless/) 7 | 8 |  9 | 10 |  -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/osx,node,linux,windows 3 | 4 | ### Linux ### 5 | *~ 6 | 7 | # temporary files which can be created if a process still has a handle open of a deleted file 8 | .fuse_hidden* 9 | 10 | # KDE directory preferences 11 | .directory 12 | 13 | # Linux trash folder which might appear on any partition or disk 14 | .Trash-* 15 | 16 | # .nfs files are created when an open file is removed but is still being accessed 17 | .nfs* 18 | 19 | ### Node ### 20 | # Logs 21 | logs 22 | *.log 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # Runtime data 28 | pids 29 | *.pid 30 | *.seed 31 | *.pid.lock 32 | 33 | # Directory for instrumented libs generated by jscoverage/JSCover 34 | lib-cov 35 | 36 | # Coverage directory used by tools like istanbul 37 | coverage 38 | 39 | # nyc test coverage 40 | .nyc_output 41 | 42 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | bower_components 47 | 48 | # node-waf configuration 49 | .lock-wscript 50 | 51 | # Compiled binary addons (http://nodejs.org/api/addons.html) 52 | build/Release 53 | 54 | # Dependency directories 55 | node_modules/ 56 | jspm_packages/ 57 | 58 | # Typescript v1 declaration files 59 | typings/ 60 | 61 | # Optional npm cache directory 62 | .npm 63 | 64 | # Optional eslint cache 65 | .eslintcache 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # dotenv environment variables file 77 | .env 78 | 79 | 80 | ### OSX ### 81 | *.DS_Store 82 | .AppleDouble 83 | .LSOverride 84 | 85 | # Icon must end with two \r 86 | Icon 87 | 88 | # Thumbnails 89 | ._* 90 | 91 | # Files that might appear in the root of a volume 92 | .DocumentRevisions-V100 93 | .fseventsd 94 | .Spotlight-V100 95 | .TemporaryItems 96 | .Trashes 97 | .VolumeIcon.icns 98 | .com.apple.timemachine.donotpresent 99 | 100 | # Directories potentially created on remote AFP share 101 | .AppleDB 102 | .AppleDesktop 103 | Network Trash Folder 104 | Temporary Items 105 | .apdisk 106 | 107 | ### Windows ### 108 | # Windows thumbnail cache files 109 | Thumbs.db 110 | ehthumbs.db 111 | ehthumbs_vista.db 112 | 113 | # Folder config file 114 | Desktop.ini 115 | 116 | # Recycle Bin used on file shares 117 | $RECYCLE.BIN/ 118 | 119 | # Windows Installer files 120 | *.cab 121 | *.msi 122 | *.msm 123 | *.msp 124 | 125 | # Windows shortcuts 126 | *.lnk 127 | 128 | .aws-sam 129 | samconfig.toml 130 | # End of https://www.gitignore.io/api/osx,node,linux,windows -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # backend 2 | 3 | This project contains source code and supporting files for a serverless application that you can deploy with the SAM CLI. It includes the following files and folders: 4 | 5 | - functions - Code for the application's Lambda functions to check the value of, buy, or sell shares of a stock. 6 | - statemachines - Definition for the state machine that orchestrates the stock trading workflow. 7 | - template.yaml - A template that defines the application's AWS resources. 8 | 9 | This application creates a mock stock trading workflow which runs on a pre-defined schedule (note that the schedule is disabled by default to avoid incurring charges). It demonstrates the power of Step Functions to orchestrate Lambda functions and other AWS resources to form complex and robust workflows, coupled with event-driven development using Amazon EventBridge. 10 | 11 | AWS Step Functions lets you coordinate multiple AWS services into serverless workflows so you can build and update apps quickly. Using Step Functions, you can design and run workflows that stitch together services, such as AWS Lambda, AWS Fargate, and Amazon SageMaker, into feature-rich applications. 12 | 13 | The application uses several AWS resources, including Step Functions state machines, Lambda functions and an EventBridge rule trigger. These resources are defined in the `template.yaml` file in this project. You can update the template to add AWS resources through the same deployment process that updates your application code. 14 | 15 | If you prefer to use an integrated development environment (IDE) to build and test the Lambda functions within your application, you can use the AWS Toolkit. The AWS Toolkit is an open source plug-in for popular IDEs that uses the SAM CLI to build and deploy serverless applications on AWS. The AWS Toolkit also adds a simplified step-through debugging experience for Lambda function code. See the following links to get started: 16 | 17 | * [CLion](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) 18 | * [GoLand](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) 19 | * [IntelliJ](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) 20 | * [WebStorm](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) 21 | * [Rider](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) 22 | * [PhpStorm](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) 23 | * [PyCharm](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) 24 | * [RubyMine](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) 25 | * [DataGrip](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) 26 | * [VS Code](https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/welcome.html) 27 | * [Visual Studio](https://docs.aws.amazon.com/toolkit-for-visual-studio/latest/user-guide/welcome.html) 28 | 29 | The AWS Toolkit for VS Code includes full support for state machine visualization, enabling you to visualize your state machine in real time as you build. The AWS Toolkit for VS Code includes a language server for Amazon States Language, which lints your state machine definition to highlight common errors, provides auto-complete support, and code snippets for each state, enabling you to build state machines faster. 30 | 31 | ## Deploy the sample application 32 | 33 | The Serverless Application Model Command Line Interface (SAM CLI) is an extension of the AWS CLI that adds functionality for building and testing Lambda applications. It uses Docker to run your functions in an Amazon Linux environment that matches Lambda. 34 | 35 | To use the SAM CLI, you need the following tools: 36 | 37 | * SAM CLI - [Install the SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) 38 | * Node.js - [Install Node.js 14](https://nodejs.org/en/), including the NPM package management tool. 39 | * Docker - [Install Docker community edition](https://hub.docker.com/search/?type=edition&offering=community) 40 | 41 | To build and deploy your application for the first time, run the following in your shell: 42 | 43 | ```bash 44 | sam build 45 | sam deploy --guided 46 | ``` 47 | 48 | The first command will build the source of your application. The second command will package and deploy your application to AWS, with a series of prompts: 49 | 50 | * **Stack Name**: The name of the stack to deploy to CloudFormation. This should be unique to your account and region, and a good starting point would be something matching your project name. 51 | * **AWS Region**: The AWS region you want to deploy your app to. 52 | * **Confirm changes before deploy**: If set to yes, any change sets will be shown to you before execution for manual review. If set to no, the AWS SAM CLI will automatically deploy application changes. 53 | * **Allow SAM CLI IAM role creation**: Many AWS SAM templates, including this example, create AWS IAM roles required for the AWS Lambda function(s) included to access AWS services. By default, these are scoped down to minimum required permissions. To deploy an AWS CloudFormation stack which creates or modifies IAM roles, the `CAPABILITY_IAM` value for `capabilities` must be provided. If permission isn't provided through this prompt, to deploy this example you must explicitly pass `--capabilities CAPABILITY_IAM` to the `sam deploy` command. 54 | * **Save arguments to samconfig.toml**: If set to yes, your choices will be saved to a configuration file inside the project, so that in the future you can just re-run `sam deploy` without parameters to deploy changes to your application. 55 | 56 | You can find your State Machine ARN in the output values displayed after deployment. 57 | 58 | ## Use the SAM CLI to build and test locally 59 | 60 | Build the Lambda functions in your application with the `sam build --use-container` command. 61 | 62 | ```bash 63 | backend$ sam build 64 | ``` 65 | 66 | The SAM CLI installs dependencies defined in `functions/*/package.json`, creates a deployment package, and saves it in the `.aws-sam/build` folder. 67 | 68 | ## Add a resource to your application 69 | The application template uses AWS Serverless Application Model (AWS SAM) to define application resources. AWS SAM is an extension of AWS CloudFormation with a simpler syntax for configuring common serverless application resources such as functions, triggers, and APIs. For resources not included in [the SAM specification](https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md), you can use standard [AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html) resource types. 70 | 71 | ## Fetch, tail, and filter Lambda function logs 72 | 73 | To simplify troubleshooting, SAM CLI has a command called `sam logs`. `sam logs` lets you fetch logs generated by your deployed Lambda function from the command line. In addition to printing the logs on the terminal, this command has several nifty features to help you quickly find the bug. 74 | 75 | `NOTE`: This command works for all AWS Lambda functions; not just the ones you deploy using SAM. 76 | 77 | ```bash 78 | backend$ sam logs -n StockCheckerFunction --stack-name backend --tail 79 | ``` 80 | 81 | You can find more information and examples about filtering Lambda function logs in the [SAM CLI Documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-logging.html). 82 | 83 | ## Unit tests 84 | 85 | Tests are defined in the `functions/*/tests` folder in this project. Use NPM to install the [Mocha test framework](https://mochajs.org/) and run unit tests. 86 | 87 | ```bash 88 | backend$ cd functions/stock-checker 89 | stock-checker$ npm install 90 | stock-checker$ npm run test 91 | ``` 92 | 93 | ## Cleanup 94 | 95 | To delete the sample application that you created, use the AWS CLI. Assuming you used your project name for the stack name, you can run the following: 96 | 97 | ```bash 98 | aws cloudformation delete-stack --stack-name backend 99 | ``` 100 | 101 | ## Resources 102 | 103 | See the [AWS SAM developer guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) for an introduction to SAM specification, the SAM CLI, and serverless application concepts. 104 | 105 | Next, you can use AWS Serverless Application Repository to deploy ready to use Apps that go beyond hello world samples and learn how authors developed their applications: [AWS Serverless Application Repository main page](https://aws.amazon.com/serverless/serverlessrepo/) 106 | -------------------------------------------------------------------------------- /backend/functions/call-webhook/app.js: -------------------------------------------------------------------------------- 1 | const AWS = require("aws-sdk"); 2 | const hmacSHA256 = require("crypto-js/hmac-sha256"); 3 | const axios = require("axios"); 4 | var stepfunctions = new AWS.StepFunctions(); 5 | 6 | exports.lambdaHandler = async (event, context) => { 7 | for (const record of event.Records) { 8 | console.log(record) 9 | let body = JSON.parse(record.body); 10 | let payload = body["payload"]; 11 | let url = body["url"]; 12 | let taskToken = body["taskToken"]; 13 | 14 | try { 15 | const webhook = await axios.post(url, payload, { 16 | "Content-Type": "application/json", 17 | }); 18 | 19 | let params = { 20 | taskToken: taskToken, 21 | output: JSON.stringify({ 22 | status: "success", 23 | output: {}, 24 | }), 25 | }; 26 | 27 | await stepfunctions.sendTaskSuccess(params).promise(); 28 | 29 | console.log("Stepfunction notified with task success"); 30 | } catch (error) { 31 | let params = { 32 | taskToken: taskToken, 33 | cause: "Webhook call failure", 34 | error: error.message, 35 | }; 36 | 37 | await stepfunctions.sendTaskFailure(params).promise(); 38 | 39 | console.log("Stepfunction notified with task failed"); 40 | } 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /backend/functions/call-webhook/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webhook_management", 3 | "version": "1.0.0", 4 | "description": "Webhook management system", 5 | "main": "app.js", 6 | "author": "SAM CLI", 7 | "license": "MIT", 8 | "dependencies": { 9 | "axios": "^0.24.0", 10 | "crypto-js": "^4.1.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/functions/create-candidate/app.js: -------------------------------------------------------------------------------- 1 | const AWS = require("aws-sdk"); 2 | var docClient = new AWS.DynamoDB.DocumentClient(); 3 | var emailValidator = require("email-validator"); 4 | const { v4: uuidv4 } = require("uuid"); 5 | 6 | exports.lambdaHandler = async (event, context) => { 7 | let Body = JSON.parse(event.body); 8 | let validationErrors = validateInput(Body); 9 | 10 | if (validationErrors !== null) { 11 | return validationErrors; 12 | } 13 | 14 | let pk = uuidv4(); 15 | 16 | var params = { 17 | Item: { 18 | pk: pk, 19 | type: "candidate", 20 | companyId: Body.companyId, 21 | email: Body.email, 22 | firstName: Body.firstName, 23 | lastName: Body.lastName, 24 | createdAt: new Date().toISOString(), 25 | }, 26 | ReturnConsumedCapacity: "TOTAL", 27 | TableName: process.env.DB_TABLE, 28 | }; 29 | 30 | try { 31 | await docClient.put(params).promise(); 32 | return { 33 | statusCode: 200, 34 | headers: { 35 | "Content-Type": "application/json", 36 | "Access-Control-Allow-Origin": "*", 37 | }, 38 | body: JSON.stringify({ 39 | message: "Candidate created", 40 | data: { 41 | id: pk, 42 | }, 43 | }), 44 | }; 45 | } catch (error) { 46 | console.error("Error", error.stack); 47 | 48 | return { 49 | statusCode: 500, 50 | headers: { 51 | "Content-Type": "application/json", 52 | "Access-Control-Allow-Origin": "*", 53 | }, 54 | body: JSON.stringify({ 55 | message: "Candidate creation failed", 56 | error: error.stack, 57 | }), 58 | }; 59 | } 60 | }; 61 | 62 | function validateInput(body) { 63 | let errors = []; 64 | 65 | if (!emailValidator.validate(body.email)) { 66 | errors.push("Required field email not found or invalid"); 67 | } 68 | 69 | if (isNaN(body.companyId)) { 70 | errors.push("Required field companyId not found or invalid"); 71 | } 72 | 73 | if (!body.firstName) { 74 | errors.push("Required field first name not found"); 75 | } 76 | 77 | if (!body.lastName) { 78 | errors.push("Required field last name not found"); 79 | } 80 | 81 | if (errors.length > 0) { 82 | return { 83 | statusCode: 422, 84 | headers: { 85 | "Content-Type": "application/json", 86 | "Access-Control-Allow-Origin": "*", 87 | }, 88 | body: JSON.stringify({ 89 | message: "Validation errors", 90 | errors: errors, 91 | }), 92 | }; 93 | } 94 | 95 | return null; 96 | } 97 | -------------------------------------------------------------------------------- /backend/functions/create-candidate/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webhook_management", 3 | "version": "1.0.0", 4 | "description": "Webhook management system", 5 | "main": "app.js", 6 | "author": "SAM CLI", 7 | "license": "MIT", 8 | "dependencies": { 9 | "email-validator": "^2.0.4", 10 | "uuid": "^8.3.2" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/functions/create-webhook-call/app.js: -------------------------------------------------------------------------------- 1 | const AWS = require("aws-sdk"); 2 | var docClient = new AWS.DynamoDB.DocumentClient(); 3 | const hmacSHA256 = require("crypto-js/hmac-sha256"); 4 | const { v4: uuidv4 } = require("uuid"); 5 | 6 | exports.lambdaHandler = async (event, context) => { 7 | let webhookPayload = event.taskResult.resourceData; 8 | webhookPayload.id = webhookPayload.pk; 9 | let url = event.webhookUrl; 10 | let signingToken = event.webhookSignToken; 11 | let resourceId = webhookPayload.pk; 12 | let currentTime = new Date().toISOString(); 13 | 14 | delete webhookPayload.pk; 15 | delete webhookPayload.companyId; 16 | 17 | let postData = { 18 | resource: webhookPayload, 19 | resourceId: resourceId, 20 | resourceType: webhookPayload.type, 21 | triggeredAt: currentTime, 22 | token: hmacSHA256(resourceId + currentTime, signingToken).toString(), 23 | }; 24 | 25 | let pk = event.webhookId + '_' + webhookPayload.id + '_' + uuidv4(); 26 | var params = { 27 | Item: { 28 | pk: pk, 29 | type: "webhookcall", 30 | url: event.webhookUrl, 31 | companyId: event.companyId, 32 | payload: postData, 33 | status: 'pending', 34 | createdAt: currentTime, 35 | }, 36 | ReturnConsumedCapacity: "TOTAL", 37 | TableName: process.env.DB_TABLE, 38 | ConditionExpression: "pk <> :pk", 39 | ExpressionAttributeValues: { 40 | ":pk": pk, 41 | }, 42 | }; 43 | 44 | await docClient.put(params).promise(); 45 | 46 | return { 47 | 'id': pk, 48 | 'payload': postData 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /backend/functions/create-webhook-call/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webhook_management", 3 | "version": "1.0.0", 4 | "description": "Webhook management system", 5 | "main": "app.js", 6 | "author": "SAM CLI", 7 | "license": "MIT", 8 | "dependencies": { 9 | "crypto-js": "^4.1.1", 10 | "uuid": "^8.3.2" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/functions/fetch-resource-data/app.js: -------------------------------------------------------------------------------- 1 | const AWS = require("aws-sdk"); 2 | var docClient = new AWS.DynamoDB.DocumentClient(); 3 | 4 | exports.lambdaHandler = async (event, context) => { 5 | if (!event.key || !event.type) { 6 | throw new Error("Required fields not found. key and type required"); 7 | } 8 | 9 | let data = await fetchResourceData(event.key, event.type); 10 | 11 | if (data !== undefined) { 12 | return data; 13 | } else { 14 | return null; 15 | } 16 | }; 17 | 18 | async function fetchResourceData(pk, type) { 19 | var params = { 20 | Key: { 21 | pk: pk, 22 | type: type, 23 | }, 24 | TableName: process.env.DB_TABLE, 25 | }; 26 | 27 | let data = await docClient.get(params).promise(); 28 | 29 | return data.Item; 30 | } 31 | -------------------------------------------------------------------------------- /backend/functions/fetch-resource-data/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webhook_management", 3 | "version": "1.0.0", 4 | "description": "Webhook management system", 5 | "main": "app.js", 6 | "author": "SAM CLI", 7 | "license": "MIT", 8 | "dependencies": { 9 | "valid-url": "^1.0.9" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/functions/fetch-webhook-history/app.js: -------------------------------------------------------------------------------- 1 | const AWS = require("aws-sdk"); 2 | var docClient = new AWS.DynamoDB.DocumentClient(); 3 | 4 | exports.lambdaHandler = async (event, context) => { 5 | let companyId = event.pathParameters.companyId; 6 | 7 | if (!companyId) { 8 | return { 9 | statusCode: 422, 10 | headers: { 11 | "Content-Type": "application/json", 12 | "Access-Control-Allow-Origin": "*", 13 | }, 14 | body: JSON.stringify({ 15 | message: "Validation errors", 16 | errors: "companyId required", 17 | }), 18 | }; 19 | } 20 | 21 | let data = await fetchWebhookHistory(companyId); 22 | 23 | if (data !== undefined) { 24 | result = data; 25 | } else { 26 | result = []; 27 | } 28 | 29 | return { 30 | statusCode: 200, 31 | headers: { 32 | "Content-Type": "application/json", 33 | "Access-Control-Allow-Origin": "*", 34 | }, 35 | body: JSON.stringify({ 36 | data: result, 37 | }), 38 | }; 39 | }; 40 | 41 | async function fetchWebhookHistory(companyId) { 42 | var params = { 43 | IndexName: "gsiTypeAndCompanyId", 44 | KeyConditionExpression: "#type = :type and companyId = :companyId", 45 | ExpressionAttributeValues: { 46 | ":type": "webhookcall", 47 | ":companyId": companyId, 48 | }, 49 | ExpressionAttributeNames: { 50 | "#type": "type", 51 | "#status": "status", 52 | "#url": "url", 53 | "#output": "output" 54 | }, 55 | ProjectionExpression: "pk, companyId, createdAt, #status, payload, #url, #output", 56 | TableName: process.env.DB_TABLE, 57 | }; 58 | 59 | let data = await docClient.query(params).promise(); 60 | 61 | return data.Items; 62 | } 63 | -------------------------------------------------------------------------------- /backend/functions/fetch-webhook-history/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webhook_management", 3 | "version": "1.0.0", 4 | "description": "Webhook management system", 5 | "main": "app.js", 6 | "author": "SAM CLI", 7 | "license": "MIT", 8 | "dependencies": { 9 | "valid-url": "^1.0.9" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/functions/register-webhook/app.js: -------------------------------------------------------------------------------- 1 | const AWS = require("aws-sdk"); 2 | var docClient = new AWS.DynamoDB.DocumentClient(); 3 | var validator = require("valid-url"); 4 | 5 | exports.lambdaHandler = async (event, context) => { 6 | let Body = JSON.parse(event.body); 7 | 8 | let validationErrors = validateInput(Body); 9 | 10 | if (validationErrors !== null) { 11 | return validationErrors; 12 | } 13 | 14 | let signedToken = gerRandomString(32); 15 | let companyId = Body.companyId; 16 | let eventType = Body.eventType; 17 | let url = Body.url; 18 | let pk = "webhook_" + companyId + "_" + eventType; 19 | 20 | var params = { 21 | Item: { 22 | pk: pk, 23 | type: "webhook", 24 | url: url, 25 | companyId: String(companyId), 26 | signedToken: signedToken, 27 | createdAt: new Date().toISOString(), 28 | }, 29 | ReturnConsumedCapacity: "TOTAL", 30 | TableName: process.env.DB_TABLE, 31 | ConditionExpression: "pk <> :pk", 32 | ExpressionAttributeValues: { 33 | ":pk": pk, 34 | }, 35 | }; 36 | 37 | try { 38 | await docClient.put(params).promise(); 39 | 40 | return { 41 | statusCode: 200, 42 | headers: { 43 | "Content-Type": "application/json", 44 | "Access-Control-Allow-Origin": "*", 45 | }, 46 | body: JSON.stringify({ 47 | message: "Webhook created", 48 | data: { 49 | token: signedToken, 50 | }, 51 | }), 52 | }; 53 | } catch (error) { 54 | if ((error.code = "ConditionalCheckFailedException")) { 55 | var errorStack = "Record exists."; 56 | } else { 57 | var errorStack = error.stack; 58 | } 59 | 60 | console.error("Error", error.stack); 61 | 62 | return { 63 | statusCode: 500, 64 | headers: { 65 | "Content-Type": "application/json", 66 | "Access-Control-Allow-Origin": "*", 67 | }, 68 | body: JSON.stringify({ 69 | message: "Webhook creation failed", 70 | error: errorStack, 71 | }), 72 | }; 73 | } 74 | }; 75 | 76 | function validateInput(body) { 77 | let errors = []; 78 | 79 | if (!validator.isUri(body.url)) { 80 | errors.push("Required field url not found or invalid"); 81 | } 82 | 83 | if (!body.companyId || isNaN(body.companyId)) { 84 | errors.push("Required field companyId not found or invalid"); 85 | } 86 | 87 | if (!body.eventType) { 88 | errors.push("Required field eventType not found or invalid"); 89 | } 90 | 91 | if (errors.length > 0) { 92 | return { 93 | statusCode: 422, 94 | headers: { 95 | "Content-Type": "application/json", 96 | "Access-Control-Allow-Origin": "*", 97 | }, 98 | body: JSON.stringify({ 99 | message: "Validation errors", 100 | error: errors, 101 | }), 102 | }; 103 | } 104 | 105 | return null; 106 | } 107 | 108 | function gerRandomString(length, onlyNumbers = false) { 109 | var result = ""; 110 | var characters = 111 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 112 | 113 | if (onlyNumbers === true) { 114 | var characters = "0123456789"; 115 | } 116 | var charactersLength = characters.length; 117 | 118 | for (var i = 0; i < length; i++) { 119 | result += characters.charAt(Math.floor(Math.random() * charactersLength)); 120 | } 121 | 122 | return result; 123 | } 124 | -------------------------------------------------------------------------------- /backend/functions/register-webhook/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webhook_management", 3 | "version": "1.0.0", 4 | "description": "Webhook management system", 5 | "main": "app.js", 6 | "author": "SAM CLI", 7 | "license": "MIT", 8 | "dependencies": { 9 | "valid-url": "^1.0.9" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/functions/trigger-resource-created/app.js: -------------------------------------------------------------------------------- 1 | const AWS = require("aws-sdk"); 2 | const eventbridge = new AWS.EventBridge(); 3 | 4 | exports.lambdaHandler = async (event, context) => { 5 | var eventsToPublish = []; 6 | for (const record of event.Records) { 7 | if ( 8 | record.eventName === "INSERT" && 9 | record.dynamodb.NewImage.type.S === "candidate" 10 | ) { 11 | let pk = record.dynamodb.NewImage.pk.S; 12 | let companyId = record.dynamodb.NewImage.companyId.S; 13 | 14 | let payload = { 15 | companyId: companyId, 16 | webhookEvent: "candidate.created", 17 | resourceType: "candidate", 18 | resourceId: pk, 19 | }; 20 | 21 | eventsToPublish.push({ 22 | Source: process.env.EVENT_SOURCE, 23 | EventBusName: process.env.EVENT_BUS, 24 | DetailType: "candidate.created", 25 | Time: new Date(), 26 | Detail: JSON.stringify(payload), 27 | }); 28 | } 29 | } 30 | 31 | if (eventsToPublish.length > 0) { 32 | await eventbridge.putEvents({ Entries: eventsToPublish }).promise(); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /backend/functions/trigger-resource-created/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webhook_management", 3 | "version": "1.0.0", 4 | "description": "Webhook management system", 5 | "main": "app.js", 6 | "author": "SAM CLI", 7 | "license": "MIT", 8 | "dependencies": { 9 | "valid-url": "^1.0.9" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/statemachine/webhook_management.asl.json: -------------------------------------------------------------------------------- 1 | { 2 | "Comment": "State machine for webhok management service", 3 | "StartAt": "Get Webhooks For Company And Event", 4 | "States": { 5 | "Get Webhooks For Company And Event": { 6 | "Type": "Task", 7 | "Resource": "arn:aws:states:::dynamodb:getItem", 8 | "Parameters": { 9 | "TableName": "${DynamoDBTableName}", 10 | "Key": { 11 | "pk": { 12 | "S.$": "States.Format('webhook_{}_{}', $.companyId, $.webhookEvent)" 13 | }, 14 | "type": { 15 | "S": "webhook" 16 | } 17 | } 18 | }, 19 | "Next": "Validate If Webhook Data Exists", 20 | "ResultPath": "$.webhookData" 21 | }, 22 | "Validate If Webhook Data Exists": { 23 | "Type": "Choice", 24 | "Choices": [ 25 | { 26 | "And": [ 27 | { 28 | "Variable": "$.webhookData.Item", 29 | "IsPresent": true 30 | }, 31 | { 32 | "Variable": "$.webhookData.Item.pk.S", 33 | "IsPresent": true 34 | }, 35 | { 36 | "Variable": "$.webhookData.Item.signedToken.S", 37 | "IsPresent": true 38 | }, 39 | { 40 | "Variable": "$.webhookData.Item.url.S", 41 | "IsPresent": true 42 | } 43 | ], 44 | "Next": "Transform Webhook Data" 45 | } 46 | ], 47 | "Default": "SkipExecution" 48 | }, 49 | "Transform Webhook Data": { 50 | "Type": "Pass", 51 | "Next": "Fetch Resource Data Lambda", 52 | "Parameters": { 53 | "webhookId.$": "$.webhookData.Item.pk.S", 54 | "webhookSignToken.$": "$.webhookData.Item.signedToken.S", 55 | "webhookUrl.$": "$.webhookData.Item.url.S", 56 | "companyId.$": "$.companyId", 57 | "resourceType.$": "$.resourceType", 58 | "resourceId.$": "$.resourceId" 59 | } 60 | }, 61 | "Fetch Resource Data Lambda": { 62 | "Type": "Task", 63 | "Resource": "arn:aws:states:::lambda:invoke", 64 | "Parameters": { 65 | "FunctionName": "${FetchResourceFunctionName}", 66 | "Payload": { 67 | "key.$": "$.resourceId", 68 | "type.$": "$.resourceType" 69 | } 70 | }, 71 | "Retry": [ 72 | { 73 | "ErrorEquals": [ 74 | "Lambda.ServiceException", 75 | "Lambda.AWSLambdaException", 76 | "Lambda.SdkClientException" 77 | ], 78 | "IntervalSeconds": 2, 79 | "MaxAttempts": 6, 80 | "BackoffRate": 2 81 | } 82 | ], 83 | "Next": "Validate If Event Object Data Exists", 84 | "ResultPath": "$.taskResult", 85 | "ResultSelector": { 86 | "resourceData.$": "$.Payload" 87 | } 88 | }, 89 | "Validate If Event Object Data Exists": { 90 | "Type": "Choice", 91 | "Choices": [ 92 | { 93 | "Not": { 94 | "Variable": "$.taskResult.resourceData", 95 | "IsNull": true 96 | }, 97 | "Next": "Create Webhook Call" 98 | } 99 | ], 100 | "Default": "Fail" 101 | }, 102 | "Create Webhook Call": { 103 | "Type": "Task", 104 | "Resource": "arn:aws:states:::lambda:invoke", 105 | "Parameters": { 106 | "Payload.$": "$", 107 | "FunctionName": "${CreateWebhookCallFunctionName}" 108 | }, 109 | "Next": "Queue Webhook Call", 110 | "ResultPath": "$.webhookCallData", 111 | "ResultSelector": { 112 | "id.$": "$.Payload.id", 113 | "payload.$": "$.Payload.payload" 114 | } 115 | }, 116 | "Queue Webhook Call": { 117 | "Type": "Task", 118 | "Resource": "arn:aws:states:::sqs:sendMessage.waitForTaskToken", 119 | "Parameters": { 120 | "QueueUrl": "${SQSQueueUrl}", 121 | "MessageBody": { 122 | "url.$": "$.webhookUrl", 123 | "webhookCallId.$": "$.webhookCallData.id", 124 | "signingToken.$": "$.webhookSignToken", 125 | "payload.$": "$.webhookCallData.payload", 126 | "taskToken.$": "$$.Task.Token" 127 | } 128 | }, 129 | "HeartbeatSeconds": 3600, 130 | "Retry": [ 131 | { 132 | "ErrorEquals": [ 133 | "States.ALL" 134 | ], 135 | "BackoffRate": 2, 136 | "IntervalSeconds": 60, 137 | "MaxAttempts": 2 138 | } 139 | ], 140 | "Next": "Update WebhookCall", 141 | "ResultPath": "$.webhookCallResult", 142 | "Catch": [ 143 | { 144 | "ErrorEquals": [ 145 | "States.ALL" 146 | ], 147 | "Next": "Transform Error", 148 | "ResultPath": "$.webhookCallResult" 149 | } 150 | ] 151 | }, 152 | "Transform Error": { 153 | "Type": "Pass", 154 | "Next": "Update WebhookCall", 155 | "ResultPath": "$.webhookCallResult", 156 | "Parameters": { 157 | "status": "failed", 158 | "payload": "", 159 | "output": { 160 | "Error.$": "$.webhookCallResult.Error", 161 | "Cause.$": "$.webhookCallResult.Cause" 162 | } 163 | } 164 | }, 165 | "Update WebhookCall": { 166 | "Type": "Task", 167 | "Resource": "arn:aws:states:::dynamodb:updateItem", 168 | "Parameters": { 169 | "TableName": "${DynamoDBTableName}", 170 | "Key": { 171 | "pk": { 172 | "S.$": "$.webhookCallData.id" 173 | }, 174 | "type": { 175 | "S": "webhookcall" 176 | } 177 | }, 178 | "UpdateExpression": "SET #status = :status, #output = :output", 179 | "ExpressionAttributeValues": { 180 | ":status": { 181 | "S.$": "$.webhookCallResult.status" 182 | }, 183 | ":output": { 184 | "S.$": "States.JsonToString($.webhookCallResult.output)" 185 | } 186 | }, 187 | "ExpressionAttributeNames": { 188 | "#status": "status", 189 | "#output": "output" 190 | } 191 | }, 192 | "End": true 193 | }, 194 | "Fail": { 195 | "Type": "Fail", 196 | "Error": "dataNotFound", 197 | "Cause": "Required resource data not exists in dynamodb" 198 | }, 199 | "SkipExecution": { 200 | "Type": "Pass", 201 | "End": true, 202 | "Comment": "Webhook not configured for this event" 203 | } 204 | } 205 | } -------------------------------------------------------------------------------- /backend/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | webhook management backend 5 | 6 | SAM Template for webhook management system 7 | 8 | Parameters: 9 | Stage: 10 | Type: String 11 | Default: 'dev' 12 | eventSource: 13 | Type: String 14 | Default: XYZCo 15 | EventBusName: 16 | Type: String 17 | Default: EventBusXYZCompany 18 | 19 | Resources: 20 | EventBus: 21 | Type: AWS::Events::EventBus 22 | Properties: 23 | Name: !Ref EventBusName 24 | 25 | SQSQueue: 26 | Type: AWS::SQS::Queue 27 | Properties: 28 | VisibilityTimeout: 60 29 | 30 | ApiGatewayApi: 31 | Type: AWS::Serverless::Api 32 | Properties: 33 | StageName: !Ref Stage 34 | Cors: 35 | AllowMethods: "'OPTIONS,POST,GET'" 36 | AllowHeaders: "'Content-Type'" 37 | AllowOrigin: "'*'" 38 | 39 | DynamodbTable: 40 | Type: AWS::DynamoDB::Table 41 | Properties: 42 | AttributeDefinitions: 43 | - AttributeName: pk 44 | AttributeType: S 45 | - AttributeName: type 46 | AttributeType: S 47 | - AttributeName: companyId 48 | AttributeType: S 49 | KeySchema: 50 | - AttributeName: pk 51 | KeyType: HASH 52 | - AttributeName: type 53 | KeyType: RANGE 54 | GlobalSecondaryIndexes: 55 | - IndexName: gsiTypeAndCompanyId 56 | KeySchema: 57 | - AttributeName: type 58 | KeyType: HASH 59 | - AttributeName: companyId 60 | KeyType: RANGE 61 | Projection: 62 | ProjectionType: INCLUDE 63 | NonKeyAttributes: 64 | - pk 65 | - createdAt 66 | - payload 67 | - status 68 | - url 69 | - output 70 | ProvisionedThroughput: 71 | ReadCapacityUnits: 1 72 | WriteCapacityUnits: 1 73 | ProvisionedThroughput: 74 | ReadCapacityUnits: 1 75 | WriteCapacityUnits: 1 76 | StreamSpecification: 77 | StreamViewType: NEW_IMAGE 78 | 79 | RegisterWebhookFunction: 80 | Type: AWS::Serverless::Function 81 | Description: Webhook registration function 82 | Properties: 83 | CodeUri: functions/register-webhook/ 84 | Handler: app.lambdaHandler 85 | Runtime: nodejs14.x 86 | Timeout: 3 87 | Environment: 88 | Variables: 89 | DB_TABLE: !Ref DynamodbTable 90 | Policies: 91 | - DynamoDBWritePolicy: 92 | TableName: !Ref DynamodbTable 93 | Events: 94 | CreateWebhookApi: 95 | Type: Api 96 | Properties: 97 | Path: /webhooks 98 | Method: post 99 | RestApiId: !Ref ApiGatewayApi 100 | 101 | CreateCandidateFunction: 102 | Type: AWS::Serverless::Function 103 | Description: Create canidate function 104 | Properties: 105 | CodeUri: functions/create-candidate/ 106 | Handler: app.lambdaHandler 107 | Runtime: nodejs14.x 108 | Timeout: 10 109 | Environment: 110 | Variables: 111 | DB_TABLE: !Ref DynamodbTable 112 | Policies: 113 | - DynamoDBWritePolicy: 114 | TableName: !Ref DynamodbTable 115 | Events: 116 | CreateCandidateApi: 117 | Type: Api 118 | Properties: 119 | Path: /candidates 120 | Method: post 121 | RestApiId: !Ref ApiGatewayApi 122 | 123 | FetchResourceFunction: 124 | Type: AWS::Serverless::Function 125 | Description: Fetch resource data from Dyanamodb 126 | Properties: 127 | CodeUri: functions/fetch-resource-data/ 128 | Handler: app.lambdaHandler 129 | Runtime: nodejs12.x 130 | Timeout: 10 131 | Environment: 132 | Variables: 133 | DB_TABLE: !Ref DynamodbTable 134 | Policies: 135 | - DynamoDBReadPolicy: 136 | TableName: !Ref DynamodbTable 137 | 138 | TriggerResourceCreatedFunction: 139 | Type: AWS::Serverless::Function 140 | Description: Put resource data to event bus when record added to Dyanamodb 141 | Properties: 142 | CodeUri: functions/trigger-resource-created/ 143 | Handler: app.lambdaHandler 144 | Runtime: nodejs12.x 145 | Timeout: 20 146 | Environment: 147 | Variables: 148 | EVENT_BUS: !Ref EventBusName 149 | EVENT_SOURCE: !Ref eventSource 150 | Policies: 151 | - EventBridgePutEventsPolicy: 152 | EventBusName: !Ref EventBusName 153 | Events: 154 | DynamodbTableStream: 155 | Type: DynamoDB 156 | Properties: 157 | Stream: !GetAtt DynamodbTable.StreamArn 158 | StartingPosition: TRIM_HORIZON 159 | BatchSize: 100 160 | FilterCriteria: 161 | Filters: 162 | - Pattern: "{\"eventName\":[\"INSERT\"],\"dynamodb\":{\"NewImage\":{\"type\":{\"S\":[\"candidate\"]}}}}" 163 | 164 | CreateWebhookCallFunction: 165 | Type: AWS::Serverless::Function 166 | Description: Create webhook call with status pending 167 | Properties: 168 | CodeUri: functions/create-webhook-call/ 169 | Handler: app.lambdaHandler 170 | Runtime: nodejs12.x 171 | Timeout: 10 172 | EventInvokeConfig: 173 | MaximumRetryAttempts: 0 174 | Environment: 175 | Variables: 176 | DB_TABLE: !Ref DynamodbTable 177 | Policies: 178 | - DynamoDBWritePolicy: 179 | TableName: !Ref DynamodbTable 180 | 181 | CallWebhookFunction: 182 | Type: AWS::Serverless::Function 183 | Description: Call webhook with the resource data 184 | Properties: 185 | CodeUri: functions/call-webhook/ 186 | Handler: app.lambdaHandler 187 | Runtime: nodejs12.x 188 | Timeout: 60 189 | EventInvokeConfig: 190 | MaximumRetryAttempts: 0 191 | Policies: 192 | - Statement: 193 | - Sid: StateStatusPermission 194 | Effect: Allow 195 | Action: 196 | - states:SendTaskSuccess 197 | - states:SendTaskFailure 198 | Resource: '*' 199 | Events: 200 | SQSEvent: 201 | Type: SQS 202 | Properties: 203 | Queue: !GetAtt SQSQueue.Arn 204 | BatchSize: 10 205 | 206 | FetchWebhookHistoryFunction: 207 | Type: AWS::Serverless::Function 208 | Description: Fetcho webhook call history by companyId 209 | Properties: 210 | CodeUri: functions/fetch-webhook-history/ 211 | Handler: app.lambdaHandler 212 | Runtime: nodejs14.x 213 | Timeout: 10 214 | Environment: 215 | Variables: 216 | DB_TABLE: !Ref DynamodbTable 217 | Policies: 218 | - DynamoDBReadPolicy: 219 | TableName: !Ref DynamodbTable 220 | Events: 221 | CreateCandidateApi: 222 | Type: Api 223 | Properties: 224 | Path: /webhook_history/{companyId} 225 | Method: get 226 | RestApiId: !Ref ApiGatewayApi 227 | 228 | WebhookManagmentStateMachine: 229 | Type: AWS::Serverless::StateMachine 230 | Properties: 231 | DefinitionUri: statemachine/webhook_management.asl.json 232 | DefinitionSubstitutions: 233 | DynamoDBTableName: !Ref DynamodbTable 234 | FetchResourceFunctionName: !Ref FetchResourceFunction 235 | SQSQueueUrl: !Ref SQSQueue 236 | CreateWebhookCallFunctionName: !Ref CreateWebhookCallFunction 237 | Events: 238 | EventBridgeRule: 239 | Type: EventBridgeRule 240 | Properties: 241 | EventBusName: !Ref EventBus 242 | Pattern: 243 | source: 244 | - !Ref eventSource 245 | detail-type: 246 | - candidate.created 247 | InputPath: $.detail 248 | Policies: 249 | - LambdaInvokePolicy: 250 | FunctionName: !Ref FetchResourceFunction 251 | - LambdaInvokePolicy: 252 | FunctionName: !Ref CreateWebhookCallFunction 253 | - SQSSendMessagePolicy: 254 | QueueName: !GetAtt SQSQueue.QueueName 255 | - DynamoDBReadPolicy: 256 | TableName: !Ref DynamodbTable 257 | - DynamoDBWritePolicy: 258 | TableName: !Ref DynamodbTable 259 | 260 | Outputs: 261 | DynamoDBTable: 262 | Description: "Dynamodb Table Arn" 263 | Value: !GetAtt DynamodbTable.Arn 264 | ApiBaseUrl: 265 | Description: "Base Url" 266 | Value: !Sub "https://${ApiGatewayApi}.execute-api.${AWS::Region}.amazonaws.com/${Stage}/" 267 | EventBus: 268 | Description: "Event Bus Arn" 269 | Value: !GetAtt EventBus.Arn 270 | FetchResourceFunction: 271 | Description: FetchResourceFunction function name 272 | Value: !Ref FetchResourceFunction 273 | SQSQueueUrl: 274 | Description: SQS Queue URL 275 | Value: !Ref SQSQueue 276 | WebhookManagmentStateMachineName: 277 | Description: Webhook Managment State Machine Name 278 | Value: !Ref WebhookManagmentStateMachine 279 | -------------------------------------------------------------------------------- /frontend/.env.example: -------------------------------------------------------------------------------- 1 | VUE_APP_API_BASE_URL=https://${api}.execute-api.${region}.amazonaws.com/${stage}/ -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env 8 | .env.local 9 | .env.*.local 10 | 11 | # Log files 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | pnpm-debug.log* 16 | 17 | # Editor directories and files 18 | .idea 19 | .vscode 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # frontend 2 | 3 | ## Project setup 4 | ``` 5 | yarn install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | yarn serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | yarn build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | yarn lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /frontend/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "@popperjs/core": "^2.10.1", 12 | "axios": "^0.21.4", 13 | "bootstrap": "^5.1.1", 14 | "core-js": "^3.18.0", 15 | "email-validator": "^2.0.4", 16 | "validator": "^13.7.0", 17 | "vue": "^3.0.0" 18 | }, 19 | "devDependencies": { 20 | "@vue/cli-plugin-babel": "~4.5.0", 21 | "@vue/cli-plugin-eslint": "~4.5.0", 22 | "@vue/cli-service": "~4.5.0", 23 | "@vue/compiler-sfc": "^3.0.0", 24 | "babel-eslint": "^10.1.0", 25 | "eslint": "^6.7.2", 26 | "eslint-plugin-vue": "^7.0.0", 27 | "vue-loader-v16": "^16.0.0-beta.5.4" 28 | }, 29 | "eslintConfig": { 30 | "root": true, 31 | "env": { 32 | "node": true 33 | }, 34 | "extends": [ 35 | "plugin:vue/vue3-essential", 36 | "eslint:recommended" 37 | ], 38 | "parserOptions": { 39 | "parser": "babel-eslint" 40 | }, 41 | "rules": {} 42 | }, 43 | "browserslist": [ 44 | "> 1%", 45 | "last 2 versions", 46 | "not dead" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pubudusj/webhook_management/70864b3bf773762227ed88144406fbf4a9067f7c/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 |Time | 16 |Webhook Url | 17 |Status | 18 |Payload | 19 |Additional Info | 20 |
---|---|---|---|---|
{{ webhookCall.createdAt }} | 25 |{{ webhookCall.url }} | 26 |{{ webhookCall.status }} | 27 |{{ webhookCall.payload }} | 28 |{{ webhookCall.output !== "{}" ? webhookCall.output : '-' }} | 29 |No data found. | 33 | 34 |