├── .gitignore ├── README.md ├── buildspec.yml ├── deploy ├── .terraform.lock.hcl ├── 00-variables.tf ├── 01-main.tf ├── 02-s3.tf ├── 03-codepipeline.tf ├── 04-codepipeline-role.tf ├── 05-codebuild.tf ├── 06-codebuild-role.tf ├── 07-dynamo-db.tf ├── 08-api-gateway.tf ├── 09-api-gateway-role.tf ├── 10-lambdas-todos.tf ├── 11-lambda-permission.tf ├── 12-sqs.tf ├── 13-lambda-process-queue.tf ├── 14-cognito.tf ├── 15-acm.tf ├── 16-cloudfront.tf ├── 17-route53.tf ├── 18-cloudwatch.tf ├── api │ └── openapi.yaml ├── codebuild-buildspecs │ └── invalidate-cloudfront-buildspec.yml ├── lambdas │ ├── createComment.js │ ├── createComment.zip │ ├── createLike.js │ ├── createLike.zip │ ├── createTodo.js │ ├── createTodo.zip │ ├── deleteCommentById.js │ ├── deleteCommentById.zip │ ├── deleteLikeById.js │ ├── deleteLikeById.zip │ ├── deleteTodoById.js │ ├── deleteTodoById.zip │ ├── getComments.js │ ├── getComments.zip │ ├── getLikes.js │ ├── getLikes.zip │ ├── getTodoById.js │ ├── getTodoById.zip │ ├── getTodos.js │ ├── getTodos.zip │ ├── sqsProcesser.js │ ├── sqsProcesser.zip │ ├── updateTodoById.js │ └── updateTodoById.zip ├── output.tf ├── policy │ └── s3-policy.json └── terraform.tfvars ├── images └── aws_react_serverless4.JPG ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.css ├── App.js ├── App.test.js ├── components │ ├── CommentsList.jsx │ ├── Footer.jsx │ ├── Likes.jsx │ └── Navbar.jsx ├── conf │ └── config.js ├── index.css ├── index.js ├── logo.svg ├── pages │ ├── AboutPage.jsx │ ├── EditTodoPage.jsx │ ├── HomePage.jsx │ ├── LoginPage.jsx │ └── NotFoundPage.jsx ├── routers │ └── Routes.jsx ├── serviceWorker.js └── setupTests.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | dist 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | ### Terraform ### 27 | # Local .terraform directories 28 | **/.terraform/* 29 | 30 | # .tfstate files 31 | *.tfstate 32 | *.tfstate.* 33 | 34 | *secret.tfvars 35 | 36 | *local.config.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS Serverless React App 2 | 3 | A reference project to deploy a serverless, full-stack React app onto AWS with Terraform. Inspired by [this](https://www.youtube.com/watch?v=Bro0uFVDrWY) YouTube tutorial by Code Engine 4 | 5 | A to-do list app which allows users to create, and read to-do's from DynamoDB 6 | 7 | App URL here: [`https://www.awsserverless.com`](https://www.awsserverless.com) 8 | 9 | ![AWS Architecture](images/aws_react_serverless4.JPG) 10 | 11 | ## Pre-requisite 12 | 13 | - Make sure you have installed [Terraform](https://learn.hashicorp.com/tutorials/terraform/install-cli), [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2-mac.html#cliv2-mac-prereq), and configured a `default` AWS CLI profile (see doc [here](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html#cli-configure-quickstart-profiles)) 14 | 15 | ```bash 16 | terraform -help # prints Terraform options 17 | which aws # prints /usr/local/bin/aws 18 | aws --version # prints aws-cli/2.0.36 Python/3.7.4 Darwin/18.7.0 botocore/2.0.0 19 | aws configure # configure your AWS CLI profile 20 | ``` 21 | 22 | ## Configuration 23 | 24 | - Create a Github project, and generate a personal access token (see doc [here](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token)) 25 | 26 | - Create an [S3 bucket](https://www.terraform.io/docs/language/settings/backends/s3.html) to store Terraform state. Populate bucket name in `01-main.tf` 27 | 28 | - Populate `terraform.tfvars`: 29 | 30 | ```bash 31 | default_region = "" 32 | github_username = "" 33 | github_project_name = "" 34 | app_name = "" 35 | environment = "" 36 | ``` 37 | 38 | ## Deploy 39 | 40 | ```bash 41 | cd deploy # change to deploy directory 42 | terraform init # initialises Terraform 43 | terraform apply # deploys AWS stack. See output for app url 44 | terraform destroy # destroys AWS stack 45 | ``` 46 | 47 | - When prompted for `github_token`, provide the value and hit Return. Alternatively, create a [local environment variable](https://www.terraform.io/docs/language/values/variables.html#environment-variables) named `TF_VAR_github_token` 48 | 49 | ## Run app locally 50 | 51 | - Run `terraform output` to print AWS resources URL/IDs 52 | - Update `src/conf/config.js` with the associated AWS resources URL/IDs 53 | 54 | ```bash 55 | npm install # installs Node dependencies 56 | yarn start # visit app at http://localhost:3000/ 57 | ``` 58 | 59 | ## Hosting website securely on AWS with a valid SSL certificate 60 | 61 | - Purchase a domain name on [Amazon Route 53](https://aws.amazon.com/route53/) 62 | - Create an email address for your Route 53 custom domain. Set-up rule to forward emails to `admin@` to your personal email address. See tutorial [here](https://medium.com/responsetap-engineering/easily-create-email-addresses-for-your-route53-custom-domain-589d099dd0f2) 63 | - Add an MX record for your domain. See documentation [here](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-mx-record.html) 64 | - Approve email validation requests from [AWS Certificate Manager](https://docs.aws.amazon.com/acm/latest/userguide/email-validation.html) 65 | - Add two DNS A records to point ``, and `www.` to the associated CloudFront distirubtions. See reference documentation [here](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resource-record-sets-creating.html) 66 | - Invalidate CloudFront cache on content updates. See tutorial [here](https://www.alexhyett.com/terraform-s3-static-website-hosting/) 67 | 68 | ## Contributing 69 | 70 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 71 | 72 | Please make sure to update tests as appropriate. 73 | 74 | If you find this project helpful, please give a :star: or even better buy me a coffee :coffee: :point_down: because I'm a caffeine addict :sweat_smile: 75 | 76 | Buy Me A Coffee 77 | 78 | ## License 79 | 80 | [MIT](https://choosealicense.com/licenses/mit/) 81 | -------------------------------------------------------------------------------- /buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | phases: 4 | install: 5 | runtime-versions: 6 | nodejs: 16 7 | commands: 8 | - npm install yarn 9 | - yarn 10 | build: 11 | commands: 12 | - echo $CODEBUILD_RESOLVED_SOURCE_VERSION 13 | - echo Building... 14 | - yarn build 15 | artifacts: 16 | files: 17 | - "**/*" 18 | discard-paths: no 19 | base-directory: build 20 | -------------------------------------------------------------------------------- /deploy/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/archive" { 5 | version = "2.7.0" 6 | hashes = [ 7 | "h1:1niS9AcwxN8CrWemnJS2Xf6vM72+48Xh3xFSS3DFWQo=", 8 | "zh:04e23bebca7f665a19a032343aeecd230028a3822e546e6f618f24c47ff87f67", 9 | "zh:5bb38114238e25c45bf85f5c9f627a2d0c4b98fe44a0837e37d48574385f8dad", 10 | "zh:64584bc1db4c390abd81c76de438d93acf967c8a33e9b923d68da6ed749d55bd", 11 | "zh:697695ab9cce351adf91a1823bdd72ce6f0d219138f5124ef7645cedf8f59a1f", 12 | "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", 13 | "zh:7edefb1d1e2fead8fd155f7b50a2cb49f2f3fed154ac3ef5f991ccaff93d6120", 14 | "zh:807fb15b75910bf14795f2ad1a2d41b069f9ef52c242131b2964c8527312e235", 15 | "zh:821d9148d261df1d1a8e5a4812df2a6a3ffaf0d2070dad3c785382e489069239", 16 | "zh:a7d92251118fb723048c482154a6ac6368aad583d28d15fffc6f5dafd9507463", 17 | "zh:b627d4cef192b3c12ddaf9cb2c4f98c10d0129883c8c2a9c0049983f9de7030d", 18 | "zh:dfb70306fcc0ad1d512ab7c24765703783cc286062d4849de4fbe23526f5dc8e", 19 | "zh:f21de276f857b7e51fa2593d8fef05a7faafb0a7b62db14ac58a03ce1be7d881", 20 | ] 21 | } 22 | 23 | provider "registry.terraform.io/hashicorp/aws" { 24 | version = "5.82.2" 25 | constraints = "5.82.2" 26 | hashes = [ 27 | "h1:ce6Dw2y4PpuqAPtnQ0dO270dRTmwEARqnfffrE1VYJ8=", 28 | "zh:0262fc96012fb7e173e1b7beadd46dfc25b1dc7eaef95b90e936fc454724f1c8", 29 | "zh:397413613d27f4f54d16efcbf4f0a43c059bd8d827fe34287522ae182a992f9b", 30 | "zh:436c0c5d56e1da4f0a4c13129e12a0b519d12ab116aed52029b183f9806866f3", 31 | "zh:4d942d173a2553d8d532a333a0482a090f4e82a2238acf135578f163b6e68470", 32 | "zh:624aebc549bfbce06cc2ecfd8631932eb874ac7c10eb8466ce5b9a2fbdfdc724", 33 | "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", 34 | "zh:9e632dee2dfdf01b371cca7854b1ec63ceefa75790e619b0642b34d5514c6733", 35 | "zh:a07567acb115b60a3df8f6048d12735b9b3bcf85ec92a62f77852e13d5a3c096", 36 | "zh:ab7002df1a1be6432ac0eb1b9f6f0dd3db90973cd5b1b0b33d2dae54553dfbd7", 37 | "zh:bc1ff65e2016b018b3e84db7249b2cd0433cb5c81dc81f9f6158f2197d6b9fde", 38 | "zh:bcad84b1d767f87af6e1ba3dc97fdb8f2ad5de9224f192f1412b09aba798c0a8", 39 | "zh:cf917dceaa0f9d55d9ff181b5dcc4d1e10af21b6671811b315ae2a6eda866a2a", 40 | "zh:d8e90ecfb3216f3cc13ccde5a16da64307abb6e22453aed2ac3067bbf689313b", 41 | "zh:d9054e0e40705df729682ad34c20db8695d57f182c65963abd151c6aba1ab0d3", 42 | "zh:ecf3a4f3c57eb7e89f71b8559e2a71e4cdf94eea0118ec4f2cb37e4f4d71a069", 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /deploy/00-variables.tf: -------------------------------------------------------------------------------- 1 | variable "github_token" {} 2 | 3 | variable "github_username" {} 4 | 5 | variable "github_project_name" {} 6 | 7 | variable "app_name" {} 8 | 9 | variable "environment" {} 10 | 11 | variable "default_region" {} 12 | 13 | variable "common_tags" { 14 | description = "Common tags to be applied to all components." 15 | } 16 | 17 | variable "domain_name" { 18 | type = string 19 | description = "The domain name for the website." 20 | } 21 | 22 | variable "bucket_name" { 23 | type = string 24 | description = "The name of the bucket without the www. prefix. Normally domain_name." 25 | } 26 | 27 | -------------------------------------------------------------------------------- /deploy/01-main.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = "us-east-1" 3 | } 4 | 5 | provider "aws" { 6 | alias = "acm_provider" 7 | region = "us-east-1" 8 | } 9 | 10 | terraform { 11 | 12 | required_providers { 13 | aws = { 14 | source = "hashicorp/aws" 15 | version = "5.82.2" 16 | } 17 | } 18 | backend "s3" { 19 | bucket = "react-serverless-app-tf-state" 20 | key = "terraform.tfstate" 21 | region = "us-east-1" 22 | } 23 | } 24 | 25 | data "aws_caller_identity" "current" {} -------------------------------------------------------------------------------- /deploy/02-s3.tf: -------------------------------------------------------------------------------- 1 | resource "aws_s3_bucket" "www_bucket" { 2 | bucket = "www.${var.bucket_name}" 3 | force_destroy = true 4 | tags = var.common_tags 5 | } 6 | 7 | resource "aws_s3_bucket_website_configuration" "www_bucket" { 8 | bucket = aws_s3_bucket.www_bucket.id 9 | 10 | index_document { 11 | suffix = "index.html" 12 | } 13 | error_document { 14 | key = "index.html" 15 | } 16 | } 17 | 18 | resource "aws_s3_bucket_cors_configuration" "www_bucket" { 19 | bucket = aws_s3_bucket.www_bucket.id 20 | cors_rule { 21 | allowed_headers = ["Authorization", "Content-Length"] 22 | allowed_methods = ["GET", "POST"] 23 | allowed_origins = ["https://www.${var.domain_name}"] 24 | max_age_seconds = 3000 25 | } 26 | } 27 | 28 | resource "aws_s3_bucket_ownership_controls" "www_bucket" { 29 | bucket = aws_s3_bucket.www_bucket.id 30 | rule { 31 | object_ownership = "BucketOwnerPreferred" 32 | } 33 | depends_on = [aws_s3_bucket_public_access_block.www_bucket] 34 | } 35 | 36 | resource "aws_s3_bucket_public_access_block" "www_bucket" { 37 | bucket = aws_s3_bucket.www_bucket.id 38 | 39 | block_public_acls = false 40 | block_public_policy = false 41 | ignore_public_acls = false 42 | restrict_public_buckets = false 43 | } 44 | 45 | resource "aws_s3_bucket_acl" "www_bucket" { 46 | depends_on = [ 47 | aws_s3_bucket_ownership_controls.www_bucket, 48 | ] 49 | bucket = aws_s3_bucket.www_bucket.id 50 | acl = "public-read" 51 | } 52 | 53 | resource "aws_s3_bucket_policy" "www_bucket" { 54 | bucket = aws_s3_bucket.www_bucket.id 55 | policy = < { 11 | const commentId = event.path.split("/")[2]; 12 | let responseCode = 200; 13 | let responseBody = ""; 14 | const params = { 15 | Key: { 16 | commentId: { 17 | S: commentId 18 | } 19 | }, 20 | TableName: "comments" 21 | }; 22 | ddb.deleteItem(params, function(err, data) { 23 | if (err) { 24 | responseCode = 500; 25 | responseBody = err; 26 | } else { 27 | responseBody = data; 28 | } 29 | const response = { 30 | statusCode: responseCode, 31 | headers: { 32 | "content-type": "application/json", 33 | "Access-Control-Allow-Origin": "*" 34 | }, 35 | body: JSON.stringify(responseBody) 36 | }; 37 | callback(null, response); 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /deploy/lambdas/deleteCommentById.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatthewCYLau/react-serverless-aws-terraform/ebb8b703d108e442f2f4f4c233d211a120c0ebd4/deploy/lambdas/deleteCommentById.zip -------------------------------------------------------------------------------- /deploy/lambdas/deleteLikeById.js: -------------------------------------------------------------------------------- 1 | // Load the AWS SDK for Node.js 2 | const AWS = require("aws-sdk"); 3 | 4 | // Set the region 5 | AWS.config.update({ region: "us-east-1" }); 6 | 7 | // Create DynamoDB service object 8 | const ddb = new AWS.DynamoDB({ apiVersion: "2012-08-10" }); 9 | 10 | exports.handler = (event, context, callback) => { 11 | const likeId = event.path.split("/")[2]; 12 | let responseCode = 200; 13 | let responseBody = ""; 14 | const params = { 15 | Key: { 16 | likeId: { 17 | S: likeId 18 | } 19 | }, 20 | TableName: "likes" 21 | }; 22 | ddb.deleteItem(params, function(err, data) { 23 | if (err) { 24 | responseCode = 500; 25 | responseBody = err; 26 | } else { 27 | responseBody = data; 28 | } 29 | const response = { 30 | statusCode: responseCode, 31 | headers: { 32 | "content-type": "application/json", 33 | "Access-Control-Allow-Origin": "*" 34 | }, 35 | body: JSON.stringify(responseBody) 36 | }; 37 | callback(null, response); 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /deploy/lambdas/deleteLikeById.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatthewCYLau/react-serverless-aws-terraform/ebb8b703d108e442f2f4f4c233d211a120c0ebd4/deploy/lambdas/deleteLikeById.zip -------------------------------------------------------------------------------- /deploy/lambdas/deleteTodoById.js: -------------------------------------------------------------------------------- 1 | // Load the AWS SDK for Node.js 2 | const AWS = require("aws-sdk"); 3 | 4 | // Set the region 5 | AWS.config.update({ region: "us-east-1" }); 6 | 7 | // Create DynamoDB service object 8 | const ddb = new AWS.DynamoDB({ apiVersion: "2012-08-10" }); 9 | 10 | exports.handler = (event, context, callback) => { 11 | const todoId = event.path.split("/")[2]; 12 | let responseCode = 200; 13 | let responseBody = ""; 14 | const params = { 15 | Key: { 16 | todoId: { 17 | S: todoId 18 | } 19 | }, 20 | TableName: "todos" 21 | }; 22 | ddb.deleteItem(params, function(err, data) { 23 | if (err) { 24 | responseCode = 500; 25 | responseBody = err; 26 | } else { 27 | responseBody = data; 28 | } 29 | const response = { 30 | statusCode: responseCode, 31 | headers: { 32 | "content-type": "application/json", 33 | "Access-Control-Allow-Origin": "*" 34 | }, 35 | body: JSON.stringify(responseBody) 36 | }; 37 | callback(null, response); 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /deploy/lambdas/deleteTodoById.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatthewCYLau/react-serverless-aws-terraform/ebb8b703d108e442f2f4f4c233d211a120c0ebd4/deploy/lambdas/deleteTodoById.zip -------------------------------------------------------------------------------- /deploy/lambdas/getComments.js: -------------------------------------------------------------------------------- 1 | // Load the AWS SDK for Node.js 2 | const AWS = require("aws-sdk"); 3 | 4 | // Set the region 5 | AWS.config.update({ region: "us-east-1" }); 6 | 7 | // Create DynamoDB service object 8 | const ddb = new AWS.DynamoDB({ apiVersion: "2012-08-10" }); 9 | 10 | exports.handler = (event, context, callback) => { 11 | // if query parameter todoId is provided 12 | // query comments by todoId 13 | // else, scan for all comments 14 | if (event.queryStringParameters) { 15 | const todoId = event.queryStringParameters.todoId; 16 | let responseCode = 200; 17 | let responseBody = ""; 18 | 19 | const params = { 20 | ExpressionAttributeValues: { 21 | ":v1": { 22 | S: todoId 23 | } 24 | }, 25 | KeyConditionExpression: "todoId = :v1", 26 | IndexName: "todoIdIndex", 27 | TableName: "comments" 28 | }; 29 | ddb.query(params, function(err, data) { 30 | if (err) { 31 | responseCode = 500; 32 | responseBody = err; 33 | } else { 34 | if (data.Items) { 35 | responseBody = data; 36 | } else { 37 | responseCode = 404; 38 | responseBody = "Data not found"; 39 | } 40 | } 41 | const response = { 42 | statusCode: responseCode, 43 | headers: { 44 | "content-type": "application/json", 45 | "Access-Control-Allow-Origin": "*" 46 | }, 47 | body: JSON.stringify(responseBody) 48 | }; 49 | callback(null, response); 50 | }); 51 | } else { 52 | let responseCode = 200; 53 | let responseBody = ""; 54 | const params = { 55 | TableName: "comments" 56 | }; 57 | 58 | ddb.scan(params, function(err, data) { 59 | if (err) { 60 | console.log("Error", err); 61 | responseCode = 500; 62 | responseBody = err; 63 | } else { 64 | console.log("Success", data); 65 | responseBody = data; 66 | } 67 | const response = { 68 | statusCode: responseCode, 69 | headers: { 70 | "content-type": "application/json", 71 | "Access-Control-Allow-Origin": "*" 72 | }, 73 | body: JSON.stringify(responseBody) 74 | }; 75 | callback(null, response); 76 | }); 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /deploy/lambdas/getComments.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatthewCYLau/react-serverless-aws-terraform/ebb8b703d108e442f2f4f4c233d211a120c0ebd4/deploy/lambdas/getComments.zip -------------------------------------------------------------------------------- /deploy/lambdas/getLikes.js: -------------------------------------------------------------------------------- 1 | // Load the AWS SDK for Node.js 2 | const AWS = require("aws-sdk"); 3 | 4 | // Set the region 5 | AWS.config.update({ region: "us-east-1" }); 6 | 7 | // Create DynamoDB service object 8 | const ddb = new AWS.DynamoDB({ apiVersion: "2012-08-10" }); 9 | 10 | exports.handler = (event, context, callback) => { 11 | // if query parameter commentId is provided 12 | // query likes by commentId 13 | // else, scan for all likes 14 | if (event.queryStringParameters) { 15 | const commentId = event.queryStringParameters.commentId; 16 | let responseCode = 200; 17 | let responseBody = ""; 18 | 19 | const params = { 20 | ExpressionAttributeValues: { 21 | ":v1": { 22 | S: commentId 23 | } 24 | }, 25 | KeyConditionExpression: "commentId = :v1", 26 | IndexName: "commentIdIndex", 27 | TableName: "likes" 28 | }; 29 | ddb.query(params, function(err, data) { 30 | if (err) { 31 | responseCode = 500; 32 | responseBody = err; 33 | } else { 34 | if (data.Items) { 35 | responseBody = data; 36 | } else { 37 | responseCode = 404; 38 | responseBody = "Data not found"; 39 | } 40 | } 41 | const response = { 42 | statusCode: responseCode, 43 | headers: { 44 | "content-type": "application/json", 45 | "Access-Control-Allow-Origin": "*" 46 | }, 47 | body: JSON.stringify(responseBody) 48 | }; 49 | callback(null, response); 50 | }); 51 | } else { 52 | let responseCode = 200; 53 | let responseBody = ""; 54 | const params = { 55 | TableName: "likes" 56 | }; 57 | 58 | ddb.scan(params, function(err, data) { 59 | if (err) { 60 | console.log("Error", err); 61 | responseCode = 500; 62 | responseBody = err; 63 | } else { 64 | console.log("Success", data); 65 | responseBody = data; 66 | } 67 | const response = { 68 | statusCode: responseCode, 69 | headers: { 70 | "content-type": "application/json", 71 | "Access-Control-Allow-Origin": "*" 72 | }, 73 | body: JSON.stringify(responseBody) 74 | }; 75 | callback(null, response); 76 | }); 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /deploy/lambdas/getLikes.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatthewCYLau/react-serverless-aws-terraform/ebb8b703d108e442f2f4f4c233d211a120c0ebd4/deploy/lambdas/getLikes.zip -------------------------------------------------------------------------------- /deploy/lambdas/getTodoById.js: -------------------------------------------------------------------------------- 1 | // Load the AWS SDK for Node.js 2 | const AWS = require("aws-sdk"); 3 | 4 | // Set the region 5 | AWS.config.update({ region: "us-east-1" }); 6 | 7 | // Create DynamoDB service object 8 | const ddb = new AWS.DynamoDB({ apiVersion: "2012-08-10" }); 9 | 10 | exports.handler = (event, context, callback) => { 11 | const todoId = event.path.split("/")[2]; 12 | let responseCode = 200; 13 | let responseBody = ""; 14 | const params = { 15 | Key: { 16 | todoId: { 17 | S: todoId 18 | } 19 | }, 20 | TableName: "todos" 21 | }; 22 | ddb.getItem(params, function(err, data) { 23 | if (err) { 24 | responseCode = 500; 25 | responseBody = err; 26 | } else { 27 | if (data.Item) { 28 | responseBody = data; 29 | } else { 30 | responseCode = 404; 31 | responseBody = "Data not found"; 32 | } 33 | } 34 | const response = { 35 | statusCode: responseCode, 36 | headers: { 37 | "content-type": "application/json", 38 | "Access-Control-Allow-Origin": "*" 39 | }, 40 | body: JSON.stringify(responseBody) 41 | }; 42 | callback(null, response); 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /deploy/lambdas/getTodoById.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatthewCYLau/react-serverless-aws-terraform/ebb8b703d108e442f2f4f4c233d211a120c0ebd4/deploy/lambdas/getTodoById.zip -------------------------------------------------------------------------------- /deploy/lambdas/getTodos.js: -------------------------------------------------------------------------------- 1 | // Load the AWS SDK for Node.js 2 | const AWS = require("aws-sdk"); 3 | 4 | // Set the region 5 | AWS.config.update({ region: "us-east-1" }); 6 | 7 | // Create DynamoDB service object 8 | const ddb = new AWS.DynamoDB({ apiVersion: "2012-08-10" }); 9 | 10 | exports.handler = function(event, context, callback) { 11 | const params = { 12 | TableName: "todos" 13 | }; 14 | 15 | let responseCode = 200; 16 | let responseBody = ""; 17 | 18 | ddb.scan(params, function(err, data) { 19 | if (err) { 20 | console.log("Error", err); 21 | responseCode = 500; 22 | responseBody = err; 23 | } else { 24 | console.log("Success", data); 25 | responseBody = data; 26 | } 27 | const response = { 28 | statusCode: responseCode, 29 | headers: { 30 | "content-type": "application/json", 31 | "Access-Control-Allow-Origin": "*" 32 | }, 33 | body: JSON.stringify(responseBody) 34 | }; 35 | 36 | callback(null, response); 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /deploy/lambdas/getTodos.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatthewCYLau/react-serverless-aws-terraform/ebb8b703d108e442f2f4f4c233d211a120c0ebd4/deploy/lambdas/getTodos.zip -------------------------------------------------------------------------------- /deploy/lambdas/sqsProcesser.js: -------------------------------------------------------------------------------- 1 | exports.handler = async function(event, context) { 2 | event.Records.forEach(record => { 3 | const { body } = record; 4 | console.log(body); 5 | }); 6 | return {}; 7 | } -------------------------------------------------------------------------------- /deploy/lambdas/sqsProcesser.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatthewCYLau/react-serverless-aws-terraform/ebb8b703d108e442f2f4f4c233d211a120c0ebd4/deploy/lambdas/sqsProcesser.zip -------------------------------------------------------------------------------- /deploy/lambdas/updateTodoById.js: -------------------------------------------------------------------------------- 1 | // Load the AWS SDK for Node.js 2 | const AWS = require("aws-sdk"); 3 | 4 | // Set the region 5 | AWS.config.update({ region: "us-east-1" }); 6 | 7 | // Create DynamoDB service object 8 | const ddb = new AWS.DynamoDB({ apiVersion: "2012-08-10" }); 9 | 10 | exports.handler = (event, context, callback) => { 11 | const todoId = event.path.split("/")[2]; 12 | let responseCode = 200; 13 | let responseBody = ""; 14 | 15 | const params = { 16 | ExpressionAttributeNames: { 17 | "#N": "name", 18 | "#D": "description" 19 | }, 20 | ExpressionAttributeValues: { 21 | ":n": { 22 | S: JSON.parse(event.body).name 23 | }, 24 | ":d": { 25 | S: JSON.parse(event.body).description 26 | } 27 | }, 28 | Key: { 29 | todoId: { 30 | S: todoId 31 | } 32 | }, 33 | ReturnValues: "ALL_NEW", 34 | TableName: "todos", 35 | UpdateExpression: "SET #N = :n, #D = :d" 36 | }; 37 | ddb.updateItem(params, function(err, data) { 38 | if (err) { 39 | responseCode = 500; 40 | responseBody = err; 41 | } else { 42 | responseBody = data; 43 | } 44 | const response = { 45 | statusCode: responseCode, 46 | headers: { 47 | "content-type": "application/json", 48 | "Access-Control-Allow-Origin": "*" 49 | }, 50 | body: JSON.stringify(responseBody) 51 | }; 52 | callback(null, response); 53 | }); 54 | }; 55 | -------------------------------------------------------------------------------- /deploy/lambdas/updateTodoById.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatthewCYLau/react-serverless-aws-terraform/ebb8b703d108e442f2f4f4c233d211a120c0ebd4/deploy/lambdas/updateTodoById.zip -------------------------------------------------------------------------------- /deploy/output.tf: -------------------------------------------------------------------------------- 1 | output "app_url" { 2 | description = "The website end-point of the S3 bucket" 3 | value = aws_s3_bucket_website_configuration.www_bucket.website_endpoint 4 | } 5 | 6 | output "api_base_url" { 7 | value = aws_api_gateway_deployment.app.invoke_url 8 | } 9 | 10 | output "user_pool_id" { 11 | value = aws_cognito_user_pool.app_user_pool.id 12 | } 13 | 14 | output "user_pool_client_id" { 15 | value = aws_cognito_user_pool_client.app_user_pool_client.id 16 | } 17 | 18 | output "identity_pool_id" { 19 | value = aws_cognito_identity_pool.app_identity_pool.id 20 | } 21 | 22 | output "domain_hosted_zone_id" { 23 | value = data.aws_route53_zone.this.zone_id 24 | } 25 | -------------------------------------------------------------------------------- /deploy/policy/s3-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "PublicReadGetObject", 6 | "Effect": "Allow", 7 | "Principal": "*", 8 | "Action": "s3:GetObject", 9 | "Resource": "arn:aws:s3:::${bucket}/*" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /deploy/terraform.tfvars: -------------------------------------------------------------------------------- 1 | default_region = "us-east-1" 2 | github_username = "MatthewCYLau" 3 | github_project_name = "react-serverless-aws-terraform" 4 | app_name = "matlau-react-serverless-app" 5 | environment = "staging" 6 | common_tags = { project = "react-serverless-aws-terraform" } 7 | domain_name = "awsserverless.com" 8 | bucket_name = "awsserverless.com" 9 | 10 | 11 | -------------------------------------------------------------------------------- /images/aws_react_serverless4.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatthewCYLau/react-serverless-aws-terraform/ebb8b703d108e442f2f4f4c233d211a120c0ebd4/images/aws_react_serverless4.JPG -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-aws-codepipeline", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@aws-amplify/ui-react": "^1.0.2", 7 | "@testing-library/jest-dom": "^4.2.4", 8 | "@testing-library/react": "^9.3.2", 9 | "@testing-library/user-event": "^7.1.2", 10 | "antd": "^4.5.4", 11 | "aws-amplify": "^3.3.20", 12 | "axios": "^0.20.0", 13 | "react": "^16.13.1", 14 | "react-dom": "^16.13.1", 15 | "react-router-dom": "^5.2.0", 16 | "react-scripts": "3.4.3" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": "react-app" 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatthewCYLau/react-serverless-aws-terraform/ebb8b703d108e442f2f4f4c233d211a120c0ebd4/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatthewCYLau/react-serverless-aws-terraform/ebb8b703d108e442f2f4f4c233d211a120c0ebd4/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatthewCYLau/react-serverless-aws-terraform/ebb8b703d108e442f2f4f4c233d211a120c0ebd4/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; 3 | import HomePage from "../src/pages/HomePage"; 4 | import Routes from "./routers/Routes"; 5 | import Navbar from "../src/components/Navbar"; 6 | import Footer from "../src/components/Footer"; 7 | 8 | // AntUI 9 | import { Layout } from "antd"; 10 | 11 | //Amplify Auth 12 | import { withAuthenticator } from "@aws-amplify/ui-react"; 13 | 14 | const App = () => ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |