├── packages ├── api │ ├── README.md │ ├── functions │ │ ├── express │ │ │ ├── wrap-async.js │ │ │ ├── lambda.js │ │ │ ├── routes │ │ │ │ └── users.js │ │ │ └── app.js │ │ └── cognito │ │ │ ├── auto-confirm-user.js │ │ │ ├── post-authentication.js │ │ │ └── auto-confirm-user.test.js │ ├── .babelrc │ ├── controllers │ │ └── user.js │ ├── dynamodb-init.js │ ├── models │ │ └── User.js │ ├── __mocks__ │ │ └── node-fetch.js │ └── package.json └── ui │ ├── .env │ ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── index.html │ ├── src │ ├── setupTests.js │ ├── App.test.js │ ├── App.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ ├── AuthenticatedApp.js │ └── serviceWorker.js │ ├── .gitignore │ ├── package.json │ └── README.md ├── .eslintignore ├── example.env.development ├── docs └── diagrams │ ├── ci-cd │ ├── diagram.png │ ├── serverless-fullstack-github-actions-cd.png │ └── diagram.drawio │ ├── cloud-architecture │ ├── api.png │ ├── data.png │ ├── rest-api.png │ ├── users-auth.png │ ├── business-logic.png │ ├── static-website-hosting.png │ ├── software-architecture-diagram.png │ └── diagram.drawio │ └── diagram.drawio ├── wizeline-amplify-serverless-banner.png ├── .gitignore ├── split-stack-splitter.js ├── scripts └── setup │ ├── utils.js │ ├── installDependencies.js │ ├── appName.js │ ├── setup.js │ ├── constants.js │ ├── createSetupFile.js │ ├── configureAWS.js │ ├── configuration.js │ └── replaceFlags.js ├── release.config.js ├── functions.webpack.config.js ├── .github └── workflows │ ├── delete-stack-on-pr-close.yaml │ ├── on-release-published.yaml │ ├── on-push-to-master.yaml │ └── on-pr.yaml ├── .eslintrc ├── LICENSE.txt ├── package.json ├── CHANGELOG.md ├── README.md └── serverless.yaml /packages/api/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/ui/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | packages/*/node_modules 2 | packages/*/build 3 | packages/*/dist -------------------------------------------------------------------------------- /example.env.development: -------------------------------------------------------------------------------- 1 | SERVERLESS_SERVICE_SUFFIX= 2 | ALARMS_NOTIFICATION_EMAIL=alert@example.com -------------------------------------------------------------------------------- /packages/ui/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /docs/diagrams/ci-cd/diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizeline/serverless-fullstack/HEAD/docs/diagrams/ci-cd/diagram.png -------------------------------------------------------------------------------- /packages/ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizeline/serverless-fullstack/HEAD/packages/ui/public/favicon.ico -------------------------------------------------------------------------------- /packages/ui/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizeline/serverless-fullstack/HEAD/packages/ui/public/logo192.png -------------------------------------------------------------------------------- /packages/ui/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizeline/serverless-fullstack/HEAD/packages/ui/public/logo512.png -------------------------------------------------------------------------------- /wizeline-amplify-serverless-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizeline/serverless-fullstack/HEAD/wizeline-amplify-serverless-banner.png -------------------------------------------------------------------------------- /docs/diagrams/cloud-architecture/api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizeline/serverless-fullstack/HEAD/docs/diagrams/cloud-architecture/api.png -------------------------------------------------------------------------------- /docs/diagrams/cloud-architecture/data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizeline/serverless-fullstack/HEAD/docs/diagrams/cloud-architecture/data.png -------------------------------------------------------------------------------- /docs/diagrams/cloud-architecture/rest-api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizeline/serverless-fullstack/HEAD/docs/diagrams/cloud-architecture/rest-api.png -------------------------------------------------------------------------------- /packages/api/functions/express/wrap-async.js: -------------------------------------------------------------------------------- 1 | const wrapAsync = fn => (req, res, next) => fn(req, res, next).catch(next) 2 | 3 | export default wrapAsync 4 | -------------------------------------------------------------------------------- /docs/diagrams/cloud-architecture/users-auth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizeline/serverless-fullstack/HEAD/docs/diagrams/cloud-architecture/users-auth.png -------------------------------------------------------------------------------- /docs/diagrams/cloud-architecture/business-logic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizeline/serverless-fullstack/HEAD/docs/diagrams/cloud-architecture/business-logic.png -------------------------------------------------------------------------------- /docs/diagrams/cloud-architecture/static-website-hosting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizeline/serverless-fullstack/HEAD/docs/diagrams/cloud-architecture/static-website-hosting.png -------------------------------------------------------------------------------- /docs/diagrams/ci-cd/serverless-fullstack-github-actions-cd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizeline/serverless-fullstack/HEAD/docs/diagrams/ci-cd/serverless-fullstack-github-actions-cd.png -------------------------------------------------------------------------------- /docs/diagrams/cloud-architecture/software-architecture-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizeline/serverless-fullstack/HEAD/docs/diagrams/cloud-architecture/software-architecture-diagram.png -------------------------------------------------------------------------------- /packages/api/functions/cognito/auto-confirm-user.js: -------------------------------------------------------------------------------- 1 | exports.handler = async (event) => { 2 | event.response = { 3 | autoConfirmUser: true, 4 | autoVerifyEmail: true, 5 | } 6 | return event 7 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | stats.json 5 | .serverless 6 | 7 | .env.development 8 | .env.local 9 | .env.development.local 10 | .env.test.local 11 | .env.production.local 12 | 13 | .cache 14 | coverage 15 | .webpack 16 | stack-outputs.json 17 | 18 | wizeline-academy/ -------------------------------------------------------------------------------- /packages/ui/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /packages/api/functions/express/lambda.js: -------------------------------------------------------------------------------- 1 | import 'source-map-support/register' 2 | import serverless from 'serverless-http' 3 | import app from './app' 4 | 5 | export const handler = serverless(app, { 6 | request(request, event) { 7 | request.requestContext = event.requestContext 8 | }, 9 | }) 10 | -------------------------------------------------------------------------------- /packages/ui/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const instructionElement = getByText(/Loading.../i); 8 | expect(instructionElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/api/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | // "modules": false 7 | } 8 | ] 9 | ], 10 | "env": { 11 | "test": { 12 | "plugins": [ 13 | "transform-es2015-modules-commonjs", 14 | "@babel/plugin-transform-async-to-generator" 15 | ] 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /packages/ui/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Container, 4 | } from '@material-ui/core'; 5 | import AuthenticatedApp from './AuthenticatedApp' 6 | 7 | function App() { 8 | return ( 9 | 10 |
11 | 12 |
13 |
14 | ); 15 | } 16 | 17 | export default App; 18 | -------------------------------------------------------------------------------- /packages/api/controllers/user.js: -------------------------------------------------------------------------------- 1 | import shortId from 'shortid' 2 | import User from '../models/User' 3 | 4 | export function createUser({ 5 | id = shortId.generate(), 6 | name, 7 | }) { 8 | return User.put({ 9 | id, 10 | name, 11 | }) 12 | } 13 | 14 | export function getUser({ id }) { 15 | return User.get({ id }) 16 | } 17 | 18 | export function scanUsers() { 19 | return User.scan() 20 | } 21 | -------------------------------------------------------------------------------- /split-stack-splitter.js: -------------------------------------------------------------------------------- 1 | module.exports = (resource, logicalId) => { 2 | if (resource.Type.startsWith('AWS::DynamoDB::')) return { destination: 'Persistence' } 3 | if (resource.Type.startsWith('AWS::Amplify::')) return { destination: 'UI' } 4 | // if (resource.Type.startsWith('AWS::Cognito::')) return { destination: 'Cognito' } 5 | // if (resource.Type.startsWith('AWS::ApiGateway::')) return { destination: 'API' } 6 | } -------------------------------------------------------------------------------- /packages/ui/.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 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /packages/api/dynamodb-init.js: -------------------------------------------------------------------------------- 1 | import DynamoDB from 'aws-sdk/clients/dynamodb' 2 | 3 | const dynamoDbConfig = { 4 | // region: 'us-east-1', 5 | convertEmptyValues: true, 6 | } 7 | export const dynamoDbDocumentClient = new DynamoDB.DocumentClient(dynamoDbConfig) 8 | // dynamo.documentClient(dynamoDb) 9 | 10 | // dynamo.AWS.config.loadFromPath('credentials.json') 11 | // dynamo.AWS.config.update({ 12 | // region: 'us-east-1', 13 | // }) 14 | -------------------------------------------------------------------------------- /scripts/setup/utils.js: -------------------------------------------------------------------------------- 1 | const getTrimmedString = (string) => string.split(' ').join('') 2 | const getLowerCaseString = (string) => string.toLowerCase() 3 | const getLowerCasedTrimmedString = (string) => getTrimmedString(getLowerCaseString(string)) 4 | 5 | const isValidProfile = (profile) => profile?.accessKey && profile?.secretAccessKey 6 | 7 | module.exports = { 8 | getTrimmedString, 9 | getLowerCaseString, 10 | getLowerCasedTrimmedString, 11 | isValidProfile, 12 | } 13 | -------------------------------------------------------------------------------- /packages/ui/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | 15 | #root { 16 | height: 100vh; 17 | } -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | const packageJson = require('./package.json') 2 | 3 | module.exports = { 4 | plugins: [ 5 | '@semantic-release/commit-analyzer', 6 | '@semantic-release/release-notes-generator', 7 | '@semantic-release/changelog', 8 | '@semantic-release/npm', 9 | ['@semantic-release/github', { 10 | assets: [ 11 | 'CHANGELOG.md', 12 | 'release.zip', 13 | ], 14 | }], 15 | '@semantic-release/git', 16 | ], 17 | preset: 'angular', 18 | } 19 | -------------------------------------------------------------------------------- /scripts/setup/installDependencies.js: -------------------------------------------------------------------------------- 1 | const npm = require('npm') 2 | const { UIPath, APIPath } = require('./constants') 3 | 4 | const installUIDependencies = () => { 5 | npm.load(() => npm.commands.install(UIPath, [])) 6 | } 7 | 8 | const installAPIDependencies = () => { 9 | npm.load(() => npm.commands.install(APIPath, [])) 10 | } 11 | 12 | const installDependencies = async () => { 13 | installUIDependencies() 14 | installAPIDependencies() 15 | } 16 | 17 | module.exports = installDependencies 18 | -------------------------------------------------------------------------------- /packages/api/models/User.js: -------------------------------------------------------------------------------- 1 | import { Table, Entity } from 'dynamodb-toolbox' 2 | import { dynamoDbDocumentClient } from '../dynamodb-init' 3 | 4 | const UserTable = new Table({ 5 | name: process.env.USER_TABLE, 6 | partitionKey: 'id', 7 | DocumentClient: dynamoDbDocumentClient, 8 | }) 9 | 10 | const User = new Entity({ 11 | name: 'User', 12 | attributes: { 13 | id: { partitionKey: true }, 14 | name: { type: 'string' }, 15 | }, 16 | table: UserTable, 17 | }) 18 | 19 | export default User 20 | -------------------------------------------------------------------------------- /scripts/setup/appName.js: -------------------------------------------------------------------------------- 1 | const configuration = require('./configuration') 2 | const { getLowerCasedTrimmedString } = require('./utils.js') 3 | 4 | const getApplicationName = configuration.applicationName 5 | 6 | const getShortApplicationName = configuration.shortApplicationName 7 | ? getLowerCasedTrimmedString(configuration.shortApplicationName) 8 | : getLowerCasedTrimmedString(configuration.applicationName) 9 | 10 | module.exports.appName = getApplicationName 11 | 12 | module.exports.shortName = getShortApplicationName 13 | -------------------------------------------------------------------------------- /packages/api/__mocks__/node-fetch.js: -------------------------------------------------------------------------------- 1 | const { Response, Headers } = jest.requireActual('node-fetch') 2 | 3 | const fetch = jest.fn(async () => { 4 | const meta = { 5 | 'Content-Type': 'application/json', 6 | Accept: '*/*', 7 | } 8 | const headers = new Headers(meta) 9 | const responseInit = { 10 | status: 200, 11 | statusText: '{ message: \'200\' }', 12 | headers, 13 | } 14 | const response = new Response(JSON.stringify({}), responseInit) 15 | return response 16 | }) 17 | 18 | module.exports = fetch 19 | -------------------------------------------------------------------------------- /scripts/setup/setup.js: -------------------------------------------------------------------------------- 1 | const replaceFlags = require('./replaceFlags') 2 | const configureAWS = require('./configureAWS') 3 | const installDependencies = require('./installDependencies') 4 | 5 | const { validateConfigurationFile } = require('./configuration') 6 | 7 | const setup = () => { 8 | replaceFlags() 9 | configureAWS() 10 | installDependencies() 11 | } 12 | 13 | const main = () => { 14 | try { 15 | validateConfigurationFile(setup) 16 | } catch (error) { 17 | console.error(error) 18 | } 19 | } 20 | 21 | main() 22 | -------------------------------------------------------------------------------- /functions.webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const serverlessWebpack = require('serverless-webpack') 3 | 4 | module.exports = { 5 | entry: serverlessWebpack.lib.entries, 6 | target: 'node', 7 | mode: serverlessWebpack.lib.webpack.isLocal ? 'development' : 'production', 8 | devtool: serverlessWebpack.lib.webpack.isLocal ? 'inline-cheap-module-source-map' : 'cheap-module-source-map', 9 | output: { 10 | libraryTarget: 'commonjs', 11 | path: path.join(__dirname, '.webpack'), 12 | filename: '[name].js', 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/api/functions/cognito/post-authentication.js: -------------------------------------------------------------------------------- 1 | import { getUser, createUser } from '../../controllers/user' 2 | 3 | exports.handler = async (event) => { 4 | const { 5 | sub: userId, 6 | email, 7 | } = event.request.userAttributes 8 | console.info(`Successfully authenticated ${email} (${userId})`) 9 | const user = await getUser({ id: userId }) 10 | 11 | if (!user) { 12 | console.info(`User doesn't exist. Creating user with id ${userId}...`) 13 | await createUser({ 14 | id: userId, 15 | }) 16 | } 17 | 18 | return event 19 | } 20 | -------------------------------------------------------------------------------- /packages/ui/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "MyApp", 3 | "name": "My App", 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 | } -------------------------------------------------------------------------------- /packages/ui/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import 'typeface-roboto' 4 | // import { AccessAlarm } from '@material-ui/icons'; 5 | import './index.css'; 6 | import App from './App'; 7 | import * as serviceWorker from './serviceWorker'; 8 | 9 | ReactDOM.render( 10 | 11 | 12 | , 13 | document.getElementById('root') 14 | ); 15 | 16 | // If you want your app to work offline and load faster, you can change 17 | // unregister() to register() below. Note this comes with some pitfalls. 18 | // Learn more about service workers: https://bit.ly/CRA-PWA 19 | serviceWorker.unregister(); 20 | -------------------------------------------------------------------------------- /packages/api/functions/express/routes/users.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import wrapAsync from '../wrap-async' 3 | import { createUser, scanUsers, getUser } from '../../../controllers/user' 4 | 5 | const userRouter = express.Router() 6 | userRouter.get('/', wrapAsync(async (req, res) => { 7 | const users = await scanUsers() 8 | res.json(users) 9 | })) 10 | 11 | userRouter.get('/:userId', wrapAsync(async (req, res) => { 12 | const { userId } = req.params 13 | const users = await getUser({ id: userId }) 14 | res.json(users) 15 | })) 16 | 17 | userRouter.post('/', wrapAsync(async (req, res) => { 18 | const { name } = req.body 19 | const user = await createUser({ name }) 20 | res.json(user) 21 | })) 22 | 23 | export default userRouter 24 | -------------------------------------------------------------------------------- /scripts/setup/constants.js: -------------------------------------------------------------------------------- 1 | const flags = [/myapp/g, /MyApp/g] 2 | const longNameFlags = [/My App/g] 3 | 4 | /* DIR PATHS */ 5 | const UIPath = 'packages/ui/' 6 | const APIPath = 'packages/api/' 7 | 8 | const setupConfigFile = './setup.config.json' 9 | // TODO: add support for Windows 10 | const AWSCredentials = (username) => `/Users/${username}/.aws/credentials` 11 | const packageJson = 'package.json' 12 | const servelessStack = 'serverless.yaml' 13 | const manifest = 'public/manifest.json' 14 | const index = 'public/index.html' 15 | 16 | const MAX_LENGTH_ALLOWED = 44 17 | 18 | module.exports = { 19 | setupConfigFile, 20 | flags, 21 | longNameFlags, 22 | UIPath, 23 | APIPath, 24 | packageJson, 25 | servelessStack, 26 | manifest, 27 | index, 28 | AWSCredentials, 29 | MAX_LENGTH_ALLOWED, 30 | } 31 | -------------------------------------------------------------------------------- /scripts/setup/createSetupFile.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const { setupConfigFile } = require('./constants') 3 | 4 | const setupConfigPlaceholder = `{ 5 | "applicationName": "", 6 | "shortApplicationName": "", 7 | "awsProfiles": { 8 | "dev": { 9 | "accessKey": "", 10 | "secretAccessKey": "" 11 | }, 12 | "staging": { 13 | "accessKey": "", 14 | "secretAccessKey": "" 15 | }, 16 | "prod": { 17 | "accessKey": "", 18 | "secretAccessKey": "" 19 | } 20 | } 21 | }` 22 | 23 | function main() { 24 | fs.writeFileSync(setupConfigFile, Buffer.from(setupConfigPlaceholder), (err) => { 25 | if (err) { 26 | console.error('Setup config file could not be created!') 27 | return 28 | } 29 | 30 | console.info('Setup config file created!') 31 | }) 32 | } 33 | 34 | main() 35 | -------------------------------------------------------------------------------- /.github/workflows/delete-stack-on-pr-close.yaml: -------------------------------------------------------------------------------- 1 | name: Delete preview stack on PR close 2 | 3 | on: 4 | pull_request: 5 | types: [closed] 6 | 7 | jobs: 8 | delete-stack: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | 14 | - name: Setup Node.js 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: 12.x 18 | 19 | - name: Install root 20 | run: npm ci 21 | 22 | - name: Setup AWS Credentials 23 | run: | 24 | mkdir ~/.aws 25 | echo "[myapp_dev] 26 | aws_access_key_id = ${{ secrets.AWS_ACCESS_KEY_ID_DEV }} 27 | aws_secret_access_key = ${{ secrets.AWS_SECRET_ACCESS_KEY_DEV }}" > ~/.aws/credentials 28 | 29 | - name: Delete PR preview stack 30 | run: SERVERLESS_SERVICE_SUFFIX=-${{ github.head_ref }} npm run remove-stack:dev 31 | -------------------------------------------------------------------------------- /packages/api/functions/cognito/auto-confirm-user.test.js: -------------------------------------------------------------------------------- 1 | import 'regenerator-runtime/runtime' 2 | import eventMocks from '@serverless/event-mocks' 3 | import awsLambdaMockContext from 'aws-lambda-mock-context' 4 | import { handler } from './auto-confirm-user' 5 | 6 | describe('auto-confirm-user: happy paths ', () => { 7 | test('Works', async () => { 8 | const context = awsLambdaMockContext() 9 | const event = eventMocks( 10 | 'aws:cognitoUserPool', 11 | ) 12 | await expect(handler(event, context)).resolves.toEqual({ 13 | callerContext: { awsSdkVersion: '1', clientId: 'abc1234' }, 14 | region: 'us-east-1', 15 | request: { userAttributes: { someAttr: 'someValue' } }, 16 | response: { autoConfirmUser: true, autoVerifyEmail: true }, 17 | triggerSource: 'string', 18 | userName: 'myNameIsAJ', 19 | userPoolId: 'abcd123', 20 | version: 2, 21 | }) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /packages/api/functions/express/app.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import bodyParser from 'body-parser' 3 | import cors from 'cors' 4 | import usersRouter from './routes/users' 5 | 6 | const IS_PRODUCTION = process.env.NODE_ENV === 'production' 7 | 8 | const app = express() 9 | const router = express.Router() 10 | // router.use(cookieParser()) 11 | router.use(cors()) 12 | router.use(bodyParser.json()) 13 | app.use('/', router) 14 | app.use('/users', usersRouter) 15 | 16 | app.use((req, res, next) => { 17 | const err = new Error('Not Found') 18 | err.statusCode = 404 19 | next(err) 20 | }) 21 | 22 | // eslint-disable-next-line no-unused-vars 23 | app.use((err, req, res, next) => { 24 | const { statusCode = 500 } = err 25 | const response = { 26 | message: err.message, 27 | } 28 | 29 | if (!IS_PRODUCTION) { 30 | response.trace = err.stack 31 | } 32 | 33 | res 34 | .status(statusCode) 35 | .json(response) 36 | }) 37 | 38 | export default app 39 | -------------------------------------------------------------------------------- /.github/workflows/on-release-published.yaml: -------------------------------------------------------------------------------- 1 | name: On release published 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | deploy_prod: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Download release 13 | uses: Legion2/download-release-action@v2.1.0 14 | with: 15 | repository: ${{ github.repository }} 16 | token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} 17 | tag: latest 18 | file: release.zip 19 | 20 | - name: Unzip release 21 | run: unzip release.zip 22 | 23 | - name: Setup AWS Credentials 24 | run: | 25 | mkdir ~/.aws 26 | echo "[myapp_prod] 27 | aws_access_key_id = ${{ secrets.AWS_ACCESS_KEY_ID_PROD }} 28 | aws_secret_access_key = ${{ secrets.AWS_SECRET_ACCESS_KEY_PROD }}" > ~/.aws/credentials 29 | 30 | - name: Setup Node.js 31 | uses: actions/setup-node@v1 32 | with: 33 | node-version: 12.x 34 | 35 | - name: Install 36 | run: npm ci 37 | 38 | - name: Deploy 39 | run: npm run deploy-prepack:prod -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "airbnb", 4 | ], 5 | "env": { 6 | "node": true, 7 | "jest": true 8 | }, 9 | "rules": { 10 | "default-case": "off", 11 | "semi": [ 12 | "error", 13 | "never" 14 | ], 15 | "comma-dangle": [ 16 | "error", 17 | "always-multiline" 18 | ], 19 | "max-len": [ 20 | "error", 21 | { 22 | "code": 200 23 | } 24 | ], 25 | "no-use-before-define": "off", 26 | "import/prefer-default-export": "off", 27 | "no-param-reassign": "off", 28 | "radix": "off", 29 | "no-console": [ 30 | "error", 31 | { 32 | "allow": [ 33 | "log", 34 | "info", 35 | "warn", 36 | "error" 37 | ] 38 | } 39 | ], 40 | "no-plusplus": [ 41 | "error", 42 | { 43 | "allowForLoopAfterthoughts": true 44 | } 45 | ], 46 | "quotes": [ 47 | "error", 48 | "single" 49 | ], 50 | "import/no-extraneous-dependencies": "off", 51 | "global-require": "off", 52 | "import/no-dynamic-require": "off", 53 | } 54 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Wizeline, Inc. https://www.wizeline.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /scripts/setup/configureAWS.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const { username } = require('os').userInfo() 3 | const { AWSCredentials, setupConfigFile } = require('./constants') 4 | const configuration = require('./configuration') 5 | const { shortName } = require('./appName') 6 | const { isValidProfile } = require('./utils') 7 | 8 | const stages = ['dev', 'staging', 'prod'] 9 | const defaultStage = 'dev' 10 | 11 | const writeInFile = (content) => { 12 | fs.appendFileSync(AWSCredentials(username), content, (err) => { 13 | if (err) throw err 14 | }) 15 | } 16 | 17 | const getStageContent = (stage, credentials) => ` 18 | [${shortName}_${stage}] 19 | aws_access_key_id=${credentials.accessKey} 20 | aws_secret_access_key=${credentials.secretAccessKey} 21 | ` 22 | 23 | const configureAWS = () => { 24 | const { awsProfiles } = configuration 25 | 26 | stages.forEach((stage) => { 27 | const content = isValidProfile(awsProfiles[stage]) 28 | ? getStageContent(stage, awsProfiles[stage]) 29 | : getStageContent(stage, awsProfiles[defaultStage]) 30 | writeInFile(content) 31 | }) 32 | 33 | fs.unlinkSync(setupConfigFile) 34 | } 35 | 36 | module.exports = configureAWS 37 | module.exports.isValidProfile = isValidProfile 38 | -------------------------------------------------------------------------------- /scripts/setup/configuration.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const { setupConfigFile, MAX_LENGTH_ALLOWED } = require('./constants') 3 | const { isValidProfile, getLowerCasedTrimmedString } = require('./utils') 4 | 5 | const validateConfigurationFile = (callback) => { 6 | if (!fs.existsSync(setupConfigFile)) { 7 | throw new Error('Could not find the setup.config.json file. Check it exists before continue') 8 | } 9 | 10 | const file = JSON.parse(fs.readFileSync(setupConfigFile)) 11 | 12 | if (!file.applicationName || file.applicationName === '') { 13 | throw new Error('Application name is a required parameter, please provide it') 14 | } 15 | 16 | if (Object.keys(file.awsProfiles).length === 0 || !isValidProfile(file.awsProfiles.dev)) { 17 | throw new Error('AWS Dev profile is required, please provide it') 18 | } 19 | 20 | const shortName = file.shortApplicationName 21 | ? getLowerCasedTrimmedString(file.shortApplicationName) 22 | : getLowerCasedTrimmedString(file.applicationName) 23 | 24 | if (shortName.length > MAX_LENGTH_ALLOWED) { 25 | throw new Error(`Short name is too large, it should be ${MAX_LENGTH_ALLOWED} characters length`) 26 | } 27 | 28 | callback() 29 | } 30 | 31 | const getConfiguration = () => { 32 | const file = fs.readFileSync(setupConfigFile) 33 | return JSON.parse(file) 34 | } 35 | 36 | module.exports = getConfiguration() 37 | module.exports.validateConfigurationFile = validateConfigurationFile 38 | -------------------------------------------------------------------------------- /scripts/setup/replaceFlags.js: -------------------------------------------------------------------------------- 1 | const replace = require('replace-in-file') 2 | const constants = require('./constants') 3 | const { appName, shortName } = require('./appName') 4 | 5 | const replaceShortFlags = () => replace.sync({ 6 | files: [ 7 | constants.packageJson, 8 | `${constants.UIPath}${constants.packageJson}`, 9 | `${constants.APIPath}${constants.packageJson}`, 10 | `${constants.UIPath}${constants.manifest}`, 11 | constants.servelessStack, 12 | ], 13 | from: constants.flags, 14 | to: shortName, 15 | }) 16 | 17 | const replaceLongFlags = () => replace.sync({ 18 | files: [ 19 | `${constants.UIPath}${constants.manifest}`, 20 | `${constants.UIPath}${constants.index}`, 21 | ], 22 | from: constants.longNameFlags, 23 | to: appName, 24 | }) 25 | 26 | const replaceFlag = () => { 27 | const changeShortName = replaceShortFlags() 28 | const changeAppName = replaceLongFlags() 29 | 30 | const filesChanged = [...changeShortName, ...changeAppName.filter((change) => { 31 | const shortNameFile = changeShortName.find((ch) => ch.file === change.file) 32 | /* conditional to get unique files */ 33 | if (shortNameFile) { 34 | return change.hasChanged !== shortNameFile.hasChanged && change.hasChanged 35 | } 36 | 37 | return change 38 | })] 39 | 40 | const filesCount = filesChanged.filter((r) => r.hasChanged).length 41 | console.info(`${filesCount} files changed!`) 42 | } 43 | 44 | module.exports = replaceFlag 45 | -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "myapp-ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^4.9.7", 7 | "@material-ui/icons": "^4.9.1", 8 | "@testing-library/jest-dom": "^4.2.4", 9 | "@testing-library/react": "^9.5.0", 10 | "@testing-library/user-event": "^7.2.1", 11 | "aws-amplify": "^3.0.8", 12 | "aws-amplify-react": "^4.1.7", 13 | "axios": "^0.19.2", 14 | "clsx": "^1.1.0", 15 | "react": "^16.13.1", 16 | "react-dom": "^16.13.1", 17 | "react-scripts": "3.4.1", 18 | "serverless": "^1.72.0", 19 | "typeface-roboto": "0.0.75" 20 | }, 21 | "scripts": { 22 | "start-cloud-api": "react-scripts start", 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test", 26 | "eject": "react-scripts eject", 27 | "package": "serverless package && zip -r ui.zip build .serverless serverless.yaml package.json package-lock.json", 28 | "deploy": "sls deploy", 29 | "deploy-package": "serverless deploy --package .serverless" 30 | }, 31 | "eslintConfig": { 32 | "extends": "react-app" 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | }, 46 | "devDependencies": { 47 | "@wizeline/serverless-amplify-plugin": "^1.2.1", 48 | "serverless-dotenv-plugin": "^2.3.2" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 25 | My App 26 | 27 | 28 | 29 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /.github/workflows/on-push-to-master.yaml: -------------------------------------------------------------------------------- 1 | name: On push to master 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | test-deploy-to-staging-and-package: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | 14 | - name: Setup Node.js 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: 12.x 18 | 19 | - name: Install package 20 | run: | 21 | (npm ci && npm audit --audit-level high && npm test) & \ 22 | (cd packages/ui && npm ci && npm audit --audit-level high && npm test) & \ 23 | (cd packages/api && npm ci && npm audit --audit-level high && npm test) 24 | 25 | - name: Deploy to staging 26 | run: | 27 | mkdir ~/.aws 28 | echo "[myapp_staging] 29 | aws_access_key_id = ${{ secrets.AWS_ACCESS_KEY_ID_STAGING }} 30 | aws_secret_access_key = ${{ secrets.AWS_SECRET_ACCESS_KEY_STAGING }}" > ~/.aws/credentials 31 | NODE_ENV=production npm run deploy:staging 32 | 33 | # TODO: Additional end-to-end tests 34 | 35 | - name: Package 36 | # TODO: Ideally, we'd package up the prod version, deploy that to stage, 37 | # then deploy the same packge to prod 38 | # NOTE: We need prod profile so that the package command can run describeStack to get 39 | # the Amplify App ID and create an Amplify deployment 40 | run: | 41 | echo " 42 | 43 | [myapp_prod] 44 | aws_access_key_id = ${{ secrets.AWS_ACCESS_KEY_ID_PROD }} 45 | aws_secret_access_key = ${{ secrets.AWS_SECRET_ACCESS_KEY_PROD }}" > ~/.aws/credentials 46 | npm run package:prod 47 | npm run zip-release 48 | 49 | - name: Release 50 | run: npx semantic-release 51 | env: 52 | # NOTE: GitHub Actions won't trigger on release when using the default GITHUB_TOKEN 53 | # Make sure the Personal Access Token is enabled for SSO if relevant to your organization/repo 54 | GITHUB_TOKEN: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} 55 | -------------------------------------------------------------------------------- /packages/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "myapp-api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "handler.js", 6 | "scripts": { 7 | "test": "jest", 8 | "start:dev": "IS_OFFLINE=true serverless offline start", 9 | "package": "serverless package && zip -r api.zip .serverless serverless.yaml package.json package-lock.json", 10 | "deploy": "sls deploy", 11 | "deploy-package": "serverless deploy --package .serverless" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "devDependencies": { 17 | "@babel/cli": "^7.8.4", 18 | "@babel/core": "^7.9.0", 19 | "@babel/node": "^7.8.7", 20 | "@babel/plugin-transform-async-to-generator": "^7.8.3", 21 | "@babel/preset-env": "^7.9.5", 22 | "@serverless/event-mocks": "^1.1.1", 23 | "aws-lambda-mock-context": "^3.2.1", 24 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", 25 | "jest": "^25.3.0", 26 | "serverless": "^1.72.0", 27 | "serverless-apigateway-service-proxy": "^1.7.0", 28 | "serverless-domain-manager": "^3.3.1", 29 | "serverless-iam-roles-per-function": "^2.0.2", 30 | "serverless-offline": "^6.1.4", 31 | "serverless-plugin-tracing": "^2.0.0", 32 | "serverless-prune-plugin": "^1.4.2", 33 | "serverless-webpack": "^5.3.1", 34 | "webpack": "^4.42.1", 35 | "webpack-cli": "^3.3.11" 36 | }, 37 | "dependencies": { 38 | "@dazn/lambda-powertools-middleware-correlation-ids": "^1.23.0", 39 | "@dazn/lambda-powertools-middleware-log-timeout": "^1.23.0", 40 | "@dazn/lambda-powertools-middleware-sample-logging": "^1.23.0", 41 | "@middy/core": "^1.0.0-beta.10", 42 | "@middy/sqs-partial-batch-failure": "^1.0.0-beta.10", 43 | "aws-sdk": "^2.656.0", 44 | "body-parser": "^1.19.0", 45 | "cors": "^2.8.5", 46 | "discord.js": "^12.1.1", 47 | "dotenv": "^8.2.0", 48 | "dynamodb-toolbox": "github:jeremydaly/dynamodb-toolbox#v0.2", 49 | "express": "^4.17.1", 50 | "lodash": "^4.17.19", 51 | "node-fetch": "^2.6.0", 52 | "serverless-http": "^2.3.2", 53 | "shortid": "^2.2.15" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /docs/diagrams/ci-cd/diagram.drawio: -------------------------------------------------------------------------------- 1 | 7Vtbc5s4FP41fkwHg43txziXbmfTmUy83d087chwDGoFokLEdn79SiBuEWBy9WXSmTToCAnpfN+5CTKwLoLNV4Yi/zt1gQxMw90MrMuBaZqGbYtfUrLNJEPTHmcSj2FXyUrBAj+CEhpKmmAX4tqNnFLCcVQXOjQMweE1GWKMruu3rSipPzVCHmiChYOILv0Hu9xX0qFhlB1/APZ89ejpWHUskfPLYzQJ1fMGprVK/2XdAcrnUvfHPnLpuiKyrgbWBaOUZ1fB5gKIVG6utmzcdUtvsW4GIe8zYDIB25lNVsOZ7aLp1DpTMzwgkihdXMIDEBoFckbTuMErcLaOUFS2fr7NdRavcUBQKFpznwdECIfi0vExcW/QliZyPTEX+slbc9FiXIEv1mnNU8WBXJpsFbqRDYKWQOaFdi8ooUx0hTR9YMwZ/VVAJZ+7oiG/RgEmkoJ/A3NRiJRYPXEqmohgLxTXjtgciPnmuv5yhQDjsKmIlD6/Ag2As624RfVOFLSK+yPVXJc8mtlK5lcoZOUUQoq7XjFziZ+4UBD2hNPshrMXhBpMupJaabRba9vcaWhqGjaqyepQk5r9TrgEFHqSox3TWw2zT57OjojgRYg4zCXrYg2LYiMvg2faBg+wWPrPrCHX4yEcxtIAuVgvE7/pOpQOkNDELe9URpqamYZuaV0S1bWPOSwi5MjetfDjT0A/CONr5XNvazTr1pjHoSrNjAYi2O9hjJaG9m2SjriD3wnEh2GPea9dV9yb2Gdfzc3C3/9B/OPhzwU12NnjuRf9uDsbaeoBVwRt1aSM+9SjISJXpfRJQCnvuaHCqjJt/gTOt0qbKOG0rmvYYP6vHP5lrFr3lZ7LjZo5bWzzRii2Wxkkm/fVvnJY2srHZfuTm2oN5m2UimnCHGjDPs+gEPOglSDjZjowIIjjh/qS3tQqhq0pR+YEWW4cIttzIRU8YFjLrhWjQSonBJAn7pLTYCT+rxtWfJK+MHcnvW3aOiBfOBxrqJ/LgAaRDwEwJAdHGdJlQDOwRNhhIAKym9YEkg6JHCdNN8jFGeQGCmULEULXBW0yTqUDAxQmonNb9BVDeJMz/qRMxeXvhTMjjTLfUSz3fkBx0zIPLW6OP+NmK5U64+ZMD5uNCrb2FTln3YFTuFFPxkvHF8UQFG6vxWZOwb3lDuIYImI7lWqI/uXDzqiYMq0MiDwd8rS0yGKnCwTSW48TfQ3ZPoQ4juCm50MLjjwceocU3UbTPUY3ffn66dZF4evEvlO+R4RuS9uIc53myWFT2piOTNPK47SSTszHz8X8kKoGWwP8Tjg0FPc9jf4QI7FHh5YCTjS1fQuChKMlyepppULBfI5XyKlYQVlrqcoIeV7aXGOxFyEVPwsQlRTHjrgUTI0xPVrDeW54sY/GcHQG3DLqJg5vQmuPtjM1Ds128uOp062fIHTP5TtTaQ8ExbGw5FR4jUm+nMELSixFuWqJ1ajghjJL51GLpb15jdVMAT3PuKOELI/4JctzPd3kOZ7OHH9gIt3FqaY8oSHIPU0So4prPOU88V1JsPdaemhpuJyu41YEKb32Sw/GGrx2izfucTKWOc69uW39vPge9FdBUj830uzqQGo2I60FO4icq44Au27GGYjxY5pLZ2qPKA55upnxfDC+7DI39f2SGjwo0p0qRB3kbrXFM+OLadsKy97aVtPdyvVX5jJrdj2qj6erVQxcw+qV30UMGwq9IuLaRH48tGTiyuMpUogFsS7nDItihUlrvtZdtE+DZRLvds+H+E1Sa3rf/yBn99HX9EOd9T59dX59X/Pbu3x1zVOXjvsNX1LkSnn1y/3ZPt2w+Qnty6DtVRjtNcJOTx3adyqNexm22TPJasnKPyjHMj4p8FYU6Dq63cmCvbr44V6/MTgtFrR9I9wdBlqyvdfCL5rl3yJkiX35Fx/W1f8= -------------------------------------------------------------------------------- /.github/workflows/on-pr.yaml: -------------------------------------------------------------------------------- 1 | name: On Pull Request 2 | 3 | on: 4 | pull_request: 5 | types: [synchronize, opened, reopened] 6 | 7 | jobs: 8 | auto-approve-dependabot: 9 | if: github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]' 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: hmarr/auto-approve-action@v2.0.0 13 | with: 14 | github-token: "${{ secrets.GITHUB_TOKEN }}" 15 | 16 | test-and-deploy-to-sandbox: 17 | if: github.actor != 'dependabot[bot]' && github.actor != 'dependabot-preview[bot]' 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v2 22 | 23 | - name: Setup Node.js 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: 12.x 27 | 28 | - name: Install package 29 | run: | 30 | (npm ci && npm audit --audit-level high && npm test) & \ 31 | (cd packages/ui && npm ci && npm audit --audit-level high && npm test) & \ 32 | (cd packages/api && npm ci && npm audit --audit-level high && npm test) 33 | 34 | - name: Deploy to sandboxed stack 35 | id: deploy_to_sandboxed_stack 36 | run: | 37 | mkdir ~/.aws 38 | echo "[myapp_dev] 39 | aws_access_key_id = ${{ secrets.AWS_ACCESS_KEY_ID_DEV }} 40 | aws_secret_access_key = ${{ secrets.AWS_SECRET_ACCESS_KEY_DEV }}" > ~/.aws/credentials 41 | # TODO: How to also get the PR origin repo if it's coming from an external contributor 42 | # See: env.GITHUB_REF, env.GITHUB_HEAD_REF, GITHUB_BASE_REF https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables 43 | NODE_ENV=production ALARMS_NOTIFICATION_EMAIL=alert@example.com SERVERLESS_SERVICE_SUFFIX=-${{ github.head_ref }} npm run deploy:dev 44 | WEBSITE_URL=https://$(grep -o 'DefaultDomain": "[^"]*' ./stack-outputs.json | grep -o '[^"]*$') 45 | echo "::set-output name=websiteUrl::$WEBSITE_URL" 46 | 47 | - name: Comment on PR with stack outputs 48 | uses: unsplash/comment-on-pr@v1.2.0 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | with: 52 | msg: "Website: [${{steps.deploy_to_sandboxed_stack.outputs.websiteUrl}}](${{steps.deploy_to_sandboxed_stack.outputs.websiteUrl}})" 53 | check_for_duplicate_msg: true 54 | 55 | # TODO: Additional end-to-end tests 56 | -------------------------------------------------------------------------------- /packages/ui/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "myapp", 3 | "version": "1.3.0", 4 | "private": true, 5 | "description": "", 6 | "main": "handler.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "start:ui": "cd packages/ui && npm start", 10 | "start:ui:offline": "REACT_APP_ApiEndpoint=http://localhost:4911 npm run start:ui", 11 | "start:api": "sls offline cloudside", 12 | "package:dev": "sls package --stage development", 13 | "deploy-prepack:dev": "sls deploy --package .serverless --stage development", 14 | "deploy:dev": "sls deploy --stage development", 15 | "package:staging": "NODE_ENV=production sls package --stage staging", 16 | "deploy-prepack:staging": "NODE_ENV=production sls deploy --package .serverless --stage staging", 17 | "deploy:staging": "NODE_ENV=production sls deploy --stage staging", 18 | "package:prod": "NODE_ENV=production sls package --stage production", 19 | "deploy-prepack:prod": "NODE_ENV=production sls deploy --package .serverless --stage production", 20 | "deploy:prod": "NODE_ENV=production sls deploy --stage production", 21 | "remove-stack:dev": "sls remove --stage development", 22 | "zip-release": "zip -r release.zip .serverless serverless.yaml package.json package-lock.json", 23 | "setup": "node scripts/setup/setup.js", 24 | "setup-file": "node scripts/setup/createSetupFile.js" 25 | }, 26 | "keywords": [], 27 | "author": "", 28 | "license": "ISC", 29 | "devDependencies": { 30 | "@semantic-release/changelog": "^5.0.1", 31 | "@semantic-release/commit-analyzer": "^8.0.1", 32 | "@semantic-release/git": "^9.0.0", 33 | "@semantic-release/github": "^7.0.5", 34 | "@semantic-release/npm": "^7.0.5", 35 | "@semantic-release/release-notes-generator": "^9.0.1", 36 | "@wizeline/serverless-amplify-plugin": "^1.6.2", 37 | "eslint": "^5.16.0 || ^6.1.0", 38 | "eslint-config-airbnb": "^18.0.1", 39 | "eslint-plugin-import": "^2.18.2", 40 | "eslint-plugin-jsx-a11y": "^6.2.3", 41 | "eslint-plugin-react": "^7.14.3", 42 | "eslint-plugin-react-hooks": "^1.7.0", 43 | "nodemon": "^2.0.3", 44 | "semantic-release": "^17.0.4", 45 | "serverless": "^1.70.0", 46 | "serverless-apigateway-service-proxy": "^1.7.0", 47 | "serverless-cloudside-plugin": "^1.0.3", 48 | "serverless-domain-manager": "^3.3.2", 49 | "serverless-dotenv-plugin": "^2.4.2", 50 | "serverless-iam-roles-per-function": "^2.0.2", 51 | "serverless-offline": "^6.1.5", 52 | "serverless-plugin-aws-alerts": "^1.4.0", 53 | "serverless-plugin-split-stacks": "^1.9.3", 54 | "serverless-plugin-tracing": "^2.0.0", 55 | "serverless-prune-plugin": "^1.4.2", 56 | "serverless-stack-output": "^0.2.3", 57 | "serverless-stack-termination-protection": "^1.0.4", 58 | "serverless-webpack": "^5.3.2", 59 | "webpack": "^4.43.0" 60 | }, 61 | "dependencies": { 62 | "replace-in-file": "^6.0.0" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/ui/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `npm run build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /packages/ui/src/AuthenticatedApp.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Amplify, { Auth } from 'aws-amplify' 3 | import { 4 | withAuthenticator, 5 | Loading, 6 | SignIn, 7 | ConfirmSignIn, 8 | VerifyContact, 9 | SignUp, 10 | ForgotPassword, 11 | RequireNewPassword, 12 | Greetings 13 | } from 'aws-amplify-react' 14 | import '@aws-amplify/ui/dist/style.css' 15 | import axios from 'axios' 16 | 17 | const { 18 | REACT_APP_ApiEndpoint, 19 | REACT_APP_CognitoIdentityPoolId, 20 | REACT_APP_CognitoUserPoolId, 21 | REACT_APP_CognitoUserPoolClientId, 22 | } = process.env 23 | 24 | // Set Authorization header on all requests if user is signed in 25 | axios.interceptors.request.use(async function (config) { 26 | try { 27 | const currentUserSession = await Auth.currentSession() 28 | const Authorization = currentUserSession.idToken.jwtToken 29 | config.headers.Authorization = Authorization 30 | } catch (e) { /* Auth.currentSession() throws if not signed in 🤷‍♂️ */ } 31 | 32 | return config 33 | }) 34 | 35 | axios.defaults.baseURL = REACT_APP_ApiEndpoint 36 | 37 | async function fetchAndSetUsers({ setUsers }) { 38 | const fetchUsersResponse = await axios.get('/users') 39 | const users = fetchUsersResponse.data 40 | setUsers(users) 41 | } 42 | 43 | function AuthenticatedApp() { 44 | const [users, setUsers] = React.useState(null) 45 | React.useEffect(() => { 46 | fetchAndSetUsers({ setUsers }) 47 | }, []) 48 | 49 | return
50 | 53 |
54 | } 55 | 56 | Amplify.configure({ 57 | Auth: { 58 | // region: process.env.region, 59 | identityPoolId: REACT_APP_CognitoIdentityPoolId, 60 | userPoolId: REACT_APP_CognitoUserPoolId, 61 | userPoolWebClientId: REACT_APP_CognitoUserPoolClientId, 62 | }, 63 | }) 64 | 65 | // We're auto-confirming via the Lambda Function 66 | // Hack to skip the ConfirmSignUp view 67 | function ConfirmSignUpRedirectToSignIn({ authState, onStateChange }) { 68 | React.useEffect(() => { 69 | if (authState === 'confirmSignUp') onStateChange('signIn', {}) 70 | }, [authState, onStateChange]) 71 | 72 | return null 73 | } 74 | 75 | const signUpConfig = { 76 | hideAllDefaults: true, 77 | hiddenDefaults: ['phone_number'], 78 | } 79 | 80 | const federated = { 81 | // google_client_id: 'abc123abc123abc123abc123', 82 | // facebook_app_id: 'abc123abc123abc123abc123', 83 | // amazon_client_id: 'abc123abc123abc123abc123', 84 | } 85 | 86 | export default withAuthenticator(AuthenticatedApp, { 87 | usernameAttributes: 'email', 88 | signUpConfig, 89 | includeGreetings: true, 90 | hideDefault: true, 91 | authenticatorComponents: [ 92 | , 93 | , 94 | , 95 | , 96 | , 97 | , 98 | , 99 | , 100 | 101 | ], 102 | }) -------------------------------------------------------------------------------- /docs/diagrams/diagram.drawio: -------------------------------------------------------------------------------- 1 | 7V1rc5u4Gv41ntn9EAYJxOVjbMfdntOzzTTpdPdTRjYy5gQjF3Bz+fUrgbBBkm812G6yzkzHCMzlfR4970US7VmD+fOHFC9m/6MBiXvQDJ571rAHoe957F/e8FI2uNAvG8I0CsomsG64i16JaDRF6zIKSNY4MKc0zqNFs3FCk4RM8kYbTlP61DxsSuPmVRc4JErD3QTHauu3KMhnohU4/nrHHyQKZ+LSHnTLHXNcHSyeJJvhgD7VmqybnjVIKc3Lb/PnAYm57Sq7lL8bbdi7urGUJPk+PxjQm5f/Etz/8ogeFu6D9eXPv+iVJ07zA8dL8cR3Oc6jCWv7RsZZlBP27Q+a5VESiufIXyrjTKM4HtCYpmwzoQlr7Wd5Sh9J1diDFrp2Bp7D9gQ4mxF+L4Bt/CApuwiOr+MoTFhbThesdUqT/E6c3hTbulOVF6nwsFiLaozqwdiFyHOtSRjnA6Fzkqcv7BCx10ICKMFU4Irtpxrulmib1SAH0BF8E1wLV+dew8G+CEQOQMdS0fmakTRjJ7tesnt603ggD+3Ew4IaPKBpdoUHUkxOAiYXYpOm+YyGNMHxzbq1n9JlEhSG5iZcH/OJchsX1v8/yfMXoX14mVPWNMvnsdjLTJi+/CV+X2z8zTcMiKrt4XN97/BFbGU5TvNrLoJ1PrC2UcSfWxwj8cMz+Z8CKtwGKjtnSPIthuNH8QO5tbZin5KYqc+PpvjqcBQ/vaURu5cVZxzQ7MO2J1Eho8t0QsSv6mIpnQiYlmP4pueZ4gOb53Vtw2fkW+21m5cpDaJchmGBX2qHLfgB2SGPY0kcLs+4ZvTKlD9PchsqojOII1Lc1xw/MmfMHML9/W0POjHDvD9O2beQf0vJ9yXJ+AMxH83/mXHncX37UdtrPuExCxUaTMdCfSbsaiTVyNI8CoKyU5EsesXjlTIJS7KTo34PDbVk3dqlFV1ahRTiKg2vrdOrK9MwbeQ3ALsStjyS1lcAGq5t1j7SZbzmGel0mpG8J8tdC+Rw7XMoYEPIJjHOMhagNLUMHKhl5DnKC001HBailtulrJqmK7bXsso3KlXdJMfuLjnmZ7klacQw4OQu2jZKailU2yQV7am9VRDRtfYiKX6yrJ/UXlc+EZJO1JK6ItjsRTYyt94XssyjjreaAXo36u1WetYMGblZWMTIWMbkNOcaLov3bykJo4wzE44yrrbsQsnv/HdJUJBjQhg3ND/ERSTKFP+RJNkFK30lXccrPQu9bCk4vbLaEXqm807jxI5vIOtU6g5V8qxc//ViEXPuRDRRQH5TeYecB1bdvJ53QEeXB8o61VreUUUVJ847Vi6yzDQqD7mnd6zlJ3/XM5l9kpV2fPwRzhWe17ke6so8x2tyFmx3TY6Dth3fjWuq+kxNXYrcwPzAPNJT8byTGZk8anxMWe0w77mP4WYJcZRk/HkGNEwinmxcrNup+m4rCQbDyW76nZYSDNszTpZDOGoO0ZpWAcMEsC5WBvDs/QTrahX6r2N64MET11iKs8tZQsuZw77iVhXX2hO3o7yg4+7Ujyj5QR91QeonPB8HmB0xWiYTbQxzOYrhtBaoMkoDKFVRW9EL4BpVmeiliokroTuBgJynDFuFQw15cdF+6gJ6hxULTla7bVEu0GXJhbNTLlKSL1OetLJv2YImGVGVY5rSOTtAIyBlkbNKkC5XTlqrcDI1qTS4XTU5mXJUXJY58ZbzWQ9K+awt/Et9XNO3DGGaZkbb1cjm6jHWQAxxjt82EgB5FwhFVXDqIhz/NX0lSQK1EMEaa2WII9ypua87hRflTl01+tb5xNKRBhddCK4Cg1bia9tqZuTtuMQrgAwx2Cud+ARjfF4HgrBh0Oygrn32quDeXbf1quBxXbcqQW3tunLg+4Vg3o1H39JIO2BURL/DlwTP6bB/yd3da7G7Q8tv9krUTgTssTOfLAhW67D9ZRYlJOOZ0Cca8tl/bzoOg/K8mhPHYV9vXm+/Xl9//piOrqLMC9HA/X4FgK3gIiWsEih0mccMtsFq6qvGkuxvxG+iH6Y44JlqbZ9vo+EI1vYNo5SUasBRTrkp+nXkC2Qss4908E+LTx1+swZ/oQW3NIvE6cc0z1livYkfik7UNKTgTuloilqvzCWcLUpzTKNnfh/9bIYXfOf8OeRzlA38lNkG05dC7z9O+P1wuSm/NY/Ci+ghFOa3+jF/iD6ePIaFQ9Q9fAv09FzXQH7t0+RqNfJc42olcXWiul5nPFVHiVfuZLqpvHoUV4XiSFQcmmgAXIWK4uCLY2HhwEh684OUfgxsYmZcGPNhZcpT8Q5AxzWcSyObGrtsjDmO1UN76LKdh+kh+9h9/93oYVDYPhjvR8psgSdREt4XSQJqiaWOxVhamwppS4y1TsZY/YwASxsLn2mO+C+VXwF33/zqskYaUJVhqHPuJhMWVeuyp0k1rwpvmVd1OUlUxep2kihfGkaALSVRupN2n08BtTKmILnB+ZcLwkzZSTSmPmtiGzkmN/lfJck1p6IQ5ECvo3WoCvtkd8IsHU2IseQrlYw5HUcxeeAbLem/3czdkKlOz6uGouuC30be1v9q/zkM/xO8fnYWd2g8fezT4RXsYjpLfVIKQE1Nd8BBk1I2q/oede6jp65ohXqne9gQC+673Mg7kTfQ88HdGgCILnyqNREtY3wWOJ2zwqkmIBeKr7jyGtyfmyPSJrjHzoNVJ66aoKH+VlWM2LE25Cd8/XbS1qfIb5iDemw6OkK2a/uHpaMDF1hg9G7S0Ykw/alKJI7VpB/0NcGHZmlAZysD1DlOVarBcu+kwUfn+5IWOcjKSFeT0krXvF+G498gYp2J3Sm7FVP6/vv691UGczPHEb+/jwFfVZS/VBfm64eKa5eHtdst9AHwhiLMIZXAktcy21fd4DyVQFKYeEtc3wqlLcOVMidTHQ6xkVE5vjqx7a6IDXXMlpjEWbxQPNnW6RnH56+uLAG2a0C1SgqgbyDVXsx/GKgjk6lViPtZlAas6ZbFDLx/sq4COCPXXda8TemPKCjeKXGRQ376itJ2yuzm/nmBshSgFOM3NEurmNvqQW3XAWRxeiLjiHWbzJjiCRlT+qhKsgN9+8bWBzbger0AqU1UqwV8wJBX9mre5OIbQAf7url12NXh3V8P9phlMZmB5/i1Kld2AKBnGtDbBSCEBlDxk1/E0Rp4e2SBvwp4i0WsiaFAHwzAQB9DdQa0ZYDm8I2n6amOYWkGcGBni23fDNIhpWHcHXqqzvKJC2pHBSuQG/jB7gIhzULT+SKOpvzmGU4Zrazyb7Z+0sk0AoR2lun7tvyuBGh6Gv5Zrkq+qq31amFVHjrt6G8j+uYbtzhnSCdFC8/m9psjf/5aor47C/XYXfY/y9p7Wy5J7ngtzI7ju1l776nB5zcy7hXDzwpjd8pMjXp7Ks4+w5im2b8ppiMqEvO6TAl3a/x1lA8hSUha1MJ1dZRGV2hDZlxpqqivKowDVkrUKPp16OHUcDT6fNcr3tKiwNnp2PMKtIsae2bOZ0IWeRnsitkVDxHN2uEEklfU+chwVVroRqFtvyNG+JqYJwlSGgUbOvm/rChZgYWVWmGGbe/FDN3bg1BXA5jqgJUmRonjaJFxIJ9mTGPvFrjwyE/MelIyo4SDiufnEeQIz6OY2+Aez+gc91ZTAfsC3qG9bvsijAB1IUZQ/EkME6TbgI4Gw42AraBYAeaoCYz2HbMdoQUVtNT0892ihaR1GxbydN3rpICpxVzNBND3CpjjS91LV8g7JVpqGKx5oel7RcuVxNByz9+91Gqc5uUv7xUwYPIiiDQB/txdTB2GVF8w8H4R428OlxFzzxxzqMmkZgrju0VM08eQdWbEgJrtqYv23y1kctSBoHZcoh3A2Ob6/3Apy3Xr/wjHuvkH -------------------------------------------------------------------------------- /docs/diagrams/cloud-architecture/diagram.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /packages/ui/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [1.3.0](https://github.com/wizeline/serverless-fullstack/compare/v1.2.2...v1.3.0) (2020-06-26) 2 | 3 | 4 | ### Features 5 | 6 | * add cloudformation stacktermination ([#25](https://github.com/wizeline/serverless-fullstack/issues/25)) ([1e47b1a](https://github.com/wizeline/serverless-fullstack/commit/1e47b1a63e68c883f3c24fb49cad4d478f8472f9)) 7 | 8 | ## [1.2.2](https://github.com/wizeline/serverless-fullstack/compare/v1.2.1...v1.2.2) (2020-06-10) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * **dependencies:** update dependencies ([08dd83c](https://github.com/wizeline/serverless-fullstack/commit/08dd83c9d92ed092fe2ce71615bcaf585f5a0694)) 14 | 15 | ## [1.2.1](https://github.com/wizeline/serverless-fullstack/compare/v1.2.0...v1.2.1) (2020-06-10) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * shorten Lambda Fn names to mitigate hitting length limit ([#19](https://github.com/wizeline/serverless-fullstack/issues/19)) ([fea1357](https://github.com/wizeline/serverless-fullstack/commit/fea1357e933c429c96bc6e1364da37cfac95811b)) 21 | 22 | # [1.2.0](https://github.com/wizeline/serverless-fullstack/compare/v1.1.4...v1.2.0) (2020-06-09) 23 | 24 | 25 | ### Features 26 | 27 | * add setup script ([#17](https://github.com/wizeline/serverless-fullstack/issues/17)) ([094edd2](https://github.com/wizeline/serverless-fullstack/commit/094edd2e2223546529a56c2facc1deea87bc5169)) 28 | 29 | ## [1.1.4](https://github.com/wizeline/serverless-fullstack/compare/v1.1.3...v1.1.4) (2020-05-12) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * split GitHub Action Secrets for AWS Creds into DEV, STAGING, and PROD ([cd0482b](https://github.com/wizeline/serverless-fullstack/commit/cd0482b54fbcb554774529f0280c66079f212d87)) 35 | 36 | ## [1.1.3](https://github.com/wizeline/serverless-fullstack/compare/v1.1.2...v1.1.3) (2020-05-08) 37 | 38 | 39 | ### Bug Fixes 40 | 41 | * **ci:** add prod aws profile ([46c35af](https://github.com/wizeline/serverless-fullstack/commit/46c35affb437ca0849f643bec958f9a84ed47b4c)) 42 | 43 | ## [1.1.2](https://github.com/wizeline/serverless-fullstack/compare/v1.1.1...v1.1.2) (2020-05-07) 44 | 45 | 46 | ### Bug Fixes 47 | 48 | * remove stage-specific .env vars; pass via command during CI instead ([92ed71b](https://github.com/wizeline/serverless-fullstack/commit/92ed71b116f77eb7d8dd828ca61b8c9326953fa2)) 49 | * update to latest serverless-amplify-plugin to fix deploying prepacks ([e0b4ac6](https://github.com/wizeline/serverless-fullstack/commit/e0b4ac6037f88a69fb5fb318db18df1bf6f71736)) 50 | 51 | ## [1.1.1](https://github.com/wizeline/serverless-fullstack/compare/v1.1.0...v1.1.1) (2020-05-06) 52 | 53 | 54 | ### Bug Fixes 55 | 56 | * shorten cognito lambda functions and add stage-specific .env vars ([6015c68](https://github.com/wizeline/serverless-fullstack/commit/6015c6823bfa1ec33911276a560e185507f9c563)) 57 | 58 | # [1.1.0](https://github.com/wizeline/serverless-fullstack/compare/v1.0.4...v1.1.0) (2020-05-06) 59 | 60 | 61 | ### Bug Fixes 62 | 63 | * PR stack preview and commenting ([#9](https://github.com/wizeline/serverless-fullstack/issues/9)) ([b00cebe](https://github.com/wizeline/serverless-fullstack/commit/b00cebe879fd080ed2ef489e32aa97cc4f4b0aee)) 64 | * **ui:** cp-stack-outputs before running build ([b1268f2](https://github.com/wizeline/serverless-fullstack/commit/b1268f2e43335d2caa726914c3e4969d535f4973)) 65 | * update dynamodb-toolbox to fix invalid table error ([90bb4e5](https://github.com/wizeline/serverless-fullstack/commit/90bb4e5fd0089495fb95a8169b7b866aa0cc4fe6)) 66 | * use SERVERLESS_SERVICE_SUFFIX instead of cli option and rename dev stage to development ([871d3bb](https://github.com/wizeline/serverless-fullstack/commit/871d3bb575370bc3dd32ec3ed4952cb03079cfcd)) 67 | 68 | 69 | ### Features 70 | 71 | * set AWS_NODEJS_CONNECTION_REUSE_ENABLED for perf boost ([6a2962e](https://github.com/wizeline/serverless-fullstack/commit/6a2962e58c63433a4b6f56bbaeaa7731e4d74ea2)) 72 | * use Amplify manual deployments ([f0e8607](https://github.com/wizeline/serverless-fullstack/commit/f0e8607321f4bb8eda672af838c235decbab84fb)) 73 | * use stack outputs as env vars for UI build command ([f71be15](https://github.com/wizeline/serverless-fullstack/commit/f71be1508356eab4f3acb42cbcbd4ac883c76e26)) 74 | 75 | ## [1.0.4](https://github.com/wizeline/serverless-nodejs-fullstack/compare/v1.0.3...v1.0.4) (2020-04-29) 76 | 77 | 78 | ### Bug Fixes 79 | 80 | * package for prod ([5e4f07a](https://github.com/wizeline/serverless-nodejs-fullstack/commit/5e4f07a8fdc28b27638410d3b45b46c1ea90163e)) 81 | 82 | ## [1.0.3](https://github.com/wizeline/serverless-nodejs-fullstack/compare/v1.0.2...v1.0.3) (2020-04-29) 83 | 84 | 85 | ### Bug Fixes 86 | 87 | * fix release.config.js ([10010cd](https://github.com/wizeline/serverless-nodejs-fullstack/commit/10010cd90ceb05ca55b2294f993f1283bd680450)) 88 | 89 | ## [1.0.2](https://github.com/wizeline/serverless-nodejs-fullstack/compare/v1.0.1...v1.0.2) (2020-04-29) 90 | 91 | 92 | ### Bug Fixes 93 | 94 | * replace dynamodb with dynamodb-toolbox v0.2 ([267711a](https://github.com/wizeline/serverless-nodejs-fullstack/commit/267711a5c28eb1ebb4007d6e9c3b74140cc0c20f)) 95 | 96 | ## [1.0.1](https://github.com/wizeline/serverless-nodejs-fullstack/compare/v1.0.0...v1.0.1) (2020-04-29) 97 | 98 | 99 | ### Bug Fixes 100 | 101 | * add remove-stack:dev script ([#5](https://github.com/wizeline/serverless-nodejs-fullstack/issues/5)) ([6f063ab](https://github.com/wizeline/serverless-nodejs-fullstack/commit/6f063ab6afd86ec11e89b2ae7db740449c20dbcf)) 102 | 103 | # 1.0.0 (2020-04-28) 104 | 105 | 106 | ### Bug Fixes 107 | 108 | * add Authorization header on all HTTP requests when signed in ([aecfbd7](https://github.com/wizeline/serverless-nodejs-fullstack/commit/aecfbd7a8aba1a971914f08df5172b0945108d98)) 109 | * allow creating multiple stacks in same account ([e10800b](https://github.com/wizeline/serverless-nodejs-fullstack/commit/e10800be92b1ac12c3da0651a1b3ad754a49a843)) 110 | * fix alarms and add stage setting for email notification ([e24fa98](https://github.com/wizeline/serverless-nodejs-fullstack/commit/e24fa98131c272d6119a59f88a28fb275fa49970)) 111 | 112 | 113 | ### Features 114 | 115 | * add api gateway cognito authorizer ([9707c2d](https://github.com/wizeline/serverless-nodejs-fullstack/commit/9707c2d90fbaceb5eb4d6957d940fe6a35670313)) 116 | * add serverless-dotenv-plugin ([eb79e88](https://github.com/wizeline/serverless-nodejs-fullstack/commit/eb79e8865386a1c61bda63a7e2a41f69ef2a60f7)) 117 | * add serverless-plugin-aws-alerts ([77f523a](https://github.com/wizeline/serverless-nodejs-fullstack/commit/77f523a78bfd3ad7e6a9d4ee69f17d813813756a)) 118 | * separated services ([927511d](https://github.com/wizeline/serverless-nodejs-fullstack/commit/927511dfeb1970f4f5962002a66e64347c1edfdd)) 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serverless FullStack starter 2 | 3 |

4 | wizeline, serverless, and amplify banner 5 |

6 | 7 |

8 | serverless software architecture diagram 9 |

10 | 11 | Get started developing applications quickly with best practices using Serverless on AWS. 12 | 13 | ## Pre-requisites 14 | 15 | * [AWS CLI](https://docs.aws.amazon.com/polly/latest/dg/setup-aws-cli.html) 16 | * Node.js 17 | * Git 18 | * GitHub Account 19 | 20 | ## Getting started 21 | To get started, run the following commands: 22 | 23 | ``` 24 | git clone https://github.com/wizeline/serverless-fullstack 25 | cd serverless-fullstack 26 | npm run setup-file 27 | ``` 28 | 29 | The last command will create a setup.config.json file, adjust it to set your application name and your aws credentials. 30 | 31 | Application name is required, if it's not provided, the setup will throw an error. 32 | The AWS DEV profile is required, if prod and stage are not provided, the dev will be used instead. 33 | 34 | Once you're done with the configuration file, run the following commands: 35 | 36 | ``` 37 | npm i 38 | npm run setup 39 | ``` 40 | 41 | Add your AWS credentials as secrets to your GitHub Repository with the following keys: 42 | 43 | * `AWS_ACCESS_KEY_ID_DEV` 44 | * `AWS_SECRET_ACCESS_KEY_DEV` 45 | * `AWS_ACCESS_KEY_ID_STAGING` 46 | * `AWS_SECRET_ACCESS_KEY_STAGING` 47 | * `AWS_ACCESS_KEY_ID_PROD` 48 | * `AWS_SECRET_ACCESS_KEY_PROD` 49 | 50 | It's advised that development, staging, and production environments exist in separate AWS accounts. However, if you'd prefer to deploy to a single AWS Account for simplicity, you can simply specify the same credentials for each. 51 | 52 | Create a GitHub Personal Access Token and add it as a repository secret called `GH_PERSONAL_ACCESS_TOKEN`. This is used to create GitHub releases. 53 | 54 | Coming soon: setup script to automate this. 55 | 56 | ## Features 57 | 58 | ### UI 59 | 60 | User Interface 61 | 62 | The UI was bootstrapped with Create React App and modified to include an Auth flow using AWS Amplify and Cognito. 63 | 64 | Static website hosting is provided by AWS Ampify, backed by S3 and CloudFront. 65 | 66 | ### Auth 67 | 68 | Auth 69 | 70 | User authentication is provided by AWS Cognito. 71 | 72 | Social sign-in coming soon. 73 | 74 | ### REST API 75 | 76 | API 77 | 78 | A Node.js Express API running on Lambda and API Gateway allows for a familiar developer experience while leveraging the benefits of Serverless. 79 | 80 | ### Database 81 | 82 | Data 83 | 84 | DynamoDB is capable of scaling to meet any requirements you may have. 85 | 86 | ### Continuous Deployment (CI/CD) 87 | 88 | Continuous Deployment 89 | 90 | GitHub Actions is used to create a Continuous Deployment Pipeline from developer preview, to staging, to production. Each environment is deployed to an isolated AWS Account (optionally, these can be deployed to the same account for simplicity). 91 | 92 | Changes are automatically versioned with Semantic Versioning based on git commit messages and immutable release packages created as GitHub relesaes. 93 | 94 | ### Pull Request Previews 95 | 96 | Each PR gets its own stack deployed so that reviewers can see the results for themselves, and end-to-end tests can be run. 97 | 98 | ### Infrastructure as Code 99 | 100 | Serverless Framework is used to describe our infrastruture 101 | 102 | ### And more... 103 | 104 | * CloudWatch Alarms 105 | * Per-function IAM Roles 106 | * REST API Logs in JSON 107 | * Custom Domain Names 108 | * .env support 109 | * Pruning of old Lambda Function Versions 110 | * Lambda Functions optimized with Webpack 111 | 112 | ## GitHub Actions Continuous Deployments (CI/CD) 113 | 114 |

115 | serverless-fullstack github actions continous deployment flow diagram 116 |

117 | 118 | ## Manual/developer deployments 119 | 120 | Make a copy of the `example.env.development` file: 121 | 122 | ```shell 123 | cp example.env.development .env.development 124 | ``` 125 | 126 | Modify the values in your `.env.development` file. If you're using a shared developer account, you should set `SERVERLESS_SERVICE_SUFFIX=-brett`, ensuring the value you specify is unique and not used by other developers on your team. 127 | 128 | Run `npm run deploy:dev` to deploy to your dev account. 129 | 130 | To deploy to staging and production manually, you can run `npm run deploy:staging` or `npm run deploy:prod` respectively. 131 | 132 | ## Developing 133 | 134 | After deploying to your developer AWS account, run `npm run start:ui` to run your UI locally against your AWS resources in the cloud. 135 | 136 | If you want to run your API locally also, you can run `npm run start:api` and `npm run start:ui:offline` separately. 137 | 138 | ## Customization 139 | 140 | ### Remove auto-verification 141 | 142 | By default, Cognito forces users to verify their email address, but this kit comes with auto-verification of new users to reduce onboarding friction. If you want to remove this and require users to verify their accounts, perform the following: 143 | 144 | 1. Inside of `serverless.yaml`, remove the `cognitoAutoConfirmUser` function, the `CognitoAutoConfirmUserLambdaCognitoPermission` resource, and the `PreSignUp: !GetAtt CognitoAutoConfirmUserLambdaFunction.Arn` line. 145 | 2. Remove the `ConfirmSignUpRedirectToSignIn`function from `packages/ui/src/AuthenticatedApp.js`, and replace `,` with ``. 146 | 3. Delete `packages/api/functions/auto-confirm-user.js` 147 | 148 | ## TODO: 149 | - [ ] Improve setup experience (primarily, replace myapp with custom name) 150 | - [ ] Custom domains 151 | - [ ] CloudFormation rollback triggers 152 | - [ ] Enable stack termination protection on prod and staging 153 | - [ ] Add [lumigo-cli](https://www.npmjs.com/package/lumigo-cli), especially for tuning 154 | - [ ] Split stacks to mitigate chance of hitting CloudFormation 200 resource limit 155 | - [ ] Additional unit/integration tests 156 | - [ ] End-to-end tests (with Cypress?) 157 | - [ ] Make it easy to disable PR Previews for open-source projects where you don't want to allow people to create resources in your AWS account. 🔒 158 | -------------------------------------------------------------------------------- /serverless.yaml: -------------------------------------------------------------------------------- 1 | service: myapp${{env:SERVERLESS_SERVICE_SUFFIX, ''}} 2 | provider: 3 | name: aws 4 | stackName: ${{self:service}}-${{self:provider.stage}} 5 | runtime: nodejs12.x 6 | memorySize: 1024 7 | timeout: 6 8 | stage: ${{opt:stage, env:NODE_ENV, 'development'}} 9 | profile: ${{self:custom.stageConfig.profile}} 10 | region: us-east-1 11 | variableSyntax: "\\${{([ ~:a-zA-Z0-9._@\\'\",\\-\\/\\(\\)]+?)}}" 12 | logs: 13 | restApi: 14 | format: '{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "caller":"$context.identity.caller", "user":"$context.identity.user", "requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod", "resourcePath":"$context.resourcePath", "status":"$context.status", "protocol":"$context.protocol", "responseLength":"$context.responseLength" }' 15 | level: INFO # TODO: add custom field for setting this; default to ERROR for prod 16 | environment: 17 | USER_TABLE: !Ref UserTable 18 | 19 | # Enable connection reuse for AWS SDK for instant performance boost 20 | # https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/node-reusing-connections.html 21 | AWS_NODEJS_CONNECTION_REUSE_ENABLED: 1 22 | 23 | package: 24 | individually: true 25 | 26 | plugins: 27 | - serverless-dotenv-plugin 28 | - serverless-domain-manager 29 | - serverless-prune-plugin 30 | - serverless-plugin-tracing 31 | - serverless-iam-roles-per-function 32 | - serverless-webpack 33 | # - serverless-apigateway-service-proxy 34 | # - serverless-plugin-split-stacks 35 | - serverless-stack-output 36 | - serverless-cloudside-plugin 37 | - serverless-plugin-aws-alerts 38 | # - '../serverless-amplify-plugin' 39 | - '@wizeline/serverless-amplify-plugin' 40 | - serverless-offline 41 | - serverless-stack-termination-protection 42 | 43 | custom: 44 | stages: 45 | development: 46 | profile: myapp_dev 47 | amplify: 48 | api: 49 | domainEnabled: false 50 | alarms: 51 | notificationEmail: ${{env:ALARMS_NOTIFICATION_EMAIL}} 52 | staging: 53 | profile: myapp_staging 54 | api: 55 | domainEnabled: false 56 | domainName: staging.api.example.com 57 | validationDomain: example.com 58 | amplify: 59 | # domainName: staging.example.com 60 | # branch: staging 61 | alarms: 62 | notificationEmail: alert@example.com 63 | production: 64 | profile: myapp_prod 65 | api: 66 | domainEnabled: false 67 | domainName: api.example.com 68 | validationDomain: example.com 69 | amplify: 70 | # domainName: example.com 71 | alarms: 72 | notificationEmail: alert@example.com 73 | stageConfig: ${{self:custom.stages.${{self:provider.stage}}}} 74 | prune: 75 | automatic: true 76 | number: 10 77 | customDomain: 78 | domainName: ${{self:custom.stageConfig.api.domainName, ''}} 79 | certificateName: ${{self:custom.stageConfig.api.domainName, ''}} 80 | enabled: ${{self:custom.stageConfig.api.domainEnabled, false}} 81 | # createRoute53Record: ${{self:custom.stageConfig.api.isDomainRoute53, false}} 82 | serverless-offline: 83 | httpPort: 4911 84 | noPrependStageInUrl: true 85 | useChildProcesses: true # hack to get watching working 86 | useSeparateProcesses: true 87 | amplify: 88 | isManual: true 89 | domainName: ${{self:custom.stageConfig.amplify.domainName, ''}} 90 | buildSpecValues: 91 | artifactBaseDirectory: packages/ui/build 92 | preBuildWorkingDirectory: packages/ui 93 | buildCommandEnvVars: 94 | prefix: 'REACT_APP_' 95 | allow: 96 | - ApiEndpoint 97 | - CognitoIdentityPoolId, 98 | - CognitoUserPoolId, 99 | - CognitoUserPoolClientId, 100 | webpack: 101 | webpackConfig: ./functions.webpack.config.js 102 | output: 103 | file: ./stack-outputs.json 104 | # TODO: Add custom splitting with ./split-stack-splitter.js 105 | # splitStacks: 106 | # perFunction: true 107 | # custom: ./split-stack-splitter.js 108 | alerts: 109 | dashboards: true 110 | nameTemplate: $[functionName]-$[metricName]-Alarm 111 | topics: 112 | alarm: 113 | topic: ${{self:service}}-${{self:provider.stage}}-alarm 114 | notifications: 115 | - protocol: email 116 | endpoint: ${{self:custom.stageConfig.alarms.notificationEmail}} 117 | # TODO: Add short and long alarms for each 118 | alarms: 119 | - functionThrottles 120 | - functionErrors 121 | - functionInvocations 122 | - functionDuration 123 | serverlessTerminationProtection: 124 | stages: 125 | - staging 126 | - production 127 | 128 | functions: 129 | express: 130 | handler: packages/api/functions/express/lambda.handler 131 | events: 132 | - http: 133 | method: ANY 134 | path: / 135 | cors: true 136 | authorizer: 137 | type: COGNITO_USER_POOLS 138 | authorizerId: !Ref ApiGatewayAuthorizer 139 | # name: CognitoAuthorizer 140 | # type: COGNITO_USER_POOLS 141 | # arn: !GetAtt CognitoUserPool.Arn 142 | - http: 143 | method: ANY 144 | path: '{proxy+}' 145 | cors: true 146 | authorizer: 147 | type: COGNITO_USER_POOLS 148 | authorizerId: !Ref ApiGatewayAuthorizer 149 | # NOTE: Instead of creating an Authorizer ourselves, we could use the below when this is fixed 150 | # https://github.com/serverless/serverless/issues/3212#issuecomment-450574093 151 | # name: CognitoAuthorizer 152 | # type: COGNITO_USER_POOLS 153 | # arn: !GetAtt CognitoUserPool.Arn 154 | iamRoleStatements: 155 | - Effect: "Allow" 156 | Action: 157 | - "xray:PutTraceSegments" 158 | - "xray:PutTelemetryRecords" 159 | Resource: 160 | - "*" 161 | - Effect: "Allow" 162 | Action: 163 | - dynamodb:BatchGetItem 164 | - dynamodb:BatchWriteItem 165 | - dynamodb:DeleteItem 166 | - dynamodb:GetItem 167 | - dynamodb:PutItem 168 | - dynamodb:Query 169 | - dynamodb:Scan 170 | - dynamodb:UpdateItem 171 | Resource: 172 | - !GetAtt UserTable.Arn 173 | 174 | autoConfirmUser: 175 | handler: packages/api/functions/cognito/auto-confirm-user.handler 176 | 177 | postAuthN: 178 | handler: packages/api/functions/cognito/post-authentication.handler 179 | iamRoleStatements: 180 | - Effect: "Allow" 181 | Action: 182 | - dynamodb:GetItem 183 | - dynamodb:PutItem 184 | Resource: 185 | - !GetAtt UserTable.Arn 186 | 187 | resources: 188 | Conditions: 189 | IsApiCustomDomainEnabled: 190 | !Equals 191 | - ${{self:custom.customDomain.enabled}} 192 | - true 193 | 194 | Resources: 195 | AcmCertificate: 196 | Type: AWS::CertificateManager::Certificate 197 | Condition: IsApiCustomDomainEnabled 198 | Properties: 199 | DomainName: ${{self:custom.customDomain.domainName}} 200 | DomainValidationOptions: 201 | - DomainName: ${{self:custom.customDomain.domainName}} 202 | ValidationDomain: ${{self:custom.stageConfig.api.validationDomain, ''}} 203 | 204 | UserTable: 205 | Type: AWS::DynamoDB::Table 206 | Properties: 207 | BillingMode: PAY_PER_REQUEST 208 | PointInTimeRecoverySpecification: 209 | PointInTimeRecoveryEnabled: false 210 | KeySchema: 211 | - KeyType: HASH 212 | AttributeName: id 213 | AttributeDefinitions: 214 | - AttributeName: id 215 | AttributeType: S 216 | 217 | CognitoUserPool: 218 | Type: AWS::Cognito::UserPool 219 | Properties: 220 | Policies: 221 | PasswordPolicy: 222 | MinimumLength: 6 223 | Schema: 224 | - AttributeDataType: String 225 | Name: email 226 | Required: true 227 | AutoVerifiedAttributes: 228 | - email 229 | # EmailConfiguration: 230 | # EmailSendingAccount: DEVELOPER 231 | # ReplyToEmailAddress: no-reply@halfstack.software 232 | # SourceArn: arn:aws:ses:us-east-1:xxxx:identity/no-reply@halfstack.software 233 | LambdaConfig: 234 | PreSignUp: !GetAtt AutoConfirmUserLambdaFunction.Arn 235 | PostAuthentication: !GetAtt PostAuthNLambdaFunction.Arn 236 | 237 | CognitoUserPoolClient: 238 | Type: AWS::Cognito::UserPoolClient 239 | Properties: 240 | UserPoolId: !Ref CognitoUserPool 241 | ClientName: CognitoIdentityPool 242 | GenerateSecret: false 243 | RefreshTokenValidity: 30 244 | 245 | CognitoIdentityPool: 246 | Type: AWS::Cognito::IdentityPool 247 | Properties: 248 | AllowUnauthenticatedIdentities: false 249 | # SupportedLoginProviders: 250 | # graph.facebook.com: 'xxxxx' 251 | # accounts.google.com: 'xxxxx-v02jjpd5r9ig0pdacbhpill2asuqtvnf.apps.googleusercontent.com' 252 | # api.twitter.com: 253 | CognitoIdentityProviders: 254 | - ClientId: !Ref CognitoUserPoolClient 255 | ProviderName: !GetAtt CognitoUserPool.ProviderName 256 | 257 | # Allow Cognito to invoke the cognitoAutoConfirm and cognitoPostAuthN functions 258 | AutoConfirmUserLambdaCognitoPermission: 259 | Type: AWS::Lambda::Permission 260 | Properties: 261 | Action: lambda:InvokeFunction 262 | FunctionName: !GetAtt AutoConfirmUserLambdaFunction.Arn 263 | Principal: cognito-idp.amazonaws.com 264 | SourceArn: !GetAtt CognitoUserPool.Arn 265 | 266 | PostAuthNLambdaCognitoPermission: 267 | Type: AWS::Lambda::Permission 268 | Properties: 269 | Action: lambda:InvokeFunction 270 | FunctionName: !GetAtt PostAuthNLambdaFunction.Arn 271 | Principal: cognito-idp.amazonaws.com 272 | SourceArn: !GetAtt CognitoUserPool.Arn 273 | 274 | CognitoUserRole: 275 | Type: AWS::IAM::Role 276 | Properties: 277 | AssumeRolePolicyDocument: 278 | Version: '2012-10-17' 279 | Statement: 280 | # Allow authenticated users to assume this role 281 | - Effect: Allow 282 | Principal: 283 | Federated: cognito-identity.amazonaws.com 284 | Action: sts:AssumeRoleWithWebIdentity 285 | Condition: 286 | StringEquals: 287 | 'cognito-identity.amazonaws.com:aud': !Ref CognitoIdentityPool 288 | 'ForAnyValue:StringLike': 289 | 'cognito-identity.amazonaws.com:amr': authenticated 290 | # Authenticated users are allowed to invoke the API 291 | Policies: 292 | - PolicyName: InvokeApi 293 | PolicyDocument: 294 | Version: '2012-10-17' 295 | Statement: 296 | - Effect: Allow 297 | Action: 298 | - execute-api:Invoke 299 | Resource: !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ApiGatewayRestApi}/${{self:provider.stage}}/*/*' 300 | Path: '/' 301 | 302 | CognitoIdentityPoolRoles: 303 | Type: AWS::Cognito::IdentityPoolRoleAttachment 304 | Properties: 305 | IdentityPoolId: !Ref CognitoIdentityPool 306 | Roles: 307 | authenticated: !GetAtt CognitoUserRole.Arn 308 | 309 | # Due to a Serverless Framework bug, we need to create our own Authorizer, instead of 310 | # simply specifying `authorizer.arn: !GetAtt CognitoUserPool.Arn` in the function. 311 | # https://github.com/serverless/serverless/issues/3212#issuecomment-450574093 312 | ApiGatewayAuthorizer: 313 | DependsOn: 314 | - ApiGatewayRestApi 315 | Type: AWS::ApiGateway::Authorizer 316 | Properties: 317 | Name: CognitoAuthorizer 318 | IdentitySource: method.request.header.Authorization 319 | RestApiId: 320 | Ref: ApiGatewayRestApi 321 | Type: COGNITO_USER_POOLS 322 | ProviderARNs: 323 | - !GetAtt CognitoUserPool.Arn 324 | Outputs: 325 | CognitoUserPoolId: 326 | Description: ID of the Cognito User Pool 327 | Value: !Ref CognitoUserPool 328 | 329 | CognitoUserPoolClientId: 330 | Description: 'Client ID of the Cognito User Pool App: Identity Pool' 331 | Value: !Ref CognitoUserPoolClient 332 | 333 | CognitoIdentityPoolId: 334 | Description: ID of the Cognito Identity Pool 335 | Value: !Ref CognitoIdentityPool 336 | 337 | UserTableName: 338 | Value: !Ref UserTable 339 | 340 | ApiEndpoint: 341 | Value: !Sub https://${ApiGatewayRestApi}.execute-api.${AWS::Region}.amazonaws.com/${{self:provider.stage}} --------------------------------------------------------------------------------