├── .github └── PULL_REQUEST_TEMPLATE.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json ├── readmeImages │ ├── ArchDiagram.png │ ├── BackendDiagram.png │ ├── DeveloperTools.png │ ├── SummaryDiagram.png │ └── images.md ├── src │ ├── App.css │ ├── App.test.tsx │ ├── App.tsx │ ├── Routes.tsx │ ├── common │ │ └── PropsRoute.tsx │ ├── config.ts │ ├── images │ │ └── full-stack.png │ ├── index.css │ ├── index.tsx │ ├── modules │ │ ├── goal │ │ │ ├── AddEditGoal.css │ │ │ └── AddEditGoal.tsx │ │ ├── notFound │ │ │ ├── NotFound.tsx │ │ │ └── notFound.css │ │ └── signup │ │ │ ├── Home.tsx │ │ │ ├── Login.tsx │ │ │ ├── Signup.tsx │ │ │ ├── home.css │ │ │ ├── login.css │ │ │ └── signup.css │ ├── react-app-env.d.ts │ ├── registerServiceWorker.ts │ └── types │ │ ├── aws-amplify-react.d.ts │ │ └── aws-amplify-ui.d.ts └── tsconfig.json ├── extensions ├── cw-slack-chime │ ├── README.md │ ├── architecture.png │ ├── functions │ │ ├── chime.js │ │ ├── modifyAlarm.js │ │ └── slack.js │ └── master.yaml └── search-api │ ├── README.md │ ├── architecture.png │ ├── functions │ ├── searchCluster.py │ └── updateSearchCluster.py │ └── master.yaml ├── functions ├── CreateGoal.js ├── DeleteGoal.js ├── GetGoal.js ├── ListGoals.js └── UpdateGoal.js ├── readmeImages ├── ArchDiagram.png ├── BackendDiagram.png ├── DeveloperTools.png ├── SummaryDiagram.png └── images.md ├── template └── master-fullstack.yaml └── workshop └── readme.md /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 7 | -------------------------------------------------------------------------------- /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](https://github.com/awslabs/aws-full-stack-template/issues), or [recently closed](https://github.com/awslabs/aws-full-stack-template/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), 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'](https://github.com/awslabs/aws-full-stack-template/labels/help%20wanted) 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](https://github.com/awslabs/aws-full-stack-template/blob/master/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 2018 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 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 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## AWS Full-Stack Template 2 | 3 | AWS Full-Stack Template is a full-stack sample web application that creates a simple CRUD (create, read, update, delete) app, and provides the foundational services, components, and plumbing needed to get a basic web application up and running. **[Try out the deployed application here](https://d2k5b8bzo1vefz.cloudfront.net/)**! 4 | 5 | The entire application - frontend, backend, and all configuration - can deployed in your AWS account with a single CloudFormation template. Get started with building your own below! 6 |   7 | 8 | ## License Summary 9 | 10 | This sample code is made available under a modified MIT license. See the LICENSE file. 11 | 12 |   13 | 14 | ## Outline 15 | 16 | - [Overview](#overview) 17 | - [Instructions](#instructions) 18 | - [Getting started](#getting-started) 19 | - [Cleaning up](#cleaning-up) 20 | - [Architecture](#architecture) 21 | - [Implementation details](#implementation-details) 22 | - [Amazon DynamoDB](#amazon-dynamodb) 23 | - [Amazon API Gateway](#amazon-api-gateway) 24 | - [AWS Lambda](#aws-lambda) 25 | - [AWS IAM](#aws-iam) 26 | - [Amazon Cognito](#amazon-cognito) 27 | - [Amazon Cloudfront and Amazon S3](#amazon-cloudfront-and-amazon-s3) 28 | - [Amazon Cloudwatch](#amazon-cloudwatch) 29 | - [AWS CodeCommit, AWS CodePipeline, AWS CodeBuild](#aws-codecommit-aws-codepipeline-aws-codebuild) 30 | - [Running your web application locally](#running-your-web-application-locally) 31 | - [Considerations for demo purposes](#considerations-for-demo-purposes) 32 | - [Known limitations](#known-limitations) 33 | - [Additions, forks, and contributions](#additions-forks-and-contributions) 34 | - [Extensions](#extensions) 35 | - [Questions and contact](#questions-and-contact) 36 | 37 |   38 | 39 | ## Overview 40 | 41 | The goal of AWS Full-Stack Template is to provide a fully-functional web application that helps users accelerate building apps on AWS by providing an out-of-the-box template. This template is production-ready and pre-loaded with best practices. Applications today have an increasing number of building blocks and infrastructure components, and AWS Full-Stack Template will help educate professionals and students alike to design software in a modern cloud computing world. With AWS Full-Stack Template, developers can create a cohesive, production-ready application on the cloud in minutes, allowing them to focus on building the pieces that matter and add value. 42 | 43 | The provided CloudFormation template automates the entire creation and deployment of AWS Full-Stack Template. The template includes the following components: 44 | 45 | **Database components** 46 | 47 | * Goals list – Amazon DynamoDB offers fast, predictable performance for the key-value lookups needed in the goals app, and enormous scale so you can build on top of it. In this implementation, we have unique identifiers for each goal, the goal name, and description. 48 | 49 | **Application components** 50 | 51 | * Serverless service backend – Amazon API Gateway powers the interface layer between the frontend and backend, and invokes serverless compute with AWS Lambda. 52 | * Web application blueprint – We include a React web application pre-integrated out-of-the-box with best practices and tools such as React Bootstrap, React Router, Typescript, and more. 53 | 54 | **Infrastructure components** 55 | 56 | * Continuous deployment code pipeline – AWS CodePipeline and AWS CodeBuild help you build, test, and release your application code. 57 | * Serverless web application – Amazon CloudFront and Amazon S3 provide a globally-distributed application. 58 | 59 | You can choose to customize the template to create your own goals app, modify it to make a different type of simple notes or to-do application, or add onto it to make a completely different type of web application. 60 | 61 | Users can build on top of AWS Full-Stack Template to create any application they envision, whether a travel booking tool, a blog, or another web app. **[AWS Bookstore Demo App](https://github.com/aws-samples/aws-bookstore-demo-app)** is just one full-fledged example of what you might create using AWS Full-Stack Template. 62 | 63 | Once you've set up AWS Full-Stack Template, check out how you can build on top of it with [Extensions](https://github.com/awslabs/aws-full-stack-template/tree/master/extensions). 64 | 65 |   66 | 67 | --- 68 | 69 |   70 | 71 | ## Instructions 72 | 73 | ***IMPORTANT NOTE:** Creating this application in your AWS account will create and consume AWS resources, which **will cost money**. We estimate that running this demo application will cost **<$0.10/hour** with light usage. Be sure to shut down/remove all resources once you are finished to avoid ongoing charges to your AWS account (see instructions on cleaning up/tear down below).* 74 | 75 |   76 | 77 | ### Getting started 78 | 79 | To get AWS Full-Stack Template up and running in your own AWS account, follow these steps (if you do not have an AWS account, please see [How do I create and activate a new Amazon Web Services account?](https://aws.amazon.com/premiumsupport/knowledge-center/create-and-activate-aws-account/)): 80 | 81 | 1. Log into the [AWS console](https://console.aws.amazon.com/) if you are not already. 82 | *Note: If you are logged in as an IAM user, ensure your account has permissions to create and manage the necessary resources and components for this application.* 83 | 2. Choose one of the **Launch Stack** buttons below for your desired AWS region to open the AWS CloudFormation console and create a new stack. AWS Full-Stack Template is supported in the following regions: 84 | 85 | Region name | Region code | Launch 86 | --- | --- | --- 87 | US East (N. Virginia) | us-east-1 | [![Launch Stack](https://cdn.rawgit.com/buildkite/cloudformation-launch-stack-button-svg/master/launch-stack.svg)](https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/new?stackName=MyGoalsApp&templateURL=https://s3.amazonaws.com/aws-fullstack-template/master-fullstack.yaml) 88 | US West (Oregon) | us-west-2 | [![Launch Stack](https://cdn.rawgit.com/buildkite/cloudformation-launch-stack-button-svg/master/launch-stack.svg)](https://console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/new?stackName=MyGoalsApp&templateURL=https://s3.amazonaws.com/aws-fullstack-template/master-fullstack.yaml) 89 | EU (Ireland) | eu-west-1 | [![Launch Stack](https://cdn.rawgit.com/buildkite/cloudformation-launch-stack-button-svg/master/launch-stack.svg)](https://console.aws.amazon.com/cloudformation/home?region=eu-west-1#/stacks/new?stackName=MyGoalsApp&templateURL=https://s3.amazonaws.com/aws-fullstack-template/master-fullstack.yaml) 90 | EU (Frankfurt) | eu-central-1 | [![Launch Stack](https://cdn.rawgit.com/buildkite/cloudformation-launch-stack-button-svg/master/launch-stack.svg)](https://console.aws.amazon.com/cloudformation/home?region=eu-central-1#/stacks/new?stackName=MyGoalsApp&templateURL=https://s3.amazonaws.com/aws-fullstack-template/master-fullstack.yaml) 91 | 92 | 93 | 3. Continue through the CloudFormation wizard steps 94 | 1. Name your stack, e.g. MyGoalsApp 95 | 2. Provide a project name, e.g. goalsapp (must be lowercase, letters only, and **under twelve (12) characters**). This is used when naming your resources, e.g. tables, etc. 96 | 3. After reviewing, check the blue box for creating IAM resources. 97 | 4. Choose **Create stack**. This will take ~15 minutes to complete. 98 | 5. Once the CloudFormation deployment is complete, check the status of the build in the [CodePipeline](https://console.aws.amazon.com/codesuite/codepipeline/pipelines) console and ensure it has succeeded. 99 | 6. Sign into your application 100 | 1. The output of the CloudFormation stack creation will provide a CloudFront URL (in the **Outputs** table of the stack details page). Click the link or copy and paste the CloudFront URL into your browser. 101 | 2. You can sign into your application by registering an email address and a password. Choose **Sign up to explore the demo** to register. The registration/login experience is run in your AWS account, and the supplied credentials are stored in Amazon Cognito. 102 | *Note: given that this is a demo application, we highly suggest that you do not use an email and password combination that you use for other purposes (such as an AWS account, email, or e-commerce site).* 103 | 3. Once you provide your credentials, you will receive a verification code at the email address you provided. Upon entering this verification code, you will be signed into the application. 104 | 105 |   106 | 107 | ### Cleaning up 108 | 109 | To tear down your application and remove all resources associated with AWS Full-Stack Template, follow these steps: 110 | 111 | 1. Log into the AWS CloudFormation Console and find the stack you created for the demo app 112 | 3. Delete the stack 113 | 114 | *Remember to shut down/remove all related resources once you are finished to avoid ongoing charges to your AWS account.* 115 | 116 |   117 | 118 | --- 119 | 120 |   121 | 122 | ## Architecture 123 | 124 | **Summary diagram** 125 | 126 | ![Summary diagram](readmeImages/SummaryDiagram.png) 127 | 128 |   129 | 130 | **High-level, end-to-end diagram** 131 | 132 | ![High-level architectural diagram](readmeImages/ArchDiagram.png) 133 | 134 |   135 | 136 | **Frontend** 137 | 138 | Build artifacts are stored in a S3 bucket where web application assets are maintained (web graphics, etc.). Amazon CloudFront caches the frontend content from S3, presenting the application to the user via a CloudFront distribution. The frontend interacts with Amazon Cognito and Amazon API Gateway only. Amazon Cognito is used for all authentication requests, whereas API Gateway (and Lambda) is used for all API calls to DynamoDB. 139 | 140 | **Backend** 141 | 142 | The core of the backend infrastructure consists of Amazon Cognito, Amazon DynamoDB, AWS Lambda, and Amazon API Gateway. The application leverages Amazon Cognito for user authentication, and Amazon DynamoDB to store all of the data for the goals. 143 | 144 | ![Backend diagram](readmeImages/BackendDiagram.png) 145 | 146 |   147 | 148 | **Developer Tools** 149 | 150 | The code is hosted in AWS CodeCommit. AWS CodePipeline builds the web application using AWS CodeBuild. After successfully building, CodeBuild copies the build artifacts into a S3 bucket where the web application assets are maintained (web graphics, etc.). Along with uploading to Amazon S3, CodeBuild invalidates the cache so users always see the latest experience when accessing the storefront through the Amazon CloudFront distribution. AWS CodeCommit. AWS CodePipeline, and AWS CodeBuild are used in the deployment and update processes only, not while the application is in a steady-state of use. 151 | 152 | ![Developer Tools diagram](readmeImages/DeveloperTools.png) 153 | 154 |   155 | 156 | --- 157 | 158 |   159 | 160 | ## Implementation details 161 | 162 | *Note: The provided CloudFormation template contains only a portion of the resources needed to create and run the application. There are web assets (images, etc.), Lambda functions, and other resources called from the template to create the full experience. These resources are stored in a public-facing S3 bucket and referenced in the template.* 163 | 164 |   165 | 166 | ### Amazon DynamoDB 167 | 168 | The backend of AWS Full-Stack Template leverages Amazon DynamoDB to enable dynamic scaling and the ability to add features as we improve our goals application. The application creates one table in DynamoDB; the table name will match the "ProjectName" you used when creating the stack in CloudFormation. DynamoDB's primary key consists of a partition (hash) key and an optional sort (range) key. The primary key (partition and sort key together) must be unique. 169 | 170 | **Goals Table** 171 | 172 | ```js 173 | GoalsTable { 174 | userId: string (primary partition key) 175 | goalId: string (primary sort key) 176 | title: string 177 | content: string 178 | createdAt: number 179 | } 180 | ``` 181 | 182 | The table's primary key is made up of the user ID (partition key) and the goal ID (sort key). Given that this composite primary key has both pieces of information, we can query on the user ID (partition key) alone, which will return only the goals that are in the specified user's account. It also allows us to query DynamoDB on the total composite key (user ID and goal ID), which will return goal details without additional data processing. 183 | 184 |   185 | 186 | ### Amazon API Gateway 187 | 188 | Amazon API Gateway acts as the interface layer between the frontend (Amazon CloudFront, Amazon S3) and AWS Lambda, which calls the backend (database). Below are the different APIs the application uses: 189 | 190 | **Goals (DynamoDB)** 191 | 192 | GET /goals (ListGoals) 193 | POST /goals (CreateGoal) 194 | GET /goals/{:id} (GetGoal) 195 | PUT /goals/{:id} (UpdateGoal) 196 | DELETE /goals/{:id} (DeleteGoal) 197 | 198 |   199 | 200 | ### AWS Lambda 201 | 202 | AWS Lambda is used in a few different places to run the application, as shown in the architecture diagram. The important Lambda functions that are deployed as part of the template are shown below, and available in the [functions](/functions) folder. In the cases where the response fields are blank, the application will return a statusCode 200 or 500 for success or failure, respectively. 203 | 204 |   205 | 206 | **ListGoals** 207 | 208 | Lambda function that lists the user's goals. The user's account ID is retrieved through the request context (does not need to be explicitly provided in the request). 209 | 210 | ```js 211 | ListGoalsRequest { 212 | 213 | } 214 | ``` 215 | 216 | ```js 217 | ListGoalsResponse { 218 | goals: goal[] 219 | } 220 | ``` 221 | 222 | ```js 223 | goal { 224 | goalId: string 225 | title: string 226 | content: string 227 | createdAt: number 228 | } 229 | ``` 230 | 231 |   232 | 233 | **GetGoal** 234 | 235 | Lambda function that returns the properties of a goal. 236 | 237 | ```js 238 | GetGoalRequest { 239 | goalId: string 240 | } 241 | ``` 242 | 243 | ```js 244 | GetGoalResponse { 245 | goalId: string 246 | title: string 247 | content: string 248 | createdAt: number 249 | } 250 | ``` 251 | 252 |   253 | 254 | **CreateGoal** 255 | 256 | Lambda function that creates a specified goal in the user's account. 257 | 258 | ```js 259 | CreateGoalRequest { 260 | title: string 261 | content: string 262 | } 263 | ``` 264 | 265 | ```js 266 | CreateGoalResponse { 267 | 268 | } 269 | ``` 270 | 271 |   272 | 273 | **DeleteGoal** 274 | 275 | Lambda function that removes a given goal from the user's account. 276 | 277 | ```js 278 | DeleteGoalRequest { 279 | goalId: string 280 | } 281 | ``` 282 | 283 | ```js 284 | DeleteGoalResponse { 285 | 286 | } 287 | ``` 288 | 289 |   290 | 291 | **UpdateGoal** 292 | 293 | Lambda function that updates the user's goal with a new title and/or content. 294 | 295 | ```js 296 | UpdateGoalRequest { 297 | title: string 298 | content: string 299 | } 300 | ``` 301 | 302 | ```js 303 | UpdateGoalResponse { 304 | 305 | } 306 | ``` 307 | 308 |   309 | 310 | 311 | ### AWS IAM 312 | 313 | The following IAM role (and included policies) is needed to run the application: 314 | 315 | **DynamoDbLambda** 316 | *AWS managed policy* 317 | AWSLambdaBasicExecutionRole 318 | *Inline policy* 319 | GoalsPolicy 320 |     dynamodb:PutItem - table/Goals 321 |     dynamodb:Query - table/Goals 322 |     dynamodb:UpdateItem - table/Goals 323 |     dynamodb:GetItem - table/Goals 324 |     dynamodb:Scan - table/Goals 325 |     dynamodb:DeleteItem - table/Goals 326 | 327 |   328 | 329 | ### Amazon Cognito 330 | 331 | Amazon Cognito handles user account creation and login for the goals application. For the purposes of this template, you can only browse your goals after login, which could represent the architecture of different types of web apps. Users can also choose to separate the architecture, where portions of the web app are publicly available and others are available upon login. 332 | 333 | User Authentication 334 | * Email address 335 | 336 | Amazon Cognito passes the CognitoIdentityID (which AWS Full-Stack Template uses as the Customer ID) for every user along with every request from Amazon API Gateway to Lambda, which helps the services authenticate against which user is doing what. 337 | 338 |   339 | 340 | ### Amazon CloudFront and Amazon S3 341 | 342 | Amazon CloudFront hosts the web application frontend that users interface with. This includes web assets like pages and images. For demo purposes, CloudFormation pulls these resources from S3. 343 | 344 |   345 | 346 | ### Amazon CloudWatch 347 | 348 | The capabilities provided by CloudWatch are not exposed to the end users of the web app, rather the developer/administrator can use CloudWatch logs, alarms, and graphs to track the usage and performance of their web application. 349 | 350 |   351 | 352 | ### AWS CodeCommit, AWS CodePipeline, AWS CodeBuild 353 | 354 | Similar to CloudWatch, the capabilities provided by CodeCommit, CodePipeline, and CodeBuild are not exposed to the end users of the web app. The developer/administrator can use these tools to help stage and deploy the application as it is updated and improved. 355 | 356 | ## Running your web application locally 357 | 358 | 1. If you haven't setup Git credentials for AWS CodeCommit before, head to the IAM Console. If you have already you can skip to step 5. 359 | 2. Click on your IAM user. 360 | 3. Click on the **Security credentials tab**. Scroll to the bottom and click **Generate** underneath the **HTTPS Git credentials for AWS CodeCommit**. 361 | 4. Download and save these credentials. You will use these credentials when cloning your repository. 362 |   363 | 364 | 5. Go to the CodeCommit console and find your code repository. 365 | 5. Click the HTTPS button underneath the **Clone URL** column. 366 | 6. Open up your terminal, type `git clone ` paste the Clone URL and hit enter. 367 |   368 | 369 | 7. Once the repository has created, run `npm install`. 370 | 8. After all dependencies have been downloaded, run `npm run start`. 371 |   372 | 373 | Your done! Any future updates you make to your repository get pushed to your code pipeline automatically and published to your web application endpoint. 374 | 375 |   376 | 377 | --- 378 | 379 |   380 | 381 | ## Considerations for demo purposes 382 | 383 | 1. Web assets (pages, images, etc.) are pulled from a public S3 bucket via the CloudFormation template to create the frontend for AWS Full-Stack Template. When building your own web application (or customizing this one), you will likely pull from your own S3 buckets. If you customize the lambda functions, you will want to store these separately, as well. 384 | 385 | ## Known limitations 386 | 387 | * The application was written for demonstration purposes and not for production use. 388 | * Validation is working properly from an end-user standpoint, but is not cleanly implemented. For instance, the submit buttons (to create a goal, update a goal, login, signup, and enter confirmation code) are disabled (as designed) when validation fails, but we added an extra helper function to support this. This issue occured when the app was upgraded to Bootstrap 4. We plan to fix this in a future revision. 389 | * In today's implementation, we have all of the Lambda functions associated with one IAM role. Ideally, each Lambda function would have its own scoped-down IAM role and policies. 390 | * Upon the first use of a Lambda function, cold start times can be slow. Once the Lambda function has been warmed up, performance will improve. 391 | 392 | ## Additions, forks, and contributions 393 | 394 | We are excited that you are interested in using AWS Full-Stack Template! This is a great place to start if you are just beginning with AWS and want to get a functional application up and running. It is equally useful if you are looking for a sample full-stack application to fork off of and build your own custom application. We encourage developer participation via contributions and suggested additions. Of course you are welcome to create your own version! 395 | 396 | Please see the [contributing guidelines](CONTRIBUTING.md) for more information. 397 | 398 | For just one example of how you can build on top of this, check out **[AWS Bookstore Demo App](https://github.com/aws-samples/aws-bookstore-demo-app)** which was built on top of AWS Full-Stack Template. 399 | 400 | ## Extensions 401 | 402 | Are you looking for a few extensions that you can use to build on top of the Full-Stack ecosystem? Check them out in the [Extensions](https://github.com/awslabs/aws-full-stack-template/tree/master/extensions) folder! 403 | 404 | Want to contribute an extension? Leave us a comment or submit a PR! 405 | 406 | ## Questions and contact 407 | 408 | For questions on AWS Full-Stack Template, or to contact the team, please leave a comment on GitHub. 409 | -------------------------------------------------------------------------------- /assets/README.md: -------------------------------------------------------------------------------- 1 | ## AWS Full-Stack Template 2 | 3 | AWS Full-Stack Template is a full-stack sample web application that creates a simple CRUD (create, read, update, delete) app, and provides the foundational services, components, and plumbing needed to get a basic web application up and running. [Try out the deployed application here](https://d2k5b8bzo1vefz.cloudfront.net/)! 4 | 5 | The entire application - frontend, backend, and all configuration - can deployed in your AWS account with a single CloudFormation template. Get started with building your own below! 6 |   7 | 8 | ## License Summary 9 | 10 | This sample code is made available under a modified MIT license. See the LICENSE file. 11 | 12 |   13 | 14 | ## Outline 15 | 16 | - [Overview](#overview) 17 | - [Instructions](#instructions) 18 | - [Getting started](#getting-started) 19 | - [Cleaning up](#cleaning-up) 20 | - [Architecture](#architecture) 21 | - [Implementation details](#implementation-details) 22 | - [Amazon DynamoDB](#amazon-dynamodb) 23 | - [Amazon API Gateway](#amazon-api-gateway) 24 | - [AWS Lambda](#aws-lambda) 25 | - [AWS IAM](#aws-iam) 26 | - [Amazon Cognito](#amazon-cognito) 27 | - [Amazon Cloudfront and Amazon S3](#amazon-cloudfront-and-amazon-s3) 28 | - [Amazon Cloudwatch](#amazon-cloudwatch) 29 | - [AWS CodeCommit, AWS CodePipeline, AWS CodeBuild](#aws-codecommit-aws-codepipeline-aws-codebuild) 30 | - [Considerations for demo purposes](#considerations-for-demo-purposes) 31 | - [Known limitations](#known-limitations) 32 | - [Additions, forks, and contributions](#additions-forks-and-contributions) 33 | - [Questions and contact](#questions-and-contact) 34 | 35 |   36 | 37 | ## Overview 38 | 39 | The goal of the AWS Full-Stack Template is to provide a fully-functional web application that helps users accelerate building apps on AWS by providing an out-of-the-box template. This template is production-ready and pre-loaded with best practices. Applications today have an increasing number of building blocks and infrastructure components, and AWS Full-Stack Template will help educate professionals and students alike to design software in a modern cloud computing world. With AWS Full-Stack Template, developers can create a cohesive, production-ready application on the cloud in minutes, allowing them to focus on building the pieces that matter and add value. 40 | 41 | The provided CloudFormation template automates the entire creation and deployment of the AWS Full-Stack Template. The template includes the following components: 42 | 43 | **Database components** 44 | 45 | * Goals list – Amazon DynamoDB offers fast, predictable performance for the key-value lookups needed in the goals app, and enormous scale so you can build on top of it.. In this implementation, we have unique identifiers for each goal, the goal name, and description. 46 | 47 | **Application components** 48 | 49 | * Serverless service backend – Amazon API Gateway powers the interface layer between the frontend and backend, and invokes serverless compute with AWS Lambda. 50 | * Web application blueprint – We include a React web application pre-integrated out-of-the-box with best practices and tools such as React Bootstrap, React Router, Typescript, and more. 51 | 52 | **Infrastructure components** 53 | 54 | * Continuous deployment code pipeline – AWS CodePipeline and AWS CodeBuild help you build, test, and release your application code. 55 | * Serverless web application – Amazon CloudFront and Amazon S3 provide a globally-distributed application. 56 | 57 | You can choose to customize the template to create your own goals app, modify it to make a different type of simple notes or to-do application, or add onto it to make a completely different type of web application. 58 | 59 | Users can build on top of AWS Full-Stack Template to create any application they envision, whether a travel booking tool, a blog, or another web app. The **AWS Bookstore Demo App** (available at [https://github.com/aws-samples/aws-bookstore-demo-app](https://github.com/aws-samples/aws-bookstore-demo-app)) is just one full-fledged example of what you might create using AWS Full-Stack Template. 60 | 61 |   62 | 63 | --- 64 | 65 |   66 | 67 | ## Instructions 68 | 69 | ***IMPORTANT NOTE:** Creating this application in your AWS account will create and consume AWS resources, which **will cost money**. We estimate that running this demo application will cost **<$0.10/hour** with light usage. Be sure to shut down/remove all resources once you are finished to avoid ongoing charges to your AWS account (see instructions on cleaning up/tear down below).* 70 | 71 |   72 | 73 | ### Getting started 74 | 75 | To get the AWS Full-Stack Template up and running in your own AWS account, follow these steps (if you do not have an AWS account, please see [How do I create and activate a new Amazon Web Services account?](https://aws.amazon.com/premiumsupport/knowledge-center/create-and-activate-aws-account/)): 76 | 77 | 1. Log into the [AWS console](https://console.aws.amazon.com/) if you are not already 78 | 2. Choose **Launch Stack** to open the AWS CloudFormation console and create a new stack. 79 | [![Launch Stack](https://cdn.rawgit.com/buildkite/cloudformation-launch-stack-button-svg/master/launch-stack.svg)](https://console.aws.amazon.com/cloudformation/home#/stacks/new?stackName=MyGoalsApp&templateURL=https://s3.amazonaws.com/aws-fullstack-template/master-fullstack.yaml) 80 | 3. Continue through the CloudFormation wizard steps 81 | 1. Name your stack, i.e. MyGoalsApp 82 | 2. Name your S3 bucket (must be lowercase and has to unique across all existing bucket names in Amazon S3). See [bucket naming rules](https://docs.aws.amazon.com/AmazonS3/latest/dev//BucketRestrictions.html#bucketnamingrules). 83 | 3. Provide a project name (must be lowercase, letters only, and **under ten characters**). This is used when naming your resources, e.g. tables, etc.). 84 | 4. After reviewing, check the blue box for creating IAM resources. 85 | 4. Choose **Create stack**. This will take ~15 minutes to complete. 86 | 5. Sign into your application 87 | 1. The output of the CloudFormation stack creation will provide a CloudFront URL (in the *Outputs* section of your stack details page). Copy and paste the CloudFront URL into your browser. 88 | 2. You can sign into your application by registering an email address and a password. Choose **Sign up to explore the demo** to register. The registration/login experience is run in your AWS account, and the supplied credentials are stored in Amazon Cognito. 89 | *Note: given that this is a demo application, we highly suggest that you do not use an email and password combination that you use for other purposes (such as an AWS account, email, or e-commerce site).* 90 | 3. Once you provide your credentials, you will receive a verification code at the email address you provided. Upon entering this verification code, you will be signed into the application. 91 | 92 |   93 | 94 | ### Cleaning up 95 | 96 | To tear down your application and remove all resources associated with the AWS Full-Stack Template, follow these steps: 97 | 98 | 1. Log into the AWS CloudFormation Console and find the stack you created for the demo app 99 | 2. Delete the stack 100 | 1. Double-check that the S3 buckets created for the stack were successfully removed. 101 | 102 | *Remember to shut down/remove all related resources once you are finished to avoid ongoing charges to your AWS account.* 103 | 104 |   105 | 106 | --- 107 | 108 |   109 | 110 | ## Architecture 111 | 112 | **Summary diagram** 113 | 114 | ![Summary diagram](readmeImages/SummaryDiagram.png) 115 | 116 |   117 | 118 | **High-level, end-to-end diagram** 119 | 120 | ![High-level architectural diagram](readmeImages/ArchDiagram.png) 121 | 122 |   123 | 124 | **Frontend** 125 | 126 | Build artifacts are stored in a S3 bucket where web application assets are maintained (web graphics, etc.). Amazon CloudFront caches the frontend content from S3, presenting the application to the user via a CloudFront distribution. The frontend interacts with Amazon Cognito and Amazon API Gateway only. Amazon Cognito is used for all authentication requests, whereas API Gateway (and Lambda) is used for all API calls to DynamoDB. 127 | 128 | **Backend** 129 | 130 | The core of the backend infrastructure consists of Amazon Cognito, Amazon DynamoDB, AWS Lambda, and Amazon API Gateway. The application leverages Amazon Cognito for user authentication, and Amazon DynamoDB to store all of the data for the goals. 131 | 132 | ![Backend diagram](readmeImages/BackendDiagram.png) 133 | 134 |   135 | 136 | **Developer Tools** 137 | 138 | The code is hosted in AWS CodeCommit. AWS CodePipeline builds the web application using AWS CodeBuild. After successfully building, CodeBuild copies the build artifacts into a S3 bucket where the web application assets are maintained (web graphics, etc.). Along with uploading to Amazon S3, CodeBuild invalidates the cache so users always see the latest experience when accessing the storefront through the Amazon CloudFront distribution. AWS CodeCommit. AWS CodePipeline, and AWS CodeBuild are used in the deployment and update processes only, not while the application is in a steady-state of use. 139 | 140 | ![Developer Tools diagram](readmeImages/DeveloperTools.png) 141 | 142 |   143 | 144 | --- 145 | 146 |   147 | 148 | ## Implementation details 149 | 150 | *Note: The provided CloudFormation template contains only a portion of the resources needed to create and run the application. There are web assets (images, etc.), Lambda functions, and other resources called from the template to create the full experience. These resources are stored in a public-facing S3 bucket and referenced in the template.* 151 | 152 |   153 | 154 | ### Amazon DynamoDB 155 | 156 | The backend of the AWS Full-Stack Template leverages Amazon DynamoDB to enable dynamic scaling and the ability to add features as we improve our goals application. The application creates one table in DynamoDB: *Goals.* DynamoDB's primary key consists of a partition (hash) key and an optional sort (range) key. The primary key (partition and sort key together) must be unique. 157 | 158 | **Goals Table** 159 | 160 | ```js 161 | GoalsTable { 162 | userId: string (primary partition key) 163 | goalId: string (primary sort key) 164 | title: string 165 | content: string 166 | createdAt: number 167 | } 168 | ``` 169 | 170 | The table's primary key is made up of the user ID (partition key) and the goal ID (sort key). This composite primary key allows us to use DynamoDB's scan capability - with only the user ID set - and return only the goals that that are in the user's account. It also allows us to query DynamoDB on a user ID and goal ID, returning goal details without additional data processing. 171 | 172 |   173 | 174 | ### Amazon API Gateway 175 | 176 | Amazon API Gateway acts as the interface layer between the frontend (Amazon CloudFront, Amazon S3) and AWS Lambda, which calls the backend (database). Below are the different APIs the application uses: 177 | 178 | **Goals (DynamoDB)** 179 | 180 | GET /goals (ListGoals) 181 | POST /goals (CreateGoal) 182 | GET /goals/{:id} (GetGoal) 183 | PUT /goals/{:id} (UpdateGoal) 184 | DELETE /goals/{:id} (DeleteGoal) 185 | 186 |   187 | 188 | ### AWS Lambda 189 | 190 | AWS Lambda is used in a few different places to run the application, as shown in the architecture diagram. The important Lambda functions that are deployed as part of the template are shown below, and available in the [functions](/functions) folder. In the cases where the response fields are blank, the application will return a statusCode 200 or 500 for success or failure, respectively. 191 | 192 |   193 | 194 | **ListGoals** 195 | 196 | Lambda function that lists the user's goals. The user's account ID is retrieved through the request context (does not need to be explicity provided in the request). 197 | 198 | ```js 199 | ListGoalsRequest { 200 | 201 | } 202 | ``` 203 | 204 | ```js 205 | ListGoalsResponse { 206 | goals: goal[] 207 | } 208 | ``` 209 | 210 | ```js 211 | goal { 212 | goalId: string 213 | title: string 214 | content: string 215 | createdAt: number 216 | } 217 | ``` 218 | 219 |   220 | 221 | **GetGoal** 222 | 223 | Lambda function that returns the properties of a goal. 224 | 225 | ```js 226 | GetGoalRequest { 227 | goalId: string 228 | } 229 | ``` 230 | 231 | ```js 232 | GetGoalResponse { 233 | goalId: string 234 | title: string 235 | content: string 236 | createdAt: number 237 | } 238 | ``` 239 | 240 |   241 | 242 | **CreateGoal** 243 | 244 | Lambda function that creates a specified goal in the user's account. 245 | 246 | ```js 247 | CreateGoalRequest { 248 | title: string 249 | content: string 250 | } 251 | ``` 252 | 253 | ```js 254 | CreateGoalResponse { 255 | 256 | } 257 | ``` 258 | 259 |   260 | 261 | **DeleteGoal** 262 | 263 | Lambda function that removes a given goal from the user's account. 264 | 265 | ```js 266 | DeleteGoalRequest { 267 | goalId: string 268 | } 269 | ``` 270 | 271 | ```js 272 | DeleteGoalResponse { 273 | 274 | } 275 | ``` 276 | 277 |   278 | 279 | **UpdateGoal** 280 | 281 | Lambda function that updates the user's goal with a new title and/or content. 282 | 283 | ```js 284 | UpdateGoalRequest { 285 | title: string 286 | content: string 287 | } 288 | ``` 289 | 290 | ```js 291 | UpdateGoalResponse { 292 | 293 | } 294 | ``` 295 | 296 |   297 | 298 | 299 | ### AWS IAM 300 | 301 | The following IAM role (and included policies) is needed to run the application: 302 | 303 | **DynamoDbLambda** 304 | *AWS managed policy* 305 | AWSLambdaBasicExecutionRole 306 | *Inline policy* 307 | GoalsPolicy 308 |     dynamodb:PutItem - table/Goals 309 |     dynamodb:Query - table/Goals 310 |     dynamodb:UpdateItem - table/Goals 311 |     dynamodb:GetItem - table/Goals 312 |     dynamodb:Scan - table/Goals 313 |     dynamodb:DeleteItem - table/Goals 314 | 315 |   316 | 317 | ### Amazon Cognito 318 | 319 | Amazon Cognito handles user account creation and login for the goals application. For the purposes of this template, you can only browse your goals after login, which could represent the architecture of different types of web apps. Users can also choose to separate the architecture, where portions of the web app are publicly available and others are available upon login. 320 | 321 | User Authentication 322 | * Email address 323 | 324 | Amazon Cognito passes the CognitoIdentityID (which AWS Full-Stack Template uses as the Customer ID) for every user along with every request from Amazon API Gateway to Lambda, which helps the services authenticate against which user is doing what. 325 | 326 |   327 | 328 | ### Amazon CloudFront and Amazon S3 329 | 330 | Amazon CloudFront hosts the web application frontend that users interface with. This includes web assets like pages and images. For demo purposes, CloudFormation pulls these resources from S3. 331 | 332 |   333 | 334 | ### Amazon CloudWatch 335 | 336 | The capabilities provided by CloudWatch are not exposed to the end users of the web app, rather the developer/administrator can use CloudWatch logs, alarms, and graphs to track the usage and performance of their web application. 337 | 338 |   339 | 340 | ### AWS CodeCommit, AWS CodePipeline, AWS CodeBuild 341 | 342 | Similar to CloudWatch, the capabilities provided by CodeCommit, CodePipeline, and CodeBuild are not exposed to the end users of the web app. The developer/administrator can use these tools to help stage and deploy the application as it is updated and improved. 343 | 344 |   345 | 346 | --- 347 | 348 |   349 | 350 | ## Considerations for demo purposes 351 | 352 | 1. Web assets (pages, images, etc.) are pulled from a public S3 bucket via the CloudFormation template to create the frontend for the AWS Full-Stack Template. When building your own web application (or customizing this one), you will likely pull from your own S3 buckets. If you customize the lambda functions, you will want to store these separately, as well. 353 | 354 |   355 | 356 | --- 357 | 358 |   359 | 360 | ## Known limitations 361 | 362 | * The application was written for demonstration purposes and not for production use. 363 | * Validation is working properly from an end-user standpoint, but is not cleanly implemented. For instance, the submit buttons (to create a goal, update a goal, login, signup, and enter confirmation code) are disabled (as designed) when validation fails, but we added an extra helper function to support this. This issue occured when the app was upgraded to Bootstrap 4. We plan to fix this in a future revision. 364 | * In today's implementation, we have all of the Lambda functions associated with one IAM role. Ideally, each Lambda function would have its own scoped-down IAM role and policies. 365 | * Upon the first use of a Lambda function, cold start times can be slow. Once the Lambda function has been warmed up, performance will improve. 366 | 367 |   368 | 369 | --- 370 | 371 |   372 | 373 | ## Additions, forks, and contributions 374 | 375 | We are excited that you are interested in using the AWS Full-Stack Template! This is a great place to start if you are just beginning with AWS and want to get a functional application up and running. It is equally useful if you are looking for a sample full-stack application to fork off of and build your own custom application. We encourage developer participation via contributions and suggested additions. Of course you are welcome to create your own version! 376 | 377 | Please see the [contributing guidelines](CONTRIBUTING.md) for more information. 378 | 379 | For just one example of how you can build on top of this, check out **AWS Bookstore Demo App** (available at [https://github.com/aws-samples/aws-bookstore-demo-app](https://github.com/aws-samples/aws-bookstore-demo-app)), which was built on top of AWS Full Stack Template. 380 | 381 |   382 | 383 | --- 384 | 385 |   386 | 387 | ## Questions and contact 388 | 389 | For questions on the AWS Full-Stack Template, or to contact the team, please leave a comment on GitHub. 390 | -------------------------------------------------------------------------------- /assets/index.html: -------------------------------------------------------------------------------- 1 |

Hello world!

2 | -------------------------------------------------------------------------------- /assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-full-stack-template", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@aws-amplify/api": "1.0.39", 7 | "@aws-amplify/auth": "1.2.28", 8 | "@aws-amplify/core": "1.0.29", 9 | "@types/graphql": "14.2.3", 10 | "@types/jest": "^23.3.14", 11 | "@types/node": "^10.14.13", 12 | "@types/react": "^16.8.23", 13 | "@types/react-dom": "^16.8.3", 14 | "@types/react-router-bootstrap": "^0.24.5", 15 | "@types/react-router-dom": "^4.3.4", 16 | "@types/zen-observable": "0.8.0", 17 | "bootstrap": "^4.3.1", 18 | "graphql": "14.4.2", 19 | "react": "^16.8.0", 20 | "react-bootstrap": "^1.0.0-beta.9", 21 | "react-dom": "^16.8.6", 22 | "react-router-bootstrap": "^0.24.4", 23 | "react-router-dom": "^4.3.1", 24 | "react-scripts": "^3.4.1", 25 | "typescript": "3.2.4", 26 | "zen-observable": "0.8.14" 27 | }, 28 | "scripts": { 29 | "start": "react-scripts start", 30 | "build": "react-scripts build", 31 | "test": "react-scripts test --env=jsdom", 32 | "eject": "react-scripts eject" 33 | }, 34 | "devDependencies": {}, 35 | "browserslist": [ 36 | ">0.2%", 37 | "not dead", 38 | "not ie <= 11", 39 | "not op_mini all" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /assets/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-full-stack-template/ac72318d8689203b1c3fa8c71f3fed16e90248c3/assets/public/favicon.ico -------------------------------------------------------------------------------- /assets/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | Goals 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /assets/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Goals", 3 | "name": "AWS Full-Stack Template", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#252f3d", 14 | "background_color": "#252f3d" 15 | } -------------------------------------------------------------------------------- /assets/readmeImages/ArchDiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-full-stack-template/ac72318d8689203b1c3fa8c71f3fed16e90248c3/assets/readmeImages/ArchDiagram.png -------------------------------------------------------------------------------- /assets/readmeImages/BackendDiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-full-stack-template/ac72318d8689203b1c3fa8c71f3fed16e90248c3/assets/readmeImages/BackendDiagram.png -------------------------------------------------------------------------------- /assets/readmeImages/DeveloperTools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-full-stack-template/ac72318d8689203b1c3fa8c71f3fed16e90248c3/assets/readmeImages/DeveloperTools.png -------------------------------------------------------------------------------- /assets/readmeImages/SummaryDiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-full-stack-template/ac72318d8689203b1c3fa8c71f3fed16e90248c3/assets/readmeImages/SummaryDiagram.png -------------------------------------------------------------------------------- /assets/readmeImages/images.md: -------------------------------------------------------------------------------- 1 | ## AWS Full-Stack Template README Images 2 | 3 | Images in this directory are for the purposes of showing up in the README.md file 4 | -------------------------------------------------------------------------------- /assets/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | margin-top: 15px; 3 | color: white; 4 | } 5 | 6 | .navbar-default{ 7 | background-color: transparent !important; 8 | box-shadow: none !important; 9 | background-image: none !important; 10 | border: 0 !important; 11 | } 12 | 13 | .navbar-default .navbar-brand{ 14 | text-shadow: none !important; 15 | padding: 20px 0; 16 | font-size: 28px; 17 | line-height:30px; 18 | } 19 | 20 | .navbar-brand, .navbar-nav > li > a{ 21 | color: white !important; 22 | text-shadow: none !important; 23 | font-size: 20px; 24 | } 25 | 26 | .navbar-brand, .navbar-nav > li{ 27 | padding: 10px 0; 28 | line-height:30px; 29 | display: block; 30 | } 31 | 32 | .navbar-default .navbar-nav > .open > a, .navbar-default .navbar-nav > .active > a{ 33 | background-image: none !important; 34 | background-color: transparent !important; 35 | box-shadow: none !important; 36 | } 37 | .navbar-default .navbar-nav > .open > a, .navbar-default .navbar-nav > .active > a:active{ 38 | background-image: none !important; 39 | background-color: transparent !important; 40 | box-shadow: none !important; 41 | } 42 | 43 | .navbar-default .navbar-nav > .open > a, .navbar-default .navbar-nav > .active > a:focus{ 44 | background-image: none !important; 45 | background-color: transparent !important; 46 | box-shadow: none !important; 47 | } -------------------------------------------------------------------------------- /assets/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /assets/src/App.tsx: -------------------------------------------------------------------------------- 1 | import Auth from "@aws-amplify/auth"; 2 | import React, { Component } from "react"; 3 | import { withRouter } from "react-router-dom"; 4 | import { Form, Nav, Navbar, Button } from "react-bootstrap"; 5 | import "./App.css"; 6 | import { Routes } from "./Routes"; 7 | 8 | interface AppProps { 9 | history: any; 10 | } 11 | 12 | interface AppState { 13 | isAuthenticated: boolean; 14 | isAuthenticating: boolean; 15 | } 16 | 17 | class App extends Component { 18 | constructor(props: AppProps) { 19 | super(props); 20 | 21 | this.state = { 22 | isAuthenticated: false, 23 | isAuthenticating: true 24 | }; 25 | 26 | document.title = "Goals" 27 | } 28 | 29 | async componentDidMount() { 30 | try { 31 | if (await Auth.currentSession()) { 32 | this.userHasAuthenticated(true); 33 | } 34 | } 35 | catch (e) { 36 | if (e !== 'No current user') { 37 | alert(e); 38 | } 39 | } 40 | 41 | this.setState({ isAuthenticating: false }); 42 | } 43 | 44 | userHasAuthenticated = (authenticated: boolean) => { 45 | this.setState({ isAuthenticated: authenticated }); 46 | } 47 | 48 | handleLogout = async () => { 49 | await Auth.signOut(); 50 | 51 | this.userHasAuthenticated(false); 52 | this.props.history.push("/login"); 53 | } 54 | 55 | showLoggedInBar = () => ( 56 |
57 | 58 |
59 | ); 60 | 61 | showLoggedOutBar = () => ( 62 |
63 | 64 | 65 |
66 | ); 67 | 68 | render() { 69 | const childProps = { 70 | isAuthenticated: this.state.isAuthenticated, 71 | userHasAuthenticated: this.userHasAuthenticated 72 | }; 73 | 74 | return ( 75 | !this.state.isAuthenticating && 76 |
77 | 78 | AWS Full-Stack Template 79 | 80 | 81 | 84 | 85 | 86 | 87 |
88 | ); 89 | } 90 | } 91 | 92 | export default withRouter(App as any); -------------------------------------------------------------------------------- /assets/src/Routes.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route, Switch } from "react-router-dom"; 3 | import PropsRoute from "./common/PropsRoute"; 4 | import Home from "./modules/signup/Home"; 5 | import Login from "./modules/signup/Login"; 6 | import Signup from "./modules/signup/Signup"; 7 | import AddEditGoal from "./modules/goal/AddEditGoal"; 8 | import NotFound from "./modules/notFound/NotFound"; 9 | 10 | interface RouteProps { 11 | isAuthenticated: boolean; 12 | userHasAuthenticated: (authenticated: boolean) => void; 13 | } 14 | 15 | export const Routes: React.SFC = (childProps) => 16 | 17 | 18 | 19 | 20 | 21 | 22 | ; -------------------------------------------------------------------------------- /assets/src/common/PropsRoute.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route } from "react-router"; 3 | 4 | //@ts-ignore 5 | export default ({ component: C, props: cProps, ...rest }) => 6 | } />; -------------------------------------------------------------------------------- /assets/src/config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | MAX_ATTACHMENT_SIZE: 5000000, 3 | apiGateway: { 4 | REGION: "us-east-1", 5 | API_URL: "https://fjngnfrqig.execute-api.us-east-1.amazonaws.com/prod", 6 | }, 7 | cognito: { 8 | REGION: "us-east-1", 9 | USER_POOL_ID: "us-east-1_9zEFUKHfG", 10 | APP_CLIENT_ID: "q4apguuksr142bto3trnuu91e", 11 | IDENTITY_POOL_ID: "us-east-1:7a11df8f-43ae-4e5e-a787-1956fc01a2a8" 12 | } 13 | }; -------------------------------------------------------------------------------- /assets/src/images/full-stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-full-stack-template/ac72318d8689203b1c3fa8c71f3fed16e90248c3/assets/src/images/full-stack.png -------------------------------------------------------------------------------- /assets/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | background: #252f3d !important; 6 | } 7 | -------------------------------------------------------------------------------- /assets/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter as Router } from "react-router-dom"; 4 | import './index.css'; 5 | import App from './App'; 6 | import registerServiceWorker from './registerServiceWorker'; 7 | import Amplify from "@aws-amplify/core"; 8 | import config from "./config"; 9 | 10 | import 'bootstrap/dist/css/bootstrap.css'; 11 | 12 | Amplify.configure({ 13 | Auth: { 14 | mandatorySignIn: true, 15 | region: config.cognito.REGION, 16 | userPoolId: config.cognito.USER_POOL_ID, 17 | identityPoolId: config.cognito.IDENTITY_POOL_ID, 18 | userPoolWebClientId: config.cognito.APP_CLIENT_ID 19 | }, 20 | API: { 21 | endpoints: [ 22 | { 23 | name: "goals", 24 | endpoint: config.apiGateway.API_URL, 25 | region: config.apiGateway.REGION 26 | }, 27 | ] 28 | } 29 | }); 30 | 31 | ReactDOM.render( 32 | 33 | 34 | , 35 | document.getElementById('root') 36 | ); 37 | registerServiceWorker(); 38 | -------------------------------------------------------------------------------- /assets/src/modules/goal/AddEditGoal.css: -------------------------------------------------------------------------------- 1 | .goal { 2 | margin: 0 auto; 3 | width: 80%; 4 | } 5 | 6 | .goal form { 7 | padding-bottom: 15px; 8 | } 9 | 10 | .header { 11 | margin-bottom: 10px; 12 | float: right; 13 | } 14 | 15 | .form-body { 16 | clear: both; 17 | } 18 | 19 | .goal .form-group .control-label { 20 | font-size: 16px !important; 21 | } 22 | 23 | .modal-text { 24 | color: black !important; 25 | } 26 | 27 | #contained-modal { 28 | color: black; 29 | } -------------------------------------------------------------------------------- /assets/src/modules/goal/AddEditGoal.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import API from "@aws-amplify/api"; 3 | import { Button, FormGroup, FormControl, Modal, FormLabel, Spinner, Form } from "react-bootstrap"; 4 | import { Redirect } from "react-router-dom"; 5 | 6 | import "./AddEditGoal.css"; 7 | 8 | interface AddEditGoalProps { 9 | match: any; 10 | history: any; 11 | } 12 | 13 | interface AddEditGoalState { 14 | isExistingGoal: boolean; 15 | isLoading: boolean; 16 | isUpdating: boolean; 17 | isDeleting: boolean; 18 | goal: Goal; 19 | showDeleteModal: boolean; 20 | redirect: string; 21 | } 22 | 23 | interface Goal { 24 | goalId: string; 25 | title: string; 26 | content: string; 27 | } 28 | 29 | export default class AddEditGoal extends Component { 30 | constructor(props: AddEditGoalProps) { 31 | super(props); 32 | 33 | this.state = { 34 | redirect: '', 35 | isExistingGoal: false, 36 | isLoading: false, 37 | isUpdating: false, 38 | isDeleting: false, 39 | showDeleteModal: false, 40 | goal: { 41 | goalId: '', 42 | title: '', 43 | content: '', 44 | }, 45 | }; 46 | } 47 | 48 | componentDidMount() { 49 | const id = this.props.match.params.id; 50 | if (id) { 51 | this.getGoal(id); 52 | this.setState({ 53 | isExistingGoal: true, 54 | }); 55 | } 56 | 57 | } 58 | 59 | getGoal(goalId: string) { 60 | this.setState({ 61 | isLoading: true, 62 | }); 63 | 64 | return API.get("goals", `/goals/${goalId}`, null).then((value: any) => { 65 | this.setState({ 66 | isLoading: false, 67 | goal: { 68 | title: value.title, 69 | content: value.content, 70 | goalId: this.props.match.params.id, 71 | } 72 | }); 73 | }); 74 | } 75 | 76 | validateForm = () => { 77 | return this.state.goal.title.length > 0 && this.state.goal.content.length > 0; 78 | } 79 | 80 | handleChange = (event: any) => { 81 | const { id, value } = event.target; 82 | this.setState({ 83 | goal: { 84 | ...this.state.goal, 85 | [id]: value 86 | } 87 | } as any); 88 | } 89 | 90 | handleCancel = (event: any) => { 91 | this.setState({ 92 | redirect: '/' 93 | }); 94 | } 95 | 96 | handleSubmit = async (event: any) => { 97 | this.setState ({ 98 | isUpdating: true, 99 | }); 100 | event.preventDefault(); 101 | this.state.isExistingGoal ? this.updateGoal() : this.saveGoal(); 102 | } 103 | 104 | updateGoal = () => { 105 | const { goal } = this.state; 106 | return API.put("goals", `/goals/${this.props.match.params.id}`, { 107 | body: { 108 | title: goal.title, 109 | content: goal.content 110 | } 111 | }).then((value: any) => { 112 | this.setState({ 113 | isUpdating: false, 114 | redirect: '/' 115 | }); 116 | }); 117 | } 118 | 119 | saveGoal = () => { 120 | const { goal } = this.state; 121 | return API.post("goals", "/goals", { 122 | body: { 123 | title: goal.title, 124 | content: goal.content 125 | } 126 | }).then((value: any) => { 127 | this.setState({ 128 | isUpdating: false, 129 | redirect: '/' 130 | }); 131 | }); 132 | } 133 | 134 | showDeleteModal = (shouldShow: boolean) => { 135 | this.setState({ 136 | showDeleteModal: shouldShow 137 | }); 138 | } 139 | 140 | handleDelete = (event: any) => { 141 | this.setState({ 142 | isDeleting: true, 143 | }) 144 | 145 | return API.del("goals", `/goals/${this.props.match.params.id}`, null).then((value: any) => { 146 | this.setState({ 147 | isDeleting: false, 148 | showDeleteModal: false, 149 | redirect: '/' 150 | }); 151 | }); 152 | 153 | } 154 | 155 | deleteModal() { 156 | return ( 157 | this.showDeleteModal(false)} 160 | container={this} 161 | aria-labelledby="contained-modal-title" 162 | id="contained-modal"> 163 | 164 | Delete goal 165 | 166 | 167 | Are you sure you want to delete this goal? 168 | 169 | 170 | 177 | 178 | 179 | ); 180 | } 181 | 182 | render() { 183 | const { goal, isExistingGoal, showDeleteModal, redirect } = this.state; 184 | 185 | if (redirect) { 186 | return ; 187 | } 188 | 189 | return ( 190 |
191 | {this.state.isLoading ? 192 | : 193 | 194 |
195 | 196 |
197 | 198 | Goal title 199 | 0} 204 | placeholder="Enter goal title.." 205 | required /> 206 | 207 | 208 | 209 | Goal description 210 | 0} 215 | placeholder="Enter goal description.." 216 | as="textarea" 217 | required /> 218 | 219 |
220 | 221 | {isExistingGoal && 222 | } 227 | 228 | 238 | 239 | 245 |
} 246 | 247 | {showDeleteModal && this.deleteModal()} 248 | 249 |
250 | ); 251 | } 252 | } -------------------------------------------------------------------------------- /assets/src/modules/notFound/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./notFound.css"; 3 | 4 | export default () => 5 |
6 |

Sorry, page not found!

7 |
; -------------------------------------------------------------------------------- /assets/src/modules/notFound/notFound.css: -------------------------------------------------------------------------------- 1 | .NotFound { 2 | padding-top: 100px; 3 | text-align: center; 4 | } -------------------------------------------------------------------------------- /assets/src/modules/signup/Home.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Button, Table, Spinner } from "react-bootstrap"; 3 | import API from "@aws-amplify/api"; 4 | import { Redirect } from "react-router-dom"; 5 | 6 | import fullStack from "../../images/full-stack.png"; 7 | import "./home.css"; 8 | 9 | interface HomeProps { 10 | isAuthenticated: boolean; 11 | } 12 | 13 | interface HomeState { 14 | isLoading: boolean; 15 | goals: Goal[]; 16 | redirect: boolean; 17 | } 18 | 19 | interface Goal { 20 | content: string; 21 | goalId: string; 22 | title: string; 23 | createdAt: Date; 24 | } 25 | 26 | export default class Home extends Component { 27 | constructor(props: HomeProps) { 28 | super(props); 29 | 30 | this.state = { 31 | isLoading: true, 32 | goals: [], 33 | redirect: false, 34 | }; 35 | } 36 | 37 | async componentDidMount() { 38 | if (!this.props.isAuthenticated) { 39 | return; 40 | } 41 | 42 | try { 43 | const goals = await this.goals(); 44 | this.setState({ goals }); 45 | } catch (e) { 46 | alert(e); 47 | } 48 | 49 | this.setState({ isLoading: false }); 50 | } 51 | 52 | goals() { 53 | return API.get("goals", "/goals", null); 54 | } 55 | 56 | renderGoalsList(goals: Goal[]) { 57 | let goalsList: Goal[] = []; 58 | 59 | return goalsList.concat(goals).map( 60 | (goal, i) => 61 | 62 | {goal.title} 63 |
{goal.content.trim().split("\n")[0]}
64 | {new Date(goal.createdAt).toLocaleString()} 65 | 66 | ); 67 | } 68 | 69 | onCreate = () => { 70 | this.setState({ redirect: true }); 71 | } 72 | 73 | renderLanding() { 74 | return ( 75 |
76 |

AWS Goals App

77 |
78 |

This is a sample application that creates a simple CRUD (create, read, update, delete) app, and provides the foundational services, components, and plumbing needed to get a basic web application up and running. In this "goals" app, users can create goals, add descriptions, and update or remove their goals. You can get this sample application up and running in your own environment and learn more about the architecture of the app by looking at the github repository.

79 | 82 | Screenshot 83 |
); 84 | } 85 | 86 | renderHome() { 87 | return ( 88 |
89 |

Goals

90 |
91 | 92 |
93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | { 103 | this.state.isLoading ? 104 | ( 105 | 108 | ) : 109 | this.renderGoalsList(this.state.goals) 110 | } 111 | 112 |
Goal nameDescriptionDate created
106 | 107 |
113 |
114 | ); 115 | } 116 | 117 | render() { 118 | let { redirect } = this.state; 119 | if (redirect) { 120 | return ; 121 | } 122 | 123 | return ( 124 |
125 | {this.props.isAuthenticated ? this.renderHome() : this.renderLanding()} 126 |
127 | ); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /assets/src/modules/signup/Login.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Redirect } from 'react-router'; 3 | import { Form, FormGroup, FormControl, FormLabel, Button, Spinner, FormControlProps } from "react-bootstrap"; 4 | import Auth from "@aws-amplify/auth"; 5 | import "./login.css"; 6 | 7 | const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 8 | 9 | interface LoginProps { 10 | isAuthenticated: boolean; 11 | userHasAuthenticated: (authenticated: boolean) => void; 12 | } 13 | 14 | interface LoginState { 15 | loading: boolean; 16 | redirect: boolean; 17 | email: string; 18 | password: string; 19 | isValid: boolean; 20 | } 21 | 22 | export default class Login extends React.Component { 23 | constructor(props: LoginProps) { 24 | super(props); 25 | 26 | this.state = { 27 | loading: false, 28 | redirect: false, 29 | email: "", 30 | password: "", 31 | isValid: false, 32 | }; 33 | } 34 | 35 | onChange = (event: React.FormEvent) => { 36 | const target = event.target as HTMLInputElement; 37 | this.setState({ ...this.state, [target.name]: target.value }); 38 | } 39 | 40 | validateForm = () => { 41 | return emailRegex.test(this.state.email.toLowerCase()) && this.state.password; 42 | } 43 | 44 | onLogin = async (event: React.FormEvent) => { 45 | if (event.currentTarget.checkValidity()) { 46 | event.preventDefault(); 47 | event.stopPropagation(); 48 | this.setState({ isValid: true, loading: true }); 49 | 50 | try { 51 | await Auth.signIn(this.state.email, this.state.password); 52 | this.props.userHasAuthenticated(true); 53 | this.setState({ redirect: true }) 54 | } catch (e) { 55 | alert(e.message); 56 | this.setState({ loading: false }); 57 | } 58 | } 59 | } 60 | 61 | render() { 62 | if (this.state.redirect) return 63 | const { email, password, isValid } = this.state; 64 | 65 | return ( 66 |
67 |
68 | 69 | Email 70 | 77 | Must be a valid email address 78 | 79 | 80 | Password 81 | = 8} 88 | required /> 89 | Required field 90 | 91 | 99 |
100 |
101 | ); 102 | } 103 | } -------------------------------------------------------------------------------- /assets/src/modules/signup/Signup.tsx: -------------------------------------------------------------------------------- 1 | import Auth from "@aws-amplify/auth"; 2 | import React from "react"; 3 | import { Redirect } from 'react-router'; 4 | import { Form, FormGroup, FormControl, FormLabel, Button, Spinner, FormControlProps } from "react-bootstrap"; 5 | import "./signup.css"; 6 | import "./home.css"; 7 | import { ISignUpResult } from "amazon-cognito-identity-js"; 8 | 9 | const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 10 | 11 | interface SignupProps { 12 | isAuthenticated: boolean; 13 | userHasAuthenticated: (authenticated: boolean) => void; 14 | } 15 | 16 | interface SignupState { 17 | loading: boolean; 18 | email: string; 19 | password: string; 20 | confirmPassword: string; 21 | confirmationCode: string; 22 | user: any; 23 | redirect: boolean; 24 | validated: boolean; 25 | } 26 | 27 | export default class Signup extends React.Component { 28 | constructor(props: SignupProps) { 29 | super(props); 30 | 31 | this.state = { 32 | loading: false, 33 | email: "", 34 | password: "", 35 | confirmPassword: "", 36 | confirmationCode: "", 37 | user: undefined, 38 | redirect: false, 39 | validated: false, 40 | }; 41 | } 42 | 43 | onChange = (event: React.FormEvent) => { 44 | const target = event.target as HTMLInputElement; 45 | this.setState({ ...this.state, [target.name]: target.value }); 46 | } 47 | 48 | onSignup = (event: React.FormEvent) => { 49 | event.preventDefault() 50 | 51 | if (event.currentTarget.checkValidity() === false) { 52 | event.stopPropagation(); 53 | } else { 54 | this.setState({ loading: true, validated: true }); 55 | 56 | Auth.signUp({ 57 | username: this.state.email, 58 | password: this.state.password 59 | }).then((value: ISignUpResult) => { 60 | this.setState({ user: value.user, loading: false }); 61 | }).catch((e: any) => { 62 | alert(e.message); 63 | this.setState({ loading: false }); 64 | }); 65 | } 66 | this.setState({ validated: true }); 67 | 68 | } 69 | 70 | onConfirm = async (event: React.FormEvent) => { 71 | event.preventDefault(); 72 | this.setState({ loading: true }); 73 | 74 | try { 75 | await Auth.confirmSignUp(this.state.email, this.state.confirmationCode); 76 | await Auth.signIn(this.state.email, this.state.password); 77 | this.props.userHasAuthenticated(true); 78 | this.setState({ redirect: true }) 79 | } catch (e) { 80 | alert(e.message); 81 | this.setState({ loading: false }); 82 | } 83 | } 84 | 85 | validateSignupForm = () => { 86 | return emailRegex.test(this.state.email.toLowerCase()) && 87 | this.state.confirmPassword.length >= 8 && 88 | this.state.password === this.state.confirmPassword 89 | } 90 | 91 | validateConfirmForm = () => { 92 | return this.state.confirmationCode; 93 | } 94 | 95 | showConfirmationForm = () => { 96 | if (this.state.redirect) return 97 | const { confirmationCode } = this.state; 98 | 99 | return ( 100 |
101 | 102 | Confirmation code 103 | 110 | 111 | 112 | A confirmation code will be sent to the email address provided 113 | 114 | 115 | 120 |
121 | ); 122 | } 123 | 124 | showSignupForm = () => { 125 | const { email, password, confirmPassword, validated } = this.state; 126 | return ( 127 |
) => this.onSignup(e)}> 128 | 129 | Email 130 | 138 | Must be a valid email address 139 | 140 | 141 | Password 142 | = 8} 149 | required /> 150 | Must be at least 8 characters 151 | 152 | Must be at least 8 characters 153 | 154 | 155 | 156 | Confirm Password 157 | = 8 && password === confirmPassword} 164 | required /> 165 | Passwords must be identical 166 | 167 | 172 |
173 | ); 174 | } 175 | 176 | render() { 177 | return ( 178 |
179 | {this.state.user === undefined ? this.showSignupForm() : this.showConfirmationForm()} 180 |
181 | ); 182 | } 183 | } -------------------------------------------------------------------------------- /assets/src/modules/signup/home.css: -------------------------------------------------------------------------------- 1 | .Home .lander { 2 | padding: 80px 0; 3 | text-align: center; 4 | } 5 | 6 | .Home .lander h1 { 7 | font-family: "Open Sans", sans-serif; 8 | font-weight: 600; 9 | } 10 | 11 | .Home .lander p { 12 | color: white; 13 | } 14 | 15 | .Home .goals h4 { 16 | font-family: "Open Sans", sans-serif; 17 | font-weight: 600; 18 | overflow: hidden; 19 | line-height: 1.5; 20 | white-space: nowrap; 21 | text-overflow: ellipsis; 22 | } 23 | 24 | .Home .goals table { 25 | margin-top: 30px; 26 | color: white; 27 | table-layout: fixed; 28 | } 29 | 30 | .Home .goals table a { 31 | color: #4eacf9; 32 | } 33 | 34 | .list-group { 35 | background: transparent !important; 36 | box-shadow: none !important; 37 | } 38 | 39 | .list-group-item h4 { 40 | font-size: 24px; 41 | font-weight: 100 !important; 42 | } 43 | 44 | .list-group-item-text { 45 | font-weight: 100 !important; 46 | font-size: 18px !important; 47 | padding-left: 30px; 48 | } 49 | 50 | .list-group-item-heading { 51 | font-weight: 200 !important; 52 | font-size: 24px !important; 53 | padding-left:30px; 54 | } 55 | 56 | .container-fluid { 57 | padding-right: 0 !important; 58 | } 59 | 60 | .description { 61 | overflow: hidden; 62 | text-overflow: ellipsis; 63 | white-space: nowrap; 64 | } 65 | 66 | .orange-link { 67 | color: #ff9900 !important; 68 | } 69 | 70 | .center-spinner { 71 | position: absolute; 72 | margin-top: 20px; 73 | left: 50%; 74 | } -------------------------------------------------------------------------------- /assets/src/modules/signup/login.css: -------------------------------------------------------------------------------- 1 | @media all and (min-width: 480px) { 2 | .Login { 3 | max-width: 350px; 4 | height: 100%; 5 | padding: 30px; 6 | display: block; 7 | margin-left: auto; 8 | margin-right: auto; 9 | } 10 | 11 | .Login form { 12 | margin: 0 auto; 13 | max-width: 320px; 14 | } 15 | } 16 | 17 | .control-label{ 18 | color: #fff !important; 19 | text-shadow: none !important; 20 | font-size: 20px !important; 21 | font-weight: 200; 22 | } -------------------------------------------------------------------------------- /assets/src/modules/signup/signup.css: -------------------------------------------------------------------------------- 1 | @media all and (min-width: 480px) { 2 | .Signup { 3 | padding: 60px 0; 4 | } 5 | 6 | .Signup form { 7 | margin: 0 auto; 8 | max-width: 320px; 9 | } 10 | } 11 | 12 | .Signup form span.help-block { 13 | font-size: 14px; 14 | padding-bottom: 10px; 15 | color: #999; 16 | } -------------------------------------------------------------------------------- /assets/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /assets/src/registerServiceWorker.ts: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.toString()); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl: string) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | if (installingWorker) { 62 | installingWorker.onstatechange = () => { 63 | if (installingWorker.state === 'installed') { 64 | if (navigator.serviceWorker.controller) { 65 | // At this point, the old content will have been purged and 66 | // the fresh content will have been added to the cache. 67 | // It's the perfect time to display a "New content is 68 | // available; please refresh." message in your web app. 69 | console.log('New content is available; please refresh.'); 70 | } else { 71 | // At this point, everything has been precached. 72 | // It's the perfect time to display a 73 | // "Content is cached for offline use." message. 74 | console.log('Content is cached for offline use.'); 75 | } 76 | } 77 | }; 78 | } 79 | }; 80 | }) 81 | .catch(error => { 82 | console.error('Error during service worker registration:', error); 83 | }); 84 | } 85 | 86 | function checkValidServiceWorker(swUrl: string) { 87 | // Check if the service worker can be found. If it can't reload the page. 88 | fetch(swUrl) 89 | .then(response => { 90 | // Ensure service worker exists, and that we really are getting a JS file. 91 | if ( 92 | response.status === 404 || 93 | response.headers.get('content-type')!.indexOf('javascript') === -1 94 | ) { 95 | // No service worker found. Probably a different app. Reload the page. 96 | navigator.serviceWorker.ready.then(registration => { 97 | registration.unregister().then(() => { 98 | window.location.reload(); 99 | }); 100 | }); 101 | } else { 102 | // Service worker found. Proceed as normal. 103 | registerValidSW(swUrl); 104 | } 105 | }) 106 | .catch(() => { 107 | console.log( 108 | 'No internet connection found. App is running in offline mode.' 109 | ); 110 | }); 111 | } 112 | 113 | export function unregister() { 114 | if ('serviceWorker' in navigator) { 115 | navigator.serviceWorker.ready.then(registration => { 116 | registration.unregister(); 117 | }); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /assets/src/types/aws-amplify-react.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'aws-amplify-react'; 2 | -------------------------------------------------------------------------------- /assets/src/types/aws-amplify-ui.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@aws-amplify/ui' -------------------------------------------------------------------------------- /assets/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "allowJs": false, 5 | "skipLibCheck": false, 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "preserve", 16 | "lib": ["es2015", "es2017", "dom", "esnext.asynciterable"], 17 | "typeRoots": [ 18 | "./src/types", 19 | "./node_modules/@types" 20 | ] 21 | }, 22 | "include": [ 23 | "src" 24 | ], 25 | "exclude": [ 26 | "./node_modules/*" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /extensions/cw-slack-chime/README.md: -------------------------------------------------------------------------------- 1 | ## CloudWatch to Slack/Chime notifications 2 | 3 | This extension enables you to send alerts from CloudWatch alarms to Slack or Amazon Chime and creates everything necessary in a single CloudFormation template. All you need is to specify your CloudWatch alarm name and a Slack or Chime webhook URL. 4 | 5 | As part of the CloudFormation template, this extension will setup an SNS Topic, attach it to your CloudWatch alarm, and create a Lambda function that will send alerts to your Slack or Amazon Chime channel. 6 | 7 | 8 | ## License Summary 9 | 10 | This sample code is made available under a modified MIT license. See the LICENSE file. 11 | 12 | ## Outline 13 | 14 | - [Overview](#overview) 15 | - [Instructions](#instructions) 16 | - [Getting started](#getting-started) 17 | - [Cleaning up](#cleaning-up) 18 | - [Architecture](#architecture) 19 | - [Implementation details](#implementation-details) 20 | - [Amazon SNS](#amazon-sns) 21 | - [AWS Lambda](#aws-lambda) 22 | - [Limitations](#limitations) 23 | - [Suggestions](#suggestions) 24 | - [Additions, forks, and contributions](#additions-forks-and-contributions) 25 | 26 | ## Overview 27 | 28 | The goal of the CloudWatch alarms to Slack or Chime extension is to simplify and automate the process of getting your CloudWatch alarms to post in your messaging application. The provided CloudFormation template automates the entire creation and deployment of the extension. The template will: 29 | 30 | * Create a SNS topic 31 | * Add the SNS topic as an action to your CloudWatch alarm 32 | * Create a Lambda function to process the alarm and call your webhook url 33 | * Subscribe the Lambda function to the SNS topic 34 | 35 | ## Instructions 36 | 37 | *IMPORTANT NOTE*: Creating this application in your AWS account will create and consume AWS resources, which will cost *money*. We estimate that running this extension will cost *<$0.05/year* with light usage. This will vary on the number of notifications from your CloudWatch alarm. AWS Lambda offers 1M free requests per month. Be sure to shut down/remove all resources once you are finished to avoid ongoing charges to your AWS account (see instructions on cleaning up/tear down below). 38 | 39 | ## Getting started 40 | 41 | To get the CloudWatch alarm to Slack or Chime extension up and running in your AWS account, follow these steps (if you do not have an AWS account, please see [How do I create and activate a new Amazon Web Services account?](https://aws.amazon.com/premiumsupport/knowledge-center/create-and-activate-aws-account/)): 42 | 43 | 1. Log into the [AWS console](https://console.aws.amazon.com/) if you are not already 44 | 2. Choose one of the **Launch Stack** buttons below for your desired AWS region to open the AWS CloudFormation console and create a new stack. The CloudWatch alarm to Slack or Chime extension is supported in the following regions: 45 | 46 | Region name | Region code | Launch 47 | --- | --- | --- 48 | US East (N. Virginia) | us-east-1 | [![Launch Stack](https://cdn.rawgit.com/buildkite/cloudformation-launch-stack-button-svg/master/launch-stack.svg)](https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/new?stackName=CWToSlack&templateURL=https://aws-dmas.s3.amazonaws.com/alarm-to-alert/master.yaml) 49 | US West (Oregon) | us-west-2 | [![Launch Stack](https://cdn.rawgit.com/buildkite/cloudformation-launch-stack-button-svg/master/launch-stack.svg)](https://console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/new?stackName=CWToSlack&templateURL=https://aws-dmas.s3.amazonaws.com/alarm-to-alert/master.yaml) 50 | EU (Ireland) | eu-west-1 | [![Launch Stack](https://cdn.rawgit.com/buildkite/cloudformation-launch-stack-button-svg/master/launch-stack.svg)](https://console.aws.amazon.com/cloudformation/home?region=eu-west-1#/stacks/new?stackName=CWToSlack&templateURL=https://aws-dmas.s3.amazonaws.com/alarm-to-alert/master.yaml) 51 | EU (Frankfurt) | eu-central-1 | [![Launch Stack](https://cdn.rawgit.com/buildkite/cloudformation-launch-stack-button-svg/master/launch-stack.svg)](https://console.aws.amazon.com/cloudformation/home?region=eu-central-1#/stacks/new?stackName=CWToSlack&templateURL=https://aws-dmas.s3.amazonaws.com/alarm-to-alert/master.yaml) 52 | 53 | 54 | 3. Continue through the CloudFormation wizard steps 55 | 1. Name your stack, e.g. CWToSlack 56 | 2. Enter the name of the CloudWatch alarm to integrate with 57 | 3. Specify the Slack or Amazon Chime webhook you want to send alerts to 58 | 4. After reviewing, check the blue box for creating IAM resources. 59 | 4. Choose Create stack. This will take ~2 minutes to complete. 60 | 5. You're done! 61 | 1. Want to recieve alerts for additional CloudWatch alarms, see the section below. 62 | 63 | ### Adding alerts for additional CloudWatch alarms 64 | 65 | Once the stack has been created, you can add additional CloudWatch alarms that you want to receive alerts on. 66 | 1. Go to the CloudWatch console and find the alarm you would like to subscribe to. 67 | 2. Add a Notification Action to your alarm and choose the SNS Topic that was created for you. The topic name will be [CloudwatchAlarm]-topic-[Slack/Chime] 68 | 69 | 70 | ### Cleaning up 71 | 72 | To tear down your extension and remove all resources associated with the AWS CloudWatch to Slack/Chime extension, follow these steps: 73 | 74 | 1. Log into the AWS CloudFormation Console and find the stack you created for the demo app 75 | 2. Delete the stack 76 | 77 | *Remember to shut down/remove all related resources once you are finished to avoid ongoing charges to your AWS account.* 78 | 79 | ## Architecture 80 | 81 | ![Architecture](architecture.png) 82 | 83 | ## Implementation details 84 | 85 | ### Amazon SNS 86 | 87 | Amazon Simple Notification Service is a managed pub/sub messaging service we use to receive alerts from CloudWatch and trigger the Lambda function that sends a message to the Slack/Chime channel. 88 | 89 | ### AWS Lambda 90 | 91 | AWS Lambda is used in a few different places to run the application. The Lambda functions that are deployed as part of the template are shown below, and available in the [functions](https://github.com/awslabs/aws-full-stack-template/tree/master/extensions/cw-slack-chime/functions) folder. 92 | 93 | *[CloudWatch Alarm Name]-SendAlert* - Lambda function processes the CloudWatch alarm message and calls the configured Slack/Chime webhook URL. 94 | *[CloudWatch Alarm Name]-FunctionSetupAlert* - Lambda function that intakes the newly created SNS topic and adds it as an action to your CloudWatch alarm. 95 | 96 | ## Limitations 97 | 98 | - This extension only handles the SNS formatter for CloudWatch alerts. Want to contribute other formatters? Leave us a comment or a PR request! 99 | - Currenly, this extension is limited to support one webhook URL. To send to multiple webhook URLs, you will need to deploy the template again and specify the additional webhook URL. 100 | 101 | ## Suggestions 102 | 103 | Have other ideas for extensions we should build? Leave a comment on GitHub! 104 | 105 | ## Additions, forks, and contributions 106 | 107 | We are excited that you are interested in using [AWS Full-Stack Template](https://github.com/awslabs/aws-full-stack-template) and some of these extensions! This is a great place to start if you are just beginning with AWS and want to get a functional application up and running. It is equally useful if you are looking for a sample full-stack application to fork off of and build your own custom application. We encourage developer participation via contributions and suggested additions. Of course you are welcome to create your own version! 108 | Please see the [contributing guidelines](https://github.com/awslabs/aws-full-stack-template/blob/master/CONTRIBUTING.md) for more information. 109 | 110 | For just one example of how you can build on top of this, check out [AWS Bookstore Demo App](https://github.com/aws-samples/aws-bookstore-demo-app), which was built on top of AWS Full-Stack Template and the [Search API extension](https://github.com/awslabs/aws-full-stack-template/tree/master/extensions/search-api). 111 | -------------------------------------------------------------------------------- /extensions/cw-slack-chime/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-full-stack-template/ac72318d8689203b1c3fa8c71f3fed16e90248c3/extensions/cw-slack-chime/architecture.png -------------------------------------------------------------------------------- /extensions/cw-slack-chime/functions/chime.js: -------------------------------------------------------------------------------- 1 | var https = require('https'); 2 | var url = require('url'); 3 | 4 | const CLOUDWATCH_URL = "https://console.aws.amazon.com/cloudwatch/home#alarmsV2:alarm/"; 5 | 6 | exports.handler = function (event, context) { 7 | var record = event.Records[0]['Sns']; 8 | 9 | var message = getJsonFromString(record.Message); 10 | 11 | if (message == null) { 12 | context.fail('CloudWatch Alarm messsage is not valid JSON'); 13 | return; 14 | } 15 | 16 | var trigger = message.Trigger; 17 | var data = "@Present AWS Alarm\n\n :rotating_light: " + message.AlarmName; 18 | 19 | data += "\n :earth_americas: " + message.Region + " :clock1: " + new Date(message.StateChangeTime).toGMTString(); 20 | 21 | if (trigger != null) { 22 | data += "\n :information_source: " + trigger.Namespace; 23 | data += " | " + trigger.MetricName; 24 | } 25 | 26 | data += "\n :hash: " + message.AWSAccountId; 27 | data += "\n\n :link: " + CLOUDWATCH_URL + message.AlarmName; 28 | 29 | postMessage({ Content: data }, function (response) { 30 | if (response.statusCode == 200) { 31 | context.succeed(); 32 | } else { 33 | context.fail("Error " + response.statusCode + " - " + response.statusMessage); 34 | } 35 | }); 36 | }; 37 | 38 | var getJsonFromString = function (str) { 39 | try { 40 | return JSON.parse(str); 41 | } catch (e) { 42 | return null; 43 | } 44 | }; 45 | 46 | var postMessage = function (message, callback) { 47 | var body = JSON.stringify(message); 48 | var options = url.parse(process.env.WEBHOOK_URL); 49 | options.method = 'POST'; 50 | options.headers = { 51 | 'Content-Type': 'application/json' 52 | }; 53 | 54 | var postReq = https.request(options); 55 | 56 | postReq.write(body); 57 | postReq.end(); 58 | }; 59 | -------------------------------------------------------------------------------- /extensions/cw-slack-chime/functions/modifyAlarm.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | const cw = new AWS.CloudWatch(); 3 | const sns = new AWS.SNS(); 4 | const https = require('https'); 5 | const url = require('url'); 6 | 7 | const SUCCESS = "SUCCESS"; 8 | const FAILED = "FAILED"; 9 | 10 | exports.handler = function (event, context, callback) { 11 | console.log('Received event:', JSON.stringify(event, null, 2)); 12 | 13 | if (event.RequestType === 'Create') { 14 | describeAlarm().then(function (data) { 15 | modifyAlertAndSubscribe(event, callback, context, data); 16 | }).catch(function (err) { 17 | var responseData = { Error: "Failed to modify alarm and subscribe SNS topic " + err.message }; 18 | sendResponse(event, callback, context.logStreamName, FAILED, responseData); 19 | } 20 | ); 21 | } else { // Delete 22 | describeAlarm().then(function (data) { 23 | removeSubscription(event, callback, context, data); 24 | }).catch(function (err) { 25 | var responseData = { Error: "Failed to modify alarm and unsubscribe SNS topic " + err.message }; 26 | sendResponse(event, callback, context.logStreamName, FAILED, responseData); 27 | } 28 | ); return; 29 | } 30 | }; 31 | 32 | function describeAlarm() { 33 | const params = { 34 | AlarmNames: [process.env.ALARM_NAME], 35 | }; 36 | 37 | return cw.describeAlarms(params).promise(); 38 | } 39 | 40 | function modifyAlertAndSubscribe(event, callback, context, data) { 41 | if (data.MetricAlarms && data.MetricAlarms.length > 0) { 42 | const origAlarm = data.MetricAlarms[0]; 43 | 44 | const params = { 45 | "Namespace": origAlarm.Namespace, 46 | "MetricName": origAlarm.MetricName, 47 | "Dimensions": origAlarm.Dimensions, 48 | "AlarmActions": [...origAlarm.AlarmActions, process.env.SNS_TOPIC], 49 | "ComparisonOperator": origAlarm.ComparisonOperator, 50 | "DatapointsToAlarm": origAlarm.DatapointsToAlarm, 51 | "EvaluationPeriods": origAlarm.EvaluationPeriods, 52 | "Period": origAlarm.Period, 53 | "Statistic": origAlarm.Statistic, 54 | "Threshold": origAlarm.Threshold, 55 | "AlarmDescription": origAlarm.AlarmDescription, 56 | "AlarmName": origAlarm.AlarmName 57 | }; 58 | 59 | 60 | return cw.putMetricAlarm(params).promise().then(function (data) { 61 | console.log(data); 62 | confirmAlert(event, callback, context); 63 | }).catch(function (err) { 64 | var responseData = { Error: "Failed to modify alarm and subscribe SNS topic" + err }; 65 | sendResponse(event, callback, context.logStreamName, FAILED, responseData); 66 | }); 67 | } 68 | } 69 | 70 | function removeSubscription(event, callback, context, data) { 71 | if (data.MetricAlarms && data.MetricAlarms.length > 0) { 72 | const origAlarm = data.MetricAlarms[0]; 73 | 74 | const params = { 75 | "Namespace": origAlarm.Namespace, 76 | "MetricName": origAlarm.MetricName, 77 | "Dimensions": origAlarm.Dimensions, 78 | "AlarmActions": origAlarm.AlarmActions.filter(item => item !== process.env.SNS_TOPIC), 79 | "ComparisonOperator": origAlarm.ComparisonOperator, 80 | "DatapointsToAlarm": origAlarm.DatapointsToAlarm, 81 | "EvaluationPeriods": origAlarm.EvaluationPeriods, 82 | "Period": origAlarm.Period, 83 | "Statistic": origAlarm.Statistic, 84 | "Threshold": origAlarm.Threshold, 85 | "AlarmDescription": origAlarm.AlarmDescription, 86 | "AlarmName": origAlarm.AlarmName 87 | }; 88 | 89 | 90 | return cw.putMetricAlarm(params).promise().then(function (data) { 91 | sendResponse(event, callback, context.logStreamName, SUCCESS); 92 | }).catch(function (err) { 93 | var responseData = { Error: "Failed to modify alarm and unsubscribe SNS topic" + err }; 94 | sendResponse(event, callback, context.logStreamName, FAILED, responseData); 95 | }); 96 | } 97 | } 98 | 99 | function confirmAlert(event, callback, context) { 100 | const message = { "AlarmName": process.env.ALARM_NAME, "AlarmDescription": null, "AWSAccountId": "XXX", "NewStateValue": "ALARM", "NewStateReason": "", "StateChangeTime": new Date().toISOString(), "Region": process.env.AWS_REGION, "OldStateValue": "", "Trigger": { "MetricName": "Ready!", "Namespace": "AWS", "StatisticType": "", "Statistic": "", "Unit": null, "Dimensions": [{ "name": "Currency", "value": "USD" }], "Period": 86400, "EvaluationPeriods": 1, "ComparisonOperator": "GreaterThanThreshold", "Threshold": 1.0, "TreatMissingData": "", "EvaluateLowSampleCountPercentile": "", "Timestamp": "2017-10-30T13: 20: 35.855Z", "SignatureVersion": "1", "Signature": "", "SigningCertUrl": "", "UnsubscribeUrl": "", "MessageAttributes": null } }; 101 | 102 | var params = { 103 | Message: JSON.stringify(message), 104 | TopicArn: process.env.SNS_TOPIC 105 | }; 106 | 107 | sns.publish(params).promise().then(function (data) { 108 | sendResponse(event, callback, context.logStreamName, SUCCESS); 109 | }).catch(function (err) { 110 | var responseData = { Error: "Failed to send test message" + err }; 111 | sendResponse(event, callback, context.logStreamName, FAILED, responseData); 112 | }); 113 | } 114 | 115 | function sendResponse(event, callback, logStreamName, responseStatus, responseData) { 116 | const responseBody = JSON.stringify({ 117 | Status: responseStatus, 118 | Reason: `See the details in CloudWatch Log Stream: ${logStreamName}`, 119 | PhysicalResourceId: logStreamName, 120 | StackId: event.StackId, 121 | RequestId: event.RequestId, 122 | LogicalResourceId: event.LogicalResourceId, 123 | Data: responseData, 124 | }); 125 | 126 | console.log('RESPONSE BODY:\n', responseBody); 127 | 128 | const parsedUrl = url.parse(event.ResponseURL); 129 | const options = { 130 | hostname: parsedUrl.hostname, 131 | port: 443, 132 | path: parsedUrl.path, 133 | method: 'PUT', 134 | headers: { 135 | 'Content-Type': '', 136 | 'Content-Length': responseBody.length, 137 | }, 138 | }; 139 | 140 | const req = https.request(options, (res) => { 141 | console.log('STATUS:', res.statusCode); 142 | console.log('HEADERS:', JSON.stringify(res.headers)); 143 | callback(null, 'Successfully modified alarm.'); 144 | }); 145 | 146 | req.on('error', (err) => { 147 | console.log('sendResponse Error:\n', err); 148 | callback(err); 149 | }); 150 | 151 | req.write(responseBody); 152 | req.end(); 153 | } 154 | 155 | -------------------------------------------------------------------------------- /extensions/cw-slack-chime/functions/slack.js: -------------------------------------------------------------------------------- 1 | var https = require('https'); 2 | var url = require('url'); 3 | 4 | const CLOUDWATCH_URL = "https://console.aws.amazon.com/cloudwatch/home#alarmsV2:alarm/"; 5 | 6 | exports.handler = function (event, context) { 7 | var record = event.Records[0]['Sns']; 8 | 9 | var message = getJsonFromString(record.Message); 10 | 11 | if (message == null) { 12 | context.fail('CloudWatch Alarm messsage is not valid JSON'); 13 | return; 14 | } 15 | var data = { 16 | attachments: [{ 17 | title: "AWS Alarm", 18 | title_link: CLOUDWATCH_URL + message.AlarmName, 19 | fields: [ 20 | { 21 | title: 'Alarm', 22 | value: message.AlarmName, 23 | short: true, 24 | }, { 25 | title: 'Time', 26 | value: new Date(message.StateChangeTime).toGMTString(), 27 | short: true, 28 | }, { 29 | title: 'Namespace', 30 | value: message.Trigger.Namespace, 31 | short: true, 32 | }, { 33 | title: 'MetricName', 34 | value: message.Trigger.MetricName, 35 | short: true, 36 | }, { 37 | title: 'Account', 38 | value: message.AWSAccountId, 39 | short: true, 40 | }, { 41 | title: 'Region', 42 | value: message.Region, 43 | short: true, 44 | }], 45 | }], 46 | }; 47 | 48 | postMessage(data, function (response) { 49 | if (response.statusCode == 200) { 50 | context.succeed(); 51 | } else { 52 | context.fail("Error " + response.statusCode + " - " + response.statusMessage); 53 | } 54 | }); 55 | }; 56 | 57 | var getJsonFromString = function (str) { 58 | try { 59 | return JSON.parse(str); 60 | } catch (e) { 61 | return null; 62 | } 63 | }; 64 | 65 | var postMessage = function (message, callback) { 66 | var body = JSON.stringify(message); 67 | var options = url.parse(process.env.WEBHOOK_URL); 68 | options.method = 'POST'; 69 | options.headers = { 70 | 'Content-Type': 'application/json' 71 | }; 72 | 73 | var postReq = https.request(options); 74 | 75 | postReq.write(body); 76 | postReq.end(); 77 | }; 78 | -------------------------------------------------------------------------------- /extensions/cw-slack-chime/master.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Parameters: 3 | CloudwatchAlarm: 4 | Description: Name of the Cloudwatch Alarm 5 | Type: String 6 | WebhookURL: 7 | Description: Webhook URL to call 8 | Type: String 9 | WebhookType: 10 | Type: String 11 | Default: Slack 12 | AllowedValues: 13 | - Slack 14 | - Chime 15 | Description: Select the destination of your webhook 16 | Conditions: 17 | IsSlack: !Equals [!Ref WebhookType, "Slack"] 18 | Mappings: 19 | S3Buckets: 20 | us-east-1: 21 | Bucket: aws-dmas 22 | us-west-2: 23 | Bucket: aws-dmas-us-west-2 24 | eu-central-1: 25 | Bucket: aws-dmas-eu-central-1 26 | eu-west-1: 27 | Bucket: aws-dmas-eu-west-1 28 | Constants: 29 | S3Keys: 30 | SendAlertCode: alarm-to-alert/functions/SendAlertCode.zip 31 | SetupAlertCode: alarm-to-alert/functions/SetupAlertCode.zip 32 | Resources: 33 | FunctionSendAlert: 34 | Type: 'AWS::Lambda::Function' 35 | DependsOn: WebhookRole 36 | Properties: 37 | FunctionName: !Sub '${CloudwatchAlarm}-SendAlert' 38 | Description: Sends message when CloudWatch Alarm is triggered 39 | Handler: !Join 40 | - '' 41 | - - !If [IsSlack, 'slack', 'chime'] 42 | - '.handler' 43 | Runtime: nodejs8.10 44 | Role: !GetAtt 45 | - WebhookRole 46 | - Arn 47 | Timeout: 30 48 | MemorySize: 256 49 | Environment: 50 | Variables: 51 | WEBHOOK_URL: !Ref WebhookURL 52 | Code: 53 | S3Bucket: !FindInMap 54 | - S3Buckets 55 | - !Ref 'AWS::Region' 56 | - Bucket 57 | S3Key: !FindInMap 58 | - Constants 59 | - S3Keys 60 | - SendAlertCode 61 | SNSTopic: 62 | Type: 'AWS::SNS::Topic' 63 | Properties: 64 | TopicName: !Sub '${CloudwatchAlarm}-topic-${WebhookType}' 65 | Subscription: 66 | - Endpoint: !GetAtt FunctionSendAlert.Arn 67 | Protocol: lambda 68 | DependsOn: FunctionSendAlert 69 | SendAlertPermissions: 70 | Type: 'AWS::Lambda::Permission' 71 | Properties: 72 | Action: "lambda:InvokeFunction" 73 | Principal: "sns.amazonaws.com" 74 | SourceArn: !Ref SNSTopic 75 | FunctionName: !GetAtt FunctionSendAlert.Arn 76 | DependsOn: FunctionSendAlert 77 | WebhookRole: 78 | Type: 'AWS::IAM::Role' 79 | Properties: 80 | RoleName: !Sub '${CloudwatchAlarm}-WebhookRole' 81 | AssumeRolePolicyDocument: 82 | Version: 2012-10-17 83 | Statement: 84 | - Effect: Allow 85 | Principal: 86 | Service: 87 | - lambda.amazonaws.com 88 | Action: 89 | - 'sts:AssumeRole' 90 | ManagedPolicyArns: 91 | - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' 92 | SetupAlertRole: 93 | Type: 'AWS::IAM::Role' 94 | DependsOn: SNSTopic 95 | Properties: 96 | RoleName: !Sub '${CloudwatchAlarm}-SetupAlertRole' 97 | AssumeRolePolicyDocument: 98 | Version: 2012-10-17 99 | Statement: 100 | - Effect: Allow 101 | Principal: 102 | Service: 103 | - lambda.amazonaws.com 104 | Action: 105 | - 'sts:AssumeRole' 106 | ManagedPolicyArns: 107 | - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' 108 | Policies: 109 | - PolicyName: !Sub '${CloudwatchAlarm}-policy' 110 | PolicyDocument: 111 | Version: 2012-10-17 112 | Statement: 113 | - Effect: Allow 114 | Action: 115 | - 'cloudwatch:PutMetricAlarm' 116 | - 'cloudwatch:DescribeAlarms' 117 | Resource: !Join [ "", [ "arn:aws:cloudwatch:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId" , ":alarm:", !Ref CloudwatchAlarm ]] 118 | - Effect: Allow 119 | Action: 120 | - 'sns:Publish' 121 | Resource: !Ref SNSTopic 122 | 123 | FunctionSetupAlert: 124 | Type: 'AWS::Lambda::Function' 125 | DependsOn: 126 | - SetupAlertRole 127 | - SNSTopic 128 | Properties: 129 | FunctionName: !Sub '${CloudwatchAlarm}-FunctionSetupAlert' 130 | Description: Setup subscription to alarm with SNS and trigger Send to Alert Lambda function. 131 | Handler: index.handler 132 | Runtime: nodejs8.10 133 | Role: !GetAtt 134 | - SetupAlertRole 135 | - Arn 136 | Timeout: 30 137 | MemorySize: 256 138 | Environment: 139 | Variables: 140 | ALARM_NAME: !Ref CloudwatchAlarm 141 | SNS_TOPIC: !Ref SNSTopic 142 | Code: 143 | S3Bucket: !FindInMap 144 | - S3Buckets 145 | - !Ref 'AWS::Region' 146 | - Bucket 147 | S3Key: !FindInMap 148 | - Constants 149 | - S3Keys 150 | - SetupAlertCode 151 | SetupAlertPermissions: 152 | Type: 'AWS::Lambda::Permission' 153 | Properties: 154 | Action: "lambda:InvokeFunction" 155 | Principal: "cloudwatch.amazonaws.com" 156 | SourceArn: !Join [ "", [ "arn:aws:cloudwatch:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId" , ":alarm:", !Ref CloudwatchAlarm ]] 157 | FunctionName: !GetAtt FunctionSetupAlert.Arn 158 | DependsOn: FunctionSetupAlert 159 | RunSetupAlert: 160 | Type: 'Custom::CustomResource' 161 | Properties: 162 | ServiceToken: !GetAtt FunctionSetupAlert.Arn 163 | DependsOn: FunctionSetupAlert 164 | Metadata: 165 | 'AWS::CloudFormation::Designer': 166 | id: 63b08124-fccb-4874-ab13-ce6cfe6ce885 167 | Outputs: 168 | LambdaFunction: 169 | Description: Lambda function that sends a message when CloudWatch Alarm is triggered. 170 | Export: 171 | Name: !Sub '${CloudwatchAlarm}SendAlert' 172 | Value: !Ref FunctionSendAlert 173 | -------------------------------------------------------------------------------- /extensions/search-api/README.md: -------------------------------------------------------------------------------- 1 | ## Search API Extension 2 | 3 | The Search API Extension enables you to add search functionality on top of your data in DynamoDB powered by Elasticsearch and API Gateway. The extension can be created with a single CloudFormation template! 4 | 5 | This extension takes in a DynamoDB table as a parameter. It will spin up an Elasticsearch cluster, stream changes from DynamoDB to Elasticsearch, and create a Search API. You can choose an existing API Gateway ID to integrate with or have the extension create a new one. 6 | 7 | Get started with adding search to your DynamoDB data below! 8 | 9 | ## License Summary 10 | 11 | ## Outline 12 | 13 | - [Overview](#overview) 14 | - [Instructions](#instructions) 15 | - [Getting started](#getting-started) 16 | - [Cleaning up](#cleaning-up) 17 | - [Architecture](#architecture) 18 | - [Implementation details](#implementation-details) 19 | - [Amazon DynamoDB](#amazon-dynamodb-streams) 20 | - [Amazon API Gateway](#amazon-api-gateway) 21 | - [AWS Lambda](#aws-lambda) 22 | - [Using the extension](#using-the-extension) 23 | - [Limitations](#limitations) 24 | - [Suggestions](#suggestions) 25 | - [Additions, forks, and contributions](#additions-forks-and-contributions) 26 | 27 | ## Overview 28 | 29 | The provided CloudFormation template automates the entire creation and deployment of the Search API. The template includes the following features: 30 | 31 | * Enables streams on your DynamoDB table to push updates 32 | * Creates an Elasticsearch cluster with best practices 33 | * Processes records and update Elasticsearch cluster via a Lambda function 34 | * Creates a Search API with Lambda 35 | * Creates or integrates with an existing API Gateway 36 | 37 | ## Instructions 38 | 39 | ***IMPORTANT NOTE**: Creating this application in your AWS account will create and consume AWS resources, which **will cost money**. We estimate that running this extension in development mode (t2.small) will cost **<$0.10/hour** with light usage. In production mode (m4.large), we estimate this will cost around **$0.20/hr** with an additional $0.20/hr for each additional instance. Be sure to shut down/remove all resources once you are finished to avoid ongoing charges to your AWS account (see instructions on cleaning up/tear down below).* 40 | 41 | ## Getting started 42 | 43 | To get the Search API Extension up and running in your AWS account, follow these steps (if you do not have an AWS account, please see [How do I create and activate a new Amazon Web Services account?](https://aws.amazon.com/premiumsupport/knowledge-center/create-and-activate-aws-account/)): 44 | 45 | 1. Log into the [AWS console](https://console.aws.amazon.com/) if you are not already 46 | 2. Choose one of the **Launch Stack** buttons below for your desired AWS region to open the AWS CloudFormation console and create a new stack. The Search API extension is supported in the following regions: 47 | 48 | Region name | Region code | Launch 49 | --- | --- | --- 50 | US East (N. Virginia) | us-east-1 | [![Launch Stack](https://cdn.rawgit.com/buildkite/cloudformation-launch-stack-button-svg/master/launch-stack.svg)](https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/new?stackName=SearchAPI&templateURL=https://s3.amazonaws.com/aws-dmas/ddb-es/master.yaml) 51 | US West (Oregon) | us-west-2 | [![Launch Stack](https://cdn.rawgit.com/buildkite/cloudformation-launch-stack-button-svg/master/launch-stack.svg)](https://console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/new?stackName=SearchAPI&templateURL=https://s3.amazonaws.com/aws-dmas/ddb-es/master.yaml) 52 | EU (Ireland) | eu-west-1 | [![Launch Stack](https://cdn.rawgit.com/buildkite/cloudformation-launch-stack-button-svg/master/launch-stack.svg)](https://console.aws.amazon.com/cloudformation/home?region=eu-west-1#/stacks/new?stackName=SearchAPI&templateURL=https://s3.amazonaws.com/aws-dmas/ddb-es/master.yaml) 53 | EU (Frankfurt) | eu-central-1 | [![Launch Stack](https://cdn.rawgit.com/buildkite/cloudformation-launch-stack-button-svg/master/launch-stack.svg)](https://console.aws.amazon.com/cloudformation/home?region=eu-central-1#/stacks/new?stackName=SearchAPI&templateURL=https://s3.amazonaws.com/aws-dmas/ddb-es/master.yaml) 54 | 55 | 3. Continue through the CloudFormation wizard steps 56 | 1. Name your stack, e.g. SearchAPI 57 | 2. Enter the name of the DynamoDB table to integrate with 58 | 3. Specify an existing API Gateway ID to integrate with, or we will auto-create an API Gateway Resource for you 59 | 4. After reviewing, check the blue box for creating IAM resources. 60 | 4. Choose Create stack. This will take ~15 minutes to complete. 61 | 5. After the stack completes, go to the Lambda console and find the Search function ([DynamoDBTableName]-Search) 62 | 1. Modify the fields array on line 24 with fields from your DynamoDB table to query on in the format of [column name].[column type] (S, N, BOOL, etc) 63 | 2. This allows us to achieve optimal performance as its best to query Elasticsearch on specific fields. 64 | 3. If you are deploying this on top of the Full Stack app template with the existing goals schema, no additional modification is needed. 65 | 66 | Note: As described in the [limitations](#limitations), this template does not backfill data to the Elasticsearch cluster. To test the extension, create a new item and see [Using the extension](#using-the-extension) for instructions on testing your Search API. 67 | 68 | ### Cleaning up 69 | 70 | To tear down your extension and remove all resources associated with the AWS Search API Extension, follow these steps: 71 | 72 | 1. Log into the AWS CloudFormation Console and find the stack you created for the Search API Extension 73 | 2. Delete the stack 74 | 75 | *Remember to shut down/remove all related resources once you are finished to avoid ongoing charges to your AWS account.* 76 | 77 | ## Architecture 78 | 79 | ![Architecture](architecture.png) 80 | 81 | ## Implementation details 82 | 83 | ### Amazon DynamoDB Streams 84 | 85 | Amazon DynamoDB Streams push updates to the *UpdateSearchCluster* Lambda function that updates the Amazon Elasticsearch cluster. 86 | 87 | ### Amazon API Gateway 88 | 89 | Amazon API Gateway acts as the interface layer between your frontend and AWS Lambda, which calls the backend (database). Below is the different APIs the extension creates: 90 | 91 | *Search* 92 | 93 | GET /search/{:q} (Search) 94 | 95 | ### AWS Lambda 96 | 97 | AWS Lambda is used in a few different places to run the application, as shown in the architecture diagram. The important Lambda functions that are deployed as part of the template are shown below, and available in the [functions](https://github.com/awslabs/aws-full-stack-template/tree/master/extensions/search-api/functions) folder. In the cases where the response fields are blank, the application will return a statusCode 200 or 500 for success or failure, respectively. 98 | 99 | *Search* - Lambda function that returns a list of books based on provided search parameters in the request. 100 | 101 | *UpdateSearchCluster* - Lambda function that updates the Elasticsearch cluster when new items are added to the DynamoDB table. 102 | 103 | ### Amazon VPC 104 | 105 | The Elasticsearch cluster is secured in an Amazon VPC (Virtual Private Cloud) for production-level security. The Lambda functions interact with the cluster through an ENI (Elastic Network Interface) that is automatically setup for you. 106 | 107 | ## Using the extension 108 | 109 | 1. Head to the API Gateway console and click on your API Gateway resource 110 | 2. Select the search GET method 111 | 3. Click test and enter a search query q={query term} 112 | 113 | ## Limitations 114 | 115 | - This extension does not backfill your Elasticsearch cluster with previous data. Want to contribute? Leave us a comment or a PR request! 116 | - Upon the first use of a Lambda function, cold start times in a VPC can be slow. Once the Lambda function has been warmed up, performance will improve. Consider creating triggers to create your Lambda "warm" for production purposes. 117 | 118 | ## Suggestions 119 | 120 | Have other ideas for extensions we should build? Leave a comment on GitHub! 121 | 122 | ## Additions, forks, and contributions 123 | 124 | We are excited that you are interested in using [AWS Full-Stack Template](https://github.com/awslabs/aws-full-stack-template) and some of these extensions! This is a great place to start if you are just beginning with AWS and want to get a functional application up and running. It is equally useful if you are looking for a sample full-stack application to fork off of and build your own custom application. We encourage developer participation via contributions and suggested additions. Of course you are welcome to create your own version! 125 | Please see the [contributing guidelines](https://github.com/awslabs/aws-full-stack-template/blob/master/CONTRIBUTING.md) for more information. 126 | 127 | For just one example of how you can build on top of this, check out [AWS Bookstore Demo App](https://github.com/aws-samples/aws-bookstore-demo-app), which was built on top of AWS Full-Stack Template and the [Search API extension](https://github.com/awslabs/aws-full-stack-template/tree/master/extensions/search-api). 128 | -------------------------------------------------------------------------------- /extensions/search-api/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-full-stack-template/ac72318d8689203b1c3fa8c71f3fed16e90248c3/extensions/search-api/architecture.png -------------------------------------------------------------------------------- /extensions/search-api/functions/searchCluster.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import json 3 | import os 4 | import requests 5 | from requests_aws4auth import AWS4Auth 6 | 7 | region = os.environ["REGION"] 8 | service = "es" 9 | credentials = boto3.Session().get_credentials() 10 | awsauth = AWS4Auth(credentials.access_key, credentials.secret_key, region, service, session_token=credentials.token) 11 | 12 | index = "lambda-index" 13 | type = "lambda-type" 14 | url = "https://" + os.environ["ESENDPOINT"] + "/_search" # ElasticSearch cluster URL 15 | 16 | def handler(event, context): 17 | # Put the user query into the query DSL for more accurate search results. 18 | query = { 19 | "size": 25, 20 | "query": { 21 | "bool" : { 22 | "must": { 23 | "match" : { 24 | "userId.S" : event["requestContext"]["identity"]["cognitoIdentityId"] 25 | } 26 | }, 27 | "must": { 28 | "multi_match" : { 29 | "query" : event["queryStringParameters"]["q"], 30 | "fields" : ["title.S"] # TODO: Fill with columns to search on. Format: [column name].[column type] 31 | } 32 | } 33 | } 34 | } 35 | } 36 | print query 37 | 38 | # ES 6.x requires an explicit Content-Type header 39 | headers = { "Content-Type": "application/json" } 40 | 41 | # Make the signed HTTP request 42 | r = requests.get(url, auth=awsauth, headers=headers, data=json.dumps(query)) 43 | 44 | # Create the response and add some extra content to support CORS 45 | response = { 46 | "statusCode": r.status_code, 47 | "headers": { 48 | "Access-Control-Allow-Origin": "*", 49 | "Access-Control-Allow-Credentials": True 50 | }, 51 | "body": r.text 52 | } 53 | 54 | # Add the search results to the response 55 | print response 56 | return response 57 | -------------------------------------------------------------------------------- /extensions/search-api/functions/updateSearchCluster.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import os 3 | import json 4 | from botocore.vendored import requests 5 | from requests_aws4auth import AWS4Auth 6 | 7 | region = os.environ["REGION"] 8 | service = "es" 9 | credentials = boto3.Session().get_credentials() 10 | awsauth = AWS4Auth(credentials.access_key, credentials.secret_key, region, service, session_token=credentials.token) 11 | 12 | host = "https://" + os.environ["ESENDPOINT"] # the Amazon ElaticSearch domain, with https:// 13 | index = "lambda-index" 14 | type = "lambda-type" 15 | 16 | url = host + "/" + index + "/" + type + "/" 17 | 18 | headers = { "Content-Type": "application/json" } 19 | 20 | # UpdateSearchCluster - Updates Elasticsearch as new items are added to DynamoDB 21 | def handler(event, context): 22 | count = 0 23 | for record in event["Records"]: 24 | # Generate a UUID from the item primary key for use as the Elasticsearch ID 25 | id = generateUUID(record) 26 | 27 | if record['eventName'] == 'REMOVE': 28 | r = requests.delete(url + id, auth=awsauth) 29 | else: 30 | document = record['dynamodb']['NewImage'] 31 | r = requests.put(url + id, auth=awsauth, json=document, headers=headers) 32 | count += 1 33 | 34 | return str(count) + " records processed." 35 | 36 | def generateUUID(record): 37 | keys = record['dynamodb']['Keys'] 38 | 39 | # If range key exists, concatenate hash and range key with - 40 | id = "" 41 | i = 0 42 | for key, value in keys.items(): 43 | if (i > 0): 44 | id += "-" 45 | valueAttr = list(value.values()); 46 | id += valueAttr[0] 47 | i += 1 48 | 49 | print(id) 50 | return id 51 | -------------------------------------------------------------------------------- /extensions/search-api/master.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Parameters: 3 | DynamoDBTable: 4 | Description: Name of your existing DynamoDBTable 5 | Type: String 6 | ExistingAPIGatewayId: 7 | Description: OPTIONAL - Specify an existing API Gateway ID to integrate with or we will auto-create an API Gateway Resource for you. 8 | Type: String 9 | Conditions: 10 | IADRegion: !Equals [!Ref "AWS::Region", "us-east-1"] 11 | CreateNewAPIGateway: !Equals [ !Ref ExistingAPIGatewayId, "" ] 12 | Mappings: 13 | 14 | S3Buckets: 15 | us-east-1: 16 | Bucket: aws-dmas 17 | SeederFunctionBucket: fsd-aws-wildrydes-us-east-1 18 | us-west-2: 19 | Bucket: aws-dmas-us-west-2 20 | SeederFunctionBucket: fsd-aws-wildrydes-us-west-2 21 | eu-central-1: 22 | Bucket: aws-dmas-eu-central-1 23 | SeederFunctionBucket: fsd-aws-wildrydes-eu-central-1 24 | eu-west-1: 25 | Bucket: aws-dmas-eu-west-1 26 | SeederFunctionBucket: fsd-aws-wildrydes-eu-west-1 27 | Constants: 28 | S3Keys: 29 | EnableStreamsCode: ddb-es/functions/EnableStreamsCode.zip 30 | CreateESRoleCode: ddb-es/functions/CreateESRole.zip 31 | UpdateSearchCode: ddb-es/functions/UpdateSearchCode.zip 32 | SearchCode: ddb-es/functions/SearchCode.zip 33 | GetAPIGatewayInfoCode: ddb-es/functions/GetAPIGatewayInfoCode.zip 34 | Metadata: 35 | 'AWS::CloudFormation::Designer': 36 | 82b269e2-158a-4a8c-9262-b695e74e50ea: 37 | size: 38 | width: 60 39 | height: 60 40 | position: 41 | x: 251 42 | 'y': 103 43 | z: 0 44 | embeds: [] 45 | Resources: 46 | FunctionEnableStreams: 47 | Type: 'AWS::Lambda::Function' 48 | Properties: 49 | FunctionName: !Sub '${DynamoDBTable}-EnableStreams' 50 | Description: Enable streams for DynamoDB table 51 | Handler: index.handler 52 | Runtime: nodejs12.x 53 | Role: !GetAtt 54 | - DynamoDBRole 55 | - Arn 56 | Timeout: 120 57 | Environment: 58 | Variables: 59 | TABLE_NAME: !Ref DynamoDBTable 60 | Code: 61 | S3Bucket: !FindInMap 62 | - S3Buckets 63 | - !Ref 'AWS::Region' 64 | - Bucket 65 | S3Key: !FindInMap 66 | - Constants 67 | - S3Keys 68 | - EnableStreamsCode 69 | 70 | RunEnableStreams: 71 | Type: 'Custom::CustomResource' 72 | Properties: 73 | ServiceToken: !GetAtt FunctionEnableStreams.Arn 74 | ParameterOne: Parameter to pass into Custom Lambda Function 75 | DependsOn: 76 | - FunctionEnableStreams 77 | 78 | # ---------- ROLE FOR DYNAMODB --------- 79 | DynamoDBRole: 80 | Type: 'AWS::IAM::Role' 81 | Properties: 82 | RoleName: !Sub '${DynamoDBTable}-StreamingRole' 83 | AssumeRolePolicyDocument: 84 | Version: 2012-10-17 85 | Statement: 86 | - Effect: Allow 87 | Principal: 88 | Service: 89 | - lambda.amazonaws.com 90 | Action: 91 | - 'sts:AssumeRole' 92 | ManagedPolicyArns: 93 | - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' 94 | Policies: 95 | - PolicyName: EnableStreamsPolicy 96 | PolicyDocument: 97 | Version: 2012-10-17 98 | Statement: 99 | - Effect: Allow 100 | Action: 101 | - 'dynamodb:DescribeTable' 102 | - 'dynamodb:UpdateTable' 103 | Resource: !Join [ "", [ "arn:aws:dynamodb:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId" , ":table/", !Ref DynamoDBTable ]] 104 | # ---------- ROLE FOR CREATING ELASTICSEARCH ROLE --------- 105 | RoleForCreateESRoleFunction: 106 | Type: 'AWS::IAM::Role' 107 | Properties: 108 | RoleName: !Sub '${DynamoDBTable}-RoleForCreateESRoleFunction' 109 | AssumeRolePolicyDocument: 110 | Version: 2012-10-17 111 | Statement: 112 | - Effect: Allow 113 | Principal: 114 | Service: 115 | - lambda.amazonaws.com 116 | Action: 117 | - 'sts:AssumeRole' 118 | ManagedPolicyArns: 119 | - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' 120 | Policies: 121 | - PolicyName: CreateRolePolicy 122 | PolicyDocument: 123 | Version: 2012-10-17 124 | Statement: 125 | - Effect: Allow 126 | Action: 127 | - 'iam:CreateServiceLinkedRole' 128 | Resource: 'arn:aws:iam::*:role/aws-service-role/es.amazonaws.com/AWSServiceRoleForAmazonElasticsearchService' 129 | CreateESRoleFunction: 130 | Properties: 131 | Code: 132 | S3Bucket: !FindInMap 133 | - S3Buckets 134 | - !Ref 'AWS::Region' 135 | - Bucket 136 | S3Key: !FindInMap 137 | - Constants 138 | - S3Keys 139 | - CreateESRoleCode 140 | Description: Create Elasticsearch role 141 | Handler: index.handler 142 | Role: 143 | 'Fn::GetAtt': 144 | - RoleForCreateESRoleFunction 145 | - Arn 146 | Runtime: nodejs12.x 147 | Timeout: 300 148 | Type: 'AWS::Lambda::Function' 149 | ESRoleCreator: 150 | Type: 'Custom::CustomResource' 151 | Properties: 152 | ServiceToken: !GetAtt CreateESRoleFunction.Arn 153 | ParameterOne: Parameter to pass into Custom Lambda Function 154 | DependsOn: CreateESRoleFunction 155 | # ---------- VPC - SUBNET - SECURITY GROUPS --------- 156 | VPC: 157 | Type: "AWS::EC2::VPC" 158 | Properties: 159 | CidrBlock: '172.31.0.0/16' 160 | VPCSubnetforES: 161 | Type: "AWS::EC2::Subnet" 162 | Properties: 163 | CidrBlock: 164 | Fn::Select: 165 | - 0 166 | - Fn::Cidr: 167 | - Fn::GetAtt: [VPC, CidrBlock] 168 | - 3 169 | - 8 170 | VpcId: 171 | Ref: VPC 172 | AvailabilityZone: 173 | Fn::Select: 174 | - 0 175 | - Fn::GetAZs: 176 | Ref: "AWS::Region" 177 | ElasticsearchDomain: 178 | Type: 'AWS::Elasticsearch::Domain' 179 | DependsOn: 180 | - ESRoleCreator 181 | # TODO: Dev and Prod recommended settings 182 | Properties: 183 | DomainName: !Ref DynamoDBTable 184 | ElasticsearchVersion: 6.5 185 | ElasticsearchClusterConfig: 186 | DedicatedMasterEnabled: 'false' 187 | InstanceCount: '1' 188 | ZoneAwarenessEnabled: 'false' 189 | InstanceType: t2.small.elasticsearch 190 | VPCOptions: 191 | SubnetIds: 192 | - Ref: VPCSubnetforES 193 | EBSOptions: 194 | EBSEnabled: true 195 | Iops: 0 196 | VolumeSize: 10 197 | VolumeType: gp2 198 | NodeToNodeEncryptionOptions: 199 | Enabled: true 200 | AccessPolicies: 201 | Version: 2012-10-17 202 | Statement: 203 | - Effect: Allow 204 | Principal: 205 | AWS: '*' 206 | Action: 207 | - 'es:*' 208 | Resource: !Join [ "", [ "arn:aws:es:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId" , ":domain/", !Ref DynamoDBTable, "/*" ]] 209 | AdvancedOptions: 210 | rest.action.multi.allow_explicit_index: true 211 | 212 | # ---------- LAMBDA FUNCTION AND ROLE FOR ELASTICSEARCH --------- 213 | StreamingAndSearchRole: 214 | Type: 'AWS::IAM::Role' 215 | Properties: 216 | RoleName: !Sub '${DynamoDBTable}-StreamingAndSearchRole' 217 | AssumeRolePolicyDocument: 218 | Version: 2012-10-17 219 | Statement: 220 | - Effect: Allow 221 | Principal: 222 | Service: 223 | - lambda.amazonaws.com 224 | Action: 225 | - 'sts:AssumeRole' 226 | Path: / 227 | ManagedPolicyArns: 228 | - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' 229 | - 'arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole' 230 | Policies: 231 | - PolicyName: !Sub '${DynamoDBTable}-lambda-policy' 232 | PolicyDocument: 233 | Version: 2012-10-17 234 | Statement: 235 | - Effect: Allow 236 | Action: 237 | - 'es:ESHttpPost' 238 | - 'es:ESHttpGet' 239 | Resource: !Join [ "", [ "arn:aws:es:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId" , ":domain/", !Ref ElasticsearchDomain, "/*"]] 240 | - Effect: Allow 241 | Action: 242 | - 'dynamodb:DescribeStream' 243 | - 'dynamodb:GetRecords' 244 | - 'dynamodb:GetShardIterator' 245 | - 'dynamodb:ListStreams' 246 | Resource: !Join [ "", [ "arn:aws:dynamodb:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId" , ":table/", !Ref DynamoDBTable, "/stream/*"]] 247 | UpdateSearchCluster: 248 | Type: 'AWS::Lambda::Function' 249 | DependsOn: 250 | - StreamingAndSearchRole 251 | - ElasticsearchDomain 252 | Properties: 253 | FunctionName: !Sub '${DynamoDBTable}-UpdateSearchCluster' 254 | Description: 'Update Elasticsearch cluster as books are added' 255 | Handler: index.handler 256 | Role: !GetAtt 257 | - StreamingAndSearchRole 258 | - Arn 259 | Runtime: python2.7 260 | Timeout: '60' 261 | VpcConfig: 262 | SecurityGroupIds: 263 | - Fn::GetAtt: [VPC, DefaultSecurityGroup] 264 | SubnetIds: 265 | - Ref: VPCSubnetforES 266 | Code: 267 | S3Bucket: !FindInMap 268 | - S3Buckets 269 | - !Ref 'AWS::Region' 270 | - Bucket 271 | S3Key: !FindInMap 272 | - Constants 273 | - S3Keys 274 | - UpdateSearchCode 275 | Environment: 276 | Variables: 277 | ESENDPOINT: !GetAtt 278 | - ElasticsearchDomain 279 | - DomainEndpoint 280 | REGION: !Ref 'AWS::Region' 281 | FunctionSearch: 282 | Type: 'AWS::Lambda::Function' 283 | DependsOn: 284 | - StreamingAndSearchRole 285 | - ElasticsearchDomain 286 | Properties: 287 | FunctionName: !Sub '${DynamoDBTable}-Search' 288 | Description: Lambda function to query ElasticSearch 289 | Handler: index.handler 290 | MemorySize: 256 291 | Role: !GetAtt 292 | - StreamingAndSearchRole 293 | - Arn 294 | Runtime: python2.7 295 | Timeout: '60' 296 | VpcConfig: 297 | SecurityGroupIds: 298 | - Fn::GetAtt: [VPC, DefaultSecurityGroup] 299 | SubnetIds: 300 | - Ref: VPCSubnetforES 301 | Code: 302 | S3Bucket: !FindInMap 303 | - S3Buckets 304 | - !Ref 'AWS::Region' 305 | - Bucket 306 | S3Key: !FindInMap 307 | - Constants 308 | - S3Keys 309 | - SearchCode 310 | Environment: 311 | Variables: 312 | ESENDPOINT: !GetAtt 313 | - ElasticsearchDomain 314 | - DomainEndpoint 315 | REGION: !Ref 'AWS::Region' 316 | 317 | # ---------- STREAMING TRIGGERS --------- 318 | DataTableStream: 319 | DependsOn: 320 | - UpdateSearchCluster 321 | - FunctionEnableStreams 322 | - RunEnableStreams 323 | Type: 'AWS::Lambda::EventSourceMapping' 324 | Properties: 325 | BatchSize: 1 326 | Enabled: true 327 | EventSourceArn: !GetAtt 328 | - RunEnableStreams 329 | - StreamArn 330 | FunctionName: !GetAtt 331 | - UpdateSearchCluster 332 | - Arn 333 | StartingPosition: TRIM_HORIZON 334 | 335 | # ---------- SEARCH API --------- 336 | FunctionSearchPermissions: 337 | Type: 'AWS::Lambda::Permission' 338 | Properties: 339 | Action: 'lambda:InvokeFunction' 340 | FunctionName: !Ref FunctionSearch 341 | Principal: apigateway.amazonaws.com 342 | SourceArn: !Join [ "", [ "arn:aws:execute-api:", !Ref "AWS::Region", ":", !Ref "AWS::AccountId" , ":", !If [CreateNewAPIGateway, !Ref NewAPIGateway, !Ref ExistingAPIGatewayId], "/*"]] 343 | Metadata: 344 | 'AWS::CloudFormation::Designer': 345 | id: 06a821b7-638e-471c-ab50-51710b73767c 346 | SearchApiResource: 347 | Type: 'AWS::ApiGateway::Resource' 348 | Properties: 349 | RestApiId: !If [CreateNewAPIGateway, !Ref NewAPIGateway, !Ref ExistingAPIGatewayId] 350 | ParentId: !GetAtt 351 | - RunGetAPIGatewayInfo 352 | - RootResourceId 353 | PathPart: search 354 | SearchApiRequestGET: 355 | DependsOn: 356 | - FunctionSearchPermissions 357 | - FunctionSearch 358 | - SearchApiResource 359 | Type: 'AWS::ApiGateway::Method' 360 | Properties: 361 | AuthorizationType: AWS_IAM 362 | HttpMethod: GET 363 | Integration: 364 | Type: AWS_PROXY 365 | IntegrationHttpMethod: POST 366 | Uri: !Join 367 | - '' 368 | - - 'arn:aws:apigateway:' 369 | - !Ref 'AWS::Region' 370 | - ':lambda:path/2015-03-31/functions/' 371 | - !GetAtt 372 | - FunctionSearch 373 | - Arn 374 | - /invocations 375 | IntegrationResponses: 376 | - StatusCode: 200 377 | RequestParameters: 378 | method.request.querystring.q: false 379 | ResourceId: !Ref SearchApiResource 380 | RestApiId: !If [CreateNewAPIGateway, !Ref NewAPIGateway, !Ref ExistingAPIGatewayId] 381 | MethodResponses: 382 | - StatusCode: 200 383 | ResponseModels: 384 | application/json: Empty 385 | SearchApiRequestOPTIONS: 386 | Type: 'AWS::ApiGateway::Method' 387 | Properties: 388 | ResourceId: !Ref SearchApiResource 389 | RestApiId: !If [CreateNewAPIGateway, !Ref NewAPIGateway, !Ref ExistingAPIGatewayId] 390 | AuthorizationType: None 391 | HttpMethod: OPTIONS 392 | Integration: 393 | Type: MOCK 394 | IntegrationResponses: 395 | - ResponseParameters: 396 | method.response.header.Access-Control-Allow-Headers: >- 397 | 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token' 398 | method.response.header.Access-Control-Allow-Methods: '''GET,POST,PUT,DELETE,OPTIONS,HEAD,PATCH''' 399 | method.response.header.Access-Control-Allow-Origin: '''*''' 400 | ResponseTemplates: 401 | application/json: '' 402 | StatusCode: '200' 403 | PassthroughBehavior: WHEN_NO_MATCH 404 | RequestTemplates: 405 | application/json: '{"statusCode": 200}' 406 | MethodResponses: 407 | - ResponseModels: 408 | application/json: Empty 409 | ResponseParameters: 410 | method.response.header.Access-Control-Allow-Headers: true 411 | method.response.header.Access-Control-Allow-Methods: true 412 | method.response.header.Access-Control-Allow-Origin: true 413 | StatusCode: '200' 414 | 415 | # ---------- NEW API GATEWAY [OPTIONAL] --------- 416 | NewAPIGateway: 417 | Type: 'AWS::ApiGateway::RestApi' 418 | Condition: CreateNewAPIGateway 419 | Properties: 420 | Name: !Sub '${DynamoDBTable}-Search' 421 | Description: Search API 422 | FailOnWarnings: true 423 | 424 | APIDeployment: 425 | DependsOn: 426 | - SearchApiRequestGET 427 | - SearchApiRequestOPTIONS 428 | Type: 'AWS::ApiGateway::Deployment' 429 | Properties: 430 | Description: Prod deployment for API 431 | RestApiId: !If [CreateNewAPIGateway, !Ref NewAPIGateway, !Ref ExistingAPIGatewayId] 432 | StageName: prod 433 | 434 | # ---------- RETRIEVE API GATEWAY INFO --------- 435 | FunctionGetAPIGatewayInfo: 436 | Type: 'AWS::Lambda::Function' 437 | Properties: 438 | FunctionName: !Sub '${DynamoDBTable}-APIGatewayInfo' 439 | Description: Retrieve info about API Gateway resource 440 | Handler: index.handler 441 | Runtime: nodejs12.x 442 | Role: !GetAtt 443 | - GetAPIGatewayInfoRole 444 | - Arn 445 | Timeout: 120 446 | Environment: 447 | Variables: 448 | REST_API_ID: !If [CreateNewAPIGateway, !Ref NewAPIGateway, !Ref ExistingAPIGatewayId] 449 | Code: 450 | S3Bucket: !FindInMap 451 | - S3Buckets 452 | - !Ref 'AWS::Region' 453 | - Bucket 454 | S3Key: !FindInMap 455 | - Constants 456 | - S3Keys 457 | - GetAPIGatewayInfoCode 458 | 459 | RunGetAPIGatewayInfo: 460 | Type: 'Custom::CustomResource' 461 | Properties: 462 | ServiceToken: !GetAtt FunctionGetAPIGatewayInfo.Arn 463 | ParameterOne: Parameter to pass into Custom Lambda Function 464 | DependsOn: 465 | - FunctionGetAPIGatewayInfo 466 | GetAPIGatewayInfoRole: 467 | Type: 'AWS::IAM::Role' 468 | Properties: 469 | RoleName: !Sub '${DynamoDBTable}-APIGatewayInfo' 470 | AssumeRolePolicyDocument: 471 | Version: 2012-10-17 472 | Statement: 473 | - Effect: Allow 474 | Principal: 475 | Service: 476 | - apigateway.amazonaws.com 477 | - lambda.amazonaws.com 478 | Action: 479 | - 'sts:AssumeRole' 480 | ManagedPolicyArns: 481 | - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' 482 | Policies: 483 | - PolicyName: GetAPIInfo 484 | PolicyDocument: 485 | Version: 2012-10-17 486 | Statement: 487 | - Effect: Allow 488 | Action: 489 | - 'apigateway:GET' 490 | Resource: !Join [ "", [ "arn:aws:apigateway:", !Ref "AWS::Region", "::/restapis/", !If [CreateNewAPIGateway, !Ref NewAPIGateway, !Ref ExistingAPIGatewayId], "/*"]] 491 | Outputs: 492 | ElasticsearchEndpoint: 493 | Description: The Elasticsearch domain endpoint that you use to submit search requests. 494 | Value: !GetAtt ElasticsearchDomain.DomainEndpoint 495 | SearchAPI: 496 | Description: Seach API endpoint. 497 | Export: 498 | Name: !Sub '${DynamoDBTable}SearchAPI' 499 | Value: !Join [ "", [ "https://", !If [CreateNewAPIGateway, !Ref NewAPIGateway, !Ref ExistingAPIGatewayId], ".execute-api.", !Ref "AWS::Region", ".amazonaws.com/prod"]] 500 | -------------------------------------------------------------------------------- /functions/CreateGoal.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | 3 | const dynamoDb = new AWS.DynamoDB.DocumentClient(); 4 | 5 | exports.handler = (event, context, callback) => { 6 | // Request body is passed in as a JSON encoded string in 'event.body' 7 | const data = JSON.parse(event.body); 8 | 9 | const params = { 10 | TableName: process.env.TABLE_NAME, //[ProjectName]-Goals 11 | // 'Item' contains the attributes of the item to be created 12 | // - 'userId': user identities are federated through the 13 | // Cognito Identity Pool, we will use the identity id 14 | // as the user id of the authenticated user 15 | // - 'goalId': a unique identifier for the goal (uuid) 16 | // - 'title': user generated title for the goal (parsed from request body) 17 | // - 'content': user generated content for the goal (parsed from request body) 18 | // - 'createdAt': current Unix timestamp 19 | Item: { 20 | userId: event.requestContext.identity.cognitoIdentityId, 21 | goalId: context.awsRequestId, 22 | title: data.title, 23 | content: data.content, 24 | createdAt: Date.now() 25 | } 26 | }; 27 | 28 | dynamoDb.put(params, (error, data) => { 29 | // Set response headers to enable CORS (Cross-Origin Resource Sharing) 30 | const headers = { 31 | "Access-Control-Allow-Origin": "*", 32 | "Access-Control-Allow-Credentials" : true 33 | }; 34 | 35 | // Return status code 500 on error 36 | if (error) { 37 | const response = { 38 | statusCode: 500, 39 | headers: headers, 40 | body: error 41 | }; 42 | callback(null, response); 43 | return; 44 | } 45 | 46 | // Return status code 200 on success 47 | const response = { 48 | statusCode: 200, 49 | headers: headers 50 | }; 51 | callback(null, response); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /functions/DeleteGoal.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | 3 | const dynamoDb = new AWS.DynamoDB.DocumentClient(); 4 | 5 | exports.handler = (event, context, callback) => { 6 | const params = { 7 | TableName: process.env.TABLE_NAME, //[ProjectName]-Goals, 8 | // 'Key' defines the partition key and sort key of the item to be removed 9 | // - 'userId': user identities are federated through the Cognito Identity 10 | // Pool, we will use the identity id as the user id of the 11 | // authenticated user 12 | // - 'goalId': a unique identifier for the goal (uuid) 13 | Key: { 14 | userId: event.requestContext.identity.cognitoIdentityId, 15 | goalId: event.pathParameters.id 16 | } 17 | }; 18 | 19 | dynamoDb.delete(params, (error, data) => { 20 | // Set response headers to enable CORS (Cross-Origin Resource Sharing) 21 | const headers = { 22 | "Access-Control-Allow-Origin": "*", 23 | "Access-Control-Allow-Credentials" : true 24 | }; 25 | 26 | // Return status code 500 on error 27 | if (error) { 28 | const response = { 29 | statusCode: 500, 30 | headers: headers, 31 | body: error 32 | }; 33 | callback(null, response); 34 | return; 35 | } 36 | 37 | // Return status code 200 on success 38 | const response = { 39 | statusCode: 200, 40 | headers: headers, 41 | }; 42 | callback(null, response); 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /functions/GetGoal.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | 3 | const dynamoDb = new AWS.DynamoDB.DocumentClient(); 4 | 5 | // GetGoal - Get goal information for a given user id 6 | exports.handler = (event, context, callback) => { 7 | const params = { 8 | TableName: process.env.TABLE_NAME, //[ProjectName]-Goals, 9 | // 'Key' defines the partition key and sort key of the item to be retrieved 10 | // - 'userId': user identities are federated through the Cognito Identity 11 | // Pool, we will use the identity id as the user id of the 12 | // authenticated user 13 | // - 'goalId': a unique identifier for the goal (uuid) 14 | Key: { 15 | userId: event.requestContext.identity.cognitoIdentityId, 16 | goalId: event.pathParameters.id 17 | } 18 | }; 19 | 20 | dynamoDb.get(params, (error, data) => { 21 | // Set response headers to enable CORS (Cross-Origin Resource Sharing) 22 | const headers = { 23 | "Access-Control-Allow-Origin": "*", 24 | "Access-Control-Allow-Credentials" : true 25 | }; 26 | 27 | // Return status code 500 on error 28 | if (error) { 29 | const response = { 30 | statusCode: 500, 31 | headers: headers, 32 | body: error 33 | }; 34 | callback(null, response); 35 | return; 36 | } 37 | 38 | // Return status code 200 and the retrieved item on success 39 | const response = { 40 | statusCode: 200, 41 | headers: headers, 42 | body: JSON.stringify(data.Item) 43 | }; 44 | callback(null, response); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /functions/ListGoals.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | 3 | const dynamoDb = new AWS.DynamoDB.DocumentClient(); 4 | 5 | exports.handler = (event, context, callback) => { 6 | const params = { 7 | TableName: process.env.TABLE_NAME, //[ProjectName]-Goals 8 | // 'KeyConditionExpression' defines the condition for the query 9 | // - 'userId = :userId': only return items with matching 'userId' partition key 10 | // 'ExpressionAttributeValues' defines the value in the condition 11 | // - ':userId': defines 'userId' to be Identity Pool identity id of the 12 | // authenticated user 13 | KeyConditionExpression: "userId = :userId", 14 | ExpressionAttributeValues: { 15 | ":userId": event.requestContext.identity.cognitoIdentityId 16 | } 17 | }; 18 | 19 | dynamoDb.query(params, (error, data) => { 20 | // Set response headers to enable CORS (Cross-Origin Resource Sharing) 21 | const headers = { 22 | "Access-Control-Allow-Origin": "*", 23 | "Access-Control-Allow-Credentials" : true 24 | }; 25 | 26 | // Return status code 500 on error 27 | if (error) { 28 | const response = { 29 | statusCode: 500, 30 | headers: headers, 31 | body: error 32 | }; 33 | callback(null, response); 34 | return; 35 | } 36 | 37 | // Return status code 200 and the retrieved items on success 38 | const response = { 39 | statusCode: 200, 40 | headers: headers, 41 | body: JSON.stringify(data.Items) 42 | }; 43 | callback(null, response); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /functions/UpdateGoal.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | 3 | const dynamoDb = new AWS.DynamoDB.DocumentClient(); 4 | 5 | exports.handler = (event, context, callback) => { 6 | // Request body is passed in as a JSON encoded string in 'event.body' 7 | const data = JSON.parse(event.body); 8 | 9 | const params = { 10 | TableName: process.env.TABLE_NAME, //[ProjectName]-Goals, 11 | // 'Key' defines the partition key and sort key of the item to be updated 12 | // - 'userId': user identities are federated through the Cognito Identity 13 | // Pool, we will use the identity id as the user id of the 14 | // authenticated user 15 | // - 'goalId': a unique identifier for the goal (uuid) 16 | Key: { 17 | userId: event.requestContext.identity.cognitoIdentityId, 18 | goalId: event.pathParameters.id 19 | }, 20 | // 'UpdateExpression' defines the attributes to be updated 21 | // 'ExpressionAttributeValues' defines the value in the update expression 22 | // - ':title': defines 'title' to be the title parsed from the request body 23 | // - ':title': defines 'content' to be the content parsed from the request body 24 | UpdateExpression: "SET title = :title, content = :content", 25 | ExpressionAttributeValues: { 26 | ":title": data.title ? data.title : null, 27 | ":content": data.content ? data.content : null 28 | }, 29 | ReturnValues: "ALL_NEW" 30 | }; 31 | 32 | dynamoDb.update(params, (error, data) => { 33 | // Set response headers to enable CORS (Cross-Origin Resource Sharing) 34 | const headers = { 35 | "Access-Control-Allow-Origin": "*", 36 | "Access-Control-Allow-Credentials" : true 37 | }; 38 | 39 | // Return status code 500 on error 40 | if (error) { 41 | console.log(error); 42 | const response = { 43 | statusCode: 500, 44 | headers: headers, 45 | body: error 46 | }; 47 | callback(null, response); 48 | return; 49 | } 50 | 51 | // Return status code 200 on success 52 | const response = { 53 | statusCode: 200, 54 | headers: headers 55 | }; 56 | callback(null, response); 57 | }); 58 | }; 59 | -------------------------------------------------------------------------------- /readmeImages/ArchDiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-full-stack-template/ac72318d8689203b1c3fa8c71f3fed16e90248c3/readmeImages/ArchDiagram.png -------------------------------------------------------------------------------- /readmeImages/BackendDiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-full-stack-template/ac72318d8689203b1c3fa8c71f3fed16e90248c3/readmeImages/BackendDiagram.png -------------------------------------------------------------------------------- /readmeImages/DeveloperTools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-full-stack-template/ac72318d8689203b1c3fa8c71f3fed16e90248c3/readmeImages/DeveloperTools.png -------------------------------------------------------------------------------- /readmeImages/SummaryDiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amazon-archives/aws-full-stack-template/ac72318d8689203b1c3fa8c71f3fed16e90248c3/readmeImages/SummaryDiagram.png -------------------------------------------------------------------------------- /readmeImages/images.md: -------------------------------------------------------------------------------- 1 | ## AWS Full-Stack Template README Images 2 | 3 | Images in this directory are for the purposes of showing up in the README.md file 4 | -------------------------------------------------------------------------------- /workshop/readme.md: -------------------------------------------------------------------------------- 1 | # Workshop readme 2 | 3 | Hello workshoppers! We are excited for you to join us to learn how to build your own stack in just a few clicks! During this workshop, you will create two full-fledged web applications ([AWS Full-Stack Template](https://github.com/awslabs/aws-full-stack-template) and [AWS Bookstore Demo App](https://github.com/aws-samples/aws-bookstore-demo-app)), explore how they work, and learn how you might customize them for your own purposes. 4 | 5 | ## License Summary 6 | 7 | This sample code is made available under a modified MIT license. See the LICENSE file. 8 | 9 |   10 | 11 | ## Outline 12 | 13 | - [Overview](#overview) 14 | - [Prerequisite: AWS account](#prerequisite-aws-account) 15 | - [Part 1: AWS Full-Stack Template](#part-1-aws-full-stack-template) 16 | - [Section 1: Get to know the app](#section-1-get-to-know-the-app) 17 | - [Section 2: Explore the backend](#section-2-explore-the-backend) 18 | - [Section 3: Add on to the application](#section-3-add-on-to-the-application) 19 | - [Part 2: Extensions](#part-2-extensions) 20 | - [Section 1: Deploy a search extension to AWS Full-Stack Template](#section-1-deploy-a-search-extension-to-aws-full-stack-template) 21 | - [Section 2: Build your own extension! (optional)](#section-2-build-your-own-extension-optional) 22 | - [Part 3: AWS Bookstore Demo App](#part-3-aws-bookstore-demo-app) 23 | - [Section 1: Get to know the app](#section-1-get-to-know-the-app-1) 24 | - [Section 2: Explore the backend](#section-2-explore-the-backend-1) 25 | - [Section 3: Change the application (optional)](#section-3-change-the-application-optional) 26 | - [Part 4: Cleanup!](#part-4-cleanup) 27 | - [Part 5: Build on!](#part-5-build-on) 28 | - [Questions and contact](#questions-and-contact) 29 | 30 |   31 | 32 | ## Overview 33 | 34 | If you complete this workshop in it's entirety, good for you! We are very impressed. This workshop is not only designed to help you learn how to leverage these application templates, but also it is intended to leave you with ideas for how you might change and extend these (or other) applications in the future. There are several advanced sections to the workshop (marked as optional) that you can take home with you after the workshop session. 35 | 36 | ### Prerequisite: AWS account 37 | 38 | In order to maximize your time at the workshop, please make sure you have an AWS account set up. If you do not have an AWS account, please see [How do I create and activate a new Amazon Web Services account?](https://aws.amazon.com/premiumsupport/knowledge-center/create-and-activate-aws-account/) 39 | * Your account must have sufficient privileges to create, modify, and delete resources. For the purposes of this workshop, **we recommend you have admin privileges**. 40 | * Ensure that you have < 5 VPCs and < 100 S3 buckets in your account. 41 | 42 | *Be sure to shut down/remove all resources once you are finished with the workshop to avoid ongoing charges to your AWS account (see instructions on cleaning up/tear down in **Part 4: Cleanup** below.* 43 | 44 |   45 | 46 | --- 47 | 48 |   49 | 50 | ## Part 1: AWS Full-Stack Template 51 | 52 | In this part of the workshop, you will spin up a complete instance of [AWS Full-Stack Template](https://github.com/awslabs/aws-full-stack-template), understand the application architecture, manually change some components, and explore some additions. 53 | 54 | 55 | ### Section 1: Get to know the app 56 | 57 | #### Step 1: Play with the deployed goals app 58 | 59 | AWS Full-Stack Template is a full-stack sample web application that creates a simple CRUD (create, read, update, delete) app, and provides the foundational services, components, and plumbing needed to get a basic web application up and running. 60 | 61 | **[Try out the deployed application here](https://d2k5b8bzo1vefz.cloudfront.net/)**! This will open a new window with a fully-deployed version of AWS Full-Stack Template. Sign up using an email address and password (choose **Sign up to explore the demo**). 62 | *Note: Given that this is a demo application, we highly suggest that you do not use an email and password combination that you use for other purposes (such as an AWS account, email, or e-commerce site).* 63 | 64 | Once you provide your credentials, you will receive a verification code at the email address you provided. Upon entering this verification code, you will be signed into the application. 65 | 66 | Add a goal and a description, and choose save. Try editing a goal, and then deleting a goal. Well done - you can CRUD! 67 | 68 | #### Step 2: Deploy AWS Full-Stack Template in your own AWS account 69 | 70 | ***IMPORTANT NOTE:** Creating this application in your AWS account will create and consume AWS resources, which **will cost money**. We estimate that running this demo application will cost **<$0.10/hour** with light usage. Be sure to shut down/remove all resources once you are finished with the workshop to avoid ongoing charges to your AWS account (see instructions on cleaning up/tear down in **Part 4: Cleanup** below.* 71 |   72 | 73 | To get the AWS Full-Stack Template up and running in your own AWS account, follow these steps: 74 | 75 | 1. Log into the [AWS console](https://console.aws.amazon.com/) if you are not already. 76 | *Note: If you are logged in as an IAM user, ensure your account has permissions to create and manage the necessary resources and components for this application.* 77 | 2. Choose one of the **Launch Stack** buttons below for your desired AWS region to open the AWS CloudFormation console and create a new stack. AWS Full-Stack Template is supported in the following regions: 78 | 79 | Region name | Region code | Launch 80 | --- | --- | --- 81 | US East (N. Virginia) | us-east-1 | [![Launch Stack](https://cdn.rawgit.com/buildkite/cloudformation-launch-stack-button-svg/master/launch-stack.svg)](https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/new?stackName=MyGoalsApp&templateURL=https://s3.amazonaws.com/aws-fullstack-template/master-fullstack.yaml) 82 | US West (Oregon) | us-west-2 | [![Launch Stack](https://cdn.rawgit.com/buildkite/cloudformation-launch-stack-button-svg/master/launch-stack.svg)](https://console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/new?stackName=MyGoalsApp&templateURL=https://s3.amazonaws.com/aws-fullstack-template/master-fullstack.yaml) 83 | EU (Ireland) | eu-west-1 | [![Launch Stack](https://cdn.rawgit.com/buildkite/cloudformation-launch-stack-button-svg/master/launch-stack.svg)](https://console.aws.amazon.com/cloudformation/home?region=eu-west-1#/stacks/new?stackName=MyGoalsApp&templateURL=https://s3.amazonaws.com/aws-fullstack-template/master-fullstack.yaml) 84 | EU (Frankfurt) | eu-central-1 | [![Launch Stack](https://cdn.rawgit.com/buildkite/cloudformation-launch-stack-button-svg/master/launch-stack.svg)](https://console.aws.amazon.com/cloudformation/home?region=eu-central-1#/stacks/new?stackName=MyGoalsApp&templateURL=https://s3.amazonaws.com/aws-fullstack-template/master-fullstack.yaml) 85 | 86 | 87 | 3. Continue through the CloudFormation wizard steps 88 | 1. Name your stack, e.g. MyGoalsApp 89 | 2. Provide a project name, e.g. goalsapp (must be lowercase, letters only, and **under twelve (12) characters**). This is used when naming your resources, e.g. tables, etc. 90 | 3. Choose next, then next. 91 | 4. On the last review page, check the blue box for creating IAM resources. 92 | 4. Choose **Create stack**. This will take ~15 minutes to complete. 93 | 94 | #### Step 3: Review architecture 95 | 96 | While the application is deploying in CloudFormation (should take ~15 minutes), browse the [readme](https://github.com/awslabs/aws-full-stack-template/blob/master/README.md) for AWS Full-Stack Template. The **Overview** section provides a basic understanding of what the application consists of. The remainder of the readme file dives deeper, including the **Architecture**, **Implementation details**, and **Considerations for demo purposes** sections. This familiarization with how the app is structured will come in handy once deployment is complete and we browse the components of the application. 97 | 98 | #### Step 4: Open the endpoint from CloudFormation 99 | 100 | Once the CloudFormation deployment is complete, check the status of the build in the [CodePipeline](https://console.aws.amazon.com/codesuite/codepipeline/pipelines) console. When that has succeeded, go back to the CloudFormation console, open the stack details page for your application, go to the **Outputs** table, and find the CloudFront URL. This is the public endpoint to your deployed application. Click the link to open and explore your brand-new goals app! 101 | 102 | Since this is a completely new instance of the application, your username and password that you used before in Step 1 won't work. Sign up for an account in your goals app, and test out the app! Test to make sure the email verification works, that you can create, update, and delete goals, etc. The registration/login experience is run in your AWS account, and the supplied credentials are stored in Amazon Cognito. 103 | *Note: If you are having issues with the sign up page using Firefox, try using Chrome.* 104 | 105 | 106 | ### Section 2: Explore the backend 107 | 108 | Now that the application is up and running, let's open the hood and play around with some of the backend components. 109 | 110 | #### Step 1: Change the details for one of your goals directly in DynamoDB 111 | 112 | Let's try changing one of the goals directly in DynamoDB. Open the [DynamoDB](https://console.aws.amazon.com/dynamodb) console, find the table that corresponds to this project (the table name will match the "ProjectName" you used when creating the stack in CloudFormation), and choose one of the goal items to modify. Change either the "title" string or the "content" string (which maps to the "Description" field in the app), and save your change in DynamoDB. Return to the goals app, and refresh the page. You should see your change reflected in the list. 113 | 114 | #### Step 2: Delete a user in Cognito 115 | 116 | Open the Cognito console, and choose "Manage User Pools." Look for the user pool that matches the "ProjectName" you used when creating the stack in CloudFormation and open this user pool. Choose "Users and groups" in the left navigation menu and choose one of your users. If you only signed up yourself, you can choose to delete your own user and then sign up again, or create another user from the frontend and delete that user. Next, choose "Disable user" and then click the "Delete user" button that appears. Tada! You are an amazing administrator. 117 | 118 | #### Step 3: Change the application into a notes application *(optional)* 119 | The goals application is not that far off from a simple notes application - it contains a title and a description field. You could easily turn the goals app into a notes-taking app by simply changing the titles, column headers, and buttons on the pages. Of course, that wouldn't change the backend (APIs, Lambda functions, and Tables), so if you were really passionate about changing the entire application into a notes app, you could make the requisite changes throughout. 120 | 121 | Hint: To get started with changing just the page-level items, open the CodeCommit console, choose "Repositories" and then "Code" from the left navigation. Navigate to /src/modules/signup/Home.tsx and /src/modules/goal/AddEditGoal.tsx. Choose "Edit" in the upper-right, make your changes, and then choose "Commit changes" at the bottom. 122 | 123 | Next, go to the pipeline that was set up for you in CodePipeline (choose "Pipeline" from the left navigation, then choose "Pipelines"). Open the pipeline associated with your goals app project, and choose "Release change." 124 | 125 | Watch the CodePipeline console for the build to complete (this will take a few minutes). When that has succeeded, refresh your goals app page. If you don't see the new column show up, do a **hard refresh** (ctrl/command + shift + R) in your browser. Cool! 126 | 127 | This entire step is optional, but illustrative if you want to learn more about the different backend pieces. 128 | 129 | 130 | ### Section 3: Add on to the application 131 | 132 | By this point, you should have a pretty good feel for the different architectural components of AWS Full-Stack Template. Since this is designed to provide you with the foundational services, components, and plumbing needed to get a basic web application up and running (it's a template, after all!) the next step is to add on to the application and make it your own. 133 | 134 | #### Step 1: Add a "Last updated" or "Last modified" parameter 135 | 136 | First, you'll need to modify the "UpdateGoal" Lambda function to add the current time for the update, which will pass this information to DynamoDB. To do this, open the [AWS Lambda console](https://console.aws.amazon.com/lambda) and search for "goal" which should return a list of the functions created in your account by the CloudFormation template. Choose the one that ends in "UpdateGoal," scroll down to the "Function code" card, and make your modifications. For extra credit, try to make the changes without looking at the answers provided in the expandable section below! 137 | 138 |
139 | Expand to see code (answers!) 140 | 141 | 142 | Add a parameter at the end of the "UpdateExpression" line and a new line under "ExpressionAttributeValues" so it looks like this: 143 | ```js 144 | UpdateExpression: "SET title = :title, content = :content, lastUpdated = :lastUpdated", 145 | ExpressionAttributeValues: { 146 | ":title": data.title ? data.title : null, 147 | ":content": data.content ? data.content : null, 148 | ":lastUpdated": Date.now() 149 | }, 150 | ``` 151 | 152 | Note we have added the "lastUpdated" parameter in **two** places. Also, don't forget the comma after the "null". 153 | 154 |
155 | 156 | Save your function. Since the function hasn't been used yet, there won't be any "lastUpdated" record created until the function is called, so go modify one of your goals in the app. You can change the title or description (up to you), and choose "Update goal." 157 | 158 | Next, let's go to DynamoDB to verify the new "lastUpdated" information is passed through. Open the DynamoDB console and find the table that corresponds to this goals project (the table name will match the "ProjectName" you used when creating the stack in CloudFormation). Open the item corresponding to the goal you updated, and you should now see the "lastUpdated" information in the item. 159 | 160 | #### Step 2: Add a "Last updated" column to the goals list page 161 | 162 | Now that you have a snazzy new parameter to track when your goal was last updated, the next step is to add this information to the goals list page. Here's a big hint: you only need to change one file. To get started, open the CodeCommit console, choose "Repositories" and then "Code" from the left navigation. Navigate to /src/modules/signup/Home.tsx. Choose "Edit" in the upper-right and make your modifications. For extra credit, try to make the changes without looking at the answers provided in the expandable section below! 163 | 164 |
165 | Expand to see code (answers!) 166 | 167 | 168 | Add new lines under "interface Goal," "return goalsList," and "Table" so it looks like this: 169 | ```js 170 | interface Goal { 171 | content: string; 172 | goalId: string; 173 | title: string; 174 | createdAt: Date; 175 | lastUpdated: Date; //<--This line! 176 | } 177 | ``` 178 | ```js 179 | return goalsList.concat(goals).map( 180 | (goal, i) => 181 | 182 | {goal.title} 183 |
{goal.content.trim().split("\n")[0]}
184 | {new Date(goal.createdAt).toLocaleString()} 185 | {new Date(goal.lastUpdated).toLocaleString()} //<--This line! 186 | 187 | ); 188 | ``` 189 | ```js 190 | 191 | 192 | 193 | 194 | 195 | 196 | //<--This line! 197 | 198 | ``` 199 | 200 | 201 | 202 | Make your changes and then choose "Commit changes" at the bottom. 203 | 204 | Next, go to the pipeline that was set up for you in CodePipeline (choose "Pipeline" from the left navigation, then choose "Pipelines"). Open the pipeline associated with your goals app project, and choose "Release change." 205 | 206 | Watch the CodePipeline console for the build to complete (this will take a few minutes). When that has succeeded, refresh your goals app page. If you don't see the new column show up, do a **hard refresh** (ctrl/command + shift + R) in your browser. Cool! What else can you add? 207 | 208 | 209 | ## End of Part 1 210 | 211 | You finished Part 1 of the workshop! If you do not plan to immediately continue to **Part 2: Extensions** or **Part 3: AWS Bookstore Demo App**, please skip ahead to **Part 4: Cleanup!** 212 | 213 |   214 | 215 | --- 216 | 217 |   218 | 219 | ## Part 2: Extensions 220 | 221 | In this part of the workshop, you will spin up the [Search API Extension](https://github.com/awslabs/aws-full-stack-template/tree/master/extensions/search-api), explore the architecture of the Extension, and understand how to take a basic application like the Goals app in AWS Full-Stack Template and turn it into something different by adding components. 222 | 223 | 224 | ### Prerequisite 225 | This part requires [Part 1: Step 2](#step-2-deploy-aws-full-stack-template-in-your-own-aws-account) and [Part 1: Step 4](#step-4-open-the-endpoint-from-cloudformation) of the workshop to be completed first. This will ensure you have a functioning instance of AWS Full-Stack Template up and running that you can add on to. 226 | 227 | ### Section 1: Deploy a search extension to AWS Full-Stack Template 228 | 229 | #### Step 1: Deploy the search extension in your AWS account 230 | 231 | The Search API Extension enables you to add search functionality on top of your data in DynamoDB powered by Elasticsearch and API Gateway. The extension can be created with a single CloudFormation template! This extension takes in a DynamoDB table and API Gateway ID as parameters. It will spin up an Elasticsearch cluster, stream changes from DynamoDB to Elasticsearch, and create a Search API. Normally you can choose between integrating with an existing API Gateway ID or having the extension create a new one, but since we are building on top of AWS Full-Stack Template, we will integrate with the existing resources. 232 | 233 | ***IMPORTANT NOTE:** Creating this application in your AWS account will create and consume AWS resources, which **will cost money**. We estimate that running this demo application will cost **<$0.10/hour** with light usage. Be sure to shut down/remove all resources once you are finished with the workshop to avoid ongoing charges to your AWS account (see instructions on cleaning up/tear down in **Part 4: Cleanup** below.* 234 |   235 | 236 | To get the Search API Extension up and running in your AWS account, follow these steps: 237 | 238 | 1. Log into the [AWS console](https://console.aws.amazon.com/) if you are not already. 239 | *Note: If you are logged in as an IAM user, ensure your account has permissions to create and manage the necessary resources and components for this application.* 240 | 2. Choose one of the **Launch Stack** buttons below for your desired AWS region to open the AWS CloudFormation console and create a new stack. The Search API extension is supported in the following regions: 241 | 242 | Region name | Region code | Launch 243 | --- | --- | --- 244 | US East (N. Virginia) | us-east-1 | [![Launch Stack](https://cdn.rawgit.com/buildkite/cloudformation-launch-stack-button-svg/master/launch-stack.svg)](https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/new?stackName=SearchAPI&templateURL=https://s3.amazonaws.com/aws-dmas/ddb-es/master.yaml) 245 | US West (Oregon) | us-west-2 | [![Launch Stack](https://cdn.rawgit.com/buildkite/cloudformation-launch-stack-button-svg/master/launch-stack.svg)](https://console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/new?stackName=SearchAPI&templateURL=https://s3.amazonaws.com/aws-dmas/ddb-es/master.yaml) 246 | EU (Ireland) | eu-west-1 | [![Launch Stack](https://cdn.rawgit.com/buildkite/cloudformation-launch-stack-button-svg/master/launch-stack.svg)](https://console.aws.amazon.com/cloudformation/home?region=eu-west-1#/stacks/new?stackName=SearchAPI&templateURL=https://s3.amazonaws.com/aws-dmas/ddb-es/master.yaml) 247 | EU (Frankfurt) | eu-central-1 | [![Launch Stack](https://cdn.rawgit.com/buildkite/cloudformation-launch-stack-button-svg/master/launch-stack.svg)](https://console.aws.amazon.com/cloudformation/home?region=eu-central-1#/stacks/new?stackName=SearchAPI&templateURL=https://s3.amazonaws.com/aws-dmas/ddb-es/master.yaml) 248 | 249 | 3. Continue through the CloudFormation wizard steps 250 | 1. Name your stack, e.g. SearchAPI 251 | 2. Enter the DynamoDB table to integrate with. (You can find this in the CloudFormation console in the Resources tab of your deployed AWS Full-Stack Template. In the TGoals row, use the value in the **Physical ID** column.) 252 | 3. Specify the API Gateway ID to integrate with (You can find this in the CloudFormation console in the Resources tab of your deployed AWS Full-Stack Template. In the AppApi row, use the value in the **Physical ID** column.) 253 | 4. Choose next, then next. 254 | 5. On the last review page, check the blue box for creating IAM resources. 255 | 4. Choose Create stack. This will take ~15 minutes to complete. 256 | 257 | #### Step 2: Play with the search capability 258 | 259 | Now that you've deployed the search extension, let's test it to make sure search results are being returned properly. 260 | 261 | Before proceeding, either create a new goal or update one of your existing goals. When the search extension is deployed, it does not currently backfill the ES cluster with existing data from DynamoDB (pull request, please!). Creating a new goal or updating an existing one will stream the new/updated goal data to the ES cluster. 262 | 263 | Open the DynamoDB console and find the table associated with your goals app. Go to the items tab and choose the new/updated goal you just modified. Copy the userID and the title of the goal and make a note of them for use in a minute. 264 | 265 | In a new tab, open the Lambda console and look for the search function that was created as part of the extension you just ran in CloudFormation. It should be titled "yourextensionprojectname-Search." Open the Lambda function, choose the "Select a test event" dropdown towards the top, and choose "Configure test events." In the "Event template" dropdown, choose "Amazon API Gateway AWS Proxy." Here we are setting up a test for our search extension. 266 | 267 | Next, enter an event name (e.g. searchtest) and change the following lines of code from: 268 | 269 | ```js 270 | "queryStringParameters": { 271 | "foo": "bar" 272 | }, 273 | ``` 274 | to: 275 | ```js 276 | "queryStringParameters": { 277 | "q": "title of your goal" 278 | }, 279 | ``` 280 | 281 | and 282 | 283 | ```js 284 | "cognitoIdentityId": null, 285 | ``` 286 | to: 287 | ```js 288 | "cognitoIdentityId": "your Cognito Identity ID", 289 | ``` 290 | 291 | Where "title of your goal" is the goal title you made a note of earlier (or perhaps one word of your goal title), and "your Cognito Identity ID" is the userID (don't forget to add quotations around this parameter!). Entering the goal title simulates the string that you might put into a search bar, and passing in the userID ensures that only goals created or updated by the logged-in user are shown. 292 | 293 | Choose "Create" at the bottom of the window. Next, choose "Test" at the top of the window and verify that you see your goal information returned. 294 | 295 | #### Step 3: Modify the search capability 296 | 297 | With the search capability as-is, you are currently able to search by the goal title of one of your goals. Now we're going to add the ability to search by the goal description, as well. 298 | 299 | Open the AWS Lambda console to the same Lambda function from Step 2 (the one titled "yourextensionprojectname-Search"). Scroll down to the "Function code" card and make your modifications (hint: look for the big "# TODO" code comment). For extra credit, try to make the changes without looking at the answers provided in the expandable section below! 300 | 301 |
302 | Expand to see code (answers!) 303 | 304 | 305 | Add a parameter to the "fields" attribute (Line 30) under the "query" definition so it looks like the below. You can remove the code comment if you like. 306 | ```js 307 | "fields" : [“title.S”, “content.S"] 308 | ``` 309 | 310 |
311 | 312 | Save your function. If you have not already created a new goal or updated a goal (i.e. in Step 2), go do this in the app now. 313 | 314 | Next, let's verify that we can now search by a goal description. Using the same dropdown you used to create your test event in Step 2, choose "Configure test events" again, and change the query from one of your goal titles to match one of your goal descriptions. Choose "Save" at the bottom of the window. Next, choose "Test" at the top of the window and verify that you see your goal information returned. 315 | 316 | #### Step 4: Add a search bar to the goals app *(optional)* 317 | 318 | If you would like, you can go even further by integrating the search functionality into the frontend application. Feel free to explore how AWS Bookstore Demo App did this, or experiment! 319 | 320 | *Hint:* Check out [SearchBar.tsx](https://github.com/aws-samples/aws-bookstore-demo-app/blob/master/assets/src/modules/search/searchBar/SearchBar.tsx) for a sample search implementation. 321 | 322 | 323 | ### Section 2: Build your own extension! *(optional)* 324 | 325 | In this section, you will try to build your own extension on top of an existing application. 326 | 327 | #### Step 1: Tear apart the search extension in Section 1 328 | 329 | In order to figure out how you can build a generic extension on top of an existing application (ideally any application), first start by tearing apart the search extension we discussed in Part 2, Section 1. Open the CloudFormation template and understand the different elements that are being deployed. Open the Lambda functions to explore the logic for how/when conditional checks are made. You can find all the resources for the search extension along with the CloudFormation template in the [**Extensions** folder](https://github.com/awslabs/aws-full-stack-template/tree/master/extensions/search-api). 330 | 331 | #### Step 2: Decide what type of extension you want to build 332 | 333 | We decided to build a search extension to add search functionality on top of any application. What will you come up with? You could build a caching extension, a visualization extension, or something else. What feature do you wish your web app had? Let your imagination run wild. 334 | 335 | #### Step 3: Build your extension 336 | 337 | Following the generic structure of the search extension, build a CloudFormation template that will add your extension on top of an existing application. Let us know what you built! 338 | 339 | 340 | ## End of Part 2 341 | 342 | You finished Part 2 of the workshop! If you would like to save your work, please do so! If you are interested in sharing your work, please see **Part 5: Build on!** for how you might contribute additions and ideas to either AWS Full-Stack Template or AWS Bookstore Demo App. 343 | 344 | Don't forget to finish **Part 4: Cleanup!** to avoid ongoing charges to your AWS account. 345 | 346 |   347 | 348 | --- 349 | 350 |   351 | 352 | ## Part 3: AWS Bookstore Demo App 353 | 354 | In this part of the workshop, you will spin up a complete instance of [AWS Bookstore Demo App](https://github.com/aws-samples/aws-bookstore-demo-app), understand the application architecture, manually change some components, and explore some additions. If you just completed **Part 1: AWS Full-Stack Template**, some of these instructions will be repetitive. 355 | 356 | 357 | ### Section 1: Get to know the app 358 | 359 | #### Step 1: Play with the deployed Bookstore 360 | 361 | AWS Bookstore Demo App is a full-stack sample web application that creates a storefront (and backend) for customers to shop for fictitious books. You can browse and search for books, look at recommendations and best sellers, manage your cart, checkout, view your orders, and more. 362 | 363 | **[Try out the deployed application here](https://d2h3ljlsmzojxz.cloudfront.net/)**! This will open a new window with a fully-deployed version of AWS Bookstore Demo App. Sign up using an email address and password (choose **Sign up to explore the demo**). 364 | *Note: Given that this is a demo application, we highly suggest that you do not use an email and password combination that you use for other purposes (such as an AWS account, email, or e-commerce site).* 365 | 366 | Once you provide your credentials, you will receive a verification code at the email address you provided. Upon entering this verification code, you will be signed into the application. 367 | 368 | View the different product categories, add some items to your cart, and checkout. Search for a few books by title, author, or category using the search bar. View the *Best Sellers* list, and see if you can move something to the top of the list by ordering a bunch of books. Finally, take a look at the social recommendations on the home page and the *Best Sellers* page. Look at you, savvy book shopper! 369 | 370 | #### Step 2: Deploy AWS Bookstore Demo App in your own AWS account 371 | 372 | ***IMPORTANT NOTE:** Creating this application in your AWS account will create and consume AWS resources, which **will cost money**. We estimate that running this demo application will cost **<$0.45/hour** with light usage. Be sure to shut down/remove all resources once you are finished with the workshop to avoid ongoing charges to your AWS account (see instructions on cleaning up/tear down in **Part 4: Cleanup** below.* 373 |   374 | 375 | To get the AWS Bookstore Demo App up and running in your own AWS account, follow these steps: 376 | 377 | 1. Log into the [AWS console](https://console.aws.amazon.com/) if you are not already. 378 | *Note: If you are logged in as an IAM user, ensure your account has permissions to create and manage the necessary resources and components for this application.* 379 | 2. Choose one of the **Launch Stack** buttons below for your desired AWS region to open the AWS CloudFormation console and create a new stack. AWS Bookstore Demo App is supported in the following regions: 380 | 381 | Region name | Region code | Launch 382 | --- | --- | --- 383 | US East (N. Virginia) | us-east-1 | [![Launch Stack](https://cdn.rawgit.com/buildkite/cloudformation-launch-stack-button-svg/master/launch-stack.svg)](https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/new?stackName=MyBookstore&templateURL=https://s3.amazonaws.com/aws-bookstore-demo/master-fullstack.yaml) 384 | US West (Oregon) | us-west-2 | [![Launch Stack](https://cdn.rawgit.com/buildkite/cloudformation-launch-stack-button-svg/master/launch-stack.svg)](https://console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/new?stackName=MyBookstore&templateURL=https://s3.amazonaws.com/aws-bookstore-demo/master-fullstack.yaml) 385 | EU (Ireland) | eu-west-1 | [![Launch Stack](https://cdn.rawgit.com/buildkite/cloudformation-launch-stack-button-svg/master/launch-stack.svg)](https://console.aws.amazon.com/cloudformation/home?region=eu-west-1#/stacks/new?stackName=MyBookstore&templateURL=https://s3.amazonaws.com/aws-bookstore-demo/master-fullstack.yaml) 386 | EU (Frankfurt) | eu-central-1 | [![Launch Stack](https://cdn.rawgit.com/buildkite/cloudformation-launch-stack-button-svg/master/launch-stack.svg)](https://console.aws.amazon.com/cloudformation/home?region=eu-central-1#/stacks/new?stackName=MyBookstore&templateURL=https://s3.amazonaws.com/aws-bookstore-demo/master-fullstack.yaml) 387 | 388 | 389 | 3. Continue through the CloudFormation wizard steps 390 | 1. Name your stack, e.g. MyBookstore 391 | 2. Provide a project name (must be lowercase, letters only, and **under twelve (12) characters**). This is used when naming your resources, e.g. tables, search domain, etc. 392 | 3. Choose next, then next. 393 | 4. On the last review page, check the blue box for creating IAM resources. 394 | 4. Choose **Create stack**. This will take ~20 minutes to complete. 395 | 396 | #### Step 3: Review architecture 397 | 398 | While the application is deploying in CloudFormation (should take ~20 minutes), browse the [readme](https://github.com/aws-samples/aws-bookstore-demo-app/blob/master/README.md) for AWS Bookstore Demo App. The **Overview** section provides a basic understanding of what the application consists of. The remainder of the readme file dives deeper, including the **Architecture**, **Implementation details**, and **Considerations for demo purposes** sections. This familiarization with how the app is structured will come in handy once deployment is complete and we browse the components of the application. 399 | 400 | If you are coming from **Part 1** of this workshop, you should notice many similarities in the architecture. That is because AWS Bookstore Demo App was built on top of AWS Full-Stack Template. They contain the same developer infrastructure and frontend architectures, and a similar backend interface through Amazon API Gateway and AWS Lambda. 401 | 402 | What differences can you find in the architecture? How many DynamoDB tables are there in this app compared to the other? How many additional APIs are there? 403 | 404 | #### Step 4: Open the endpoint from CloudFormation 405 | 406 | Once the CloudFormation deployment is complete, check the status of the build in the [CodePipeline](https://console.aws.amazon.com/codesuite/codepipeline/pipelines) console. When that has succeeded, go back to the CloudFormation console, open the stack details page for your application, go to the **Outputs** table, and find the CloudFront URL. This is the public endpoint to your deployed application. Click the link to open and explore your brand-new Bookstore! 407 | 408 | Since this is a completely new instance of the application, your username and password that you used before in Step 1 won't work. Sign up for an account in your Bookstore, and test out the app! Test to make sure the email verification works, and run through some of the use cases from before like search, cart, ordering, and best sellers. 409 | *Note: If you are having issues with the sign up page using Firefox, try using Chrome.* 410 | 411 | 412 | ### Section 2: Explore the backend 413 | 414 | Now that the application is up and running, let's open the hood and play around with some of the backend componentry. 415 | 416 | #### Step 1: Change the author of a book in DynamoDB 417 | 418 | Since you've deployed this application, you deserve a reward! Let's make you the instant author of your favorite book in the bookstore. Open the DynamoDB console, find the BooksTable, find the name of the book you want to be the author of, and change the Author name. Refresh the bookstore page. Congratulations, you are an author in your own personal bookstore! 419 | 420 | #### Step 2: Update search to reflect your new authorship 421 | 422 | This might be the easiest step. After you updated the Author name, in the backend, DynamoDB Streams automatically pushed this information to the Elasticsearch cluster for your application. In the bookstore page, try searching for your name. The book you authored should be returned. 423 | 424 | #### Step 3: Manually edit the leaderboard/Best Sellers list 425 | 426 | First, make sure you have at least two books in your *Past orders* list with different order quantities. Unless you've already created multiple accounts for the boosktore, the books in your *Past orders* list will match the books in the *Best Sellers* list (since the bookstore started with no orders). 427 | 428 | If you're like us, you ordered over 1000 copies of one of the books to bump it to the top of the list. That was clearly an ordering error, so let's go correct the *Best Sellers* list. In order to do this, open the DynamoDB console and find the orders table. Next, find the entry for your big order and open the item to edit it. Open the "books" list, and then edit the "quantity" to be less than one of your other ordered books. Click save. Wait a few moments, and then go to the bookstore endpoint and refresh the page. You should notice the book moved in the list. 429 | 430 | Just like with updating the search experience, in the backend, DynamoDB Streams automatically pushed this information to ElastiCache for Redis, and the *Best Sellers* list was updated. 431 | 432 | 433 | ### Section 3: Change the application *(optional)* 434 | 435 | By this point, you should have a pretty good feel for the different architectural components of AWS Bookstore Demo App, and how it is just one example of what you might create with AWS Full-Stack Template. You can choose to start with the basic template and add on, or start with something more full-featured (like the bookstore) and change it. Either way, you have the foundational services, components, and plumbing you need to start building any web application. 436 | 437 | Here are a few ideas for how you might change AWS Bookstore Demo App. Note: many of these are advanced! You are also welcome to move on to **Part 3: Extensions** if you like. 438 | 439 | #### Option 1: Use it for your own storefront *(medium)* 440 | 441 | Perhaps you are interested in spinning up your own storefront. You will likely still want to have a product catalog, categories, search, best sellers, etc., so this app is a very good place to start. 442 | 443 | Let's imagine that you are going to sell furniture. Go into DynamoDB and change the structure (and content) of all of the tables to reflect that you are selling furniture. You'll also want to go to API Gateway and make the requisite changes. Finally, you can make changes to the frontend assets in CodeCommit to reflect your new look, furniture categories (instead of books), etc., and then push these changes through CodePipeline to update your storefront. 444 | 445 | #### Option 2: Add individual product pages *(hard)* 446 | 447 | One thing missing from the bookstore (or your furniture store) is individual pages for your products. Build out an additional table(s) or rows in DynamoDB to contain the structured information for your product pages. 448 | 449 | #### Option 3: Turn the bookstore into a blog *(hard)* 450 | 451 | Now that you've added individual product pages, you are not that far off from a robust blog application. The search experience will help viewers find your content, the *Best Sellers* list can effectively return your most popular posts, and the product catalog and product pages can be your blog entries! What else can you do? 452 | 453 | #### Option 4: Use a different database *(hardest)* 454 | 455 | Perhaps you want to use a different database for your product catalog (i.e. Amazon Aurora). Change the backend structure, API calls, and other lookups to use a different database structure and call the other service. 456 | 457 | #### Option 5: Build out the “Friends” functionality in the Bookstore *(hardest)* 458 | 459 | One of the main items that is missing from the current AWS Bookstore Demo App implementation is that there is no way to manage (view, add, remove) your friends. Build a friends management system and add it into the bookstore. 460 | 461 | 462 | ## End of Part 3 463 | 464 | You finished Part 3 of the workshop! If you would like to save your work, please do so! If you are interested in sharing your work, please see **Part 5: Build on!** for how you might contribute additions and ideas to either AWS Full-Stack Template or AWS Bookstore Demo App. 465 | 466 | Don't forget to finish **Part 4: Cleanup!** to avoid ongoing charges to your AWS account. 467 | 468 |   469 | 470 | --- 471 | 472 |   473 | 474 | ## Part 4: Cleanup! 475 | 476 | To make sure you don't continue to incur charges on your AWS account, make sure to tear down your applications and remove all resources associated with both AWS Full-Stack Template and AWS Bookstore Demo App. 477 | 478 | 1. Log into the [Amazon S3 Console](https://console.aws.amazon.com/s3) and delete the buckets created for this workshop. 479 | - There should be two buckets created for AWS Full-Stack Template and two buckets created for AWS Bookstore Demo App. The buckets will be titled "X" and "X-pipeline", where "X" is the name you specified in the CloudFormation wizard under the AssetsBucketName parameter. 480 | - *Note: Please be **very careful** to only delete the buckets associated with this workshop that you are absolutely sure you want to delete.* 481 | 2. Log into the AWS CloudFormation Console and find the stack(s) you created during this workshop 482 | 3. Delete the stack(s) 483 | 484 | *Remember to shut down/remove all related resources once you are finished to avoid ongoing charges to your AWS account.* 485 | 486 |   487 | 488 | --- 489 | 490 |   491 | 492 | ## Part 5: Build on! 493 | 494 | Now that you are an expert in creating full-stack applications in just a few clicks, go build something awesome! Let us know what you come up with. We encourage developer participation via contributions and suggested additions to both AWS Full-Stack Template and AWS Bookstore Demo App. Of course you are welcome to create your own version! 495 | 496 | Please see the [contributing guidelines](https://github.com/awslabs/aws-full-stack-template/blob/master/CONTRIBUTING.md) for more information. 497 | 498 |   499 | 500 | --- 501 | 502 |   503 | 504 | ## Questions and contact 505 | 506 | For questions on the AWS Full-Stack Template, or to contact the team, please leave a comment on GitHub. 507 | --------------------------------------------------------------------------------
Goal nameDescriptionDate createdDate modified