From 7b62fa44692194d32172bcbbb539b795f8f9b8d4 Mon Sep 17 00:00:00 2001 From: Jay Date: Wed, 25 Aug 2021 21:27:43 -0400 Subject: [PATCH] Migrate guide to SST (#569) * Working on draft for move to SST * Working on the backend chapters * Editing sst versions * Reorganizing chapters * Updating screenshots * Reorganizing chapters * Editing * Editing the Serverless Framework section * Editing extra credit chapters * Updating ToC in lander * Adding comment links --- README.md | 2 +- _chapters/add-a-billing-api.md | 1 + _chapters/add-a-create-note-api.md | 13 + _chapters/add-a-delete-note-api.md | 3 +- _chapters/add-a-get-note-api.md | 1 + _chapters/add-a-list-all-the-notes-api.md | 1 + _chapters/add-an-api-to-create-a-note.md | 319 +++++++++++++++ _chapters/add-an-api-to-delete-a-note.md | 94 +++++ _chapters/add-an-api-to-get-a-note.md | 88 +++++ _chapters/add-an-api-to-handle-billing.md | 147 +++++++ _chapters/add-an-api-to-list-all-the-notes.md | 89 +++++ _chapters/add-an-api-to-update-a-note.md | 101 +++++ _chapters/add-an-update-note-api.md | 1 + _chapters/add-app-favicons.md | 13 +- _chapters/add-stripe-keys-to-config.md | 24 +- _chapters/add-the-create-note-page.md | 6 +- _chapters/add-the-session-to-the-state.md | 22 +- .../adding-auth-to-our-serverless-app.md | 173 +++++++++ _chapters/adding-links-in-the-navbar.md | 14 +- _chapters/allow-users-to-change-passwords.md | 14 +- .../allow-users-to-change-their-email.md | 4 +- ...ess-apis.md => auth-in-serverless-apps.md} | 46 +-- ...-practices-for-building-serverless-apps.md | 2 +- _chapters/building-a-cdk-app-with-sst.md | 56 --- _chapters/call-the-list-api.md | 4 +- .../code-splitting-in-create-react-app.md | 19 +- _chapters/configure-aws-amplify.md | 42 +- .../configure-cognito-identity-pool-in-cdk.md | 365 ------------------ ...ure-cognito-identity-pool-in-serverless.md | 149 +++++++ .../configure-cognito-user-pool-in-cdk.md | 108 ------ ...nfigure-cognito-user-pool-in-serverless.md | 75 ++++ _chapters/configure-dynamodb-in-cdk.md | 181 --------- _chapters/configure-dynamodb-in-serverless.md | 131 +++++++ _chapters/configure-multiple-aws-profiles.md | 4 +- _chapters/configure-s3-in-cdk.md | 103 ----- _chapters/configure-s3-in-serverless.md | 63 +++ _chapters/configure-secrets-in-seed.md | 36 +- ...t-serverless-framework-and-cdk-with-sst.md | 196 ---------- _chapters/connect-the-billing-form.md | 33 +- .../connect-to-api-gateway-with-iam-auth.md | 2 +- _chapters/create-a-billing-form.md | 60 +-- _chapters/create-a-cognito-identity-pool.md | 6 +- ...custom-react-hook-to-handle-form-fields.md | 16 +- _chapters/create-a-dynamodb-table-in-sst.md | 105 +++++ _chapters/create-a-dynamodb-table.md | 2 +- _chapters/create-a-hello-world-api.md | 90 +++++ _chapters/create-a-login-page.md | 4 +- _chapters/create-a-netlify-build-script.md | 22 +- _chapters/create-a-new-reactjs-app.md | 157 +++++++- _chapters/create-a-route-that-redirects.md | 8 +- _chapters/create-a-settings-page.md | 12 +- .../create-an-s3-bucket-for-file-uploads.md | 46 ++- _chapters/create-an-s3-bucket-in-sst.md | 66 ++++ _chapters/create-an-sst-app.md | 47 +++ _chapters/create-containers.md | 30 +- _chapters/create-the-signup-form.md | 12 +- .../creating-a-ci-cd-pipeline-for-react.md | 8 +- ...reating-a-ci-cd-pipeline-for-serverless.md | 46 ++- _chapters/creating-feature-environments.md | 2 +- .../creating-pull-request-environments.md | 2 +- .../cross-stack-references-in-serverless.md | 12 +- _chapters/custom-domain-in-netlify.md | 8 +- .../custom-domains-for-react-apps-on-aws.md | 96 +++++ .../custom-domains-in-serverless-apis.md | 59 +++ .../customize-the-serverless-iam-policy.md | 12 +- .../debugging-full-stack-serverless-apps.md | 4 +- _chapters/debugging-serverless-api-issues.md | 2 +- ...ploy-a-serverless-app-with-dependencies.md | 4 +- _chapters/deploy-the-api-services-repo.md | 4 +- _chapters/deploy-the-apis.md | 16 +- _chapters/deploy-the-resources-repo.md | 4 +- _chapters/deploy-your-hello-world-api.md | 12 +- .../deploy-your-serverless-infrastructure.md | 69 +--- _chapters/deploying-a-react-app-to-netlify.md | 24 ++ _chapters/deploying-only-updated-services.md | 12 +- _chapters/deploying-through-seed.md | 93 ++--- .../deploying-to-multiple-aws-accounts.md | 2 +- _chapters/display-a-note.md | 6 +- ...ate-social-share-images-with-serverless.md | 2 +- _chapters/environments-in-create-react-app.md | 6 +- _chapters/environments-in-serverless-apps.md | 6 +- _chapters/errors-in-api-gateway.md | 59 +-- _chapters/errors-outside-lambda-functions.md | 61 ++- ...ok-login-with-cognito-using-aws-amplify.md | 12 +- ...low.md => frontend-workflow-in-netlify.md} | 16 +- _chapters/further-reading.md | 16 +- _chapters/getting-production-ready.md | 74 +--- _chapters/give-feedback-while-logging-in.md | 10 +- _chapters/handle-404s.md | 4 +- _chapters/handle-api-gateway-cors-errors.md | 79 +--- .../handle-cors-in-s3-for-file-uploads.md | 68 ++-- _chapters/handle-cors-in-serverless-apis.md | 159 ++------ _chapters/handle-forgot-and-reset-password.md | 16 +- _chapters/handle-routes-with-react-router.md | 10 +- _chapters/handling-secrets-in-sst.md | 61 +++ _chapters/hosting-your-react-app.md | 17 - _chapters/how-to-get-help.md | 2 + ...nd-repo.md => initialize-a-github-repo.md} | 21 +- _chapters/initialize-the-frontend-repo.md | 72 ---- .../invoke-api-gateway-endpoints-locally.md | 2 +- ...ure-cognito-identity-pool-in-serverless.md | 2 +- .../ko/monitoring-deployments-in-seed.md | 2 +- ...vironment-variables-in-lambda-functions.md | 4 +- _chapters/list-all-the-notes.md | 6 +- _chapters/load-the-state-from-the-session.md | 2 +- _chapters/logic-errors-in-lambda-functions.md | 10 +- _chapters/login-with-aws-cognito.md | 2 +- .../manage-environment-related-config.md | 4 +- ...manage-environments-in-create-react-app.md | 14 +- .../manage-user-accounts-in-aws-amplify.md | 4 +- _chapters/monitor-usage-for-environments.md | 4 +- _chapters/organizing-serverless-projects.md | 2 +- .../package-lambdas-with-serverless-bundle.md | 2 +- _chapters/promoting-to-production.md | 4 +- _chapters/purchase-a-domain-with-route-53.md | 18 +- _chapters/redirect-on-login-and-logout.md | 2 +- _chapters/redirect-on-login.md | 15 +- _chapters/render-the-note-form.md | 2 +- _chapters/report-api-errors-in-react.md | 4 +- _chapters/review-our-app-architecture.md | 8 +- _chapters/rollback-changes.md | 4 +- _chapters/save-changes-to-a-note.md | 2 +- _chapters/secure-our-serverless-apis.md | 202 ++++++++++ _chapters/secure-the-apis.md | 2 +- _chapters/serverless-nodejs-starter.md | 8 +- _chapters/set-custom-domains-through-seed.md | 10 +- .../setting-up-your-project-on-netlify.md | 16 +- _chapters/setting-up-your-project-on-seed.md | 62 +-- _chapters/setup-a-stripe-account.md | 21 +- _chapters/setup-an-error-boundary-in-react.md | 36 +- _chapters/setup-bootstrap.md | 6 +- .../setup-error-logging-in-serverless.md | 37 +- _chapters/setup-error-reporting-in-react.md | 17 +- _chapters/setup-the-serverless-framework.md | 19 +- .../share-an-api-endpoint-between-services.md | 4 +- _chapters/share-code-between-services.md | 2 +- ...re-route-53-domains-across-aws-accounts.md | 2 +- _chapters/signup-with-aws-cognito.md | 4 +- _chapters/stages-in-serverless-framework.md | 2 +- .../storing-secrets-in-serverless-apps.md | 2 +- ...ucture-environments-across-aws-accounts.md | 2 +- _chapters/test-the-apis.md | 14 +- _chapters/test-the-billing-api.md | 14 +- _chapters/test-the-configured-apis.md | 130 ------- .../tracing-serverless-apps-with-x-ray.md | 8 +- _chapters/understanding-react-hooks.md | 10 +- .../unexpected-errors-in-lambda-functions.md | 42 +- _chapters/unit-tests-in-serverless.md | 92 ++--- _chapters/upload-a-file-to-s3.md | 4 +- _chapters/use-the-redirect-routes.md | 4 +- ...using-aws-cdk-with-serverless-framework.md | 199 +++++++++- ...rna-and-yarn-workspaces-with-serverless.md | 10 +- _chapters/what-does-this-guide-cover.md | 77 ++-- _chapters/what-is-aws-cdk.md | 9 +- _chapters/what-is-infrastructure-as-code.md | 15 +- _chapters/what-is-sst.md | 28 ++ _chapters/who-is-this-guide-for.md | 5 - _chapters/working-on-serverless-apps.md | 4 +- _chapters/working-with-3rd-party-apis.md | 4 +- _chapters/wrapping-up-the-best-practices.md | 2 +- _chapters/wrapping-up.md | 5 +- _config.yml | 3 + _data/chapterlist.yml | 317 ++++++++------- ...to-create-a-reactjs-app-with-serverless.md | 2 +- _includes/hero.html | 4 - _includes/post-checkpoint.html | 6 +- _includes/post-sidebar.html | 27 +- _sass/lander.scss | 29 +- assets/diagrams/serverless-ci-cd-pipeline.png | Bin 111067 -> 0 bytes .../click-activity-in-seed.png | Bin 713200 -> 0 bytes .../click-deploy-in-seed-pipeline.png | Bin 637652 -> 637490 bytes .../deploy-debug-branch-in-seed.png | Bin 908825 -> 909854 bytes .../promote-error-logging-to-prod-in-seed.png | Bin 653870 -> 0 bytes ...lect-branch-and-confirm-deploy-in-seed.png | Bin 858364 -> 818371 bytes assets/part2/add-new-service.png | Bin 543148 -> 0 bytes .../add-secret-dev-environment-variable.png | Bin 520498 -> 0 bytes assets/part2/added-notes-service.png | Bin 697026 -> 0 bytes .../part2/click-enable-unit-tsts-in-seed.png | Bin 487855 -> 524334 bytes assets/part2/click-pipeline.png | Bin 563753 -> 0 bytes assets/part2/confirm-promote-dev-build.png | Bin 1024536 -> 0 bytes .../part2/copy-new-client-github-repo-url.png | Bin 585370 -> 0 bytes assets/part2/copy-new-github-repo-url.png | Bin 590164 -> 569019 bytes assets/part2/create-notes-in-production.png | Bin 0 -> 514524 bytes assets/part2/dev-build-api-output.png | Bin 945363 -> 0 bytes .../part2/dev-build-infrastructure-output.png | Bin 860707 -> 0 bytes assets/part2/dev-build-logs-in-progress.png | Bin 607317 -> 0 bytes .../dev-build-page-phase-1-in-progress.png | Bin 543817 -> 0 bytes .../dev-build-page-phase-2-in-progress.png | Bin 545278 -> 0 bytes assets/part2/dev-build-ready-to-promote.png | Bin 593271 -> 0 bytes assets/part2/dev-build-run-tests.png | Bin 808533 -> 0 bytes assets/part2/manage-deploy-phases.png | Bin 553651 -> 0 bytes assets/part2/multiple-deploy-phases.png | Bin 580799 -> 0 bytes .../name-new-client-github-repository.png | Bin 507357 -> 0 bytes assets/part2/name-new-github-repository.png | Bin 505524 -> 520944 bytes assets/part2/notes-app-in-production.png | Bin 0 -> 495711 bytes assets/part2/one-deploy-phase.png | Bin 689346 -> 0 bytes assets/part2/prod-build-api-output.png | Bin 946917 -> 0 bytes assets/part2/prod-build-details.png | Bin 0 -> 635080 bytes assets/part2/prod-build-in-progress.png | Bin 741380 -> 0 bytes .../prod-build-infrastructure-output.png | Bin 857051 -> 0 bytes assets/part2/prod-build-logs-in-progress.png | Bin 0 -> 681194 bytes assets/part2/prod-build-run-tests.png | Bin 0 -> 712574 bytes assets/part2/prod-build-stack-outputs.png | Bin 0 -> 815650 bytes .../react-app-hosted-on-custom-domain.png | Bin 0 -> 472548 bytes assets/part2/review-promote-change-set.png | Bin 997630 -> 0 bytes assets/part2/search-new-service.png | Bin 723924 -> 0 bytes assets/part2/seed-app-homepage.png | Bin 0 -> 663995 bytes assets/part2/seed-dev-build-in-progress.png | Bin 569987 -> 0 bytes assets/part2/seed-prod-build-in-progress.png | Bin 0 -> 665806 bytes .../select-branch-to-auto-deploy-to-prod.png | Bin 0 -> 707534 bytes assets/part2/select-dev-stage-in-settings.png | Bin 559516 -> 512197 bytes assets/part2/select-git-provider.png | Bin 706949 -> 671118 bytes .../part2/select-prod-stage-in-settings.png | Bin 557382 -> 510247 bytes assets/part2/serverless-yml-detected.png | Bin 735089 -> 0 bytes .../part2/show-dev-env-variables-settings.png | Bin 498731 -> 0 bytes assets/part2/sst-app-detected.png | Bin 0 -> 686433 bytes assets/part2/sst-hello-world-api-invoked.png | Bin 0 -> 486031 bytes assets/part2/sst-json-detected.png | Bin 742333 -> 0 bytes assets/part2/turn-off-auto-deploy-for-dev.png | Bin 0 -> 500949 bytes assets/part2/turn-on-auto-deploy-for-prod.png | Bin 0 -> 500817 bytes assets/part2/view-new-seed-app.png | Bin 715519 -> 0 bytes index.md | 42 +- 222 files changed, 3729 insertions(+), 2646 deletions(-) create mode 100644 _chapters/add-an-api-to-create-a-note.md create mode 100644 _chapters/add-an-api-to-delete-a-note.md create mode 100644 _chapters/add-an-api-to-get-a-note.md create mode 100644 _chapters/add-an-api-to-handle-billing.md create mode 100644 _chapters/add-an-api-to-list-all-the-notes.md create mode 100644 _chapters/add-an-api-to-update-a-note.md create mode 100644 _chapters/adding-auth-to-our-serverless-app.md rename _chapters/{handling-auth-in-serverless-apis.md => auth-in-serverless-apps.md} (79%) delete mode 100644 _chapters/building-a-cdk-app-with-sst.md delete mode 100644 _chapters/configure-cognito-identity-pool-in-cdk.md create mode 100644 _chapters/configure-cognito-identity-pool-in-serverless.md delete mode 100644 _chapters/configure-cognito-user-pool-in-cdk.md create mode 100644 _chapters/configure-cognito-user-pool-in-serverless.md delete mode 100644 _chapters/configure-dynamodb-in-cdk.md create mode 100644 _chapters/configure-dynamodb-in-serverless.md delete mode 100644 _chapters/configure-s3-in-cdk.md create mode 100644 _chapters/configure-s3-in-serverless.md delete mode 100644 _chapters/connect-serverless-framework-and-cdk-with-sst.md create mode 100644 _chapters/create-a-dynamodb-table-in-sst.md create mode 100644 _chapters/create-a-hello-world-api.md create mode 100644 _chapters/create-an-s3-bucket-in-sst.md create mode 100644 _chapters/create-an-sst-app.md create mode 100644 _chapters/custom-domains-for-react-apps-on-aws.md create mode 100644 _chapters/custom-domains-in-serverless-apis.md create mode 100644 _chapters/deploying-a-react-app-to-netlify.md rename _chapters/{frontend-workflow.md => frontend-workflow-in-netlify.md} (89%) create mode 100644 _chapters/handling-secrets-in-sst.md delete mode 100644 _chapters/hosting-your-react-app.md rename _chapters/{initialize-the-backend-repo.md => initialize-a-github-repo.md} (70%) delete mode 100644 _chapters/initialize-the-frontend-repo.md create mode 100644 _chapters/secure-our-serverless-apis.md delete mode 100644 _chapters/test-the-configured-apis.md create mode 100644 _chapters/what-is-sst.md delete mode 100644 assets/diagrams/serverless-ci-cd-pipeline.png delete mode 100644 assets/monitor-debug-errors/click-activity-in-seed.png delete mode 100644 assets/monitor-debug-errors/promote-error-logging-to-prod-in-seed.png delete mode 100644 assets/part2/add-new-service.png delete mode 100644 assets/part2/add-secret-dev-environment-variable.png delete mode 100644 assets/part2/added-notes-service.png delete mode 100644 assets/part2/click-pipeline.png delete mode 100644 assets/part2/confirm-promote-dev-build.png delete mode 100644 assets/part2/copy-new-client-github-repo-url.png create mode 100644 assets/part2/create-notes-in-production.png delete mode 100644 assets/part2/dev-build-api-output.png delete mode 100644 assets/part2/dev-build-infrastructure-output.png delete mode 100644 assets/part2/dev-build-logs-in-progress.png delete mode 100644 assets/part2/dev-build-page-phase-1-in-progress.png delete mode 100644 assets/part2/dev-build-page-phase-2-in-progress.png delete mode 100644 assets/part2/dev-build-ready-to-promote.png delete mode 100644 assets/part2/dev-build-run-tests.png delete mode 100644 assets/part2/manage-deploy-phases.png delete mode 100644 assets/part2/multiple-deploy-phases.png delete mode 100644 assets/part2/name-new-client-github-repository.png create mode 100644 assets/part2/notes-app-in-production.png delete mode 100644 assets/part2/one-deploy-phase.png delete mode 100644 assets/part2/prod-build-api-output.png create mode 100644 assets/part2/prod-build-details.png delete mode 100644 assets/part2/prod-build-in-progress.png delete mode 100644 assets/part2/prod-build-infrastructure-output.png create mode 100644 assets/part2/prod-build-logs-in-progress.png create mode 100644 assets/part2/prod-build-run-tests.png create mode 100644 assets/part2/prod-build-stack-outputs.png create mode 100644 assets/part2/react-app-hosted-on-custom-domain.png delete mode 100644 assets/part2/review-promote-change-set.png delete mode 100644 assets/part2/search-new-service.png create mode 100644 assets/part2/seed-app-homepage.png delete mode 100644 assets/part2/seed-dev-build-in-progress.png create mode 100644 assets/part2/seed-prod-build-in-progress.png create mode 100644 assets/part2/select-branch-to-auto-deploy-to-prod.png delete mode 100644 assets/part2/serverless-yml-detected.png delete mode 100644 assets/part2/show-dev-env-variables-settings.png create mode 100644 assets/part2/sst-app-detected.png create mode 100644 assets/part2/sst-hello-world-api-invoked.png delete mode 100644 assets/part2/sst-json-detected.png create mode 100644 assets/part2/turn-off-auto-deploy-for-dev.png create mode 100644 assets/part2/turn-on-auto-deploy-for-prod.png delete mode 100644 assets/part2/view-new-seed-app.png diff --git a/README.md b/README.md index 8ed593a72..d2144be75 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ The [Serverless Stack Guide](https://serverless-stack.com/#guide) is a comprehensive open source tutorial for building and deploying full-stack apps using Serverless and React on AWS. -We are going to create a [note taking app](https://demo2.serverless-stack.com) from scratch using React.js, AWS Lambda, API Gateway, DynamoDB, and Cognito. +We are going to create a [note taking app](https://demo.serverless-stack.com) from scratch using React.js, AWS Lambda, API Gateway, DynamoDB, and Cognito. ![Demo App](assets/completed-app-desktop.png) diff --git a/_chapters/add-a-billing-api.md b/_chapters/add-a-billing-api.md index 829b0314b..9c3a331e0 100644 --- a/_chapters/add-a-billing-api.md +++ b/_chapters/add-a-billing-api.md @@ -91,6 +91,7 @@ Let's add a reference to our new API and Lambda function. events: - http: path: billing + cors: true method: post authorizer: aws_iam ``` diff --git a/_chapters/add-a-create-note-api.md b/_chapters/add-a-create-note-api.md index 57b7c32d6..102d16534 100644 --- a/_chapters/add-a-create-note-api.md +++ b/_chapters/add-a-create-note-api.md @@ -38,16 +38,24 @@ export async function main(event, context) { }, }; + // Set response headers to enable CORS (Cross-Origin Resource Sharing) + const headers = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Credentials": true + }; + try { await dynamoDb.put(params).promise(); return { statusCode: 200, + headers: headers, body: JSON.stringify(params.Item), }; } catch (e) { return { statusCode: 500, + headers: headers, body: JSON.stringify({ error: e.message }), }; } @@ -117,6 +125,7 @@ functions: events: - http: path: notes + cors: true method: post ``` @@ -256,6 +265,10 @@ export default function handler(lambda) { return { statusCode, body: JSON.stringify(body), + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Credentials": true, + }, }; }; } diff --git a/_chapters/add-a-delete-note-api.md b/_chapters/add-a-delete-note-api.md index f12755cd4..a30a2395d 100644 --- a/_chapters/add-a-delete-note-api.md +++ b/_chapters/add-a-delete-note-api.md @@ -49,6 +49,7 @@ This makes a DynamoDB `delete` call with the `userId` & `noteId` key to delete t events: - http: path: notes/{id} + cors: true method: delete ``` @@ -83,4 +84,4 @@ And the response should look similar to this. } ``` -Now that our APIs are complete; we are almost ready to deploy them. +Now that our APIs are complete, let's deploy them next! diff --git a/_chapters/add-a-get-note-api.md b/_chapters/add-a-get-note-api.md index 52f23a12c..728eeef9c 100644 --- a/_chapters/add-a-get-note-api.md +++ b/_chapters/add-a-get-note-api.md @@ -53,6 +53,7 @@ This follows exactly the same structure as our previous `create.js` function. Th events: - http: path: notes/{id} + cors: true method: get ``` diff --git a/_chapters/add-a-list-all-the-notes-api.md b/_chapters/add-a-list-all-the-notes-api.md index bc19c8c1b..c92fd47e7 100644 --- a/_chapters/add-a-list-all-the-notes-api.md +++ b/_chapters/add-a-list-all-the-notes-api.md @@ -54,6 +54,7 @@ This is pretty much the same as our `get.js` except we use a condition to only r events: - http: path: notes + cors: true method: get ``` diff --git a/_chapters/add-an-api-to-create-a-note.md b/_chapters/add-an-api-to-create-a-note.md new file mode 100644 index 000000000..9d3f763c0 --- /dev/null +++ b/_chapters/add-an-api-to-create-a-note.md @@ -0,0 +1,319 @@ +--- +layout: post +title: Add an API to Create a Note +date: 2021-08-17 00:00:00 +lang: en +description: In this chapter we are adding an API to create a note. It'll trigger a Lambda function when we hit the API and create a new note in our DynamoDB table. +ref: add-an-api-to-create-a-note +comments_id: add-an-api-to-create-a-note/2451 +--- + +Let's get started by creating the API for our notes app. + +We'll first add an API to create a note. This API will take the note object as the input and store it in the database with a new id. The note object will contain the `content` field (the content of the note) and an `attachment` field (the URL to the uploaded file). + +### Creating a Stack + +{%change%} Create a new file in `lib/ApiStack.js` and add the following. + +``` js +import * as sst from "@serverless-stack/resources"; + +export default class ApiStack extends sst.Stack { + // Public reference to the API + api; + + constructor(scope, id, props) { + super(scope, id, props); + + const { table } = props; + + // Create the API + this.api = new sst.Api(this, "Api", { + defaultFunctionProps: { + environment: { + TABLE_NAME: table.tableName, + }, + }, + routes: { + "POST /notes": "src/create.main", + }, + }); + + // Allow the API to access the table + this.api.attachPermissions([table]); + + // Show the API endpoint in the output + this.addOutputs({ + ApiEndpoint: this.api.url, + }); + } +} +``` + +We are doing a couple of things of note here. + +- We are creating a new stack for our API. We could've used the stack we had previously created for DynamoDB and S3. But this is a good way to talk about how to share resources between stacks. + +- This new `ApiStack` expects a `table` resource to be passed in. We'll be passing in the DynamoDB table from the `StorageStack` that we created previously. + +- We are creating an API using SST's [`Api`](https://docs.serverless-stack.com/constructs/Api) construct. + +- We are passing in the name of our DynamoDB table as an environment variable called `TABLE_NAME`. We'll need this to query our table. + +- The first route we are adding to our API is the `POSTS /notes` route. It'll be used to create a note. + +- We are giving our API permission to access our DynamoDB table by calling `this.api.attachPermissions([table])`. + +- Finally, we are printing out the URL of our API as an output by calling `this.addOutputs`. We are also exposing the API publicly so we can refer to it in other stacks. + +### Adding to the App + +Let's add this new stack to the rest of our app. + +{%change%} Replace the `main` function in `lib/index.js` with. + +``` js +export default function main(app) { + const storageStack = new StorageStack(app, "storage"); + + new ApiStack(app, "api", { + table: storageStack.table, + }); +} +``` + +Here you'll notice that we using the public reference of the table from the `StorageStack` and passing it in to our `ApiStack`. + +{%change%} Also, import the new stack at the top. + +``` js +import ApiStack from "./ApiStack"; +``` + +### Add the Function + +Now let's add the function that'll be creating our note. + +{%change%} Create a new file in `src/create.js` with the following. + +``` javascript +import * as uuid from "uuid"; +import AWS from "aws-sdk"; + +const dynamoDb = new AWS.DynamoDB.DocumentClient(); + +export async function main(event) { + // Request body is passed in as a JSON encoded string in 'event.body' + const data = JSON.parse(event.body); + + const params = { + TableName: process.env.TABLE_NAME, + Item: { + // The attributes of the item to be created + userId: "123", // The id of the author + noteId: uuid.v1(), // A unique uuid + content: data.content, // Parsed from request body + attachment: data.attachment, // Parsed from request body + createdAt: Date.now(), // Current Unix timestamp + }, + }; + + try { + await dynamoDb.put(params).promise(); + + return { + statusCode: 200, + body: JSON.stringify(params.Item), + }; + } catch (e) { + return { + statusCode: 500, + body: JSON.stringify({ error: e.message }), + }; + } +} +``` + +There are some helpful comments in the code but let's go over them quickly. + +- Parse the input from the `event.body`. This represents the HTTP request body. +- It contains the contents of the note, as a string — `content`. +- It also contains an `attachment`, if one exists. It's the filename of file that will been uploaded to [our S3 bucket]({% link _chapters/create-an-s3-bucket-in-sst.md %}). +- We read the name of our DynamoDB table from the environment variable using `process.env.TABLE_NAME`. You'll recall that we set this above while configuring our API. +- The `userId` is the id for the author of the note. For now we are hardcoding it to `123`. Later we'll be setting this based on the authenticated user. +- Make a call to DynamoDB to put a new object with a generated `noteId` and the current date as the `createdAt`. +- And if the DynamoDB call fails then return an error with the HTTP status code `500`. + +Let's go ahead and install the npm packages that we are using here. + +{%change%} Run the following in our project root. + +``` bash +$ npm install aws-sdk uuid@7.0.3 +``` + +- **aws-sdk** allows us to talk to the various AWS services. +- **uuid** generates unique ids. + +### Deploy Our Changes + +If you switch over to your terminal, you'll notice that you are being prompted to redeploy your changes. Go ahead and hit _ENTER_. + +Note that, you'll need to have `sst start` running for this to happen. If you had previously stopped it, then running `npx sst start` will deploy your changes again. + +You should see that the new API stack has been deployed. + +``` bash +Stack dev-notes-api + Status: deployed + Outputs: + ApiEndpoint: https://5bv7x0iuga.execute-api.us-east-1.amazonaws.com +``` + +It includes the API endpoint that we created. + +### Test the API + +Now we are ready to test our new API. + +{%change%} Run the following in your terminal. + +Make sure to keep your local environment (`sst start`) running in another window. + +``` bash +$ curl -X POST \ +-H 'Content-Type: application/json' \ +-d '{"content":"Hello World","attachment":"hello.jpg"}' \ +https://5bv7x0iuga.execute-api.us-east-1.amazonaws.com/notes +``` + +Here we are making a POST request to our create note API. We are passing in the `content` and `attachment` as a JSON string. In this case the attachment is a made up file name. We haven't uploaded anything to S3 yet. + +The response should look something like this. + +``` json +{"userId":"123","noteId":"a46b7fe0-008d-11ec-a6d5-a1d39a077784","content":"Hello World","attachment":"hello.jpg","createdAt":1629336889054} +``` + +Make a note of the `noteId` in the response. We are going to use this newly created note in the next chapter. + +### Refactor Our Code + +Before we move on to the next chapter, let's quickly refactor the code since we are going to be doing much of the same for all of our APIs. + +{%change%} Start by replacing our `create.js` with the following. + +``` javascript +import * as uuid from "uuid"; +import handler from "./util/handler"; +import dynamoDb from "./util/dynamodb"; + +export const main = handler(async (event) => { + const data = JSON.parse(event.body); + const params = { + TableName: process.env.TABLE_NAME, + Item: { + // The attributes of the item to be created + userId: "123", // The id of the author + noteId: uuid.v1(), // A unique uuid + content: data.content, // Parsed from request body + attachment: data.attachment, // Parsed from request body + createdAt: Date.now(), // Current Unix timestamp + }, + }; + + await dynamoDb.put(params); + + return params.Item; +}); +``` + +This code doesn't work just yet but it shows you what we want to accomplish: + +- We want to make our Lambda function `async`, and simply return the results. +- We want to simplify how we make calls to DynamoDB. We don't want to have to create a `new AWS.DynamoDB.DocumentClient()`. +- We want to centrally handle any errors in our Lambda functions. +- Finally, since all of our Lambda functions will be handling API endpoints, we want to handle our HTTP responses in one place. + +Let's start by creating the `dynamodb` util. + +{%change%} From the project root run the following to create a `src/util` directory. + +``` bash +$ mkdir src/util +``` + +{%change%} Create a `src/util/dynamodb.js` file with: + +``` javascript +import AWS from "aws-sdk"; + +const client = new AWS.DynamoDB.DocumentClient(); + +export default { + get: (params) => client.get(params).promise(), + put: (params) => client.put(params).promise(), + query: (params) => client.query(params).promise(), + update: (params) => client.update(params).promise(), + delete: (params) => client.delete(params).promise(), +}; +``` + +Here we are creating a convenience object that exposes the DynamoDB client methods that we are going to need in this guide. + +{%change%} Also create a `src/util/handler.js` file with the following. + +``` javascript +export default function handler(lambda) { + return async function (event, context) { + let body, statusCode; + + try { + // Run the Lambda + body = await lambda(event, context); + statusCode = 200; + } catch (e) { + console.error(e); + body = { error: e.message }; + statusCode = 500; + } + + // Return HTTP response + return { + statusCode, + body: JSON.stringify(body), + }; + }; +} +``` + +Let's go over this in detail. + +- We are creating a `handler` function that we'll use as a wrapper around our Lambda functions. +- It takes our Lambda function as the argument. +- We then run the Lambda function in a `try/catch` block. +- On success, we `JSON.stringify` the result and return it with a `200` status code. +- If there is an error then we return the error message with a `500` status code. + +It's **important to note** that the `handler.js` needs to be **imported before we import anything else**. This is because we'll be adding some error handling to it later that needs to be initialized when our Lambda function is first invoked. + +Next, we are going to add the API to get a note given its id. + +--- + +#### Common Issues + +- Response `statusCode: 500` + + If you see a `statusCode: 500` response when you invoke your function, here is how to debug it. The error is generated by our code in the `catch` block. Adding a `console.log` in our `util/handler.js`, should give you a clue about what the issue is. + + ``` javascript + } catch (e) { + // Print out the full error + console.log(e); + + body = { error: e.message }; + statusCode = 500; + } + ``` diff --git a/_chapters/add-an-api-to-delete-a-note.md b/_chapters/add-an-api-to-delete-a-note.md new file mode 100644 index 000000000..3072ec790 --- /dev/null +++ b/_chapters/add-an-api-to-delete-a-note.md @@ -0,0 +1,94 @@ +--- +layout: post +title: Add an API to Delete a Note +date: 2021-08-17 00:00:00 +lang: en +description: In this chapter we are adding an API to delete a given note. It'll trigger a Lambda function when we hit the API and delete the note from our DynamoDB table. +ref: add-an-api-to-delete-a-note +comments_id: add-an-api-to-delete-a-note/2452 +--- + +Finally, we are going to create an API that allows a user to delete a given note. + +### Add the Function + +{%change%} Create a new file in `src/delete.js` and paste the following. + +``` javascript +import handler from "./util/handler"; +import dynamoDb from "./util/dynamodb"; + +export const main = handler(async (event) => { + const params = { + TableName: process.env.TABLE_NAME, + // 'Key' defines the partition key and sort key of the item to be removed + Key: { + userId: "123", // The id of the author + noteId: event.pathParameters.id, // The id of the note from the path + }, + }; + + await dynamoDb.delete(params); + + return { status: true }; +}); +``` + +This makes a DynamoDB `delete` call with the `userId` & `noteId` key to delete the note. We are still hard coding the `userId` for now. + +### Add the Route + +Let's add a new route for the delete note API. + +{%change%} Add the following below the `PUT /notes{id}` route in `lib/ApiStack.js`. + +``` js +"DELETE /notes/{id}": "src/delete.main", +``` + +### Deploy Our Changes + +If you switch over to your terminal, you'll notice that you are being prompted to redeploy your changes. Go ahead and hit _ENTER_. + +Note that, you'll need to have `sst start` running for this to happen. If you had previously stopped it, then running `npx sst start` will deploy your changes again. + +You should see that the API stack is being updated. + +``` bash +Stack dev-notes-api + Status: deployed + Outputs: + ApiEndpoint: https://5bv7x0iuga.execute-api.us-east-1.amazonaws.com +``` + +### Test the API + +Let's test the delete note API. + +{%change%} Run the following in your terminal. + +Make sure to keep your local environment (`sst start`) running in another window. + +``` bash +$ curl -X DELETE https://5bv7x0iuga.execute-api.us-east-1.amazonaws.com/notes/NOTE_ID +``` + +Make sure to replace the id at the end of the URL with the `noteId` from when we [created our note]({% link _chapters/add-an-api-to-create-a-note.md %}). + +Here we are making a DELETE request to the note that we want to delete. The response should look something like this. + +``` json +{"status":true} +``` + +### Commit the Changes + +{%change%} Let's commit and push our changes to GitHub. + +``` bash +$ git add . +$ git commit -m "Adding the API" +$ git push +``` + +So our API is publicly available, this means that anybody can access it and create notes. And it’s always connecting to the `123` user id. Let’s fix these next by handling users and authentication. diff --git a/_chapters/add-an-api-to-get-a-note.md b/_chapters/add-an-api-to-get-a-note.md new file mode 100644 index 000000000..64a128ddc --- /dev/null +++ b/_chapters/add-an-api-to-get-a-note.md @@ -0,0 +1,88 @@ +--- +layout: post +title: Add an API to Get a Note +date: 2021-08-17 00:00:00 +lang: en +description: In this chapter we are adding an API to get a note. It'll trigger a Lambda function when we hit the API and get the requested note from our DynamoDB table. +ref: add-an-api-to-get-a-note +comments_id: add-an-api-to-get-a-note/2453 +--- + +Now that we [created a note]({% link _chapters/add-an-api-to-create-a-note.md %}) and saved it to our database. Let's add an API to retrieve a note given its id. + +### Add the Function + +{%change%} Create a new file in `src/get.js` in your project root with the following: + +``` javascript +import handler from "./util/handler"; +import dynamoDb from "./util/dynamodb"; + +export const main = handler(async (event) => { + const params = { + TableName: process.env.TABLE_NAME, + // 'Key' defines the partition key and sort key of the item to be retrieved + Key: { + userId: "123", // The id of the author + noteId: event.pathParameters.id, // The id of the note from the path + }, + }; + + const result = await dynamoDb.get(params); + if (!result.Item) { + throw new Error("Item not found."); + } + + // Return the retrieved item + return result.Item; +}); +``` + +This follows exactly the same structure as our previous `create.js` function. The major difference here is that we are doing a `dynamoDb.get(params)` to get a note object given the `userId` (still hardcoded) and `noteId` that's passed in through the request. + +### Add the route + +Let's add a new route for the get note API. + +{%change%} Add the following below the `POST /notes` route in `lib/ApiStack.js`. + +``` js +"GET /notes/{id}": "src/get.main", +``` + +### Deploy our changes + +If you switch over to your terminal, you'll notice that you are being prompted to redeploy your changes. Go ahead and hit _ENTER_. + +Note that, you'll need to have `sst start` running for this to happen. If you had previously stopped it, then running `npx sst start` will deploy your changes again. + +You should see that the API stack is being updated. + +``` bash +Stack dev-notes-api + Status: deployed + Outputs: + ApiEndpoint: https://5bv7x0iuga.execute-api.us-east-1.amazonaws.com +``` + +### Test the API + +Let's test the get notes API. In the [previous chapter]({% link _chapters/add-an-api-to-get-a-note.md %}) we tested our create note API. It should've returned the new note's id as the `noteId`. + +{%change%} Run the following in your terminal. + +``` bash +$ curl https://5bv7x0iuga.execute-api.us-east-1.amazonaws.com/notes/NOTE_ID +``` + +Make sure to replace the id at the end of the URL with the `noteId` that was created previously. + +Since we are making a simple GET request, we could also go to this URL directly in your browser. + +The response should look something like this. + +``` json +{"attachment":"hello.jpg","content":"Hello World","createdAt":1629336889054,"noteId":"a46b7fe0-008d-11ec-a6d5-a1d39a077784","userId":"123"} +``` + +Next, let’s create an API to list all the notes a user has. diff --git a/_chapters/add-an-api-to-handle-billing.md b/_chapters/add-an-api-to-handle-billing.md new file mode 100644 index 000000000..89dffb8ee --- /dev/null +++ b/_chapters/add-an-api-to-handle-billing.md @@ -0,0 +1,147 @@ +--- +layout: post +title: Add an API to Handle Billing +date: 2021-08-17 00:00:00 +lang: en +description: In this chapter we'll add an API to our serverless app to handle billing. We'll use the Stripe npm package in our Lambda function to charge a credit card. +ref: add-an-api-to-handle-billing +comments_id: add-an-api-to-handle-billing/2454 +--- + +Now let's get started with creating an API to handle billing. It's going to take a Stripe token and the number of notes the user wants to store. + +### Add a Billing Lambda + +{%change%} Start by installing the Stripe NPM package. Run the following in the root of our project. + +``` bash +$ npm install stripe +``` + +{%change%} Create a new file in `src/billing.js` with the following. + +``` js +import Stripe from "stripe"; +import handler from "./util/handler"; +import { calculateCost } from "./util/cost"; + +export const main = handler(async (event) => { + const { storage, source } = JSON.parse(event.body); + const amount = calculateCost(storage); + const description = "Scratch charge"; + + // Load our secret key from the environment variables + const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); + + await stripe.charges.create({ + source, + amount, + description, + currency: "usd", + }); + + return { status: true }; +}); +``` + +Most of this is fairly straightforward but let's go over it quickly: + +- We get the `storage` and `source` from the request body. The `storage` variable is the number of notes the user would like to store in his account. And `source` is the Stripe token for the card that we are going to charge. + +- We are using a `calculateCost(storage)` function (that we are going to add soon) to figure out how much to charge a user based on the number of notes that are going to be stored. + +- We create a new Stripe object using our Stripe Secret key. We are getting this from the environment variable that we configured in the [previous chapter]({% link _chapters/handling-secrets-in-sst.md %}). + +- Finally, we use the `stripe.charges.create` method to charge the user and respond to the request if everything went through successfully. + +Note, if you are testing this from India, you'll need to add some shipping information as well. Check out the [details from our forums](https://discourse.serverless-stack.com/t/test-the-billing-api/172/20). + +### Add the Business Logic + +Now let's implement our `calculateCost` method. This is primarily our *business logic*. + +{%change%} Create a `src/util/cost.js` and add the following. + +``` js +export function calculateCost(storage) { + const rate = storage <= 10 ? 4 : storage <= 100 ? 2 : 1; + return rate * storage * 100; +} +``` + +This is basically saying that if a user wants to store 10 or fewer notes, we'll charge them $4 per note. For 11 to 100 notes, we'll charge $2 and any more than 100 is $1 per note. Since Stripe expects us to provide the amount in pennies (the currency’s smallest unit) we multiply the result by 100. + +Clearly, our serverless infrastructure might be cheap but our service isn't! + +### Add the Route + +Let's add a new route for our billing API. + +{%change%} Add the following below the `DELETE /notes/{id}` route in `lib/ApiStack.js`. + +``` js +"POST /billing": "src/billing.main", +``` + +### Deploy Our Changes + +If you switch over to your terminal, you'll notice that you are being prompted to redeploy your changes. Go ahead and hit _ENTER_. + +Note that, you'll need to have `sst start` running for this to happen. If you had previously stopped it, then running `npx sst start` will deploy your changes again. + +You should see that the API stack is being updated. + +``` bash +Stack dev-notes-api + Status: deployed + Outputs: + ApiEndpoint: https://5bv7x0iuga.execute-api.us-east-1.amazonaws.com +``` + +### Test the Billing API + +Now that we have our billing API all set up, let's do a quick test in our local environment. + +We'll be using the same CLI from [a few chapters ago]({% link _chapters/secure-our-serverless-apis.md %}). + +{%change%} Run the following in your terminal. + +``` bash +$ npx aws-api-gateway-cli-test \ +--username='admin@example.com' \ +--password='Passw0rd!' \ +--user-pool-id='USER_POOL_ID' \ +--app-client-id='USER_POOL_CLIENT_ID' \ +--cognito-region='COGNITO_REGION' \ +--identity-pool-id='IDENTITY_POOL_ID' \ +--invoke-url='API_ENDPOINT' \ +--api-gateway-region='API_REGION' \ +--path-template='/billing' \ +--method='POST' \ +--body='{"source":"tok_visa","storage":21}' +``` + +Make sure to replace the `USER_POOL_ID`, `USER_POOL_CLIENT_ID`, `COGNITO_REGION`, `IDENTITY_POOL_ID`, `API_ENDPOINT`, and `API_REGION` with the [same values we used a couple of chapters ago]({% link _chapters/secure-our-serverless-apis.md %}). + +Here we are testing with a Stripe test token called `tok_visa` and with `21` as the number of notes we want to store. You can read more about the Stripe test cards and tokens in the [Stripe API Docs here](https://stripe.com/docs/testing#cards). + +If the command is successful, the response will look similar to this. + +``` bash +Authenticating with User Pool +Getting temporary credentials +Making API request +{ status: 200, statusText: 'OK', data: { status: true } } +``` + +### Commit the Changes + +{%change%} Let's commit and push our changes to GitHub. + +``` bash +$ git add . +$ git commit -m "Adding a billing API" +$ git push +``` + +Now that we have our new billing API ready. Let's look at how to setup unit tests in serverless. We'll be using that to ensure that our infrastructure and business logic has been configured correctly. diff --git a/_chapters/add-an-api-to-list-all-the-notes.md b/_chapters/add-an-api-to-list-all-the-notes.md new file mode 100644 index 000000000..47bb0e644 --- /dev/null +++ b/_chapters/add-an-api-to-list-all-the-notes.md @@ -0,0 +1,89 @@ +--- +layout: post +title: Add an API to List All the Notes +date: 2021-08-17 00:00:00 +lang: en +description: In this chapter we are adding an API to get a list of all the notes a user has. It'll trigger a Lambda function when we hit the API and get the list of notes from our DynamoDB table. +ref: add-an-api-to-list-all-the-notes +comments_id: add-an-api-to-list-all-the-notes/2455 +--- + +Now we are going to add an API that returns a list of all the notes a user has. This'll be very similar to the [previous chapter]({% link _chapters/add-an-api-to-get-a-note.md %}) where we were returning a single note. + +### Add the Function + +{%change%} Create a new file in `src/list.js` with the following. + +``` js +import handler from "./util/handler"; +import dynamoDb from "./util/dynamodb"; + +export const main = handler(async () => { + const params = { + TableName: process.env.TABLE_NAME, + // 'KeyConditionExpression' defines the condition for the query + // - 'userId = :userId': only return items with matching 'userId' + // partition key + KeyConditionExpression: "userId = :userId", + // 'ExpressionAttributeValues' defines the value in the condition + // - ':userId': defines 'userId' to be the id of the author + ExpressionAttributeValues: { + ":userId": "123", + }, + }; + + const result = await dynamoDb.query(params); + + // Return the matching list of items in response body + return result.Items; +}); +``` + +This is pretty much the same as our `get.js` except we use a condition to only return the items that have the same `userId` as the one we are passing in. In our case, it's still hardcoded to `123`. + +### Add the Route + +Let's add the route for this new endpoint. + +{%change%} Add the following above the `POST /notes` route in `lib/ApiStack.js`. + +``` js +"GET /notes": "src/list.main", +``` + +### Deploy Our Changes + +If you switch over to your terminal, you'll notice that you are being prompted to redeploy your changes. Go ahead and hit _ENTER_. + +Note that, you'll need to have `sst start` running for this to happen. If you had previously stopped it, then running `npx sst start` will deploy your changes again. + +You should see that the API stack is being updated. + +``` bash +Stack dev-notes-api + Status: deployed + Outputs: + ApiEndpoint: https://5bv7x0iuga.execute-api.us-east-1.amazonaws.com +``` + +### Test the API + +Let's test list all notes API. + +{%change%} Run the following in your terminal. + +``` bash +$ curl https://5bv7x0iuga.execute-api.us-east-1.amazonaws.com/notes +``` + +Since we are making a simple GET request, we could also go to this URL directly in your browser. + +The response should look something like this. + +``` json +[{"attachment":"hello.jpg","content":"Hello World","createdAt":1629336889054,"noteId":"a46b7fe0-008d-11ec-a6d5-a1d39a077784","userId":"123"}] +``` + +Note that, we are getting an array of notes. Instead of a single note. + +Next we are going to add an API to update a note. diff --git a/_chapters/add-an-api-to-update-a-note.md b/_chapters/add-an-api-to-update-a-note.md new file mode 100644 index 000000000..d7cb6df96 --- /dev/null +++ b/_chapters/add-an-api-to-update-a-note.md @@ -0,0 +1,101 @@ +--- +layout: post +title: Add an API to Update a Note +date: 2021-08-17 00:00:00 +lang: en +description: In this chapter we are adding an API to update a given note. It'll trigger a Lambda function when we hit the API and update the note in our DynamoDB table. +ref: add-an-api-to-update-a-note +comments_id: add-an-api-to-update-a-note/2456 +--- + +Now let's create an API that allows a user to update a note with a new note object given the id. + +### Add the Function + +{%change%} Create a new file in `src/update.js` and paste the following. + +``` javascript +import handler from "./util/handler"; +import dynamoDb from "./util/dynamodb"; + +export const main = handler(async (event) => { + const data = JSON.parse(event.body); + const params = { + TableName: process.env.TABLE_NAME, + // 'Key' defines the partition key and sort key of the item to be updated + Key: { + userId: "123", // The id of the author + noteId: event.pathParameters.id, // The id of the note from the path + }, + // 'UpdateExpression' defines the attributes to be updated + // 'ExpressionAttributeValues' defines the value in the update expression + UpdateExpression: "SET content = :content, attachment = :attachment", + ExpressionAttributeValues: { + ":attachment": data.attachment || null, + ":content": data.content || null, + }, + // 'ReturnValues' specifies if and how to return the item's attributes, + // where ALL_NEW returns all attributes of the item after the update; you + // can inspect 'result' below to see how it works with different settings + ReturnValues: "ALL_NEW", + }; + + await dynamoDb.update(params); + + return { status: true }; +}); +``` + +This should look similar to the `create.js` function. Here we make an `update` DynamoDB call with the new `content` and `attachment` values in the `params`. + +### Add the Route + +Let's add a new route for the get note API. + +{%change%} Add the following below the `GET /notes/{id}` route in `lib/ApiStack.js`. + +``` js +"PUT /notes/{id}": "src/update.main", +``` + +### Deploy Our Changes + +If you switch over to your terminal, you'll notice that you are being prompted to redeploy your changes. Go ahead and hit _ENTER_. + +Note that, you'll need to have `sst start` running for this to happen. If you had previously stopped it, then running `npx sst start` will deploy your changes again. + +You should see that the API stack is being updated. + +``` bash +Stack dev-notes-api + Status: deployed + Outputs: + ApiEndpoint: https://5bv7x0iuga.execute-api.us-east-1.amazonaws.com +``` + +### Test the API + +Now we are ready to test the new API. + +{%change%} Run the following in your terminal. + +Make sure to keep your local environment (`sst start`) running in another window. + +``` bash +$ curl -X PUT \ +-H 'Content-Type: application/json' \ +-d '{"content":"New World","attachment":"new.jpg"}' \ +https://5bv7x0iuga.execute-api.us-east-1.amazonaws.com/notes/NOTE_ID +``` + +Make sure to replace the id at the end of the URL with the `noteId` from when we [created our note]({% link _chapters/add-an-api-to-create-a-note.md %}). + +Here we are making a PUT request to a note that we want to update. We are passing in the new `content` and `attachment` as a JSON string. + +The response should look something like this. + +``` json +{"status":true} +``` + +Next we are going to add the API to delete a note given its id. diff --git a/_chapters/add-an-update-note-api.md b/_chapters/add-an-update-note-api.md index d11a5ad8a..dfd2fc05a 100644 --- a/_chapters/add-an-update-note-api.md +++ b/_chapters/add-an-update-note-api.md @@ -61,6 +61,7 @@ This should look similar to the `create.js` function. Here we make an `update` D events: - http: path: notes/{id} + cors: true method: put ``` diff --git a/_chapters/add-app-favicons.md b/_chapters/add-app-favicons.md index 655ea3a5d..faa6c4b8f 100644 --- a/_chapters/add-app-favicons.md +++ b/_chapters/add-app-favicons.md @@ -8,7 +8,7 @@ description: To generate app icons and favicons for our React.js app we will use comments_id: add-app-favicons/155 --- -Create React App generates a simple favicon for our app and places it in `public/favicon.ico`. However, getting the favicon to work on all browsers and mobile platforms requires a little more work. There are quite a few different requirements and dimensions. And this gives us a good opportunity to learn how to include files in the `public/` directory of our app. +Create React App generates a simple favicon for our app and places it in `public/favicon.ico` of our app. However, getting the favicon to work on all browsers and mobile platforms requires a little more work. There are quite a few different requirements and dimensions. And this gives us a good opportunity to learn how to include files in the `public/` directory of our app. For our example, we are going to start with a simple image and generate the various versions from it. @@ -16,7 +16,7 @@ For our example, we are going to start with a simple image and generate the vari App Icon -To ensure that our icon works for most of our targeted platforms we'll use a service called the [Favicon Generator](http://realfavicongenerator.net). +To ensure that our icon works for most of our targeted platforms we'll use a service called the [**Favicon Generator**](http://realfavicongenerator.net). Click **Select your Favicon picture** to upload our icon. @@ -32,11 +32,14 @@ This should generate your favicon package and the accompanying code. ![Realfavicongenerator.net completed screenshot](/assets/realfavicongenerator-completed.png) -{%change%} Remove the `public/logo192.png` and `public/logo512.png` files. +Let's remove the old icons files. + +**Note that, moving forward we'll be working exclusively in the `frontend/` directory.** + +{%change%} Run the following from our `frontend/` directory. ``` bash -$ rm public/logo192.png -$ rm public/logo512.png +$ rm public/logo192.png public/logo512.png ``` {%change%} Then replace the contents of `public/manifest.json` with the following: diff --git a/_chapters/add-stripe-keys-to-config.md b/_chapters/add-stripe-keys-to-config.md index e32b6fbe8..1e609ca5c 100644 --- a/_chapters/add-stripe-keys-to-config.md +++ b/_chapters/add-stripe-keys-to-config.md @@ -3,7 +3,7 @@ layout: post title: Add Stripe Keys to Config lang: en date: 2017-01-31 09:00:00 -description: We are going to use the Stripe React JS SDK in our Create React App. To do so, we are going to store our Stripe Publishable API Key in our React app config. We also need to include Stripe.js in our HTML page. +description: We are going to use the Stripe React JS SDK in our Create React App. To do so, we are going to store our Stripe Publishable API Key in our React app config. We also need to include Stripe.js packages. ref: add-stripe-keys-to-config comments_id: add-stripe-keys-to-config/185 --- @@ -18,12 +18,12 @@ STRIPE_KEY: "YOUR_STRIPE_PUBLIC_KEY", Make sure to replace, `YOUR_STRIPE_PUBLIC_KEY` with the **Publishable key** from the [Setup a Stripe account]({% link _chapters/setup-a-stripe-account.md %}) chapter. -Let's also include Stripe.js in our HTML. +Let's also add the Stripe.js packages -{%change%} Append the following to the `` block in our `public/index.html`. +{%change%} Run the following in the `frontend/` directory and **not** in your project root. -``` html - +``` bash +$ npm install @stripe/stripe-js ``` And load the Stripe config in our settings page. @@ -31,13 +31,15 @@ And load the Stripe config in our settings page. {%change%} Add the following at top of the `Settings` component in `src/containers/Settings.js` above the `billUser()` function. ``` javascript -const [stripe, setStripe] = useState(null); - -useEffect(() => { - setStripe(window.Stripe(config.STRIPE_KEY)); -}, []); +const stripePromise = loadStripe(config.STRIPE_KEY); ``` -This loads the Stripe object from Stripe.js with the Stripe key when our settings page loads. And saves it to the state. +This loads the Stripe object from Stripe.js with the Stripe key when our settings page loads. We'll be using this in the coming chapters. + +{%change%} We'll also import this function at the top. + +``` js +import { loadStripe } from "@stripe/stripe-js"; +``` Next, we'll build our billing form. diff --git a/_chapters/add-the-create-note-page.md b/_chapters/add-the-create-note-page.md index 9408877e0..ae0bb72c0 100644 --- a/_chapters/add-the-create-note-page.md +++ b/_chapters/add-the-create-note-page.md @@ -16,12 +16,12 @@ First we are going to create the form for a note. It'll take some content and a {%change%} Create a new file `src/containers/NewNote.js` and add the following. -``` coffee +``` jsx import React, { useRef, useState } from "react"; import Form from "react-bootstrap/Form"; import { useHistory } from "react-router-dom"; import LoaderButton from "../components/LoaderButton"; -import { onError } from "../libs/errorLib"; +import { onError } from "../lib/errorLib"; import config from "../config"; import "./NewNote.css"; @@ -110,7 +110,7 @@ MAX_ATTACHMENT_SIZE: 5000000, {%change%} Finally, add our container as a route in `src/Routes.js` below our signup route. -``` coffee +``` jsx diff --git a/_chapters/add-the-session-to-the-state.md b/_chapters/add-the-session-to-the-state.md index c7558330b..5c143be05 100644 --- a/_chapters/add-the-session-to-the-state.md +++ b/_chapters/add-the-session-to-the-state.md @@ -43,13 +43,15 @@ We are going to have to pass the session related info to all of our containers. We'll create a context for our entire app that all of our containers will use. -{%change%} Create a `src/libs/` directory. We'll use this to store all our common code. +{%change%} Create a `src/lib/` directory in the `frontend/` React directory. ``` bash -$ mkdir src/libs/ +$ mkdir src/lib/ ``` -{%change%} Add the following to `src/libs/contextLib.js`. +We'll use this to store all our common code. + +{%change%} Add the following to `src/lib/contextLib.js`. ``` javascript import { useContext, createContext } from "react"; @@ -70,21 +72,21 @@ If you are not sure how Contexts work, don't worry, it'll make more sense once w {%change%} Import our new app context in the header of `src/App.js`. ``` javascript -import { AppContext } from "./libs/contextLib"; +import { AppContext } from "./lib/contextLib"; ``` Now to add our session to the context and to pass it to our containers: {%change%} Wrap our `Routes` component in the `return` statement of `src/App.js`. -``` coffee +``` jsx ``` {%change%} With this. {% raw %} -``` coffee +``` jsx @@ -104,7 +106,7 @@ The second part of the Context API is the consumer. We'll add that to the Login {%change%} Start by importing it in the header of `src/containers/Login.js`. ``` javascript -import { useAppContext } from "../libs/contextLib"; +import { useAppContext } from "../lib/contextLib"; ``` {%change%} Include the hook by adding it below the `export default function Login() {` line. @@ -125,7 +127,7 @@ userHasAuthenticated(true); We can now use this to display a Logout button once the user logs in. Find the following in our `src/App.js`. -``` coffee +``` jsx Signup @@ -136,7 +138,7 @@ We can now use this to display a Logout button once the user logs in. Find the f {%change%} And replace it with this: -``` coffee +``` jsx {isAuthenticated ? ( Logout ) : ( @@ -161,7 +163,7 @@ function handleLogout() { } ``` -Now head over to your browser and try logging in with the admin credentials we created in the [Create a Cognito Test User]({% link _chapters/create-a-cognito-test-user.md %}) chapter. You should see the Logout button appear right away. +Now head over to your browser and try logging in with the admin credentials we created in the [Secure Our Serverless APIs]({% link _chapters/secure-our-serverless-apis.md %}) chapter. You should see the Logout button appear right away. ![Login state updated screenshot](/assets/login-state-updated.png) diff --git a/_chapters/adding-auth-to-our-serverless-app.md b/_chapters/adding-auth-to-our-serverless-app.md new file mode 100644 index 000000000..d87991eb5 --- /dev/null +++ b/_chapters/adding-auth-to-our-serverless-app.md @@ -0,0 +1,173 @@ +--- +layout: post +title: Adding Auth to Our Serverless App +date: 2021-08-17 00:00:00 +lang: en +description: In this chapter we'll be adding a Cognito User Pool and Identity Pool to our serverless app. We'll be using SST's higher-level Auth construct to make this easy. +redirect_from: + - /chapters/configure-cognito-user-pool-in-cdk.html + - /chapters/configure-cognito-identity-pool-in-cdk.html +ref: adding-auth-to-our-serverless-app +comments_id: adding-auth-to-our-serverless-app/2457 +--- + +So far we've created the [DynamoDB table]({% link _chapters/create-a-dynamodb-table-in-sst.md %}), [S3 bucket]({% link _chapters/create-an-s3-bucket-in-sst.md %}), and [API]({% link _chapters/add-an-api-to-create-a-note.md %}) parts of our serverless backend. Now let's add auth into the mix. As we talked about in the [previous chapter]({% link _chapters/auth-in-serverless-apps.md %}), we are going to use [Cognito User Pool](https://aws.amazon.com/cognito/) to manage user sign ups and logins. While we are going to use [Cognito Identity Pool](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-identity.html) to manage which resources our users have access to. + +Setting this all up can be pretty complicated in CDK. SST has a simple [`Auth`](https://docs.serverless-stack.com/constructs/Auth) construct to help with this. + +### Create a Stack + +{%change%} Add the following to a new file in `lib/AuthStack.js`. + +``` js +import * as iam from "@aws-cdk/aws-iam"; +import * as sst from "@serverless-stack/resources"; + +export default class AuthStack extends sst.Stack { + // Public reference to the auth instance + auth; + + constructor(scope, id, props) { + super(scope, id, props); + + const { api, bucket } = props; + + // Create a Cognito User Pool and Identity Pool + this.auth = new sst.Auth(this, "Auth", { + cognito: { + userPool: { + // Users can login with their email and password + signInAliases: { email: true }, + }, + }, + }); + + this.auth.attachPermissionsForAuthUsers([ + // Allow access to the API + api, + // Policy granting access to a specific folder in the bucket + new iam.PolicyStatement({ + actions: ["s3:*"], + effect: iam.Effect.ALLOW, + resources: [ + bucket.bucketArn + "/private/${cognito-identity.amazonaws.com:sub}/*", + ], + }), + ]); + + // Show the auth resources in the output + this.addOutputs({ + Region: scope.region, + UserPoolId: this.auth.cognitoUserPool.userPoolId, + IdentityPoolId: this.auth.cognitoCfnIdentityPool.ref, + UserPoolClientId: this.auth.cognitoUserPoolClient.userPoolClientId, + }); + } +} +``` + +Let's quickly go over what we are doing here. + +- We are creating a new stack for our auth infrastructure. We don't need to create a separate stack but we are using it as an example to show how to work with multiple stacks. + +- The `Auth` construct creates a Cognito User Pool for us. We are using the `signInAliases` prop to state that we want our users to be login with their email. + +- The `Auth` construct also creates an Identity Pool. The `attachPermissionsForAuthUsers` function allows us to specify the resources our authenticated users have access to. + +- In this case, we want them to access our API. We'll be passing that in as a prop. + +- And we want them to access our S3 bucket. We'll look at this in detail below. + +- Finally, we output the ids of the auth resources that've been created. + +We also need to install a CDK package for the IAM policy that we are creating. + +{%change%} Run the following in your project root. + +``` bash +$ npx sst add-cdk @aws-cdk/aws-iam +``` + +We are using this command instead of `npm install` because there's [a known issue with CDK](https://docs.serverless-stack.com/known-issues) where mismatched versions can cause a problem. + +### Securing Access to Uploaded Files + +We are creating a specific IAM policy to secure the files our users will upload to our S3 bucket. + +``` js +// Policy granting access to a specific folder in the bucket +new iam.PolicyStatement({ + actions: ["s3:*"], + effect: iam.Effect.ALLOW, + resources: [ + bucket.bucketArn + "/private/${cognito-identity.amazonaws.com:sub}/*", + ], +}), +``` + +Let's look at how this works. + +In the above policy we are granting our logged in users access to the path `private/${cognito-identity.amazonaws.com:sub}/` within our S3 bucket's ARN. Where `cognito-identity.amazonaws.com:sub` is the authenticated user’s federated identity id (their user id). So a user has access to only their folder within the bucket. This allows us to separate access to our user's file uploads within the same S3 bucket. + +One other thing to note is that, the federated identity id is a UUID that is assigned by our Identity Pool. This id is different from the one that a user is assigned in a User Pool. This is because you can have multiple authentication providers. The Identity Pool federates these identities and gives each user a unique id. + +### Add to the App + +Let's add this stack to our app. + +{%change%} Replace the `main` function in `lib/index.js` with this. + +``` js +export default function main(app) { + const storageStack = new StorageStack(app, "storage"); + + const apiStack = new ApiStack(app, "api", { + table: storageStack.table, + }); + + new AuthStack(app, "auth", { + api: apiStack.api, + bucket: storageStack.bucket, + }); +} +``` + +Here you'll notice that we are passing in our API and S3 Bucket to the auth stack. + +{%change%} Also, import the new stack at the top. + +``` js +import AuthStack from "./AuthStack"; +``` + +### Add Auth to the API + +We also need to enable authentication in our API. + +{%change%} Add the following above the `defaultFunctionProps: {` line in `lib/ApiStack.js`. + +``` js +defaultAuthorizationType: "AWS_IAM", +``` + +This tells our API that we want to use `AWS_IAM` across all our routes. + +### Deploy the App + +If you switch over to your terminal, you'll notice that you are being prompted to redeploy your changes. Go ahead and hit _ENTER_. + +Note that, you'll need to have `sst start` running for this to happen. If you had previously stopped it, then running `npx sst start` will deploy your changes again. + +You should see something like this at the end of the deploy process. + +``` bash +Stack dev-notes-auth + Status: deployed + Outputs: + Region: us-east-1 + IdentityPoolId: us-east-1:9bd0357e-2ac1-418d-a609-bc5e7bc064e3 + UserPoolClientId: 3fetogamdv9aqa0393adsd7viv + UserPoolId: us-east-1_TYEz7XP7P +``` + +Now that the auth services in our infrastructure have been created, let's use them to secure our APIs. diff --git a/_chapters/adding-links-in-the-navbar.md b/_chapters/adding-links-in-the-navbar.md index 3ad1899dd..dfe2e1a9e 100644 --- a/_chapters/adding-links-in-the-navbar.md +++ b/_chapters/adding-links-in-the-navbar.md @@ -12,7 +12,7 @@ Now that we have our first route set up, let's add a couple of links to the navb {%change%} Replace the `App` function component in `src/App.js` with the following. -``` coffee +``` jsx function App() { return (
@@ -42,7 +42,7 @@ And let's include the `Nav` component in the header. {%change%} Add the following import to the top of your `src/App.js`. -``` coffee +``` jsx import Nav from "react-bootstrap/Nav"; ``` @@ -54,23 +54,23 @@ Unfortunately, when you click on them they refresh your browser while redirectin To fix this we need a component that works with React Router and React Bootstrap called [React Router Bootstrap](https://github.com/react-bootstrap/react-router-bootstrap). It can wrap around your `Navbar` links and use the React Router to route your app to the required link without refreshing the browser. -{%change%} Run the following command in your working directory. +{%change%} Run the following command in the `frontend/` directory and **not** in your project root. ``` bash -$ npm install react-router-bootstrap --save +$ npm install react-router-bootstrap ``` Let's also import it. {%change%} Add this to the top of your `src/App.js`. -``` coffee +``` jsx import { LinkContainer } from "react-router-bootstrap"; ``` {%change%} We will now wrap our links with the `LinkContainer`. Replace the `App` function component in your `src/App.js` with this. -``` coffee +``` jsx function App() { return (
@@ -100,7 +100,7 @@ function App() { We are doing one other thing here. We are grabbing the current path the user is on from the `window.location` object. And we set it as the `activeKey` of our `Nav` component. This'll highlight the link when we are on that page. -``` coffee +``` jsx