├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── backend ├── api │ └── locations │ │ ├── get.js │ │ ├── local │ │ ├── getTest.js │ │ ├── getTestEvent.json │ │ ├── postTest.js │ │ └── postTestEvent.json │ │ ├── package.json │ │ └── post.js ├── layers │ └── sharp-layer │ │ ├── package.json │ │ └── template.yaml ├── realtime.yaml ├── streams │ └── ddb │ │ ├── app.js │ │ ├── localTest.js │ │ ├── localTestEvent.json │ │ └── package.json ├── template.yaml └── upload │ ├── complete.js │ ├── local │ ├── localTest.js │ └── localTestEvent.json │ ├── package.json │ └── request.js ├── frontend ├── README.md ├── auth_config.json ├── babel.config.js ├── package.json ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.vue │ ├── assets │ │ └── logo.png │ ├── auth │ │ └── index.js │ ├── components │ │ ├── IoT.vue │ │ └── Snackbar.vue │ ├── main.js │ ├── plugins │ │ └── vuetify.js │ ├── router │ │ └── index.js │ ├── store │ │ └── index.js │ └── views │ │ ├── HomeView.vue │ │ └── PlaceDetail.vue └── vue.config.js ├── localTesting ├── events │ ├── ddbevent-processed.json │ ├── ddbevent-rejection.json │ └── event.json └── test.js └── workflows ├── functions ├── analyze │ ├── dimensions │ │ ├── app.js │ │ └── package.json │ ├── moderator │ │ ├── app.js │ │ └── package.json │ └── people │ │ ├── app.js │ │ └── package.json ├── publish │ ├── app.js │ └── package.json └── resizer │ ├── app.js │ └── package.json ├── statemachines ├── v1.asl.json ├── v2.asl.json ├── v3.asl.json └── v4.asl.json └── templates ├── v1 └── template.yaml ├── v2 └── template.yaml ├── v3 └── template.yaml └── v4 └── template.yaml /.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 | 129 | # End of https://www.gitignore.io/api/osx,node,linux,windows 130 | 131 | # Editor directories and files 132 | .idea 133 | .vscode 134 | *.suo 135 | *.ntvs* 136 | *.njsproj 137 | *.sln 138 | *.sw? 139 | 140 | #AWS/SAM 141 | .aws-sam 142 | packaged.yaml 143 | samconfig.toml 144 | 145 | #jbesw-specific 146 | testData/ 147 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Happy Path - Building flexible serverless backends for web apps 2 | 3 | This example application shows how to build flexible serverless backends for web apps. This sample application is called *Happy Path*. 4 | 5 | This app is designed to help state parks and nonprofit organizations digitalize printed materials, such as flyers and maps. It allows visitors to capture images of documents and photos of hiking trails. They can share these with other users to reduce printed waste. 6 | 7 | The frontend displays and captures images for different locations, and the backend processes this data according to a set of business rules. This web application is designed for smartphones so it’s used while visitors are at the locations. 8 | 9 | To learn more about how this application works, see the 3-part series on the AWS Compute Blog: 10 | * Part 1: https://aws.amazon.com/blogs/compute/using-serverless-backends-to-iterate-quickly-on-web-apps-part-1/. 11 | * Part 2: https://aws.amazon.com/blogs/compute/using-serverless-backends-to-iterate-quickly-on-web-apps-part-2/. 12 | * Part 3: https://aws.amazon.com/blogs/compute/using-serverless-backends-to-iterate-quickly-on-web-apps-part-3/. 13 | 14 | Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. 15 | 16 | ```bash 17 | . 18 | ├── README.MD <-- This instructions file 19 | ├── frontend <-- Source code for the Vue.js 20 | ├── backend <-- Source code for the serverless backend 21 | ├── workflows <-- Step Functions applications 22 | ├── localTesting <-- Test harness/test events for local debugging 23 | ``` 24 | 25 | ## Requirements 26 | 27 | * AWS CLI already configured with Administrator permission 28 | * [AWS SAM CLI installed](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) - **minimum version 0.48**. 29 | * [NodeJS 12.x installed](https://nodejs.org/en/download/) 30 | * [Vue.js and Vue CLI installed](https://vuejs.org/v2/guide/installation.html) 31 | * [Google Maps API key](https://developers.google.com/maps/documentation/javascript/get-api-key) 32 | * Sign up for an [Auth0 account](https://auth0.com/) 33 | 34 | ## Auth0 configuration 35 | 36 | 1. Sign up for an Auth0 account at https://auth0.com. 37 | 2. Select Applications from the menu, then choose **Create Application**. 38 | 3. Enter the name *Happy Path* and select *Single Page Web Applications* for application type. Choose **Create**. 39 | 4. Select the *Settings* tab, and note the `Domain` and `ClientID` for installation of the application backend and frontend. 40 | 5. Under *Allowed Callback URLs*, *Allowed Logout URLs* and *Allowed Web Origins* and *Allowed Origins (CORS)*, enter **http://localhost:8080**. Choose **Save Changes**. 41 | 6. Select APIS from the menu, then choose **Create API**. 42 | 7. Enter the name *Happy Path*, and set the *Identifier* to **https://auth0-jwt-authorizer**. Choose **Create**. 43 | 44 | Auth0 is now configured for you to use. The backend uses the `domain` value to validate the JWT token. The frontend uses the identifier (also known as the audience), together with the *Client ID* to validate authentication for this single application. For more information, see the [Auth0 documentation](https://auth0.com/docs/api/authentication). 45 | 46 | ## Installation Instructions 47 | 48 | 1. [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and login. 49 | 50 | 2. Clone the repo onto your local development machine: 51 | ``` 52 | git clone https://github.com/aws-samples/happy-path 53 | ``` 54 | 55 | ### 1. Installing the realtime stack 56 | 57 | 1. From the command line, install the realtime messaging stack: 58 | ``` 59 | cd backend 60 | sam deploy --guided --template-file realtime.yaml 61 | ``` 62 | During the prompts, enter `happy-path-realtime` for the Stack Name, enter your preferred Region, and accept the defaults for the remaining questions. 63 | 64 | 2. Retrieve the IoT endpointAddress - note this for the frontend installation: 65 | ``` 66 | aws iot describe-endpoint --endpoint-type iot:Data-ATS 67 | ``` 68 | 3. Retrieve the Cognito Pool ID - note this for the frontend installation: 69 | ``` 70 | aws cognito-identity list-identity-pools --max-results 10 71 | ``` 72 | 73 | ### 2. Create the Lambda layer 74 | 75 | Note that you must run these instructions on a Linux x64 platform to ensure the correct Sharp binaries are included. If you are using another operating system, see the [AWS Lambda Sharp installation instructions](https://sharp.pixelplumbing.com/install#aws-lambda). Alternatively, use [AWS Cloud9](https://aws.amazon.com/cloud9/) to complete this step to ensure compatibility. 76 | 77 | 1. Change to the layer directory: 78 | ``` 79 | cd ./backend/layers/sharp-layer 80 | ``` 81 | 2. Install and prepare package: 82 | ``` 83 | npm install 84 | mkdir -p ./layer/nodejs 85 | mv ./node_modules ./layer/nodejs 86 | ``` 87 | 3. Deploy the SAM template to create the layer: 88 | ``` 89 | sam deploy --guided 90 | ``` 91 | 4. When prompted for parameters, enter: 92 | - Stack Name: lambda-layer-sharp 93 | - AWS Region: your preferred AWS Region (e.g. us-east-1) 94 | - Accept all other defaults. 95 | 96 | Take a note of the output ARN - it is used in subsequent deployments wherever you see the `SharpLayerARN` parameter. 97 | 98 | ### 3. Installing the backend application 99 | 100 | From the command line, deploy the SAM template. Note that your SAM version must be at least 0.48 - if you receive build errors, it is likely that your SAM CLI version is not up to date. 101 | Run: 102 | 103 | ``` 104 | cd ../.. 105 | sam build 106 | sam deploy --guided 107 | ``` 108 | 109 | When prompted for parameters, enter: 110 | - Stack Name: happy-path-backend 111 | - AWS Region: your preferred AWS Region (e.g. us-east-1) 112 | - ApplicationTableName: leave as default 113 | - S3UploadBucketName: enter a unique name for your bucket. 114 | - S3DistributionBucketName: enter a unique name for your bucket. 115 | - IoTDataEndpoint: the IoT endpointAddress noted in the realtime stack installation. 116 | - Auth0issuer: this is the URL for the Auth0 account (the format is https://dev-abc12345.auth0.com/). 117 | - Accept all other defaults. 118 | 119 | This takes several minutes to deploy. At the end of the deployment, note the output values, as you need these in the frontend installation. 120 | 121 | ### 4. Installing the frontend application 122 | 123 | The frontend code is saved in the `frontend` subdirectory. 124 | 125 | 1. Before running, you need to set environment variables in the `src\main.js` file: 126 | 127 | - GoogleMapsKey: [sign up](https://developers.google.com/maps/documentation/javascript/get-api-key) for a Google Maps API key and enter the value here. 128 | - APIurl: this is the `APIendpoint` value from the last section. 129 | - PoolId: your Cognito pool ID from earlier. 130 | - Host: your IoT endpoint from earlier. 131 | - Region: your preferred AWS Region (e.g. us-east-1). 132 | 133 | 2. Open the `src\auth_config.json` and enter the values from your Auth0 account: 134 | - domain: this is your account identifier, in the format `dev-12345678.auth0.com`. 135 | - clientId: a unique string identifying the client application. 136 | - audience: set to https://auth0-jwt-authorizer. 137 | 138 | 3. Change directory into the frontend code, and run the NPM installation: 139 | 140 | ``` 141 | cd ../frontend 142 | npm install 143 | ``` 144 | 4. After installation is complete, you can run the application locally: 145 | 146 | ``` 147 | npm run serve 148 | ``` 149 | 150 | ### 5. Installing the workflows 151 | 152 | To show the progression of increasingly complex workflows, this repo uses four separate AWS Step Functions state machines. Part 3 of the blog series shows how to deploy each one. The deployment pattern is the same for each version. 153 | 154 | - Workflow SAM templates are in `workflows/templates`. 155 | - State machine definitions are stored in `workflows/state machines`. 156 | 157 | To deploy: 158 | 1. Remove any previous workflow version by deleting their CloudFormation stack. 159 | 2. Change directory to the version of the workflow you want to deploy, e.g.: 160 | ``` 161 | cd workflows/templates/v1 162 | ``` 163 | 3. Build and deploy: 164 | ``` 165 | sam build 166 | sam deploy --guided 167 | ``` 168 | 4. When prompted for parameters, enter the parameters noted as outputs in the previous deployments. 169 | 170 | ## Next steps 171 | 172 | [A sample photo dataset](https://jbesw-public-downloads.s3.us-east-2.amazonaws.com/happy-path-photos-dataset.zip) is provided for download. These photos are provided by the blog author ([James Beswick](https://twitter.com/jbesw)) under the [Creative Commons Attribution 4.0 International license](http://creativecommons.org/licenses/by/4.0/). 173 | 174 | The AWS Compute Blog series and video link at the top of this README file contains additional information about the application design and architecture. 175 | 176 | If you have any questions, please contact the author or raise an issue in the GitHub repo. 177 | 178 | ============================================== 179 | 180 | Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 181 | 182 | SPDX-License-Identifier: MIT-0 183 | -------------------------------------------------------------------------------- /backend/api/locations/get.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 9 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 10 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 11 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 12 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 13 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | */ 15 | 16 | const AWS = require('aws-sdk') 17 | AWS.config.update({region: process.env.AWS_REGION}) 18 | const documentClient = new AWS.DynamoDB.DocumentClient() 19 | 20 | // Returns details of a Place ID where the app has user-generated content. 21 | 22 | exports.handler = async (event) => { 23 | console.log(event) 24 | if (!event.queryStringParameters) { 25 | return { 26 | statusCode: 422, 27 | body: JSON.stringify("Missing parameters") 28 | } 29 | } 30 | 31 | const placeId = event.queryStringParameters.placeId 32 | console.log(`Searching for: ${placeId}`) 33 | 34 | // Use query method to retrieve all values for a given Primary Key. 35 | // See https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GettingStarted.NodeJs.04.html 36 | 37 | const place = await documentClient.query({ 38 | TableName: process.env.TableName, 39 | KeyConditionExpression: "#pk = :pk", 40 | ExpressionAttributeNames: { 41 | "#pk": "PK" 42 | }, 43 | ExpressionAttributeValues: { 44 | ":pk": placeId 45 | } 46 | }).promise() 47 | console.log('Place: ', place) 48 | 49 | return { 50 | statusCode: 200, 51 | body: JSON.stringify(place.Items) 52 | } 53 | } -------------------------------------------------------------------------------- /backend/api/locations/local/getTest.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 9 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 10 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 11 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 12 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 13 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | */ 15 | 16 | // Mock event 17 | const event = require('./getTestEvent') 18 | 19 | // Mock environment variables 20 | process.env.AWS_REGION = 'us-east-1' 21 | process.env.localTest = true 22 | process.env.TableName = 'hp-application' 23 | 24 | // Lambda handler 25 | const { handler } = require('../get') 26 | 27 | const main = async () => { 28 | console.time('localTest') 29 | console.dir(await handler(event)) 30 | console.timeEnd('localTest') 31 | } 32 | 33 | main().catch(error => console.error(error)) -------------------------------------------------------------------------------- /backend/api/locations/local/getTestEvent.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": "eyJ0ZXN0IjoiYm9keSJ9", 3 | "resource": "/{proxy+}", 4 | "path": "/path/to/resource", 5 | "httpMethod": "POST", 6 | "isBase64Encoded": true, 7 | "queryStringParameters": { 8 | "placeId": "ChIJDZ4oVn3A4okR2NFbt8kHZwU" 9 | }, 10 | "pathParameters": { 11 | "proxy": "/path/to/resource" 12 | }, 13 | "stageVariables": { 14 | "baz": "qux" 15 | }, 16 | "headers": { 17 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", 18 | "Accept-Encoding": "gzip, deflate, sdch", 19 | "Accept-Language": "en-US,en;q=0.8", 20 | "Cache-Control": "max-age=0", 21 | "CloudFront-Forwarded-Proto": "https", 22 | "CloudFront-Is-Desktop-Viewer": "true", 23 | "CloudFront-Is-Mobile-Viewer": "false", 24 | "CloudFront-Is-SmartTV-Viewer": "false", 25 | "CloudFront-Is-Tablet-Viewer": "false", 26 | "CloudFront-Viewer-Country": "US", 27 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com", 28 | "Upgrade-Insecure-Requests": "1", 29 | "User-Agent": "Custom User Agent String", 30 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", 31 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", 32 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2", 33 | "X-Forwarded-Port": "443", 34 | "X-Forwarded-Proto": "https" 35 | }, 36 | "multiValueHeaders": { 37 | "Accept": [ 38 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" 39 | ], 40 | "Accept-Encoding": [ 41 | "gzip, deflate, sdch" 42 | ], 43 | "Accept-Language": [ 44 | "en-US,en;q=0.8" 45 | ], 46 | "Cache-Control": [ 47 | "max-age=0" 48 | ], 49 | "CloudFront-Forwarded-Proto": [ 50 | "https" 51 | ], 52 | "CloudFront-Is-Desktop-Viewer": [ 53 | "true" 54 | ], 55 | "CloudFront-Is-Mobile-Viewer": [ 56 | "false" 57 | ], 58 | "CloudFront-Is-SmartTV-Viewer": [ 59 | "false" 60 | ], 61 | "CloudFront-Is-Tablet-Viewer": [ 62 | "false" 63 | ], 64 | "CloudFront-Viewer-Country": [ 65 | "US" 66 | ], 67 | "Host": [ 68 | "0123456789.execute-api.us-east-1.amazonaws.com" 69 | ], 70 | "Upgrade-Insecure-Requests": [ 71 | "1" 72 | ], 73 | "User-Agent": [ 74 | "Custom User Agent String" 75 | ], 76 | "Via": [ 77 | "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)" 78 | ], 79 | "X-Amz-Cf-Id": [ 80 | "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==" 81 | ], 82 | "X-Forwarded-For": [ 83 | "127.0.0.1, 127.0.0.2" 84 | ], 85 | "X-Forwarded-Port": [ 86 | "443" 87 | ], 88 | "X-Forwarded-Proto": [ 89 | "https" 90 | ] 91 | }, 92 | "requestContext": { 93 | "accountId": "123456789012", 94 | "resourceId": "123456", 95 | "stage": "prod", 96 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", 97 | "requestTime": "09/Apr/2015:12:34:56 +0000", 98 | "requestTimeEpoch": 1428582896000, 99 | "identity": { 100 | "cognitoIdentityPoolId": null, 101 | "accountId": null, 102 | "cognitoIdentityId": null, 103 | "caller": null, 104 | "accessKey": null, 105 | "sourceIp": "127.0.0.1", 106 | "cognitoAuthenticationType": null, 107 | "cognitoAuthenticationProvider": null, 108 | "userArn": null, 109 | "userAgent": "Custom User Agent String", 110 | "user": null 111 | }, 112 | "path": "/prod/path/to/resource", 113 | "resourcePath": "/{proxy+}", 114 | "httpMethod": "POST", 115 | "apiId": "1234567890", 116 | "protocol": "HTTP/1.1" 117 | } 118 | } -------------------------------------------------------------------------------- /backend/api/locations/local/postTest.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 9 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 10 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 11 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 12 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 13 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | */ 15 | 16 | // Mock event 17 | const event = require('./postTestEvent') 18 | 19 | // Mock environment variables 20 | process.env.AWS_REGION = 'us-east-1' 21 | process.env.localTest = true 22 | process.env.TableName = 'hp-application' 23 | 24 | // Lambda handler 25 | const { handler } = require('../app') 26 | 27 | const main = async () => { 28 | console.time('localTest') 29 | console.dir(await handler(event)) 30 | console.timeEnd('localTest') 31 | } 32 | 33 | main().catch(error => console.error(error)) -------------------------------------------------------------------------------- /backend/api/locations/local/postTestEvent.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "routeKey": "POST /locations", 4 | "rawPath": "/locations", 5 | "rawQueryString": "", 6 | "headers": { 7 | "accept": "application/json, text/plain, */*", 8 | "accept-encoding": "gzip, deflate, br", 9 | "accept-language": "en-US,en;q=0.9", 10 | "content-length": "149", 11 | "content-type": "application/json;charset=UTF-8", 12 | "host": "bly6y4jqaj.execute-api.us-east-1.amazonaws.com", 13 | "origin": "http://localhost:8080", 14 | "referer": "http://localhost:8080/", 15 | "sec-fetch-dest": "empty", 16 | "sec-fetch-mode": "cors", 17 | "sec-fetch-site": "cross-site", 18 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36", 19 | "x-amzn-trace-id": "Root=1-5efb40c7-0768bc2a44049b3a7fdaf583", 20 | "x-forwarded-for": "72.21.196.65", 21 | "x-forwarded-port": "443", 22 | "x-forwarded-proto": "https" 23 | }, 24 | "requestContext": { 25 | "accountId": "123456789012", 26 | "apiId": "bly6y4jqaj", 27 | "domainName": "bly6y4jqaj.execute-api.us-east-1.amazonaws.com", 28 | "domainPrefix": "bly6y4jqaj", 29 | "http": { 30 | "method": "POST", 31 | "path": "/locations", 32 | "protocol": "HTTP/1.1", 33 | "sourceIp": "72.21.196.65", 34 | "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36" 35 | }, 36 | "requestId": "O8cPNh7boAMEVwQ=", 37 | "routeKey": "POST /locations", 38 | "stage": "$default", 39 | "time": "30/Jun/2020:13:40:23 +0000", 40 | "timeEpoch": 1593524423660 41 | }, 42 | "body": "{\"place_id\":\"ChIJsbfYOBXA4okRAzASPj3c4iU\",\"name\":\"Camp Seawood\",\"vicinity\":\"350 Banfield Road, Portsmouth\",\"lat\":43.04326679999999,\"lng\":-70.7892576}", 43 | "isBase64Encoded": false 44 | } -------------------------------------------------------------------------------- /backend/api/locations/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-locations", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "James Beswick, AWS Serverless", 11 | "license": "MIT-0", 12 | "dependencies": { 13 | "aws-sdk": "^2.658.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /backend/api/locations/post.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 9 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 10 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 11 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 12 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 13 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | */ 15 | 16 | const AWS = require('aws-sdk') 17 | AWS.config.update({region: process.env.AWS_REGION}) 18 | const documentClient = new AWS.DynamoDB.DocumentClient() 19 | 20 | // Save place info to database 21 | 22 | exports.handler = async (event) => { 23 | // console.log(JSON.stringify(event, null, 0)) 24 | 25 | if (!event.body) { 26 | return { 27 | statusCode: 422, 28 | body: JSON.stringify("Missing body") 29 | } 30 | } 31 | 32 | const body = JSON.parse(event.body) 33 | const Item = { 34 | PK: body.place_id, 35 | SK: "listing", 36 | name: body.name, 37 | type: body.type, 38 | lat: body.lat, 39 | lng: body.lng, 40 | vicinity: body.vicinity, 41 | created: event.requestContext.timeEpoch 42 | } 43 | 44 | const result = await documentClient.put({ 45 | TableName: process.env.TableName, 46 | Item 47 | }).promise() 48 | console.log(result) 49 | 50 | return { 51 | statusCode: 200, 52 | body: JSON.stringify(Item) 53 | } 54 | } -------------------------------------------------------------------------------- /backend/layers/sharp-layer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sharp-layer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "James Beswick, AWS Serverless", 11 | "license": "MIT-0", 12 | "dependencies": { 13 | "sharp": ">=0.32.6" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /backend/layers/sharp-layer/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: AWS SDK Layer 4 | 5 | Resources: 6 | SDKlayer: 7 | Type: AWS::Serverless::LayerVersion 8 | Properties: 9 | LayerName: Sharp 10 | Description: Sharp NPM package. 11 | ContentUri: './layer' 12 | CompatibleRuntimes: 13 | - nodejs12.x 14 | LicenseInfo: 'Available under the Apache-2.0 license.' 15 | RetentionPolicy: Retain 16 | 17 | -------------------------------------------------------------------------------- /backend/realtime.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: Happy Path - Realtime messaging 4 | 5 | Resources: 6 | ########################################## 7 | # Resources for realtime messaging # 8 | ########################################## 9 | HappyPathRealtime: 10 | Type: "AWS::IoT::Thing" 11 | Properties: 12 | ThingName: "happypath-realtime" 13 | AttributePayload: 14 | Attributes: {} 15 | 16 | UserPool: 17 | Type: "AWS::Cognito::UserPool" 18 | Properties: 19 | UserPoolName: HappyPathUserPool 20 | MfaConfiguration: "OFF" 21 | Schema: 22 | - Name: email 23 | AttributeDataType: String 24 | Mutable: false 25 | Required: true 26 | 27 | # Creates a User Pool Client to be used by the identity pool 28 | UserPoolClient: 29 | Type: "AWS::Cognito::UserPoolClient" 30 | Properties: 31 | ClientName: HappyPathUserPoolClient 32 | GenerateSecret: false 33 | UserPoolId: !Ref UserPool 34 | 35 | # Creates a federated Identity pool 36 | IdentityPool: 37 | Type: "AWS::Cognito::IdentityPool" 38 | Properties: 39 | IdentityPoolName: HappyPathIdentityPool 40 | AllowUnauthenticatedIdentities: true 41 | CognitoIdentityProviders: 42 | - ClientId: !Ref UserPoolClient 43 | ProviderName: !GetAtt UserPool.ProviderName 44 | 45 | # Create a role for unauthorized access to AWS resources. 46 | CognitoUnAuthorizedRole: 47 | Type: "AWS::IAM::Role" 48 | Properties: 49 | AssumeRolePolicyDocument: 50 | Version: "2012-10-17" 51 | Statement: 52 | - Effect: "Allow" 53 | Principal: 54 | Federated: "cognito-identity.amazonaws.com" 55 | Action: 56 | - "sts:AssumeRoleWithWebIdentity" 57 | Condition: 58 | StringEquals: 59 | "cognito-identity.amazonaws.com:aud": !Ref IdentityPool 60 | "ForAnyValue:StringLike": 61 | "cognito-identity.amazonaws.com:amr": unauthenticated 62 | Policies: 63 | - PolicyName: "CognitoUnauthorizedPolicy" 64 | PolicyDocument: 65 | Version: "2012-10-17" 66 | Statement: 67 | - Effect: "Allow" 68 | Action: 69 | - "cognito-sync:*" 70 | Resource: !Join [ "", [ "arn:aws:cognito-sync:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":identitypool/", !Ref IdentityPool] ] 71 | - Effect: Allow 72 | Action: 73 | - iot:Connect 74 | Resource: !Join [ "", [ "arn:aws:iot:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":client/happyPath-*" ] ] 75 | - Effect: Allow 76 | Action: 77 | - iot:Subscribe 78 | Resource: "*" 79 | - Effect: Allow 80 | Action: 81 | - iot:Receive 82 | Resource: !Join [ "", [ "arn:aws:iot:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":topic/*" ] ] 83 | 84 | # Create a role for authorized acces to AWS resources. 85 | CognitoAuthorizedRole: 86 | Type: "AWS::IAM::Role" 87 | Properties: 88 | AssumeRolePolicyDocument: 89 | Version: "2012-10-17" 90 | Statement: 91 | - Effect: "Allow" 92 | Principal: 93 | Federated: "cognito-identity.amazonaws.com" 94 | Action: 95 | - "sts:AssumeRoleWithWebIdentity" 96 | Condition: 97 | StringEquals: 98 | "cognito-identity.amazonaws.com:aud": !Ref IdentityPool 99 | "ForAnyValue:StringLike": 100 | "cognito-identity.amazonaws.com:amr": authenticated 101 | Policies: 102 | - PolicyName: "CognitoAuthorizedPolicy" 103 | PolicyDocument: 104 | Version: "2012-10-17" 105 | Statement: 106 | - Effect: "Allow" 107 | Action: 108 | - "cognito-sync:*" 109 | Resource: !Join [ "", [ "arn:aws:cognito-sync:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":identitypool/", !Ref IdentityPool] ] 110 | - Effect: Allow 111 | Action: 112 | - iot:Connect 113 | Resource: !Join [ "", [ "arn:aws:iot:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":client/happyPath-*" ] ] 114 | - Effect: Allow 115 | Action: 116 | - iot:Subscribe 117 | Resource: "*" 118 | - Effect: Allow 119 | Action: 120 | - iot:Receive 121 | Resource: !Join [ "", [ "arn:aws:iot:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId", ":topic/*" ] ] 122 | # Assigns the roles to the Identity Pool 123 | IdentityPoolRoleMapping: 124 | Type: "AWS::Cognito::IdentityPoolRoleAttachment" 125 | Properties: 126 | IdentityPoolId: !Ref IdentityPool 127 | Roles: 128 | authenticated: !GetAtt CognitoAuthorizedRole.Arn 129 | unauthenticated: !GetAtt CognitoUnAuthorizedRole.Arn 130 | 131 | Outputs: 132 | IotEndpoint: 133 | Value: !Ref HappyPathRealtime 134 | -------------------------------------------------------------------------------- /backend/streams/ddb/app.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 9 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 10 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 11 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 12 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 13 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | */ 15 | 16 | 'use strict' 17 | 18 | const AWS = require('aws-sdk') 19 | AWS.config.region = process.env.AWS_REGION 20 | const iotdata = new AWS.IotData({ endpoint: process.env.IOT_DATA_ENDPOINT }) 21 | 22 | // The standard Lambda handler 23 | exports.handler = async (event) => { 24 | console.log (JSON.stringify(event, null, 2)) 25 | 26 | await Promise.all( 27 | event.Records.map(async (record) => { 28 | try { 29 | if (!record.dynamodb.NewImage) return // Deletion 30 | 31 | const data = record.dynamodb.NewImage 32 | 33 | // This function only handles workflow completions 34 | if (data.PK.S === 'workflow') { 35 | if (data.objStatus.S === 'REJECTED') { 36 | console.log('Item rejection') 37 | const iotTopic = data.placeId.S 38 | data.result = 'REJECTED' 39 | await iotPublish(iotTopic, data) 40 | } 41 | } 42 | if (data.PK.S != 'workflow' && data.SK.S != 'listing') { 43 | console.log('Item processed') 44 | const iotTopic = data.PK.S 45 | data.result = 'PROCESSED' 46 | await iotPublish(iotTopic, data) 47 | } 48 | } catch (err) { 49 | console.error('Error: ', err) 50 | } 51 | }) 52 | ) 53 | } 54 | 55 | // Publishes the message to the IoT topic 56 | const iotPublish = async function (topic, message) { 57 | console.log('iotPublish: ', topic, message) 58 | try { 59 | await iotdata.publish({ 60 | topic, 61 | qos: 0, 62 | payload: JSON.stringify(message) 63 | }).promise(); 64 | console.log('iotPublish success to topic: ', topic, message) 65 | } catch (err) { 66 | console.error('iotPublish error:', err) 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /backend/streams/ddb/localTest.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 9 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 10 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 11 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 12 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 13 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | */ 15 | 16 | // Mock event 17 | const event = require('./localTestEvent') 18 | 19 | // Mock environment variables 20 | process.env.AWS_REGION = 'us-east-1' 21 | process.env.localTest = true 22 | process.env.TableName = 'aamQuestions' 23 | process.env.IOT_DATA_ENDPOINT = 'a2ty1m17b5znw2-ats.iot.us-east-1.amazonaws.com' 24 | 25 | // Lambda handler 26 | const { handler } = require('./app') 27 | 28 | const main = async () => { 29 | console.time('localTest') 30 | await handler(event) 31 | console.timeEnd('localTest') 32 | } 33 | 34 | main().catch(error => console.error(error)) 35 | -------------------------------------------------------------------------------- /backend/streams/ddb/localTestEvent.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "eventID": "902bcd5816d1a8654c4826f731ca2b33", 5 | "eventName": "MODIFY", 6 | "eventVersion": "1.1", 7 | "eventSource": "aws:dynamodb", 8 | "awsRegion": "us-east-1", 9 | "dynamodb": { 10 | "ApproximateCreationDateTime": 1593631095, 11 | "Keys": { 12 | "SK": { 13 | "S": "listing" 14 | }, 15 | "PK": { 16 | "S": "ChIJ26rBvg2_4okRqS0h3-hOhhE" 17 | } 18 | }, 19 | "NewImage": { 20 | "lng": { 21 | "N": "-70.754409" 22 | }, 23 | "created": { 24 | "N": "1593526807832" 25 | }, 26 | "SK": { 27 | "S": "listing" 28 | }, 29 | "name": { 30 | "S": "Aldrich Parkk" 31 | }, 32 | "vicinity": { 33 | "S": "371 Court Street, Portsmouth" 34 | }, 35 | "PK": { 36 | "S": "ChIJ26rBvg2_4okRqS0h3-hOhhE" 37 | }, 38 | "lat": { 39 | "N": "43.0766217" 40 | } 41 | }, 42 | "OldImage": { 43 | "lng": { 44 | "N": "-70.754409" 45 | }, 46 | "created": { 47 | "N": "1593526807832" 48 | }, 49 | "SK": { 50 | "S": "listing" 51 | }, 52 | "name": { 53 | "S": "Aldrich Park" 54 | }, 55 | "vicinity": { 56 | "S": "371 Court Street, Portsmouth" 57 | }, 58 | "PK": { 59 | "S": "ChIJ26rBvg2_4okRqS0h3-hOhhE" 60 | }, 61 | "lat": { 62 | "N": "43.0766217" 63 | } 64 | }, 65 | "SequenceNumber": "10440400000000018778526598", 66 | "SizeBytes": 285, 67 | "StreamViewType": "NEW_AND_OLD_IMAGES" 68 | }, 69 | "eventSourceARN": "arn:aws:dynamodb:us-east-1:123456789012:table/hp-application/stream/2020-06-29T14:29:10.716" 70 | } 71 | ] 72 | } -------------------------------------------------------------------------------- /backend/streams/ddb/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "process-answers-stream", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "James Beswick", 11 | "license": "MIT-0", 12 | "dependencies": { 13 | "aws-sdk": "^2.663.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /backend/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: Happy Path - Example application 4 | 5 | Parameters: 6 | ApplicationTableName: 7 | Type: String 8 | Description: Name of application's DynamoDB table 9 | Default: hp-application 10 | 11 | IoTDataEndpoint: 12 | Type: String 13 | Description: The IoT data endpoint for the application. 14 | Default: a2ty1m17b5znw2-ats.iot.us-east-1.amazonaws.com 15 | 16 | Auth0issuer: 17 | Type: String 18 | Description: The issuer URL from your Auth0 account. 19 | Default: https://dev-y6clwyi3.us.auth0.com/ 20 | 21 | S3UploadBucketName: 22 | Type: String 23 | Description: The name for the S3 upload bucket. 24 | Default: 'happy-path-upload' 25 | 26 | S3DistributionBucketName: 27 | Type: String 28 | Description: The name for the S3 upload bucket. 29 | Default: 'happy-path-distribution' 30 | 31 | Globals: 32 | Function: 33 | Timeout: 5 34 | Runtime: nodejs12.x 35 | Tags: 36 | Application: HappyPath 37 | 38 | Resources: 39 | # HTTP API 40 | MyApi: 41 | Type: AWS::Serverless::HttpApi 42 | Properties: 43 | # HTTP API authorizer - see https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-httpapi-httpapiauth.html 44 | Auth: 45 | Authorizers: 46 | MyAuthorizer: 47 | JwtConfiguration: 48 | issuer: !Ref Auth0issuer 49 | audience: 50 | - https://auth0-jwt-authorizer 51 | IdentitySource: "$request.header.Authorization" 52 | DefaultAuthorizer: MyAuthorizer 53 | 54 | # CORS configuration - this is open for development only and should be restricted in prod. 55 | # See https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-httpapi-httpapicorsconfiguration.html 56 | CorsConfiguration: 57 | AllowMethods: 58 | - GET 59 | - POST 60 | - DELETE 61 | - OPTIONS 62 | AllowHeaders: 63 | - "*" 64 | AllowOrigins: 65 | - "*" 66 | 67 | ## Lambda functions 68 | UploadRequestFunction: 69 | # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction 70 | Type: AWS::Serverless::Function 71 | Properties: 72 | CodeUri: upload/ 73 | Handler: request.handler 74 | Runtime: nodejs12.x 75 | MemorySize: 128 76 | Environment: 77 | Variables: 78 | AWS_NODEJS_CONNECTION_REUSE_ENABLED: 1 79 | UploadBucket: !Ref S3UploadBucketName 80 | TableName: !Ref ApplicationTableName 81 | Policies: 82 | - S3CrudPolicy: 83 | BucketName: !Ref S3UploadBucketName 84 | - DynamoDBWritePolicy: 85 | TableName: !Ref ApplicationTableName 86 | Events: 87 | UploadAssetAPI: 88 | Type: HttpApi 89 | Properties: 90 | Path: /images 91 | Method: get 92 | ApiId: !Ref MyApi 93 | 94 | UploadCompleteFunction: 95 | Type: AWS::Serverless::Function 96 | Properties: 97 | CodeUri: upload/ 98 | Handler: complete.handler 99 | MemorySize: 128 100 | Policies: 101 | - Statement: 102 | - Effect: Allow 103 | Resource: '*' 104 | Action: 105 | - events:PutEvents 106 | Events: 107 | FileUpload: 108 | Type: S3 109 | Properties: 110 | Bucket: !Ref S3UploadBucket 111 | Events: s3:ObjectCreated:* 112 | 113 | PostLocationFunction: 114 | Type: AWS::Serverless::Function 115 | Properties: 116 | CodeUri: api/locations/ 117 | Handler: post.handler 118 | MemorySize: 128 119 | Description: Post new location to database 120 | Policies: 121 | - DynamoDBWritePolicy: 122 | TableName: !Ref ApplicationTableName 123 | Environment: 124 | Variables: 125 | AWS_NODEJS_CONNECTION_REUSE_ENABLED: 1 126 | TableName: !Ref ApplicationTableName 127 | Events: 128 | GetLocations: 129 | Type: HttpApi 130 | Properties: 131 | Path: /locations 132 | Method: post 133 | ApiId: !Ref MyApi 134 | #If you have a default authorizer but want to leave one route open, you can override that here. 135 | #Learn more at https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-httpapifunctionauth.html 136 | Auth: 137 | Authorizer: NONE 138 | 139 | GetLocationsFunction: 140 | Type: AWS::Serverless::Function 141 | Properties: 142 | CodeUri: api/locations/ 143 | Handler: get.handler 144 | MemorySize: 128 145 | Description: Returns list of locations from lat/lng query params 146 | Policies: 147 | - DynamoDBReadPolicy: 148 | TableName: !Ref ApplicationTableName 149 | Environment: 150 | Variables: 151 | AWS_NODEJS_CONNECTION_REUSE_ENABLED: 1 152 | TableName: !Ref ApplicationTableName 153 | Events: 154 | GetLocations: 155 | Type: HttpApi 156 | Properties: 157 | Path: /locations 158 | Method: get 159 | ApiId: !Ref MyApi 160 | #If you have a default authorizer but want to leave one route open, you can override that here. 161 | #Learn more at https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-httpapifunctionauth.html 162 | Auth: 163 | Authorizer: NONE 164 | 165 | ## DDB stream processor 166 | ProcessDynamoDBStream: 167 | Type: AWS::Serverless::Function 168 | Properties: 169 | CodeUri: streams/ddb/ 170 | Handler: app.handler 171 | Description: Updates IoT topics with changes to Questions DDB table 172 | ReservedConcurrentExecutions: 1 173 | Environment: 174 | Variables: 175 | IOT_DATA_ENDPOINT: !Ref IoTDataEndpoint 176 | Policies: 177 | - DynamoDBReadPolicy: 178 | TableName: !Ref ApplicationTableName 179 | - Statement: 180 | - Effect: "Allow" 181 | Action: 182 | - "iot:*" 183 | Resource: "*" 184 | Events: 185 | Stream: 186 | Type: DynamoDB 187 | Properties: 188 | Stream: !GetAtt ApplicationTable.StreamArn 189 | BatchSize: 100 190 | StartingPosition: TRIM_HORIZON 191 | 192 | ## DynamoDB table 193 | ApplicationTable: 194 | Type: AWS::DynamoDB::Table 195 | Properties: 196 | TableName: !Ref ApplicationTableName 197 | AttributeDefinitions: 198 | - AttributeName: PK 199 | AttributeType: S 200 | - AttributeName: SK 201 | AttributeType: S 202 | KeySchema: 203 | - AttributeName: PK 204 | KeyType: HASH 205 | - AttributeName: SK 206 | KeyType: RANGE 207 | BillingMode: PAY_PER_REQUEST 208 | StreamSpecification: 209 | StreamViewType: NEW_AND_OLD_IMAGES 210 | 211 | ## S3 buckets 212 | S3UploadBucket: 213 | Type: AWS::S3::Bucket 214 | Properties: 215 | BucketName: !Ref S3UploadBucketName 216 | CorsConfiguration: 217 | CorsRules: 218 | - AllowedHeaders: 219 | - "*" 220 | AllowedMethods: 221 | - GET 222 | - PUT 223 | - POST 224 | - DELETE 225 | - HEAD 226 | AllowedOrigins: 227 | - "*" 228 | 229 | S3DistributionBucket: 230 | Type: AWS::S3::Bucket 231 | Properties: 232 | BucketName: !Ref S3DistributionBucketName 233 | 234 | # S3BucketPolicy: 235 | # Type: AWS::S3::BucketPolicy 236 | # Properties: 237 | # BucketName: !Ref S3DistributionBucketName 238 | # Policies: 239 | # - Statement: 240 | # - Effect: Allow 241 | # Action: 's3:GetObject' 242 | # Resource: 243 | # - !Sub "arn:aws:s3:::${S3DistributionBucketName}/*" 244 | # Principal: 245 | # AWS: !Sub "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${CloudFrontOriginAccessIdentity}" 246 | 247 | ## CloudFront distribution 248 | CloudFrontOriginAccessIdentity: 249 | Type: AWS::CloudFront::CloudFrontOriginAccessIdentity 250 | Properties: 251 | CloudFrontOriginAccessIdentityConfig: 252 | Comment: 'Happy Path CloudFront distribution' 253 | 254 | CloudFrontDistribution: 255 | Type: AWS::CloudFront::Distribution 256 | Properties: 257 | DistributionConfig: 258 | Comment: "CloudFront distribution for Happy Path assets" 259 | Enabled: true 260 | HttpVersion: http2 261 | # List of origins for CloudFront distribution 262 | Origins: 263 | - Id: happy-path-distribution-s3-bucket 264 | DomainName: !GetAtt S3DistributionBucket.DomainName 265 | S3OriginConfig: 266 | # Restrict bucket access using an origin access identity 267 | OriginAccessIdentity: 268 | Fn::Sub: 'origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}' 269 | DefaultCacheBehavior: 270 | Compress: true 271 | AllowedMethods: 272 | - GET 273 | - HEAD 274 | ForwardedValues: 275 | QueryString: false 276 | TargetOriginId: happy-path-distribution-s3-bucket 277 | ViewerProtocolPolicy : redirect-to-https 278 | 279 | ## Take a note of the outputs for deploying the workflow templates in this sample application 280 | Outputs: 281 | APIendpoint: 282 | Description: HTTP API endpoint URL. 283 | Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com" 284 | DynamoDBstreamARN: 285 | Description: Stream ARN used for workflows. 286 | Value: !GetAtt ApplicationTable.StreamArn 287 | CloudFrontEndpoint: 288 | Description: Endpoint for CloudFront distribution. 289 | Value: !Ref 'CloudFrontDistribution' 290 | -------------------------------------------------------------------------------- /backend/upload/complete.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 9 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 10 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 11 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 12 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 13 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | */ 15 | 16 | const AWS = require('aws-sdk') 17 | AWS.config.region = process.env.AWS_REGION 18 | const eventbridge = new AWS.EventBridge() 19 | 20 | // The standard Lambda handler 21 | exports.handler = async (event) => { 22 | console.log(JSON.stringify(event, null, 2)) 23 | 24 | // Handle each record in the event 25 | await Promise.all( 26 | event.Records.map(async (event) => { 27 | try { 28 | const key = event.s3.object.key.split('/')[1] 29 | await eventbridge.putEvents({ 30 | Entries: [{ 31 | // Event envelope fields 32 | Source: 'custom.happyPath', 33 | EventBusName: 'default', 34 | DetailType: 'uploadComplete', 35 | Time: new Date(), 36 | 37 | // Main event body 38 | Detail: JSON.stringify({ 39 | bucket: event.s3.bucket.name, 40 | key: event.s3.object.key, 41 | 42 | // Parse place ID and workflow ID from event 43 | placeId: event.s3.object.key.split('/')[0], 44 | ID: key.split('.')[0] 45 | }) 46 | }] 47 | }).promise() 48 | } catch (err) { 49 | console.error(`Handler error: ${err}`) 50 | } 51 | }) 52 | ) 53 | } -------------------------------------------------------------------------------- /backend/upload/local/localTest.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 9 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 10 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 11 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 12 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 13 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | */ 15 | 16 | // Mock event 17 | const event = require('./localTestEvent') 18 | 19 | // Mock environment variables 20 | process.env.AWS_REGION = 'us-east-1' 21 | process.env.localTest = true 22 | process.env.TableName = 'hp-application' 23 | process.env.UploadBucket = 'process.env.UploadBucket' 24 | 25 | // Lambda handler 26 | const { handler } = require('../complete') 27 | 28 | const main = async () => { 29 | console.time('localTest') 30 | console.dir(await handler(event)) 31 | console.timeEnd('localTest') 32 | } 33 | 34 | main().catch(error => console.error(error)) -------------------------------------------------------------------------------- /backend/upload/local/localTestEvent.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "eventVersion": "2.1", 5 | "eventSource": "aws:s3", 6 | "awsRegion": "us-east-1", 7 | "eventTime": "2020-07-08T17:24:42.909Z", 8 | "eventName": "ObjectCreated:Put", 9 | "userIdentity": { 10 | "principalId": "AWS:AROA3DTKMGNKP7OCJBACT:happypath-backend-UploadRequestFunction-S5KLA32CXWFB" 11 | }, 12 | "requestParameters": { 13 | "sourceIPAddress": "72.21.196.68" 14 | }, 15 | "responseElements": { 16 | "x-amz-request-id": "8Y8N4Z8N4M8H4H5W", 17 | "x-amz-id-2": "RZ0tHxk/wawNBiyVBc71ia1pX4HBL0uQWuAndjRAd2pt17M38poXj42VGXzmkmc6dUD6faP6brC1HXg7IK8V9biZEJsfrjfi" 18 | }, 19 | "s3": { 20 | "s3SchemaVersion": "1.0", 21 | "configurationId": "2e28f59c-8532-42fa-a6f0-9dff1f308286", 22 | "bucket": { 23 | "name": "happy-path-upload", 24 | "ownerIdentity": { 25 | "principalId": "ASRUZ4ZWI5GFR" 26 | }, 27 | "arn": "arn:aws:s3:::happy-path-upload" 28 | }, 29 | "object": { 30 | "key": "ChIJn6yiduG6j4ARBP9ci2gGMws/e6233bf8-fc82-490c-8f24-5b4390e3ae59.jpg", 31 | "size": 302458, 32 | "eTag": "716ff2cb7efd112f606d02746288c788", 33 | "sequencer": "005F060162357DD438" 34 | } 35 | } 36 | } 37 | ] 38 | } -------------------------------------------------------------------------------- /backend/upload/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "upload-process", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "James Beswick", 11 | "license": "MIT-0", 12 | "dependencies": { 13 | "aws-sdk": "^2.708.0", 14 | "uuid": "^8.2.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /backend/upload/request.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 9 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 10 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 11 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 12 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 13 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | */ 15 | 16 | 'use strict' 17 | 18 | const { v4: uuidv4 } = require('uuid') 19 | const AWS = require('aws-sdk') 20 | AWS.config.update({ region: process.env.AWS_REGION }) 21 | const documentClient = new AWS.DynamoDB.DocumentClient() 22 | const s3 = new AWS.S3() 23 | 24 | const URL_EXPIRATION_SECONDS = 60 25 | 26 | // Main Lambda entry point 27 | exports.handler = async (event) => { 28 | const result = await getUploadURL(event) 29 | console.log('Result: ', result) 30 | return result 31 | } 32 | 33 | const getUserId = (event) => { 34 | try { 35 | return event.requestContext.authorizer.jwt.claims.sub 36 | } catch (err) { 37 | return 'test-user' 38 | } 39 | } 40 | 41 | const getUploadURL = async function(event) { 42 | console.log(event) 43 | const actionId = uuidv4() 44 | 45 | if (!event.queryStringParameters) { 46 | return { 47 | statusCode: 422, 48 | body: JSON.stringify("Missing parameters") 49 | } 50 | } 51 | 52 | const placeId = event.queryStringParameters.placeId 53 | const key = `${actionId}.jpg` 54 | const userId = getUserId(event) 55 | 56 | // Add to DynamoDB table 57 | const result = await documentClient.put({ 58 | TableName: process.env.TableName, 59 | Item: { 60 | PK: 'workflow', 61 | SK: actionId, 62 | placeId, 63 | userId, 64 | objStatus: 'PENDING_UPLOAD', 65 | created: Date.now() 66 | } 67 | }).promise() 68 | console.log(result) 69 | 70 | // Get signed URL from S3 71 | 72 | const s3Params = { 73 | Bucket: process.env.UploadBucket, 74 | Key: `${placeId}/${actionId}.jpg`, 75 | Expires: URL_EXPIRATION_SECONDS, 76 | ContentType: 'image/jpeg' 77 | } 78 | 79 | console.log('Params: ', s3Params) 80 | const uploadURL = await s3.getSignedUrlPromise('putObject', s3Params) 81 | 82 | return JSON.stringify({ 83 | "uploadURL": uploadURL, 84 | "photoFilename": `${actionId}.jpg` 85 | }) 86 | } -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # happy-path 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /frontend/auth_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "<< ENTER YOUR ID>>", 3 | "clientId": "<< ENTER YOUR ID>>", 4 | "audience": "https://auth0-jwt-authorizer" 5 | } -------------------------------------------------------------------------------- /frontend/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 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 | "@auth0/auth0-spa-js": "^1.13.5", 12 | "aws-iot-device-sdk": "^2.2.6", 13 | "aws-sdk": "^2.823.0", 14 | "axios": "^1.6.0", 15 | "core-js": "^3.8.2", 16 | "gmaps-heatmap": "^1.0.2", 17 | "moment": "^2.29.1", 18 | "ngeohash": "^0.6.3", 19 | "nodes2ts": "^2.0.0", 20 | "vue": "^2.6.12", 21 | "vue-router": "^3.4.9", 22 | "vue2-gmap-custom-marker": "^6.0.0", 23 | "vue2-google-maps": "^0.10.7", 24 | "vuetify": "^2.4.2", 25 | "vuex": "^3.6.0" 26 | }, 27 | "devDependencies": { 28 | "@vue/cli-plugin-babel": "~4.5.10", 29 | "@vue/cli-plugin-eslint": "~4.5.10", 30 | "@vue/cli-plugin-router": "^4.5.10", 31 | "@vue/cli-service": "~4.5.10", 32 | "babel-eslint": "^10.1.0", 33 | "eslint": "^7.17.0", 34 | "eslint-plugin-vue": "^7.4.1", 35 | "sass": "^1.32.1", 36 | "sass-loader": "^10.1.0", 37 | "vue-cli-plugin-vuetify": "~2.0.9", 38 | "vue-template-compiler": "^2.6.12", 39 | "vuetify-loader": "^1.6.0" 40 | }, 41 | "eslintConfig": { 42 | "root": true, 43 | "env": { 44 | "node": true 45 | }, 46 | "extends": [ 47 | "plugin:vue/essential", 48 | "eslint:recommended" 49 | ], 50 | "parserOptions": { 51 | "parser": "babel-eslint" 52 | }, 53 | "rules": {} 54 | }, 55 | "browserslist": [ 56 | "> 1%", 57 | "last 2 versions", 58 | "not dead" 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/happy-path/368ab10506c9834b8056a4973a461134d982d764/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Happy Path | Share park maps, save paper 9 | 10 | 11 | 12 | 13 | 14 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 85 | -------------------------------------------------------------------------------- /frontend/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/happy-path/368ab10506c9834b8056a4973a461134d982d764/frontend/src/assets/logo.png -------------------------------------------------------------------------------- /frontend/src/auth/index.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | // See Auth0 tutorials for creating this code 4 | // https://auth0.com/docs/quickstart/spa/vuejs/01-login#configure-logout-urls 5 | 6 | 7 | import Vue from "vue" 8 | import createAuth0Client from "@auth0/auth0-spa-js" 9 | import { bus } from '../main' 10 | 11 | /** Define a default action to perform after authentication */ 12 | const DEFAULT_REDIRECT_CALLBACK = () => 13 | window.history.replaceState({}, document.title, window.location.pathname); 14 | 15 | let instance; 16 | 17 | /** Returns the current instance of the SDK */ 18 | export const getInstance = () => instance; 19 | 20 | /** Creates an instance of the Auth0 SDK. If one has already been created, it returns that instance */ 21 | export const useAuth0 = ({ 22 | onRedirectCallback = DEFAULT_REDIRECT_CALLBACK, 23 | redirectUri = window.location.origin, 24 | ...options 25 | }) => { 26 | if (instance) return instance; 27 | 28 | // The 'instance' is simply a Vue object 29 | instance = new Vue({ 30 | data() { 31 | return { 32 | loading: true, 33 | isAuthenticated: false, 34 | user: {}, 35 | auth0Client: null, 36 | popupOpen: false, 37 | error: null 38 | }; 39 | }, 40 | methods: { 41 | /** Authenticates the user using a popup window */ 42 | async loginWithPopup(o) { 43 | this.popupOpen = true; 44 | 45 | try { 46 | await this.auth0Client.loginWithPopup(o); 47 | } catch (e) { 48 | // eslint-disable-next-line 49 | console.error(e); 50 | } finally { 51 | this.popupOpen = false; 52 | } 53 | 54 | this.user = await this.auth0Client.getUser(); 55 | this.isAuthenticated = true; 56 | }, 57 | /** Handles the callback when logging in using a redirect */ 58 | async handleRedirectCallback() { 59 | this.loading = true; 60 | try { 61 | await this.auth0Client.handleRedirectCallback(); 62 | this.user = await this.auth0Client.getUser(); 63 | this.isAuthenticated = true; 64 | bus.$emit('authenticated', this.user) 65 | } catch (e) { 66 | this.error = e; 67 | } finally { 68 | bus.$emit('loaded', true) 69 | this.loading = false; 70 | } 71 | }, 72 | /** Authenticates the user using the redirect method */ 73 | loginWithRedirect(o) { 74 | return this.auth0Client.loginWithRedirect(o); 75 | }, 76 | /** Returns all the claims present in the ID token */ 77 | getIdTokenClaims(o) { 78 | return this.auth0Client.getIdTokenClaims(o); 79 | }, 80 | /** Returns the access token. If the token is invalid or missing, a new one is retrieved */ 81 | getTokenSilently(o) { 82 | return this.auth0Client.getTokenSilently(o); 83 | }, 84 | /** Gets the access token using a popup window */ 85 | 86 | getTokenWithPopup(o) { 87 | return this.auth0Client.getTokenWithPopup(o); 88 | }, 89 | /** Logs the user out and removes their session on the authorization server */ 90 | logout(o) { 91 | return this.auth0Client.logout(o); 92 | } 93 | }, 94 | /** Use this lifecycle method to instantiate the SDK client */ 95 | async created() { 96 | // Create a new instance of the SDK client using members of the given options object 97 | this.auth0Client = await createAuth0Client({ 98 | domain: options.domain, 99 | client_id: options.clientId, 100 | audience: options.audience, 101 | redirect_uri: redirectUri 102 | }); 103 | 104 | try { 105 | // If the user is returning to the app after authentication.. 106 | if ( 107 | window.location.search.includes("code=") && 108 | window.location.search.includes("state=") 109 | ) { 110 | // handle the redirect and retrieve tokens 111 | const { appState } = await this.auth0Client.handleRedirectCallback(); 112 | 113 | // Notify subscribers that the redirect callback has happened, passing the appState 114 | // (useful for retrieving any pre-authentication state) 115 | onRedirectCallback(appState); 116 | } 117 | } catch (e) { 118 | this.error = e; 119 | } finally { 120 | // Initialize our internal authentication state 121 | this.isAuthenticated = await this.auth0Client.isAuthenticated(); 122 | this.user = await this.auth0Client.getUser(); 123 | this.loading = false; 124 | bus.$emit('authenticated', this.user) 125 | bus.$emit('loaded', true) 126 | } 127 | } 128 | }); 129 | 130 | return instance; 131 | }; 132 | 133 | // Create a simple Vue plugin to expose the wrapper object throughout the application 134 | export const Auth0Plugin = { 135 | install(Vue, options) { 136 | Vue.prototype.$auth = useAuth0(options); 137 | } 138 | }; -------------------------------------------------------------------------------- /frontend/src/components/IoT.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 140 | -------------------------------------------------------------------------------- /frontend/src/components/Snackbar.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 9 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 10 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 11 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 12 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 13 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | */ 15 | 16 | 'use strict' 17 | import Vue from 'vue' 18 | import * as VueGoogleMaps from 'vue2-google-maps' 19 | 20 | import App from './App.vue' 21 | import vuetify from './plugins/vuetify' 22 | import router from './router' 23 | import store from './store' 24 | import IoT from '@/components/IoT' 25 | 26 | import moment from 'moment' 27 | Vue.prototype.moment = moment 28 | 29 | // Import the Auth0 configuration 30 | import { domain, clientId, audience } from "../auth_config.json" 31 | 32 | // Import the plugin here 33 | import { Auth0Plugin } from "./auth" 34 | 35 | // Realtime websocket notifications 36 | Vue.component('iot', IoT) 37 | 38 | /* =================================================== 39 | CONFIGURATION 40 | You must add your own values here! See the tutorial 41 | in the GitHub repo for more information. @jbesw 42 | =================================================== */ 43 | 44 | Vue.config.productionTip = false 45 | Vue.prototype.$appName = 'Happy Path' 46 | // Google Maps key - see https://developers.google.com/maps/documentation/javascript/get-api-key 47 | Vue.prototype.$GoogleMapsKey = '<< ENTER YOUR ID>>' 48 | 49 | // ** Websocket connection ** 50 | // PoolId: Retrieve this with the CLI command: aws cognito-identity list-identity-pools --max-results 10 51 | Vue.prototype.$poolId = '<< ENTER YOUR ID>>', // 'YourCognitoIdentityPoolId' 52 | // IoTendpoint: Retrieve this with the CLI command: aws iot describe-endpoint --endpoint-type iot:Data-ATS 53 | Vue.prototype.$host = '<< ENTER YOUR ID>>', // 'YourAwsIoTEndpoint', e.g. 'prefix.iot.us-east-1.amazonaws.com' 54 | 55 | // ** Backend config ** 56 | // API Gateway endpoint - e.g. https://abc123abc.execute-api.us-east-1.amazonaws.com 57 | Vue.prototype.$APIurl = '<< ENTER YOUR ID>>' 58 | // This is the region you selected in the SAM template deployment. 59 | Vue.prototype.$region = '<< ENTER YOUR REGION>>' // Your region 60 | 61 | // ** Business mode ** 62 | // This determines which business types appear in the map. 63 | Vue.prototype.$businessType = 'park' 64 | 65 | 66 | /* =================================================== 67 | END CONFIGURATION 68 | =================================================== */ 69 | 70 | Vue.use(VueGoogleMaps, { 71 | load: { 72 | key: Vue.prototype.$GoogleMapsKey, 73 | libraries: 'places', 74 | }, 75 | installComponents: true 76 | }) 77 | 78 | // Install the authentication plugin here 79 | Vue.use(Auth0Plugin, { 80 | domain, 81 | clientId, 82 | audience, 83 | onRedirectCallback: appState => { 84 | router.push( 85 | appState && appState.targetUrl 86 | ? appState.targetUrl 87 | : window.location.pathname 88 | ) 89 | } 90 | }) 91 | 92 | export const bus = new Vue() 93 | 94 | new Vue({ 95 | vuetify, 96 | router, 97 | store, 98 | render: h => h(App) 99 | }).$mount('#app') 100 | -------------------------------------------------------------------------------- /frontend/src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 9 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 10 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 11 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 12 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 13 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | */ 15 | 16 | 'use strict' 17 | 18 | import Vue from 'vue' 19 | import Vuetify from 'vuetify/lib' 20 | 21 | Vue.use(Vuetify) 22 | 23 | export default new Vuetify({ 24 | }) 25 | -------------------------------------------------------------------------------- /frontend/src/router/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 9 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 10 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 11 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 12 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 13 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | */ 15 | 16 | 'use strict' 17 | 18 | 19 | import Vue from 'vue' 20 | import VueRouter from 'vue-router' 21 | 22 | import HomeView from '@/views/HomeView.vue' 23 | import PlaceDetail from '@/views/PlaceDetail.vue' 24 | 25 | Vue.use(VueRouter) 26 | 27 | const routes = [ 28 | { 29 | path: '/', 30 | name: 'HomeView', 31 | component: HomeView 32 | }, 33 | { 34 | path: '/place', 35 | name: 'PlaceDetail', 36 | component: PlaceDetail 37 | } 38 | ] 39 | 40 | const router = new VueRouter({ 41 | mode: 'history', 42 | base: '/', 43 | routes 44 | }) 45 | 46 | export default router 47 | -------------------------------------------------------------------------------- /frontend/src/store/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 9 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 10 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 11 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 12 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 13 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | */ 15 | 16 | 'use strict' 17 | 18 | import Vue from 'vue' 19 | import Vuex from 'vuex' 20 | 21 | Vue.use(Vuex) 22 | 23 | // initial state 24 | const state = { 25 | initialized: false, 26 | position: { 27 | latitude: '-70.8238872', 28 | longitude: '43.0385959' 29 | }, 30 | user: {}, 31 | places: {} 32 | } 33 | 34 | // getters 35 | const getters = { 36 | isInitialized: (state) => state.initialized, 37 | getPosition: (state) => state.position, 38 | getUser: (state) => state.user, 39 | places: (state) => state.places, 40 | } 41 | 42 | //mutations 43 | const mutations = { 44 | setPosition(state, position) { 45 | state.position.latitude = position.coords.latitude 46 | state.position.longitude = position.coords.longitude; 47 | }, 48 | savePlace(state, data) { 49 | console.log('store::savePlace:', data) 50 | 51 | // Add main listing info 52 | let listing = data.find (el => el.SK === 'listing') 53 | console.log('Store adding listing: ', listing) 54 | state.places[listing.PK] = {} 55 | state.places[listing.PK].listing = listing 56 | 57 | // Add assets 58 | let assets = data.filter (el => el.SK !== 'listing') 59 | state.places[listing.PK].assets = assets 60 | 61 | console.log('Store places: ', state.places) 62 | }, 63 | saveAsset(state, data) { 64 | console.log('store::saveAsset: ', data) 65 | const asset = { 66 | PK: data.asset.PK.S, 67 | SK: data.asset.SK.S, 68 | created: data.asset.created.N, 69 | scaledURL: data.asset.scaledURL.S, 70 | tileURL: data.asset.tileURL.S, 71 | type: data.asset.type.S 72 | } 73 | console.log('Adding: ', asset) 74 | state.places[data.placeId].assets.push(asset) 75 | }, 76 | setUser(state, user) { 77 | state.user = user 78 | }, 79 | setInitialized(state, val) { 80 | console.log('setInitalized') 81 | state.initialized = val 82 | } 83 | } 84 | 85 | export default new Vuex.Store({ 86 | // strict: true, 87 | state, 88 | getters, 89 | mutations 90 | }) 91 | -------------------------------------------------------------------------------- /frontend/src/views/HomeView.vue: -------------------------------------------------------------------------------- 1 | 113 | 114 | 289 | -------------------------------------------------------------------------------- /frontend/src/views/PlaceDetail.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 164 | -------------------------------------------------------------------------------- /frontend/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "devServer": { 3 | "host": 'localhost' 4 | }, 5 | "transpileDependencies": [ 6 | "vuetify" 7 | ] 8 | } -------------------------------------------------------------------------------- /localTesting/events/ddbevent-processed.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "eventID": "54d6644c1eacf43a5998aada92fcc4f2", 5 | "eventName": "INSERT", 6 | "eventVersion": "1.1", 7 | "eventSource": "aws:dynamodb", 8 | "awsRegion": "us-east-1", 9 | "dynamodb": { 10 | "ApproximateCreationDateTime": 1594400638, 11 | "Keys": { 12 | "SK": { 13 | "S": "ba5177b7-68b4-4138-967a-20ca21116fdd" 14 | }, 15 | "PK": { 16 | "S": "ChIJM7V6okI_tokRDNVDA6Wvrcg" 17 | } 18 | }, 19 | "NewImage": { 20 | "tileURL": { 21 | "S": "https://happy-path-distribution.s3.amazonaws.com/ChIJM7V6okI_tokRDNVDA6Wvrcg/ba5177b7-68b4-4138-967a-20ca21116fdd-tile.jpg" 22 | }, 23 | "created": { 24 | "N": "1594400638296" 25 | }, 26 | "SK": { 27 | "S": "ba5177b7-68b4-4138-967a-20ca21116fdd" 28 | }, 29 | "PK": { 30 | "S": "ChIJM7V6okI_tokRDNVDA6Wvrcg" 31 | }, 32 | "type": { 33 | "S": "jpg" 34 | }, 35 | "scaledURL": { 36 | "S": "https://happy-path-distribution.s3.amazonaws.com/ChIJM7V6okI_tokRDNVDA6Wvrcg/ba5177b7-68b4-4138-967a-20ca21116fdd-scaled.jpg" 37 | } 38 | }, 39 | "SequenceNumber": "52847300000000049943649595", 40 | "SizeBytes": 418, 41 | "StreamViewType": "NEW_AND_OLD_IMAGES" 42 | }, 43 | "eventSourceARN": "arn:aws:dynamodb:us-east-1:123456789012:table/hp-application/stream/2020-06-29T14:29:10.716" 44 | } 45 | ] 46 | } -------------------------------------------------------------------------------- /localTesting/events/ddbevent-rejection.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "eventID": "41e8ff1e298f95da31dd53b4217ce487", 5 | "eventName": "MODIFY", 6 | "eventVersion": "1.1", 7 | "eventSource": "aws:dynamodb", 8 | "awsRegion": "us-east-1", 9 | "dynamodb": { 10 | "ApproximateCreationDateTime": 1594400494, 11 | "Keys": { 12 | "SK": { 13 | "S": "b90f8805-79ae-47d3-845d-7660d57f8149" 14 | }, 15 | "PK": { 16 | "S": "workflow" 17 | } 18 | }, 19 | "NewImage": { 20 | "created": { 21 | "N": "1594400489040" 22 | }, 23 | "SK": { 24 | "S": "b90f8805-79ae-47d3-845d-7660d57f8149" 25 | }, 26 | "placeId": { 27 | "S": "ChIJAXebfB8_tokRPH3f9okhDMg" 28 | }, 29 | "PK": { 30 | "S": "workflow" 31 | }, 32 | "detail": { 33 | "S": "{\"reason\":\"FAILED_MODERATION\",\"reasonDetail\":\"[{\\\"Confidence\\\":96.25624084472656,\\\"Name\\\":\\\"Female Swimwear Or Underwear\\\",\\\"ParentName\\\":\\\"Suggestive\\\"},{\\\"Confidence\\\":96.25624084472656,\\\"Name\\\":\\\"Suggestive\\\",\\\"ParentName\\\":\\\"\\\"}]\"}" 34 | }, 35 | "userId": { 36 | "S": "test-user" 37 | }, 38 | "objStatus": { 39 | "S": "REJECTED" 40 | } 41 | }, 42 | "OldImage": { 43 | "created": { 44 | "N": "1594400489040" 45 | }, 46 | "SK": { 47 | "S": "b90f8805-79ae-47d3-845d-7660d57f8149" 48 | }, 49 | "placeId": { 50 | "S": "ChIJAXebfB8_tokRPH3f9okhDMg" 51 | }, 52 | "PK": { 53 | "S": "workflow" 54 | }, 55 | "userId": { 56 | "S": "test-user" 57 | }, 58 | "objStatus": { 59 | "S": "PENDING_UPLOAD" 60 | } 61 | }, 62 | "SequenceNumber": "52856500000000027614296485", 63 | "SizeBytes": 553, 64 | "StreamViewType": "NEW_AND_OLD_IMAGES" 65 | }, 66 | "eventSourceARN": "arn:aws:dynamodb:us-east-1:123456789012:table/hp-application/stream/2020-06-29T14:29:10.716" 67 | } 68 | ] 69 | } -------------------------------------------------------------------------------- /localTesting/events/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0", 3 | "id": "4d243389-1234-1234-1234-459ef3d88e93", 4 | "detail-type": "uploadComplete", 5 | "source": "custom.happyPath", 6 | "account": "123456789012", 7 | "time": "2020-07-03T15:05:00Z", 8 | "region": "us-east-1", 9 | "resources": [], 10 | "detail": { 11 | "bucket": "happy-path-upload", 12 | "key": "ChIJx849xvC5j4AR8tx7wdHJb4Y/57e2f096-1234-1234-9581-55ef79c6d1bd.jpg" 13 | } 14 | } -------------------------------------------------------------------------------- /localTesting/test.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 9 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 10 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 11 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 12 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 13 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | */ 15 | 16 | // Mock event 17 | const event = require('./events/ddbevent-processed') 18 | 19 | // Mock environment variables 20 | process.env.AWS_REGION = 'us-east-1' 21 | process.env.localTest = true 22 | process.env.TableName = 'hp-application' 23 | process.env.OutputBucket = 'happy-path-distribution' 24 | process.env.minPixels = 800 25 | process.env.allowedTypes = 'jpg,jepg' 26 | process.env.IOT_DATA_ENDPOINT = 'au33be31eziux-ats.iot.us-east-1.amazonaws.com' 27 | 28 | // Lambda handler 29 | //const { handler } = require('../workflows/functions/analyze/people/app') 30 | const { handler } = require('../backend/streams/ddb/app') 31 | 32 | const main = async () => { 33 | console.time('localTest') 34 | console.dir(await handler(event)) 35 | console.timeEnd('localTest') 36 | } 37 | 38 | main().catch(error => console.error(error)) -------------------------------------------------------------------------------- /workflows/functions/analyze/dimensions/app.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 9 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 10 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 11 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 12 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 13 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | */ 15 | 16 | /* Expected event format: 17 | { 18 | "version": "0", 19 | "id": "4d243389-1234-1234-1234-459ef3d88e93", 20 | "detail-type": "uploadComplete", 21 | "source": "custom.happyPath", 22 | "account": "123456789012", 23 | "time": "2020-07-08T17:35:22Z", 24 | "region": "us-east-1", 25 | "resources": [], 26 | "detail": { 27 | "bucket": "happy-path-upload", 28 | "key": "4b02d0c3-1234-1234-1234-36af82130d72.jpg", 29 | "placeId": "ChIJxQx-bXa_4okR_RRYZ1b_qhE", 30 | "ID": "4b02d0c3-1234-1234-1234-36af82130d72" 31 | } 32 | } 33 | */ 34 | 'use strict' 35 | 36 | const AWS = require('aws-sdk') 37 | AWS.config.update({region: process.env.AWS_REGION}) 38 | const s3 = new AWS.S3() 39 | const sizeOf = require('image-size') 40 | 41 | // Standard Lambda handler 42 | exports.handler = async (event) => { 43 | console.log(event) 44 | 45 | // Get original object 46 | const s3Object = await s3.getObject({ 47 | Bucket: event.detail.bucket, 48 | Key: event.detail.key 49 | }).promise() 50 | 51 | // Check dimensions 52 | event.dimensions = sizeOf(s3Object.Body) 53 | event.dimensionsResult = true 54 | 55 | if (event.dimensions.height < process.env.minPixels || event.dimensions.width < process.env.minPixels) { 56 | event.dimensionsResult = false 57 | event.workflowDetail = JSON.stringify({ 58 | reason: 'IMAGE_TOO_SMALL', 59 | reasonDetail: event.dimensions 60 | }) 61 | } 62 | 63 | // Check file type is in list of allowed types 64 | const types = process.env.allowedTypes.split(',') 65 | if (!types.includes(event.dimensions.type)) { 66 | event.dimensionsResult = false 67 | event.workflowDetail = JSON.stringify({ 68 | reason: 'UNSUPPORTED_TYPE', 69 | reasonDetail: event.dimensions 70 | }) 71 | } 72 | 73 | return event 74 | } 75 | -------------------------------------------------------------------------------- /workflows/functions/analyze/dimensions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dimensions", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "James Beswick, AWS Serverless", 11 | "license": "MIT-0", 12 | "dependencies": { 13 | "aws-sdk": "^2.711.0", 14 | "image-size": "^0.8.3" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /workflows/functions/analyze/moderator/app.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 9 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 10 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 11 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 12 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 13 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | */ 15 | 16 | /* Expected EventBridge event format: 17 | { 18 | version: '0', 19 | id: '4d243389-1234-1234-1234-459ef3d88e93', 20 | 'detail-type': 'uploadComplete', 21 | source: 'custom.happyPath', 22 | account: '123456789012', 23 | time: '2020-07-03T15:05:00Z', 24 | region: 'us-east-1', 25 | resources: [], 26 | detail: { 27 | bucket: 'happy-path-upload', 28 | key: 'ChIJKU9_ejHA4okRTMnj1DSlZlg/8c97f22f-1234-1234-1234-abc21dbb7bd0.jpg' 29 | } 30 | } 31 | */ 32 | 33 | 'use strict' 34 | 35 | const AWS = require('aws-sdk') 36 | AWS.config.update({region: process.env.AWS_REGION}) 37 | const rekognition = new AWS.Rekognition(); 38 | 39 | // Standard Lambda handler 40 | exports.handler = async (event) => { 41 | console.log(event) 42 | 43 | // Append moderation result to event 44 | const labels = await processEvent(event) 45 | 46 | event.moderationResult = ( labels.length === 0 ) 47 | event.workflowDetail = JSON.stringify({ 48 | reason: 'FAILED_MODERATION', 49 | reasonDetail: JSON.stringify(labels) 50 | }) 51 | 52 | return event 53 | } 54 | 55 | // Promisified wrapper for detectModerationLabels 56 | const detectModerationLabels = (params) => { 57 | return new Promise ((resolve, reject) => { 58 | rekognition.detectModerationLabels (params, function(err, data) { 59 | if (err) reject(err) 60 | else resolve(data) 61 | }) 62 | }) 63 | } 64 | 65 | // Call Amazon Rekognition 66 | const processEvent = async (event) => { 67 | const params = { 68 | Image: { 69 | S3Object: { 70 | Bucket: event.detail.bucket, 71 | Name: event.detail.key 72 | } 73 | } 74 | } 75 | 76 | const result = await detectModerationLabels(params) 77 | return result.ModerationLabels 78 | } 79 | -------------------------------------------------------------------------------- /workflows/functions/analyze/moderator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "suitability", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "James Beswick", 11 | "license": "MIT-0", 12 | "dependencies": { 13 | "aws-sdk": "^2.709.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /workflows/functions/analyze/people/app.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 9 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 10 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 11 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 12 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 13 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | */ 15 | 16 | /* Expected EventBridge event format: 17 | { 18 | version: '0', 19 | id: '4d243389-1234-1234-1234-459ef3d88e93', 20 | 'detail-type': 'uploadComplete', 21 | source: 'custom.happyPath', 22 | account: '123456789012', 23 | time: '2020-07-03T15:05:00Z', 24 | region: 'us-east-1', 25 | resources: [], 26 | detail: { 27 | bucket: 'happy-path-upload', 28 | key: 'ChIJKU9_ejHA4okRTMnj1DSlZlg/8c97f22f-1234-1234-1234-abc21dbb7bd0.jpg' 29 | } 30 | } 31 | */ 32 | 'use strict' 33 | 34 | const AWS = require('aws-sdk') 35 | AWS.config.update({region: process.env.AWS_REGION}) 36 | const rekognition = new AWS.Rekognition(); 37 | 38 | // Standard Lambda handler 39 | exports.handler = async (event) => { 40 | console.log(event) 41 | 42 | // Get labels from image 43 | event.labels = await processEvent(event) 44 | 45 | // Count number of people in image 46 | event.totalPeople = 0 47 | event.labels.map((label) => { 48 | if (label.Name === 'Person') { 49 | event.totalPeople = label.Instances.length 50 | event.workflowDetail = JSON.stringify({ 51 | reason: 'NO_PEOPLE_ALLOWED', 52 | reasonDetail: `${label.Instances.length} found in image` 53 | }) 54 | } 55 | }) 56 | 57 | return event 58 | } 59 | 60 | // Promisified wrapper for detectModerationLabels 61 | const detectLabels = (params) => { 62 | return new Promise((resolve, reject) => { 63 | rekognition.detectLabels(params, function(err, data) { 64 | if (err) reject(err) 65 | else resolve(data) 66 | }) 67 | }) 68 | } 69 | 70 | // Call Amazon Rekognition 71 | const processEvent = async (event) => { 72 | const params = { 73 | Image: { 74 | S3Object: { 75 | Bucket: event.detail.bucket, 76 | Name: event.detail.key 77 | } 78 | } 79 | } 80 | 81 | const result = await detectLabels(params) 82 | return result.Labels 83 | } 84 | -------------------------------------------------------------------------------- /workflows/functions/analyze/people/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "people", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "James Beswick", 11 | "license": "MIT-0", 12 | "dependencies": { 13 | "aws-sdk": "^2.712.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /workflows/functions/publish/app.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 9 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 10 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 11 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 12 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 13 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | */ 15 | 16 | /* Expected input event format: 17 | { 18 | Input: { 19 | version: '0', 20 | id: '8a9b22f9-1234-1234-1234-b2757fac8db3', 21 | 'detail-type': 'uploadComplete', 22 | source: 'custom.happyPath', 23 | account: '123456789012', 24 | time: '2020-07-03T15:45:46Z', 25 | region: 'us-east-1', 26 | resources: [], 27 | detail: { 28 | bucket: 'happy-path-upload', 29 | key: 'ChIJKU9_ejHA4okRTMnj1DSlZlg/afc10693-1234-1234-1234-da02c8af2173.jpg' 30 | } 31 | } 32 | } 33 | */ 34 | 35 | const AWS = require('aws-sdk') 36 | AWS.config.update({region: process.env.AWS_REGION}) 37 | const documentClient = new AWS.DynamoDB.DocumentClient() 38 | 39 | // Standard Lambda handler 40 | exports.handler = async (event) => { 41 | console.log(event) 42 | await publishItem(event.Input) 43 | } 44 | 45 | // Update the item state in in DynamoDB 46 | const publishItem = async (event) => { 47 | 48 | const key = event.detail.key.split('/')[1] 49 | const type = key.split('.')[1] 50 | const log = JSON.stringify(event) 51 | 52 | // Update the workflow item 53 | const result = await documentClient.update({ 54 | TableName: process.env.TableName, 55 | Key:{ 56 | "PK": 'workflow', 57 | "SK": event.detail.ID 58 | }, 59 | UpdateExpression: "set objStatus = :newStatus, eventLog = :eventLog", 60 | ExpressionAttributeValues:{ 61 | ":newStatus": "PROCESSED", 62 | ":eventLog": log 63 | }, 64 | ReturnValues:"ALL_NEW" 65 | }).promise() 66 | console.log(result) 67 | 68 | // Add to placeId resources 69 | 70 | console.log(await documentClient.put({ 71 | TableName: process.env.TableName, 72 | Item: { 73 | PK: result.Attributes.placeId, 74 | SK: result.Attributes.SK, 75 | type, 76 | created: Date.now(), 77 | tileURL: event.tileURL, 78 | scaledURL: event.scaledURL 79 | } 80 | }).promise()) 81 | } 82 | -------------------------------------------------------------------------------- /workflows/functions/publish/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "publish", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "James Beswick", 11 | "license": "MIT-0" 12 | } 13 | -------------------------------------------------------------------------------- /workflows/functions/resizer/app.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 9 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 10 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 11 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 12 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 13 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | */ 15 | 16 | 17 | /* Expected event format: 18 | { 19 | "version": "0", 20 | "id": "4d243389-1234-1234-1234-459ef3d88e93", 21 | "detail-type": "uploadComplete", 22 | "source": "custom.happyPath", 23 | "account": "123456789012", 24 | "time": "2020-07-08T17:35:22Z", 25 | "region": "us-east-1", 26 | "resources": [], 27 | "detail": { 28 | "bucket": "happy-path-upload", 29 | "key": "4b02d0c3-1234-1234-1234-36af82130d72.jpg", 30 | "placeId": "ChIJxQx-bXa_4okR_RRYZ1b_qhE", 31 | "ID": "4b02d0c3-1234-1234-1234-36af82130d72" 32 | } 33 | } 34 | */ 35 | 36 | 'use strict' 37 | 38 | const AWS = require('aws-sdk') 39 | AWS.config.update({region: process.env.AWS_REGION}) 40 | 41 | const s3 = new AWS.S3() 42 | const sharp = require('sharp') 43 | const { ExifImage } = require('exif') 44 | 45 | // Standard Lambda handler 46 | exports.handler = async (event) => { 47 | console.log(event) 48 | return processEvent(event) 49 | } 50 | 51 | // Promisified wrapper for EXIF extraction 52 | const getExifData = (Buffer) => { 53 | return new Promise((resolve, reject) => { 54 | new ExifImage({ image : Buffer }, function (error, exifData) { 55 | if (error) 56 | reject(error) 57 | else 58 | resolve(exifData) 59 | }) 60 | }) 61 | } 62 | 63 | // Promisified wrapper for Sharp 64 | const getResizeBuffer = (inBuffer, x, y) => { 65 | return new Promise((resolve, reject) => { 66 | const promise1 = sharp(inBuffer) 67 | .resize(x, y) 68 | .toBuffer((err, outputBuffer, info) => { 69 | if (err) 70 | reject(err) 71 | else 72 | resolve(outputBuffer) 73 | }) 74 | }) 75 | } 76 | 77 | const processEvent = async (event) => { 78 | const params = { 79 | Bucket: event.detail.bucket, 80 | Key: event.detail.key 81 | } 82 | 83 | // Get original object 84 | const s3Object = await s3.getObject(params).promise() 85 | 86 | // Get base object name 87 | const outputBaseName = params.Key.split('.')[0] 88 | 89 | // Create/save tile version 90 | const tileBuffer = await getResizeBuffer(s3Object.Body, 400, 400) 91 | 92 | await s3.putObject({ 93 | Bucket: process.env.OutputBucket, 94 | Key: `${outputBaseName}-tile.jpg`, 95 | ContentType: 'image/jpeg', 96 | Body: tileBuffer, 97 | ACL: 'public-read' 98 | }).promise() 99 | 100 | event.tileURL = `https://${process.env.OutputBucket}.s3.amazonaws.com/${outputBaseName}-tile.jpg` 101 | 102 | // Create/save scaled smaller version 103 | const scaledBuffer = await getResizeBuffer(s3Object.Body, 600) 104 | 105 | await s3.putObject({ 106 | Bucket: process.env.OutputBucket, 107 | Key: `${outputBaseName}-scaled.jpg`, 108 | ContentType: 'image/jpeg', 109 | Body: scaledBuffer, 110 | ACL: 'public-read' 111 | }).promise() 112 | event.scaledURL = `https://${process.env.OutputBucket}.s3.amazonaws.com/${outputBaseName}-scaled.jpg` 113 | 114 | // Extract/save EXIF data 115 | try { 116 | const EXITdata = await getExifData(s3Object.Body) 117 | 118 | await s3.putObject({ 119 | Bucket: process.env.OutputBucket, 120 | Key: `${outputBaseName}-exif.json`, 121 | ContentType: 'application/json', 122 | Body: JSON.stringify(EXITdata) 123 | }).promise() 124 | 125 | } catch (err) { 126 | console.error('Exif error: ', err) 127 | } 128 | return event 129 | } 130 | -------------------------------------------------------------------------------- /workflows/functions/resizer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "resizer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "sharp": "^0.32.6" 14 | }, 15 | "dependencies": { 16 | "aws-sdk": "^2.708.0", 17 | "exif": "^0.6.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /workflows/statemachines/v1.asl.json: -------------------------------------------------------------------------------- 1 | { 2 | "Comment": "v1 - Resize uploaded images", 3 | "StartAt": "Resizer", 4 | "States": { 5 | "Resizer": { 6 | "Type": "Task", 7 | "Resource": "${ResizerFunctionArn}", 8 | "Next": "Publish" 9 | }, 10 | "Publish": { 11 | "Type": "Task", 12 | "Resource": "arn:aws:states:::lambda:invoke", 13 | "Parameters": { 14 | "FunctionName": "${PublishFunctionArn}", 15 | "Payload": { 16 | "Input.$": "$" 17 | } 18 | }, 19 | "End": true 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /workflows/statemachines/v2.asl.json: -------------------------------------------------------------------------------- 1 | { 2 | "Comment": "v2 - Moderate uploaded images", 3 | "StartAt": "Moderator", 4 | "States": { 5 | "Moderator": { 6 | "Type": "Task", 7 | "Resource": "${ModeratorFunctionArn}", 8 | "Next": "Moderation Result?" 9 | }, 10 | "Moderation Result?": { 11 | "Type": "Choice", 12 | "Choices": [ 13 | { 14 | "Variable": "$.moderationResult", 15 | "BooleanEquals": true, 16 | "Next": "Resizer" 17 | }, 18 | { 19 | "Variable": "$.moderationResult", 20 | "BooleanEquals": false, 21 | "Next": "RecordFailState" 22 | } 23 | ], 24 | "Default": "RecordFailState" 25 | }, 26 | "Resizer": { 27 | "Type": "Task", 28 | "Resource": "${ResizerFunctionArn}", 29 | "Next": "Publish" 30 | }, 31 | "Publish": { 32 | "Type": "Task", 33 | "Resource": "arn:aws:states:::lambda:invoke", 34 | "Parameters": { 35 | "FunctionName": "${PublishFunctionArn}", 36 | "Payload": { 37 | "Input.$": "$" 38 | } 39 | }, 40 | "End": true 41 | }, 42 | "RecordFailState": { 43 | "Type": "Task", 44 | "Resource": "${DDBUpdateItem}", 45 | "End": true, 46 | "Parameters": { 47 | "TableName": "${DDBTable}", 48 | "Key": { 49 | "PK": { 50 | "S": "workflow" 51 | }, 52 | "SK": { 53 | "S.$": "$.detail.ID" 54 | } 55 | }, 56 | "UpdateExpression": "set objStatus = :newStatus, detail = :detail", 57 | "ExpressionAttributeValues": { 58 | ":newStatus": { 59 | "S": "FAILED_MODERATION" 60 | }, 61 | ":detail": { 62 | "S.$": "$.workflowDetail" 63 | } 64 | } 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /workflows/statemachines/v3.asl.json: -------------------------------------------------------------------------------- 1 | { 2 | "Comment": "v3 - Check dimensions/file type", 3 | "StartAt": "Check Dimensions", 4 | "States": { 5 | "Check Dimensions": { 6 | "Type": "Task", 7 | "Resource": "${DimensionsFunctionArn}", 8 | "Next": "Dimensions Result?" 9 | }, 10 | "Dimensions Result?": { 11 | "Type": "Choice", 12 | "Choices": [ 13 | { 14 | "Variable": "$.dimensionsResult", 15 | "BooleanEquals": true, 16 | "Next": "Moderator" 17 | }, 18 | { 19 | "Variable": "$.dimensionsResult", 20 | "BooleanEquals": false, 21 | "Next": "RecordFailState" 22 | } 23 | ], 24 | "Default": "RecordFailState" 25 | }, 26 | "Moderator": { 27 | "Type": "Task", 28 | "Resource": "${ModeratorFunctionArn}", 29 | "Next": "Moderation Result?" 30 | }, 31 | "Moderation Result?": { 32 | "Type": "Choice", 33 | "Choices": [ 34 | { 35 | "Variable": "$.moderationResult", 36 | "BooleanEquals": true, 37 | "Next": "Resizer" 38 | }, 39 | { 40 | "Variable": "$.moderationResult", 41 | "BooleanEquals": false, 42 | "Next": "RecordFailState" 43 | } 44 | ], 45 | "Default": "RecordFailState" 46 | }, 47 | "Resizer": { 48 | "Type": "Task", 49 | "Resource": "${ResizerFunctionArn}", 50 | "Next": "Publish" 51 | }, 52 | "Publish": { 53 | "Type": "Task", 54 | "Resource": "arn:aws:states:::lambda:invoke", 55 | "Parameters": { 56 | "FunctionName": "${PublishFunctionArn}", 57 | "Payload": { 58 | "Input.$": "$" 59 | } 60 | }, 61 | "End": true 62 | }, 63 | "RecordFailState": { 64 | "Type": "Task", 65 | "Resource": "${DDBUpdateItem}", 66 | "End": true, 67 | "Parameters": { 68 | "TableName": "${DDBTable}", 69 | "Key": { 70 | "PK": { 71 | "S": "workflow" 72 | }, 73 | "SK": { 74 | "S.$": "$.detail.ID" 75 | } 76 | }, 77 | "UpdateExpression": "set objStatus = :newStatus, detail = :detail", 78 | "ExpressionAttributeValues": { 79 | ":newStatus": { 80 | "S": "REJECTED" 81 | }, 82 | ":detail": { 83 | "S.$": "$.workflowDetail" 84 | } 85 | } 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /workflows/statemachines/v4.asl.json: -------------------------------------------------------------------------------- 1 | { 2 | "Comment": "v4 - Two business types", 3 | "StartAt": "Check Dimensions", 4 | "States": { 5 | "Check Dimensions": { 6 | "Type": "Task", 7 | "Resource": "${DimensionsFunctionArn}", 8 | "Next": "Dimensions Result?" 9 | }, 10 | "Dimensions Result?": { 11 | "Type": "Choice", 12 | "Choices": [ 13 | { 14 | "Variable": "$.dimensionsResult", 15 | "BooleanEquals": true, 16 | "Next": "Get place type from DynamoDB" 17 | }, 18 | { 19 | "Variable": "$.dimensionsResult", 20 | "BooleanEquals": false, 21 | "Next": "RecordFailState" 22 | } 23 | ], 24 | "Default": "RecordFailState" 25 | }, 26 | 27 | "Get place type from DynamoDB": { 28 | "Type": "Task", 29 | "Resource": "${DDBGetItem}", 30 | "Parameters": { 31 | "TableName": "${DDBTable}", 32 | "Key": { 33 | "PK": { 34 | "S.$": "$.detail.placeId" 35 | }, 36 | "SK": { 37 | "S": "listing" 38 | } 39 | } 40 | }, 41 | "ResultPath": "$.placeRecord", 42 | "Next": "Place type?" 43 | }, 44 | "Place type?": { 45 | "Type": "Choice", 46 | "Choices": [ 47 | { 48 | "Variable": "$.placeRecord.Item.type.S", 49 | "StringEquals": "park", 50 | "Next": "Moderator" 51 | }, 52 | { 53 | "Variable": "$.placeRecord.Item.type.S", 54 | "StringEquals": "restaurant", 55 | "Next": "Get labels" 56 | }, 57 | { 58 | "Variable": "$.placeRecord.Item.type.S", 59 | "StringEquals": "cafe", 60 | "Next": "Get labels" 61 | }, 62 | { 63 | "Variable": "$.placeRecord.Item.type.S", 64 | "StringEquals": "bar", 65 | "Next": "Get labels" 66 | } 67 | ], 68 | "Default": "RecordFailState" 69 | }, 70 | 71 | "Moderator": { 72 | "Type": "Task", 73 | "Resource": "${ModeratorFunctionArn}", 74 | "Next": "Moderation Result?" 75 | }, 76 | "Moderation Result?": { 77 | "Type": "Choice", 78 | "Choices": [ 79 | { 80 | "Variable": "$.moderationResult", 81 | "BooleanEquals": true, 82 | "Next": "Resizer" 83 | }, 84 | { 85 | "Variable": "$.moderationResult", 86 | "BooleanEquals": false, 87 | "Next": "RecordFailState" 88 | } 89 | ], 90 | "Default": "RecordFailState" 91 | }, 92 | 93 | "Get labels": { 94 | "Type": "Task", 95 | "Resource": "${LabelsFunctionArn}", 96 | "Next": "Check for people" 97 | }, 98 | "Check for people": { 99 | "Type": "Choice", 100 | "Choices": [ 101 | { 102 | "Variable": "$.totalPeople", 103 | "NumericEquals": 0, 104 | "Next": "Resizer" 105 | } 106 | ], 107 | "Default": "RecordFailState" 108 | }, 109 | "Resizer": { 110 | "Type": "Task", 111 | "Resource": "${ResizerFunctionArn}", 112 | "Next": "Publish" 113 | }, 114 | "Publish": { 115 | "Type": "Task", 116 | "Resource": "arn:aws:states:::lambda:invoke", 117 | "Parameters": { 118 | "FunctionName": "${PublishFunctionArn}", 119 | "Payload": { 120 | "Input.$": "$" 121 | } 122 | }, 123 | "End": true 124 | }, 125 | "RecordFailState": { 126 | "Type": "Task", 127 | "Resource": "${DDBUpdateItem}", 128 | "End": true, 129 | "Parameters": { 130 | "TableName": "${DDBTable}", 131 | "Key": { 132 | "PK": { 133 | "S": "workflow" 134 | }, 135 | "SK": { 136 | "S.$": "$.detail.ID" 137 | } 138 | }, 139 | "UpdateExpression": "set objStatus = :newStatus, detail = :detail", 140 | "ExpressionAttributeValues": { 141 | ":newStatus": { 142 | "S": "REJECTED" 143 | }, 144 | ":detail": { 145 | "S.$": "$.workflowDetail" 146 | } 147 | } 148 | } 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /workflows/templates/v1/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: Happy Path - Example workflow 1 4 | 5 | Parameters: 6 | ApplicationTableName: 7 | Type: String 8 | Description: Name of application's DynamoDB table 9 | Default: hp-application 10 | 11 | S3UploadBucketName: 12 | Type: String 13 | Description: The name for the S3 upload bucket. 14 | Default: 'happy-path-upload' 15 | 16 | S3DistributionBucketName: 17 | Type: String 18 | Description: The name for the S3 upload bucket. 19 | Default: 'happy-path-distribution' 20 | 21 | StreamARN: 22 | Type: String 23 | Description: The ARN for the DynamoDB table stream. 24 | Default: 'arn:aws:dynamodb:us-east-1:123456789012:table/hp-application/stream/2020-06-29T14:29:10.716' 25 | 26 | SharpLayerARN: 27 | Type: String 28 | Description: The ARN for the Sharp Lambda Layer. 29 | Default: 'arn:aws:lambda:us-east-1:123456789012:layer:node-sharp:3' 30 | 31 | Globals: 32 | Function: 33 | Timeout: 5 34 | Runtime: nodejs12.x 35 | Tags: 36 | Application: HappyPath 37 | 38 | Resources: 39 | #Lambda functions 40 | ResizerFunction: 41 | Type: AWS::Serverless::Function 42 | Properties: 43 | CodeUri: ../../functions/resizer/ 44 | Handler: app.handler 45 | MemorySize: 2056 46 | Layers: 47 | - !Ref SharpLayerARN 48 | Environment: 49 | Variables: 50 | OutputBucket: !Ref S3DistributionBucketName 51 | TableName: !Ref ApplicationTableName 52 | Policies: 53 | - S3ReadPolicy: 54 | BucketName: !Ref S3UploadBucketName 55 | - S3CrudPolicy: 56 | BucketName: !Ref S3DistributionBucketName 57 | - DynamoDBWritePolicy: 58 | TableName: !Ref ApplicationTableName 59 | 60 | #Lambda functions 61 | PublishFunction: 62 | Type: AWS::Serverless::Function 63 | Properties: 64 | CodeUri: ../../functions/publish/ 65 | Handler: app.handler 66 | MemorySize: 128 67 | Layers: 68 | - !Ref SharpLayerARN 69 | Environment: 70 | Variables: 71 | TableName: !Ref ApplicationTableName 72 | Policies: 73 | - DynamoDBWritePolicy: 74 | TableName: !Ref ApplicationTableName 75 | 76 | # Step Functions state machine 77 | v1StateMachine: 78 | Type: AWS::Serverless::StateMachine 79 | Properties: 80 | DefinitionUri: ../../statemachines/v1.asl.json 81 | DefinitionSubstitutions: 82 | ResizerFunctionArn: !GetAtt ResizerFunction.Arn 83 | PublishFunctionArn: !GetAtt PublishFunction.Arn 84 | Events: 85 | UploadComplete: 86 | Type: EventBridgeRule 87 | Properties: 88 | Pattern: 89 | source: 90 | - custom.happyPath 91 | detail-type: 92 | - uploadComplete 93 | Policies: # Find out more about SAM policy templates: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-policy-templates.html 94 | - LambdaInvokePolicy: 95 | FunctionName: !Ref ResizerFunction 96 | - LambdaInvokePolicy: 97 | FunctionName: !Ref PublishFunction 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /workflows/templates/v2/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: Happy Path - Example workflow 2 4 | 5 | Parameters: 6 | ApplicationTableName: 7 | Type: String 8 | Description: Name of application's DynamoDB table 9 | Default: hp-application 10 | 11 | S3UploadBucketName: 12 | Type: String 13 | Description: The name for the S3 upload bucket. 14 | Default: 'happy-path-upload' 15 | 16 | S3DistributionBucketName: 17 | Type: String 18 | Description: The name for the S3 upload bucket. 19 | Default: 'happy-path-distribution' 20 | 21 | StreamARN: 22 | Type: String 23 | Description: The ARN for the DynamoDB table stream. 24 | Default: 'arn:aws:dynamodb:us-east-1:123456789012:table/hp-application/stream/2020-06-29T14:29:10.716' 25 | 26 | SharpLayerARN: 27 | Type: String 28 | Description: The ARN for the Sharp Lambda Layer. 29 | Default: 'arn:aws:lambda:us-east-1:123456789012:layer:node-sharp:3' 30 | 31 | Globals: 32 | Function: 33 | Timeout: 5 34 | Runtime: nodejs12.x 35 | Tags: 36 | Application: HappyPath 37 | 38 | Resources: 39 | #Lambda functions 40 | ResizerFunction: 41 | Type: AWS::Serverless::Function 42 | Properties: 43 | CodeUri: ../../functions/resizer/ 44 | Handler: app.handler 45 | MemorySize: 2056 46 | Layers: 47 | - !Ref SharpLayerARN 48 | Environment: 49 | Variables: 50 | OutputBucket: !Ref S3DistributionBucketName 51 | TableName: !Ref ApplicationTableName 52 | Policies: 53 | - S3ReadPolicy: 54 | BucketName: !Ref S3UploadBucketName 55 | - S3CrudPolicy: 56 | BucketName: !Ref S3DistributionBucketName 57 | - DynamoDBWritePolicy: 58 | TableName: !Ref ApplicationTableName 59 | 60 | PublishFunction: 61 | Type: AWS::Serverless::Function 62 | Properties: 63 | CodeUri: ../../functions/publish/ 64 | Handler: app.handler 65 | MemorySize: 128 66 | Layers: 67 | - !Ref SharpLayerARN 68 | Environment: 69 | Variables: 70 | TableName: !Ref ApplicationTableName 71 | Policies: 72 | - DynamoDBWritePolicy: 73 | TableName: !Ref ApplicationTableName 74 | 75 | ModeratorFunction: 76 | Type: AWS::Serverless::Function 77 | Properties: 78 | CodeUri: ../../functions/analyze/moderator 79 | Handler: app.handler 80 | MemorySize: 128 81 | Timeout: 10 82 | Policies: 83 | - S3ReadPolicy: 84 | BucketName: !Ref S3UploadBucketName 85 | - Statement: 86 | - Effect: Allow 87 | Resource: '*' 88 | Action: 89 | - rekognition:* 90 | 91 | # Step Functions state machine 92 | v2StateMachine: 93 | Type: AWS::Serverless::StateMachine 94 | Properties: 95 | DefinitionUri: ../../statemachines/v2.asl.json 96 | DefinitionSubstitutions: 97 | ResizerFunctionArn: !GetAtt ResizerFunction.Arn 98 | PublishFunctionArn: !GetAtt PublishFunction.Arn 99 | ModeratorFunctionArn: !GetAtt ModeratorFunction.Arn 100 | DDBUpdateItem: !Sub arn:${AWS::Partition}:states:::dynamodb:updateItem 101 | DDBTable: !Ref ApplicationTableName 102 | Events: 103 | UploadComplete: 104 | Type: EventBridgeRule 105 | Properties: 106 | Pattern: 107 | source: 108 | - custom.happyPath 109 | detail-type: 110 | - uploadComplete 111 | Policies: # Find out more about SAM policy templates: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-policy-templates.html 112 | - LambdaInvokePolicy: 113 | FunctionName: !Ref ResizerFunction 114 | - LambdaInvokePolicy: 115 | FunctionName: !Ref PublishFunction 116 | - LambdaInvokePolicy: 117 | FunctionName: !Ref ModeratorFunction 118 | - DynamoDBWritePolicy: 119 | TableName: !Ref ApplicationTableName 120 | 121 | 122 | -------------------------------------------------------------------------------- /workflows/templates/v3/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: Happy Path - Example workflow 3 4 | 5 | Parameters: 6 | ApplicationTableName: 7 | Type: String 8 | Description: Name of application's DynamoDB table 9 | Default: hp-application 10 | 11 | S3UploadBucketName: 12 | Type: String 13 | Description: The name for the S3 upload bucket. 14 | Default: 'happy-path-upload' 15 | 16 | S3DistributionBucketName: 17 | Type: String 18 | Description: The name for the S3 upload bucket. 19 | Default: 'happy-path-distribution' 20 | 21 | StreamARN: 22 | Type: String 23 | Description: The ARN for the DynamoDB table stream. 24 | Default: 'arn:aws:dynamodb:us-east-1:123456789012:table/hp-application/stream/2020-06-29T14:29:10.716' 25 | 26 | SharpLayerARN: 27 | Type: String 28 | Description: The ARN for the Sharp Lambda Layer. 29 | Default: 'arn:aws:lambda:us-east-1:123456789012:layer:node-sharp:3' 30 | 31 | Globals: 32 | Function: 33 | Timeout: 5 34 | Runtime: nodejs12.x 35 | Tags: 36 | Application: HappyPath 37 | 38 | Resources: 39 | #Lambda functions 40 | ResizerFunction: 41 | Type: AWS::Serverless::Function 42 | Properties: 43 | CodeUri: ../../functions/resizer/ 44 | Handler: app.handler 45 | MemorySize: 2056 46 | Layers: 47 | - !Ref SharpLayerARN 48 | Environment: 49 | Variables: 50 | OutputBucket: !Ref S3DistributionBucketName 51 | TableName: !Ref ApplicationTableName 52 | Policies: 53 | - S3ReadPolicy: 54 | BucketName: !Ref S3UploadBucketName 55 | - S3CrudPolicy: 56 | BucketName: !Ref S3DistributionBucketName 57 | - DynamoDBWritePolicy: 58 | TableName: !Ref ApplicationTableName 59 | 60 | PublishFunction: 61 | Type: AWS::Serverless::Function 62 | Properties: 63 | CodeUri: ../../functions/publish/ 64 | Handler: app.handler 65 | MemorySize: 128 66 | Layers: 67 | - !Ref SharpLayerARN 68 | Environment: 69 | Variables: 70 | TableName: !Ref ApplicationTableName 71 | Policies: 72 | - DynamoDBWritePolicy: 73 | TableName: !Ref ApplicationTableName 74 | 75 | ModeratorFunction: 76 | Type: AWS::Serverless::Function 77 | Properties: 78 | CodeUri: ../../functions/analyze/moderator 79 | Handler: app.handler 80 | MemorySize: 128 81 | Timeout: 10 82 | Policies: 83 | - S3ReadPolicy: 84 | BucketName: !Ref S3UploadBucketName 85 | - Statement: 86 | - Effect: Allow 87 | Resource: '*' 88 | Action: 89 | - rekognition:* 90 | 91 | DimensionsFunction: 92 | Type: AWS::Serverless::Function 93 | Properties: 94 | CodeUri: ../../functions/analyze/dimensions 95 | Handler: app.handler 96 | MemorySize: 128 97 | Environment: 98 | Variables: 99 | minPixels: 800 100 | allowedTypes: 'jpg' 101 | Policies: 102 | - S3ReadPolicy: 103 | BucketName: !Ref S3UploadBucketName 104 | 105 | # Step Functions state machine 106 | v3StateMachine: 107 | Type: AWS::Serverless::StateMachine 108 | Properties: 109 | DefinitionUri: ../../statemachines/v3.asl.json 110 | DefinitionSubstitutions: 111 | ResizerFunctionArn: !GetAtt ResizerFunction.Arn 112 | PublishFunctionArn: !GetAtt PublishFunction.Arn 113 | ModeratorFunctionArn: !GetAtt ModeratorFunction.Arn 114 | DimensionsFunctionArn: !GetAtt DimensionsFunction.Arn 115 | DDBUpdateItem: !Sub arn:${AWS::Partition}:states:::dynamodb:updateItem 116 | DDBTable: !Ref ApplicationTableName 117 | Events: 118 | UploadComplete: 119 | Type: EventBridgeRule 120 | Properties: 121 | Pattern: 122 | source: 123 | - custom.happyPath 124 | detail-type: 125 | - uploadComplete 126 | Policies: # Find out more about SAM policy templates: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-policy-templates.html 127 | - LambdaInvokePolicy: 128 | FunctionName: !Ref ResizerFunction 129 | - LambdaInvokePolicy: 130 | FunctionName: !Ref PublishFunction 131 | - LambdaInvokePolicy: 132 | FunctionName: !Ref ModeratorFunction 133 | - LambdaInvokePolicy: 134 | FunctionName: !Ref DimensionsFunction 135 | - DynamoDBWritePolicy: 136 | TableName: !Ref ApplicationTableName 137 | 138 | 139 | -------------------------------------------------------------------------------- /workflows/templates/v4/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: Happy Path - Example workflow 4 4 | 5 | Parameters: 6 | ApplicationTableName: 7 | Type: String 8 | Description: Name of application's DynamoDB table 9 | Default: hp-application 10 | 11 | S3UploadBucketName: 12 | Type: String 13 | Description: The name for the S3 upload bucket. 14 | Default: 'happy-path-upload' 15 | 16 | S3DistributionBucketName: 17 | Type: String 18 | Description: The name for the S3 upload bucket. 19 | Default: 'happy-path-distribution' 20 | 21 | StreamARN: 22 | Type: String 23 | Description: The ARN for the DynamoDB table stream. 24 | Default: 'arn:aws:dynamodb:us-east-1:123456789012:table/hp-application/stream/2020-06-29T14:29:10.716' 25 | 26 | SharpLayerARN: 27 | Type: String 28 | Description: The ARN for the Sharp Lambda Layer. 29 | Default: 'arn:aws:lambda:us-east-1:763653534548:layer:node-sharp:3' 30 | 31 | Globals: 32 | Function: 33 | Timeout: 5 34 | Runtime: nodejs12.x 35 | Tags: 36 | Application: HappyPath 37 | 38 | Resources: 39 | #Lambda functions 40 | ResizerFunction: 41 | Type: AWS::Serverless::Function 42 | Properties: 43 | CodeUri: ../../functions/resizer/ 44 | Handler: app.handler 45 | MemorySize: 2056 46 | Layers: 47 | - !Ref SharpLayerARN 48 | Environment: 49 | Variables: 50 | OutputBucket: !Ref S3DistributionBucketName 51 | TableName: !Ref ApplicationTableName 52 | Policies: 53 | - S3ReadPolicy: 54 | BucketName: !Ref S3UploadBucketName 55 | - S3CrudPolicy: 56 | BucketName: !Ref S3DistributionBucketName 57 | - DynamoDBWritePolicy: 58 | TableName: !Ref ApplicationTableName 59 | 60 | PublishFunction: 61 | Type: AWS::Serverless::Function 62 | Properties: 63 | CodeUri: ../../functions/publish/ 64 | Handler: app.handler 65 | MemorySize: 128 66 | Layers: 67 | - !Ref SharpLayerARN 68 | Environment: 69 | Variables: 70 | TableName: !Ref ApplicationTableName 71 | Policies: 72 | - DynamoDBWritePolicy: 73 | TableName: !Ref ApplicationTableName 74 | 75 | ModeratorFunction: 76 | Type: AWS::Serverless::Function 77 | Properties: 78 | CodeUri: ../../functions/analyze/moderator 79 | Handler: app.handler 80 | MemorySize: 128 81 | Timeout: 10 82 | Policies: 83 | - S3ReadPolicy: 84 | BucketName: !Ref S3UploadBucketName 85 | - RekognitionLabelsPolicy: {} 86 | 87 | DimensionsFunction: 88 | Type: AWS::Serverless::Function 89 | Properties: 90 | CodeUri: ../../functions/analyze/dimensions 91 | Handler: app.handler 92 | MemorySize: 128 93 | Environment: 94 | Variables: 95 | minPixels: 500 96 | allowedTypes: 'jpg' 97 | Policies: 98 | - S3ReadPolicy: 99 | BucketName: !Ref S3UploadBucketName 100 | 101 | LabelsFunction: 102 | Type: AWS::Serverless::Function 103 | Properties: 104 | CodeUri: ../../functions/analyze/people 105 | Handler: app.handler 106 | MemorySize: 128 107 | Timeout: 10 108 | Policies: 109 | - S3ReadPolicy: 110 | BucketName: !Ref S3UploadBucketName 111 | - RekognitionLabelsPolicy: {} 112 | 113 | # Step Functions state machine 114 | v4StateMachine: 115 | Type: AWS::Serverless::StateMachine 116 | Properties: 117 | DefinitionUri: ../../statemachines/v4.asl.json 118 | DefinitionSubstitutions: 119 | ResizerFunctionArn: !GetAtt ResizerFunction.Arn 120 | PublishFunctionArn: !GetAtt PublishFunction.Arn 121 | ModeratorFunctionArn: !GetAtt ModeratorFunction.Arn 122 | DimensionsFunctionArn: !GetAtt DimensionsFunction.Arn 123 | LabelsFunctionArn: !GetAtt LabelsFunction.Arn 124 | DDBGetItem: !Sub arn:${AWS::Partition}:states:::dynamodb:getItem 125 | DDBUpdateItem: !Sub arn:${AWS::Partition}:states:::dynamodb:updateItem 126 | DDBTable: !Ref ApplicationTableName 127 | Events: 128 | UploadComplete: 129 | Type: EventBridgeRule 130 | Properties: 131 | Pattern: 132 | source: 133 | - custom.happyPath 134 | detail-type: 135 | - uploadComplete 136 | Policies: # Find out more about SAM policy templates: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-policy-templates.html 137 | - LambdaInvokePolicy: 138 | FunctionName: !Ref ResizerFunction 139 | - LambdaInvokePolicy: 140 | FunctionName: !Ref PublishFunction 141 | - LambdaInvokePolicy: 142 | FunctionName: !Ref ModeratorFunction 143 | - LambdaInvokePolicy: 144 | FunctionName: !Ref DimensionsFunction 145 | - LambdaInvokePolicy: 146 | FunctionName: !Ref LabelsFunction 147 | - DynamoDBCrudPolicy: 148 | TableName: !Ref ApplicationTableName 149 | 150 | 151 | --------------------------------------------------------------------------------