├── .github └── workflows │ ├── beta.yml │ └── main.yml ├── .gitignore ├── README.md ├── lerna.json ├── package.json ├── packages ├── README.md ├── docs │ └── README.md ├── e2e-tests │ ├── .env-template │ ├── .prettierignore │ ├── .prettierrc │ ├── README.md │ ├── invoke.js │ ├── package.json │ ├── runner.js │ └── tests │ │ ├── sales-app │ │ └── signin.test.js │ │ └── shop-app │ │ └── signin.test.js ├── infrastructure │ ├── .env-template │ ├── .prettierignore │ ├── .prettierrc │ ├── README.md │ ├── cognito-stack.js │ ├── cognito-users-to-groups.js │ ├── configs-stack.js │ ├── data-imports │ │ ├── cart.json │ │ ├── import-all.sh │ │ ├── import.sh │ │ ├── product.json │ │ └── profile.json │ ├── deploy-backends.js │ ├── deploy-configs.js │ ├── deploy-frontends.js │ ├── e2e-stack.js │ ├── graphql-stack.js │ ├── graphql │ │ ├── datasources.js │ │ ├── resolvers │ │ │ ├── cart.js │ │ │ ├── image.js │ │ │ ├── invoice.js │ │ │ ├── me.js │ │ │ ├── order.js │ │ │ ├── product.js │ │ │ └── profile.js │ │ └── schema.graphql │ ├── package.json │ ├── sales-app-stack.js │ ├── shop-app-stack.js │ ├── ssm-stack.js │ ├── storybook-app-stack.js │ └── yarn.lock ├── react-components │ ├── .prettierignore │ ├── .prettierrc │ ├── README.md │ ├── package.json │ ├── src │ │ ├── atomics │ │ │ ├── Loading.tsx │ │ │ ├── LoadingButton.tsx │ │ │ └── SearchInput.tsx │ │ ├── hooks │ │ │ └── useForm.tsx │ │ ├── index.tsx │ │ ├── molecules │ │ │ ├── ConfirmDialog.tsx │ │ │ └── Topbar.tsx │ │ ├── organisms │ │ │ └── MediaLibrary.tsx │ │ ├── providers │ │ │ └── AppProvider.tsx │ │ └── templates │ │ │ └── Layout.tsx │ └── tsconfig.json ├── sales-app │ ├── .env-template │ ├── .prettierignore │ ├── .prettierrc │ ├── README.md │ ├── package.json │ └── src │ │ ├── App.jsx │ │ ├── components │ │ ├── ProductCard.jsx │ │ └── ProductForm.jsx │ │ ├── graphql │ │ ├── product-fragment.graphql │ │ ├── product-get.graphql │ │ ├── product-list.graphql │ │ ├── product-mutation-delete.graphql │ │ └── product-mutation-upsert.graphql │ │ ├── index.html │ │ ├── index.jsx │ │ ├── pages │ │ ├── dashboard.jsx │ │ ├── forgot.jsx │ │ ├── password.jsx │ │ ├── products.jsx │ │ ├── signin.jsx │ │ └── signup.jsx │ │ └── theme.js ├── shop-app │ ├── .env-template │ ├── .eslintrc.json │ ├── .prettierignore │ ├── .prettierrc │ ├── README.md │ ├── gatsby-browser.js │ ├── gatsby-config.js │ ├── gatsby-node.js │ ├── gatsby-ssr.js │ ├── package.json │ ├── src │ │ ├── components │ │ │ ├── CartSummary.jsx │ │ │ ├── ProductCard.jsx │ │ │ ├── ProfileForm.jsx │ │ │ └── SEO.jsx │ │ ├── images │ │ │ ├── header.png │ │ │ ├── ill-short-dark.svg │ │ │ ├── logo.svg │ │ │ └── moltin-light-hex.svg.svg │ │ ├── pages │ │ │ ├── 404.jsx │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ ├── login.js.snap │ │ │ │ │ └── register.js.snap │ │ │ │ ├── login.js │ │ │ │ └── register.js │ │ │ ├── confirm.jsx │ │ │ ├── index.jsx │ │ │ ├── password.jsx │ │ │ ├── profile.jsx │ │ │ ├── signin.jsx │ │ │ └── signup.jsx │ │ └── templates │ │ │ └── ProductPage.jsx │ ├── static │ │ ├── favicons │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── browserconfig.xml │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── mstile-150x150.png │ │ │ ├── safari-pinned-tab.svg │ │ │ └── site.webmanifest │ │ └── robots.txt │ ├── theme.js │ └── wrap-with-provider.js ├── ssmenv-cli │ ├── .prettierignore │ ├── .prettierrc │ ├── README.md │ ├── bin │ │ └── ssmenv.js │ └── package.json └── storybook │ ├── .env.config │ ├── .prettierignore │ ├── .prettierrc │ ├── .storybook │ ├── addons.js │ └── config.js │ ├── README.md │ ├── package.json │ └── stories │ ├── 1-Atomics.stories.js │ ├── 2-Molecules.stories.js │ ├── 3-Organisms.stories.js │ ├── 4-Templates.stories.js │ ├── 5-Providers.stories.js │ └── 6-Hooks.stories.js └── yarn.lock /.github/workflows/beta.yml: -------------------------------------------------------------------------------- 1 | name: AWS Beta Deployment 2 | on: 3 | push: 4 | branches: 5 | - beta 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: GitHub Checkout 11 | uses: actions/checkout@v1 12 | 13 | - name: Yarn Install 14 | uses: nuxt/actions-yarn@master 15 | with: 16 | args: install 17 | 18 | - name: Lerna 19 | uses: nuxt/actions-yarn@master 20 | with: 21 | args: lerna 22 | 23 | - name: AWS CDK Destroy (AppSync Fix) 24 | uses: MikeBild/aws-cdk-js-github-action@1.0.0 25 | with: 26 | args: destroy --force --app packages/infrastructure/deploy-backends.js "$CDK_STACK_NAME-$CDK_STACK_ENV-GraphQL" 27 | env: 28 | AWS_REGION: eu-central-1 29 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 30 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 31 | CDK_AWS_ACCOUNT: ${{ secrets.AWS_ACCOUNT }} 32 | CDK_AWS_REGION: eu-central-1 33 | CDK_STACK_NAME: ECommerce 34 | CDK_STACK_ENV: BETA 35 | 36 | - name: AWS CDK Deploy Backends 37 | uses: MikeBild/aws-cdk-js-github-action@1.0.0 38 | with: 39 | args: deploy --require-approval never --app packages/infrastructure/deploy-backends.js "$CDK_STACK_NAME-*" 40 | env: 41 | AWS_REGION: eu-central-1 42 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 43 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 44 | CDK_AWS_ACCOUNT: ${{ secrets.AWS_ACCOUNT }} 45 | CDK_AWS_REGION: eu-central-1 46 | CDK_STACK_NAME: ECommerce 47 | CDK_STACK_ENV: BETA 48 | 49 | - name: Build 50 | uses: nuxt/actions-yarn@master 51 | with: 52 | args: build 53 | env: 54 | AWS_REGION: eu-central-1 55 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 56 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 57 | CDK_AWS_ACCOUNT: ${{ secrets.AWS_ACCOUNT }} 58 | CDK_AWS_REGION: eu-central-1 59 | CDK_STACK_NAME: ECommerce 60 | CDK_STACK_ENV: BETA 61 | 62 | - name: AWS CDK Deploy Frontends 63 | uses: MikeBild/aws-cdk-js-github-action@1.0.0 64 | with: 65 | args: deploy --require-approval never --app packages/infrastructure/deploy-frontends.js "$CDK_STACK_NAME-*" 66 | env: 67 | AWS_REGION: eu-central-1 68 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 69 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 70 | CDK_AWS_ACCOUNT: ${{ secrets.AWS_ACCOUNT }} 71 | CDK_AWS_REGION: eu-central-1 72 | CDK_STACK_NAME: ECommerce 73 | CDK_STACK_ENV: BETA 74 | 75 | - name: Sleep (5mins) for AWS Deployment E2E Test activation 76 | uses: maddox/actions/sleep@master 77 | with: 78 | args: 300 79 | 80 | - name: End-To-End Test Run 81 | uses: nuxt/actions-yarn@master 82 | with: 83 | args: test 84 | env: 85 | AWS_REGION: eu-central-1 86 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 87 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 88 | CDK_AWS_ACCOUNT: ${{ secrets.AWS_ACCOUNT }} 89 | CDK_AWS_REGION: eu-central-1 90 | CDK_STACK_NAME: ECommerce 91 | CDK_STACK_ENV: BETA 92 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: AWS Production Deployment 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: GitHub Checkout 11 | uses: actions/checkout@v1 12 | 13 | - name: Yarn Install 14 | uses: nuxt/actions-yarn@master 15 | with: 16 | args: install 17 | 18 | - name: Lerna 19 | uses: nuxt/actions-yarn@master 20 | with: 21 | args: lerna 22 | 23 | - name: AWS CDK Destroy (AppSync Fix) 24 | uses: MikeBild/aws-cdk-js-github-action@1.0.0 25 | with: 26 | args: destroy --force --app packages/infrastructure/deploy-backends.js "$CDK_STACK_NAME-$CDK_STACK_ENV-GraphQL" 27 | env: 28 | AWS_REGION: eu-central-1 29 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 30 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 31 | CDK_AWS_ACCOUNT: ${{ secrets.AWS_ACCOUNT }} 32 | CDK_AWS_REGION: eu-central-1 33 | CDK_STACK_NAME: ECommerce 34 | CDK_STACK_ENV: PROD 35 | 36 | - name: AWS CDK Deploy Backends 37 | uses: MikeBild/aws-cdk-js-github-action@1.0.0 38 | with: 39 | args: deploy --require-approval never --app packages/infrastructure/deploy-backends.js "$CDK_STACK_NAME-*" 40 | env: 41 | AWS_REGION: eu-central-1 42 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 43 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 44 | CDK_AWS_ACCOUNT: ${{ secrets.AWS_ACCOUNT }} 45 | CDK_AWS_REGION: eu-central-1 46 | CDK_STACK_NAME: ECommerce 47 | CDK_STACK_ENV: PROD 48 | 49 | - name: Build 50 | uses: nuxt/actions-yarn@master 51 | with: 52 | args: build 53 | env: 54 | AWS_REGION: eu-central-1 55 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 56 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 57 | CDK_AWS_ACCOUNT: ${{ secrets.AWS_ACCOUNT }} 58 | CDK_AWS_REGION: eu-central-1 59 | CDK_STACK_NAME: ECommerce 60 | CDK_STACK_ENV: PROD 61 | 62 | - name: AWS CDK Deploy Frontends 63 | uses: MikeBild/aws-cdk-js-github-action@1.0.0 64 | with: 65 | args: deploy --require-approval never --app packages/infrastructure/deploy-frontends.js "$CDK_STACK_NAME-*" 66 | env: 67 | AWS_REGION: eu-central-1 68 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 69 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 70 | CDK_AWS_ACCOUNT: ${{ secrets.AWS_ACCOUNT }} 71 | CDK_AWS_REGION: eu-central-1 72 | CDK_STACK_NAME: ECommerce 73 | CDK_STACK_ENV: PROD 74 | 75 | - name: Sleep (5mins) for AWS Deployment E2E Test activation 76 | uses: maddox/actions/sleep@master 77 | with: 78 | args: 300 79 | 80 | - name: End-To-End Test Run 81 | uses: nuxt/actions-yarn@master 82 | with: 83 | args: test 84 | env: 85 | AWS_REGION: eu-central-1 86 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 87 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 88 | CDK_AWS_ACCOUNT: ${{ secrets.AWS_ACCOUNT }} 89 | CDK_AWS_REGION: eu-central-1 90 | CDK_STACK_NAME: ECommerce 91 | CDK_STACK_ENV: PROD 92 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | build 4 | .cache 5 | .DS_Store 6 | node_modules/ 7 | .env 8 | .env.* 9 | 10 | # Logs 11 | logs 12 | *.log 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | 17 | .cache/ 18 | public 19 | build 20 | dist 21 | storybook-static 22 | 23 | tmp 24 | cdk.context.json 25 | .cdk.staging 26 | cdk.out -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # E-Commerce Example 2 | 3 | ![](https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg) 4 | ![](https://github.com/mikebild/serverless-aws-cdk-ecommerce/workflows/AWS%20Production%20Deployment/badge.svg) 5 | ![](https://github.com/mikebild/serverless-aws-cdk-ecommerce/workflows/AWS%20Beta%20Deployment/badge.svg) 6 | 7 | ## System Environment 8 | 9 | - Serverless Backends using AWS 10 | 11 | - Cognito (JWT Auth) 12 | - S3 (Storage) 13 | - CloudFront (CDN) 14 | - AppSync (GraphQL Server) 15 | - Lambda (Functions) 16 | - Route53 (DNS) 17 | - DynamoDB (NoSQL) 18 | - SSM (System Manager / Parameter Store) 19 | 20 | - Frontends using 21 | - JavaScript (ECMA) and TypeScript 22 | - React 23 | - React-Router 24 | - Apollo GraphQL Client 25 | - Material-UI 26 | - Parcel (Zero Config Bundler) 27 | - Gatsby (Static Website Generator) 28 | - Storybook (Component Development Playground) 29 | - Lerna (Mono-Repo) 30 | 31 | ## Setup 32 | 33 | ```bash 34 | yarn 35 | yarn lerna 36 | ``` 37 | 38 | ## Cleanup 39 | 40 | ```bash 41 | yarn clean 42 | ``` 43 | 44 | ## Build 45 | 46 | ```bash 47 | yarn build 48 | ``` 49 | 50 | ## Development 51 | 52 | ```bash 53 | yarn develop 54 | ``` 55 | 56 | ## Deploy to AWS 57 | 58 | ```bash 59 | yarn deploy 60 | ``` 61 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "1.0.0", 6 | "useWorkspaces": true, 7 | "npmClient": "yarn" 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "root", 4 | "devDependencies": { 5 | "husky": "3.0.9", 6 | "lerna": "3.18.1" 7 | }, 8 | "workspaces": { 9 | "packages": [ 10 | "packages/*" 11 | ], 12 | "nohoist": [ 13 | "**/jest/**", 14 | "**/jest" 15 | ] 16 | }, 17 | "scripts": { 18 | "lerna": "lerna bootstrap", 19 | "develop": "lerna run develop --stream", 20 | "clean": "lerna clean --yes", 21 | "build": "lerna run clean && lerna run build --stream", 22 | "serve": "lerna run serve --stream", 23 | "test": "lerna run invoke --stream", 24 | "deploy": "npm run deploy:backends && npm run build && npm run deploy:frontends", 25 | "deploy:backends": "lerna run deploy:backends --stream -- ECommerce-*", 26 | "deploy:frontends": "lerna run deploy:frontends --stream -- ECommerce-*", 27 | "format": "lerna run format --stream" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/README.md: -------------------------------------------------------------------------------- 1 | # E-Commerce Projects 2 | 3 | - `packages/infrastructure` - AWS infrastructure as code (AWS-CDK) deployment 4 | - `packages/docs` - Documentation 5 | - `packages/sales-app` - E-Commerce Sales App 6 | - `packages/shop-app` - E-Commerce Shop App 7 | - `packages/react-components` - Shared React-Component Library 8 | - `packages/storybook` - Storybook 9 | - `packages/e2e-tests` - E2E Tests using Puppeteer and Headless-Chrome 10 | -------------------------------------------------------------------------------- /packages/docs/README.md: -------------------------------------------------------------------------------- 1 | # E-Commerce Documentation 2 | -------------------------------------------------------------------------------- /packages/e2e-tests/.env-template: -------------------------------------------------------------------------------- 1 | CDK_STACK_NAME= 2 | CDK_STACK_ENV= 3 | CDK_AWS_REGION=eu-central-1 4 | CDK_AWS_ACCESS_KEY_ID= 5 | CDK_AWS_SECRET_ACCESS_KEY= 6 | CDK_E2E_BASE_URL=http://localhost:1234 7 | CDK_E2E_USERNAME=myusername 8 | CDK_E2E_PASSWORD=mysecret -------------------------------------------------------------------------------- /packages/e2e-tests/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | build 4 | .cache 5 | .DS_Store 6 | node_modules/ 7 | .env 8 | .env.development 9 | .env.production 10 | 11 | # Logs 12 | logs 13 | *.log 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | 18 | .cache/ 19 | public 20 | build 21 | dist 22 | storybook-static 23 | 24 | tmp 25 | cdk.context.json 26 | .cdk.staging 27 | cdk.out -------------------------------------------------------------------------------- /packages/e2e-tests/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "bracketSpacing": true, 6 | "jsxBracketSameLine": false, 7 | "semi": false 8 | } 9 | -------------------------------------------------------------------------------- /packages/e2e-tests/README.md: -------------------------------------------------------------------------------- 1 | # E2E Tests 2 | 3 | **Runs E2E Tests using headless chrome with [Puppeteer](https://github.com/GoogleChrome/puppeteer) on AWS Lambda** 4 | 5 | ## Setup 6 | 7 | ``` 8 | yarn install 9 | ``` 10 | 11 | ## Start local tests 12 | 13 | ```bash 14 | yarn test 15 | ``` 16 | 17 | ## Prepare for AWS Lambda deployment 18 | 19 | ```bash 20 | yarn build 21 | ``` 22 | 23 | ## Invoke the deployed AWS Lambda 24 | 25 | ```bash 26 | yarn invoke 27 | ``` -------------------------------------------------------------------------------- /packages/e2e-tests/invoke.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk') 2 | const CDK_STACK_NAME = process.env.CDK_STACK_NAME 3 | const CDK_STACK_ENV = process.env.CDK_STACK_ENV 4 | const CDK_AWS_REGION = process.env.CDK_AWS_REGION 5 | 6 | const lambda = new AWS.Lambda({ region: CDK_AWS_REGION }) 7 | const [_, __, count = 1] = process.argv 8 | 9 | invokeE2E(parseInt(count)) 10 | 11 | async function invokeE2E(count) { 12 | const lambdaInvokeList = new Array(count).fill({}).map(() => 13 | lambda 14 | .invoke({ 15 | FunctionName: `${CDK_STACK_NAME}-${CDK_STACK_ENV}-E2ETests-Function`, 16 | InvocationType: 'RequestResponse', 17 | LogType: 'Tail', 18 | Payload: JSON.stringify({}), 19 | }) 20 | .promise() 21 | .catch(error => ({ LogResult: error.message, Payload: '{}' })) 22 | ) 23 | 24 | try { 25 | const lambdaResults = await Promise.all(lambdaInvokeList) 26 | 27 | lambdaResults.forEach(({ LogResult, Payload }) => { 28 | if (LogResult.includes('Function not found:')) return console.log(LogResult) 29 | 30 | const logResults = Buffer.from(LogResult, 'base64').toString('ascii') 31 | const { results: { success, numFailedTests, numPassedTests, numPendingTests, testResults } = {} } = JSON.parse( 32 | Payload 33 | ) 34 | 35 | console.log('Erfolgreiche Ausführung aller Tests: ', success) 36 | console.log('Erfolgreiche Tests: ', numPassedTests) 37 | console.log('Nicht erfolgreiche Tests: ', numFailedTests) 38 | console.log('Ausgesetzte Tests: ', numPendingTests) 39 | console.log('Fehlermeldungen:') 40 | testResults.forEach(x => { 41 | console.log('Pfad:', x.testFilePath) 42 | console.log('Fehler:', x.failureMessage) 43 | }) 44 | 45 | process.exit(0) 46 | }) 47 | } catch (error) { 48 | console.error(error) 49 | process.exit(1) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/e2e-tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@serverless-aws-cdk-ecommerce/e2e-tests", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "clean": "rimraf build", 7 | "build": "npm run clean && mkdir build && cp -R package.json build && npm install --production --prefix build && cp -R tests build && cp runner.js build", 8 | "test": "npm run clean && ssmenv -- jest --runInBand", 9 | "invoke": "ssmenv -- node invoke.js", 10 | "format": "prettier --write \"**/*.{js,jsx}\"" 11 | }, 12 | "dependencies": { 13 | "chrome-aws-lambda": "1.20.2", 14 | "jest": "24.9.0", 15 | "jest-puppeteer": "4.3.0", 16 | "puppeteer-core": "1.20.0" 17 | }, 18 | "devDependencies": { 19 | "@aws-cdk/aws-lambda": "1.13.1", 20 | "@aws-cdk/core": "1.13.1", 21 | "aws-cdk": "1.13.1", 22 | "aws-sdk": "2.551.0", 23 | "dotenv-cli": "3.0.0", 24 | "@mikebild/ssmenv-cli": "^1.0.0", 25 | "prettier": "1.18.2", 26 | "puppeteer": "1.20.0", 27 | "rimraf": "3.0.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/e2e-tests/runner.js: -------------------------------------------------------------------------------- 1 | const jest = require('jest') 2 | const chrome = require('chrome-aws-lambda') 3 | 4 | module.exports = { run } 5 | 6 | async function run() { 7 | try { 8 | return await jest.runCLI({ rootDir: './tests', json: true, silent: true, runInBand: true }, [__dirname]) 9 | } catch (error) { 10 | return error 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/e2e-tests/tests/sales-app/signin.test.js: -------------------------------------------------------------------------------- 1 | const chrome = require('chrome-aws-lambda') 2 | const username = process.env.CDK_E2E_USERNAME 3 | const password = process.env.CDK_E2E_PASSWORD 4 | const baseUrl = process.env.CDK_E2E_SALES_APP_URL 5 | 6 | describe('Sales App', () => { 7 | describe('Signin Page', () => { 8 | jest.setTimeout(200000) 9 | let browser 10 | let page 11 | 12 | beforeAll(async () => { 13 | browser = await chrome.puppeteer.launch({ 14 | args: chrome.args, 15 | defaultViewport: chrome.defaultViewport, 16 | executablePath: await chrome.executablePath, 17 | timeout: 200000, 18 | // slowMo: 100, 19 | // headless: chrome.headless, 20 | // devtools: true, 21 | }) 22 | page = await browser.newPage() 23 | await page.goto(baseUrl, { waitUntil: 'networkidle2' }) 24 | }) 25 | 26 | afterAll(async () => { 27 | await browser.close() 28 | }) 29 | 30 | test('signed out user, should redirect to "/signin"', async () => { 31 | await expect(page.target().url()).toBe(`${baseUrl}/signin`) 32 | }) 33 | 34 | test('page, should have page title "E-Commerce Sales"', async () => { 35 | await expect(page.title()).resolves.toBe('E-Commerce Sales') 36 | }) 37 | 38 | test('page, should display button "ANMELDEN"', async () => { 39 | const loginBtnText = await page.$eval('button', e => e.innerText) 40 | await expect(loginBtnText).toBe('ANMELDEN') 41 | }) 42 | 43 | test('"ANMELDEN" click without username, should display a error message "Username cannot be empty"', async () => { 44 | const loginBtn = await page.$('[data-testid="login-button"]') 45 | await loginBtn.click() 46 | const errorMessage = await page.$eval('[data-testid="error-message"]', e => e.innerText) 47 | await expect(errorMessage).toBe('Username cannot be empty') 48 | }) 49 | 50 | test(`"ANMELDEN" click with "${username}", should signin`, async () => { 51 | const usernameInput = await page.$('#email') 52 | await usernameInput.type(username) 53 | const passwordInput = await page.$('#password') 54 | await passwordInput.type(password) 55 | 56 | const loginBtn = await page.$('[data-testid="login-button"]') 57 | await Promise.all([loginBtn.click(), browser.waitForTarget(target => target.url() === `${baseUrl}/`)]) 58 | }) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /packages/e2e-tests/tests/shop-app/signin.test.js: -------------------------------------------------------------------------------- 1 | const chrome = require('chrome-aws-lambda') 2 | const username = process.env.CDK_E2E_USERNAME 3 | const password = process.env.CDK_E2E_PASSWORD 4 | const baseUrl = process.env.CDK_E2E_SHOP_APP_URL 5 | 6 | describe('Shop App', () => { 7 | describe('Signin Page', () => { 8 | jest.setTimeout(200000) 9 | let browser 10 | let page 11 | 12 | beforeAll(async () => { 13 | browser = await chrome.puppeteer.launch({ 14 | args: chrome.args, 15 | defaultViewport: chrome.defaultViewport, 16 | executablePath: await chrome.executablePath, 17 | timeout: 200000, 18 | // slowMo: 100, 19 | // headless: chrome.headless, 20 | // devtools: true, 21 | }) 22 | page = await browser.newPage() 23 | await page.goto(baseUrl, { waitUntil: 'networkidle2' }) 24 | }) 25 | 26 | afterAll(async () => { 27 | await browser.close() 28 | }) 29 | 30 | test('signed out user, should redirect to "/signin"', async () => { 31 | await expect(page.target().url()).toBe(`${baseUrl}/signin`) 32 | }) 33 | 34 | test('page, should have page title "E-Commerce Shop"', async () => { 35 | await expect(page.title()).resolves.toBe('E-Commerce Shop') 36 | }) 37 | 38 | test('page, should display button "ANMELDEN"', async () => { 39 | const loginBtnText = await page.$eval('button', e => e.innerText) 40 | await expect(loginBtnText).toBe('ANMELDEN') 41 | }) 42 | 43 | test('"ANMELDEN" click without username, should display a error message "Username cannot be empty"', async () => { 44 | const loginBtn = await page.$('[data-testid="login-button"]') 45 | await loginBtn.click() 46 | const errorMessage = await page.$eval('[data-testid="error-message"]', e => e.innerText) 47 | await expect(errorMessage).toBe('Username cannot be empty') 48 | }) 49 | 50 | test(`"ANMELDEN" click with "${username}", should signin`, async () => { 51 | const usernameInput = await page.$('#email') 52 | await usernameInput.type(username) 53 | const passwordInput = await page.$('#password') 54 | await passwordInput.type(password) 55 | 56 | const loginBtn = await page.$('[data-testid="login-button"]') 57 | await Promise.all([loginBtn.click(), browser.waitForTarget(target => target.url() === `${baseUrl}/`)]) 58 | }) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /packages/infrastructure/.env-template: -------------------------------------------------------------------------------- 1 | CDK_STACK_NAME= 2 | CDK_STACK_ENV= 3 | CDK_AWS_REGION=eu-central-1 4 | CDK_AWS_ACCOUNT= 5 | 6 | CDK_AWS_ROUTE53_HOSTED_ZONEID= 7 | CDK_AWS_CLOUDFRONT_CERTIFICATE_ARN= 8 | 9 | CDK_COGNITO_DEFAULT_USERNAME= 10 | CDK_COGNITO_DEFAULT_GROUPNAME= 11 | 12 | CDK_SHOP_APP_HOSTNAME= 13 | CDK_SHOP_APP_DOMAIN= 14 | 15 | CDK_SALES_APP_HOSTNAME= 16 | CDK_SALES_APP_DOMAIN= 17 | 18 | CDK_STORYBOOK_APP_HOSTNAME= 19 | CDK_STORYBOOK_APP_DOMAIN= 20 | 21 | CDK_E2E_BASE_URL= 22 | CDK_E2E_USERNAME= 23 | CDK_E2E_PASSWORD= -------------------------------------------------------------------------------- /packages/infrastructure/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | build 4 | .cache 5 | .DS_Store 6 | node_modules/ 7 | .env 8 | .env.development 9 | .env.production 10 | 11 | # Logs 12 | logs 13 | *.log 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | 18 | .cache/ 19 | public 20 | build 21 | dist 22 | storybook-static 23 | 24 | tmp 25 | cdk.context.json 26 | .cdk.staging 27 | cdk.out -------------------------------------------------------------------------------- /packages/infrastructure/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "bracketSpacing": true, 6 | "jsxBracketSameLine": false, 7 | "semi": false 8 | } 9 | -------------------------------------------------------------------------------- /packages/infrastructure/README.md: -------------------------------------------------------------------------------- 1 | # AWS Serverless Infrastructure as Code 2 | 3 | ## Deploy environments 4 | 5 | ### Setup environment "Demo" 6 | 7 | **Configure** the AWS SSM parameter store 8 | 9 | **.env** 10 | 11 | ``` 12 | CDK_STACK_NAME= 13 | CDK_STACK_ENV=Demo 14 | CDK_AWS_REGION=eu-central-1 15 | CDK_AWS_ACCOUNT= 16 | ``` 17 | 18 | **.env.Demo** 19 | 20 | ``` 21 | CDK_AWS_ROUTE53_HOSTED_ZONEID= 22 | CDK_AWS_CLOUDFRONT_CERTIFICATE_ARN= 23 | 24 | CDK_COGNITO_DEFAULT_USERNAME= 25 | CDK_COGNITO_DEFAULT_GROUPNAME= 26 | 27 | CDK_SHOP_APP_HOSTNAME= 28 | CDK_SHOP_APP_DOMAIN= 29 | 30 | CDK_SALES_APP_HOSTNAME= 31 | CDK_SALES_APP_DOMAIN= 32 | 33 | CDK_STORYBOOK_APP_HOSTNAME= 34 | CDK_STORYBOOK_APP_DOMAIN= 35 | 36 | CDK_E2E_BASE_URL= 37 | CDK_E2E_USERNAME= 38 | CDK_E2E_PASSWORD= 39 | ``` 40 | 41 | **Deploy Configuration** 42 | 43 | ``` 44 | CDK_STACK_ENV=Demo yarn deploy:configs "ECommerce-*" 45 | ``` 46 | 47 | **Deploy Backends** 48 | 49 | ``` 50 | yarn deploy:backends "ECommerce-*" 51 | ``` 52 | 53 | **Deploy Frontends** 54 | 55 | ``` 56 | yarn deploy:frontends "ECommerce-*" 57 | ``` 58 | -------------------------------------------------------------------------------- /packages/infrastructure/cognito-stack.js: -------------------------------------------------------------------------------- 1 | const { Stack, CfnOutput } = require('@aws-cdk/core') 2 | const { StringParameter } = require('@aws-cdk/aws-ssm') 3 | 4 | const { 5 | UserPool, 6 | UserPoolClient, 7 | UserPoolAttribute, 8 | AuthFlow, 9 | CfnUserPoolGroup, 10 | CfnUserPoolUser, 11 | } = require('@aws-cdk/aws-cognito') 12 | 13 | module.exports = class Cognito extends Stack { 14 | constructor(parent, id, props) { 15 | super(parent, id, props) 16 | 17 | const { CDK_COGNITO_DEFAULT_GROUPNAME, CDK_COGNITO_DEFAULT_USERNAME, CDK_STACK_NAME, CDK_STACK_ENV } = props 18 | 19 | this.userPool = new UserPool(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-UserPool`, { 20 | userPoolName: `${CDK_STACK_NAME}-${CDK_STACK_ENV}-UserPool`, 21 | autoVerifiedAttributes: [UserPoolAttribute.EMAIL], 22 | adminCreateUserConfig: { 23 | allowAdminCreateUserOnly: false, 24 | }, 25 | policies: { 26 | passwordPolicy: { 27 | minimumLength: 6, 28 | requireLowercase: false, 29 | requireNumbers: false, 30 | requireSymbols: false, 31 | requireUppercase: false, 32 | }, 33 | }, 34 | schema: [ 35 | { 36 | attributeDataType: 'String', 37 | name: 'email', 38 | required: true, 39 | }, 40 | ], 41 | }) 42 | 43 | new CfnOutput(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-UserPoolClient-UserPoolId`, { 44 | value: this.userPool.userPoolId, 45 | }) 46 | 47 | new StringParameter(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-UserPoolClient-UserPoolId-Parameter`, { 48 | parameterName: `/${CDK_STACK_NAME}/${CDK_STACK_ENV}/CDK_AWS_COGNITO_USER_POOL_ID`, 49 | stringValue: this.userPool.userPoolId, 50 | }) 51 | 52 | const userPoolClient = new UserPoolClient(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-UserPoolClient`, { 53 | clientName: `${CDK_STACK_NAME}-${CDK_STACK_ENV}-UserPoolClient`, 54 | enabledAuthFlows: [AuthFlow.ADMIN_NO_SRP], 55 | refreshTokenValidity: 30, 56 | generateSecret: false, 57 | userPoolClientName: 'ECommerceClients', 58 | userPool: this.userPool, 59 | }) 60 | 61 | new CfnOutput(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-UserPoolClient-UserPoolClientId`, { 62 | value: userPoolClient.userPoolClientId, 63 | }) 64 | 65 | new StringParameter(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-UserPoolClient-UserPoolClientId-Parameter`, { 66 | parameterName: `/${CDK_STACK_NAME}/${CDK_STACK_ENV}/CDK_AWS_COGNITO_USER_POOL_WEBCLIENT_ID`, 67 | stringValue: userPoolClient.userPoolClientId, 68 | }) 69 | 70 | const defaultUser = new CfnUserPoolUser(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-DefaultUser`, { 71 | username: CDK_COGNITO_DEFAULT_USERNAME, 72 | userPoolId: this.userPool.userPoolId, 73 | desiredDeliveryMediums: ['EMAIL'], 74 | userAttributes: [ 75 | { 76 | name: 'email', 77 | value: CDK_COGNITO_DEFAULT_USERNAME, 78 | }, 79 | ], 80 | }) 81 | 82 | const defaultGroup = new CfnUserPoolGroup(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-DefaultGroup`, { 83 | groupName: CDK_COGNITO_DEFAULT_GROUPNAME, 84 | userPoolId: this.userPool.userPoolId, 85 | }) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /packages/infrastructure/cognito-users-to-groups.js: -------------------------------------------------------------------------------- 1 | const { Stack } = require('@aws-cdk/core') 2 | 3 | const { CfnUserPoolUserToGroupAttachment } = require('@aws-cdk/aws-cognito') 4 | 5 | module.exports = class Cognito extends Stack { 6 | constructor(parent, id, props) { 7 | super(parent, id, props) 8 | 9 | const { defaultGroup, defaultUser, CDK_STACK_NAME, CDK_STACK_ENV } = props 10 | 11 | new CfnUserPoolUserToGroupAttachment(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-DefaultUserToGroupAttachment`, { 12 | userPoolId: this.userPool.userPoolId, 13 | groupName: defaultGroup.groupName, 14 | username: defaultUser.username, 15 | }) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/infrastructure/configs-stack.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv') 2 | const { Stack } = require('@aws-cdk/core') 3 | const { StringParameter } = require('@aws-cdk/aws-ssm') 4 | 5 | module.exports = class Env extends Stack { 6 | constructor(parent, id, props) { 7 | super(parent, id, props) 8 | 9 | const { CDK_STACK_NAME, CDK_STACK_ENV } = props 10 | this.configs = {} 11 | 12 | this.configs.CDK_AWS_REGION = StringParameter.valueFromLookup( 13 | this, 14 | `/${CDK_STACK_NAME}/${CDK_STACK_ENV}/CDK_AWS_REGION` 15 | ) 16 | this.configs.CDK_AWS_ACCOUNT = StringParameter.valueFromLookup( 17 | this, 18 | `/${CDK_STACK_NAME}/${CDK_STACK_ENV}/CDK_AWS_ACCOUNT` 19 | ) 20 | this.configs.CDK_AWS_ROUTE53_HOSTED_ZONEID = StringParameter.valueFromLookup( 21 | this, 22 | `/${CDK_STACK_NAME}/${CDK_STACK_ENV}/CDK_AWS_ROUTE53_HOSTED_ZONEID` 23 | ) 24 | this.configs.CDK_AWS_CLOUDFRONT_CERTIFICATE_ARN = StringParameter.valueFromLookup( 25 | this, 26 | `/${CDK_STACK_NAME}/${CDK_STACK_ENV}/CDK_AWS_CLOUDFRONT_CERTIFICATE_ARN` 27 | ) 28 | 29 | this.configs.CDK_COGNITO_DEFAULT_USERNAME = StringParameter.valueFromLookup( 30 | this, 31 | `/${CDK_STACK_NAME}/${CDK_STACK_ENV}/CDK_COGNITO_DEFAULT_USERNAME` 32 | ) 33 | this.configs.CDK_COGNITO_DEFAULT_GROUPNAME = StringParameter.valueFromLookup( 34 | this, 35 | `/${CDK_STACK_NAME}/${CDK_STACK_ENV}/CDK_COGNITO_DEFAULT_GROUPNAME` 36 | ) 37 | 38 | this.configs.CDK_SHOP_APP_HOSTNAME = StringParameter.valueFromLookup( 39 | this, 40 | `/${CDK_STACK_NAME}/${CDK_STACK_ENV}/CDK_SHOP_APP_HOSTNAME` 41 | ) 42 | this.configs.CDK_SHOP_APP_DOMAIN = StringParameter.valueFromLookup( 43 | this, 44 | `/${CDK_STACK_NAME}/${CDK_STACK_ENV}/CDK_SHOP_APP_DOMAIN` 45 | ) 46 | 47 | this.configs.CDK_SALES_APP_HOSTNAME = StringParameter.valueFromLookup( 48 | this, 49 | `/${CDK_STACK_NAME}/${CDK_STACK_ENV}/CDK_SALES_APP_HOSTNAME` 50 | ) 51 | this.configs.CDK_SALES_APP_DOMAIN = StringParameter.valueFromLookup( 52 | this, 53 | `/${CDK_STACK_NAME}/${CDK_STACK_ENV}/CDK_SALES_APP_DOMAIN` 54 | ) 55 | 56 | this.configs.CDK_STORYBOOK_APP_HOSTNAME = StringParameter.valueFromLookup( 57 | this, 58 | `/${CDK_STACK_NAME}/${CDK_STACK_ENV}/CDK_STORYBOOK_APP_HOSTNAME` 59 | ) 60 | this.configs.CDK_STORYBOOK_APP_DOMAIN = StringParameter.valueFromLookup( 61 | this, 62 | `/${CDK_STACK_NAME}/${CDK_STACK_ENV}/CDK_STORYBOOK_APP_DOMAIN` 63 | ) 64 | 65 | this.configs.CDK_E2E_BASE_URL = StringParameter.valueFromLookup( 66 | this, 67 | `/${CDK_STACK_NAME}/${CDK_STACK_ENV}/CDK_E2E_BASE_URL` 68 | ) 69 | this.configs.CDK_E2E_SALES_APP_URL = StringParameter.valueFromLookup( 70 | this, 71 | `/${CDK_STACK_NAME}/${CDK_STACK_ENV}/CDK_E2E_SALES_APP_URL` 72 | ) 73 | this.configs.CDK_E2E_SHOP_APP_URL = StringParameter.valueFromLookup( 74 | this, 75 | `/${CDK_STACK_NAME}/${CDK_STACK_ENV}/CDK_E2E_SHOP_APP_URL` 76 | ) 77 | this.configs.CDK_E2E_USERNAME = StringParameter.valueFromLookup( 78 | this, 79 | `/${CDK_STACK_NAME}/${CDK_STACK_ENV}/CDK_E2E_USERNAME` 80 | ) 81 | this.configs.CDK_E2E_PASSWORD = StringParameter.valueFromLookup( 82 | this, 83 | `/${CDK_STACK_NAME}/${CDK_STACK_ENV}/CDK_E2E_PASSWORD` 84 | ) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /packages/infrastructure/data-imports/cart.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "mike@mikebild.com", 4 | "entity": "Cart", 5 | "productIds": ["book1", "book2"] 6 | }, 7 | { 8 | "id": "heike@mikebild.com", 9 | "entity": "Cart", 10 | "productIds": ["book1"] 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /packages/infrastructure/data-imports/import-all.sh: -------------------------------------------------------------------------------- 1 | TABLE=$1 2 | 3 | ./import.sh $TABLE profile.json 4 | ./import.sh $TABLE product.json 5 | ./import.sh $TABLE cart.json -------------------------------------------------------------------------------- /packages/infrastructure/data-imports/import.sh: -------------------------------------------------------------------------------- 1 | TABLE=$1 2 | FILE=$2 3 | 4 | npx jdp $TABLE ./$FILE --beautify --output ./tmp.json 5 | aws dynamodb batch-write-item --return-consumed-capacity TOTAL --request-items file://./tmp.json 6 | rm ./tmp.json -------------------------------------------------------------------------------- /packages/infrastructure/data-imports/product.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "book1", 4 | "entity": "Product", 5 | "title": "Book 1", 6 | "description": "A first book", 7 | "price": 1.2, 8 | "logoUrl": "https://", 9 | "category": "Book" 10 | }, 11 | { 12 | "id": "book2", 13 | "entity": "Product", 14 | "title": "Book 2", 15 | "description": "A second book", 16 | "price": 12.2, 17 | "logoUrl": "https://", 18 | "category": "Book" 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /packages/infrastructure/data-imports/profile.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "mike@mikebild.com", 4 | "entity": "Profile", 5 | "firstName": "Mike", 6 | "lastName": "Bild" 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /packages/infrastructure/deploy-backends.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv') 2 | const { App, Stack } = require('@aws-cdk/core') 3 | const Configs = require('./configs-stack') 4 | const Cognito = require('./cognito-stack') 5 | const GraphQL = require('./graphql-stack') 6 | 7 | const app = new App({ autoSynth: true }) 8 | const envVars = { ...dotenv.config().parsed, ...process.env } 9 | const config = { 10 | ...envVars, 11 | env: { account: envVars.CDK_AWS_ACCOUNT, region: envVars.CDK_AWS_REGION }, 12 | } 13 | 14 | const { configs } = new Configs(app, `${config.CDK_STACK_NAME}-${config.CDK_STACK_ENV}-Configs`, { ...config }) 15 | const { userPool } = new Cognito(app, `${config.CDK_STACK_NAME}-${config.CDK_STACK_ENV}-Cognito`, { 16 | ...config, 17 | ...configs, 18 | }) 19 | const { graphQlApi, graphqlApiKey } = new GraphQL(app, `${config.CDK_STACK_NAME}-${config.CDK_STACK_ENV}-GraphQL`, { 20 | ...config, 21 | ...configs, 22 | userPool, 23 | }) 24 | -------------------------------------------------------------------------------- /packages/infrastructure/deploy-configs.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv') 2 | const { App, Stack } = require('@aws-cdk/core') 3 | const SSM = require('./ssm-stack') 4 | 5 | const stackEnv = process.env.CDK_STACK_ENV 6 | 7 | if (!stackEnv) { 8 | console.error(`CDK_STACK_ENV environment variable is missing.`) 9 | process.exit(1) 10 | } 11 | 12 | const app = new App({ autoSynth: true }) 13 | const envVars = { ...dotenv.config({ path: `.env.${stackEnv}` }).parsed } 14 | const config = { 15 | ...envVars, 16 | env: { account: envVars.CDK_AWS_ACCOUNT, region: envVars.CDK_AWS_REGION }, 17 | } 18 | 19 | new SSM(app, `${config.CDK_STACK_NAME}-${config.CDK_STACK_ENV}-SSM`, { ...config }) 20 | -------------------------------------------------------------------------------- /packages/infrastructure/deploy-frontends.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv') 2 | const { App } = require('@aws-cdk/core') 3 | const Configs = require('./configs-stack') 4 | const E2ETests = require('./e2e-stack') 5 | const SalesApp = require('./sales-app-stack') 6 | const ShopApp = require('./shop-app-stack') 7 | const StorybookApp = require('./storybook-app-stack') 8 | 9 | const app = new App({ autoSynth: true }) 10 | const envVars = { ...dotenv.config().parsed, ...process.env } 11 | const config = { 12 | ...envVars, 13 | env: { account: envVars.CDK_AWS_ACCOUNT, region: envVars.CDK_AWS_REGION }, 14 | } 15 | 16 | const { configs } = new Configs(app, `${config.CDK_STACK_NAME}-${config.CDK_STACK_ENV}-Configs`, { ...config }) 17 | new E2ETests(app, `${config.CDK_STACK_NAME}-${config.CDK_STACK_ENV}-E2ETests`, { ...config, ...configs }) 18 | new SalesApp(app, `${config.CDK_STACK_NAME}-${config.CDK_STACK_ENV}-SalesApp`, { ...config, ...configs }) 19 | new ShopApp(app, `${config.CDK_STACK_NAME}-${config.CDK_STACK_ENV}-ShopApp`, { ...config, ...configs }) 20 | new StorybookApp(app, `${config.CDK_STACK_NAME}-${config.CDK_STACK_ENV}-StorybookApp`, { ...config, ...configs }) 21 | -------------------------------------------------------------------------------- /packages/infrastructure/e2e-stack.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path') 2 | const { Stack, Duration } = require('@aws-cdk/core') 3 | const { Function, Runtime, Code } = require('@aws-cdk/aws-lambda') 4 | 5 | module.exports = class E2ETests extends Stack { 6 | constructor(parent, id, props) { 7 | super(parent, id, props) 8 | 9 | const { 10 | CDK_STACK_NAME, 11 | CDK_STACK_ENV, 12 | CDK_E2E_BASE_URL, 13 | CDK_E2E_SALES_APP_URL, 14 | CDK_E2E_SHOP_APP_URL, 15 | CDK_E2E_USERNAME, 16 | CDK_E2E_PASSWORD, 17 | } = props 18 | 19 | new Function(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-E2ETests-Function`, { 20 | functionName: `${CDK_STACK_NAME}-${CDK_STACK_ENV}-E2ETests-Function`, 21 | runtime: Runtime.NODEJS_8_10, 22 | handler: 'runner.run', 23 | timeout: Duration.seconds(300), 24 | memorySize: 768, 25 | code: Code.fromAsset(join(__dirname, '../e2e-tests/build')), 26 | environment: { 27 | CDK_E2E_BASE_URL, 28 | CDK_E2E_SALES_APP_URL, 29 | CDK_E2E_SHOP_APP_URL, 30 | CDK_E2E_USERNAME, 31 | CDK_E2E_PASSWORD, 32 | }, 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/infrastructure/graphql-stack.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path') 2 | const { readFileSync } = require('fs') 3 | const { Stack, CfnOutput } = require('@aws-cdk/core') 4 | const { StringParameter } = require('@aws-cdk/aws-ssm') 5 | const { Role, PolicyStatement, Effect, ServicePrincipal } = require('@aws-cdk/aws-iam') 6 | const { CfnGraphQLApi, CfnApiKey, CfnGraphQLSchema, CfnDataSource } = require('@aws-cdk/aws-appsync') 7 | 8 | const DataSources = require('./graphql/datasources') 9 | const ProfileResolver = require('./graphql/resolvers/profile') 10 | const ProductResolver = require('./graphql/resolvers/product') 11 | const CartResolver = require('./graphql/resolvers/cart') 12 | const OrderResolver = require('./graphql/resolvers/order') 13 | const InvoiceResolver = require('./graphql/resolvers/invoice') 14 | const ImageResolver = require('./graphql/resolvers/image') 15 | const MeResolver = require('./graphql/resolvers/me') 16 | 17 | module.exports = class GraphQL extends Stack { 18 | constructor(parent, id, props) { 19 | super(parent, id, props) 20 | 21 | const { CDK_STACK_NAME, CDK_STACK_ENV, CDK_AWS_REGION, userPool, deliveryPublishLambda } = props 22 | const definition = readFileSync(join(__dirname, 'graphql', 'schema.graphql')).toString() 23 | 24 | const logsServiceRole = new Role(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-GraphQLLogsRole`, { 25 | assumedBy: new ServicePrincipal('appsync.amazonaws.com'), 26 | }) 27 | 28 | const logsServicePolicyStatement = new PolicyStatement(Effect.Allow) 29 | logsServicePolicyStatement.addActions(['logs:*']) 30 | logsServicePolicyStatement.addAllResources() 31 | 32 | logsServiceRole.addToPolicy(logsServicePolicyStatement) 33 | 34 | this.graphQlApi = new CfnGraphQLApi(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-GraphQL`, { 35 | name: `${CDK_STACK_NAME}-${CDK_STACK_ENV}-GraphQL`, 36 | authenticationType: 'API_KEY', 37 | additionalAuthenticationProviders: [ 38 | { 39 | authenticationType: 'AMAZON_COGNITO_USER_POOLS', 40 | userPoolConfig: { 41 | awsRegion: CDK_AWS_REGION, 42 | userPoolId: userPool.userPoolId, 43 | }, 44 | }, 45 | ], 46 | logConfig: { 47 | cloudWatchLogsRoleArn: logsServiceRole.roleArn, 48 | fieldLogLevel: 'ALL', 49 | }, 50 | }) 51 | 52 | new CfnOutput(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-GraphQL-GraphQlUrl`, { 53 | value: this.graphQlApi.attrGraphQlUrl, 54 | }) 55 | 56 | new StringParameter(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-GraphQL-GraphQlUrl-Parameter`, { 57 | parameterName: `/${CDK_STACK_NAME}/${CDK_STACK_ENV}/CDK_AWS_APPSYNC_URL`, 58 | stringValue: this.graphQlApi.attrGraphQlUrl, 59 | }) 60 | 61 | const now = new Date() 62 | now.setSeconds(now.getSeconds() + 31536000) //add 365 days 63 | const expires = Math.round(now.getTime() / 1000) 64 | this.graphqlApiKey = new CfnApiKey(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-GraphQL-ApiKey`, { 65 | apiId: this.graphQlApi.attrApiId, 66 | expires, 67 | }) 68 | 69 | new CfnOutput(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-GraphQL-GraphQlApiKey`, { 70 | value: this.graphqlApiKey.attrApiKey, 71 | }) 72 | 73 | new StringParameter(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-GraphQL-GraphQlApiKey-Parameter`, { 74 | parameterName: `/${CDK_STACK_NAME}/${CDK_STACK_ENV}/CDK_AWS_APPSYNC_APIKEY`, 75 | stringValue: this.graphqlApiKey.attrApiKey, 76 | }) 77 | 78 | new CfnGraphQLSchema(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-GraphQL-Schema`, { 79 | apiId: this.graphQlApi.attrApiId, 80 | definition, 81 | }) 82 | 83 | const lambdaServiceRole = new Role(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-GraphQL-LambdaRole`, { 84 | assumedBy: new ServicePrincipal('appsync.amazonaws.com'), 85 | }) 86 | 87 | const lambdaServicePolicyStatement = new PolicyStatement(Effect.Allow) 88 | lambdaServicePolicyStatement.addActions(['lambda:*']) 89 | lambdaServicePolicyStatement.addAllResources() 90 | 91 | lambdaServiceRole.addToPolicy(lambdaServicePolicyStatement) 92 | 93 | const appSyncServiceRole = new Role(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-GraphQL-DynamoDBRole`, { 94 | assumedBy: new ServicePrincipal('appsync.amazonaws.com'), 95 | }) 96 | 97 | const dynamoDBPolicyStatement = new PolicyStatement(Effect.Allow) 98 | dynamoDBPolicyStatement.addActions(['dynamodb:*']) 99 | dynamoDBPolicyStatement.addAllResources() 100 | 101 | appSyncServiceRole.addToPolicy(dynamoDBPolicyStatement) 102 | 103 | const lambdaPolicyStatement = new PolicyStatement(Effect.Allow) 104 | lambdaPolicyStatement.addActions(['lambda:*']) 105 | lambdaPolicyStatement.addAllResources() 106 | 107 | appSyncServiceRole.addToPolicy(lambdaPolicyStatement) 108 | 109 | const { parentDataSource, dynamoDBDataSource } = new DataSources( 110 | this, 111 | `${CDK_STACK_NAME}-${CDK_STACK_ENV}-DataSources`, 112 | { 113 | CDK_STACK_NAME, 114 | CDK_STACK_ENV, 115 | CDK_AWS_REGION, 116 | appSyncServiceRole, 117 | graphQlApi: this.graphQlApi, 118 | deliveryPublishLambda, 119 | } 120 | ) 121 | 122 | new MeResolver(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-MeResolver`, { 123 | CDK_STACK_NAME, 124 | CDK_STACK_ENV, 125 | parentDataSource, 126 | graphQlApi: this.graphQlApi, 127 | }) 128 | 129 | new ProfileResolver(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-ProfileResolver`, { 130 | CDK_STACK_NAME, 131 | CDK_STACK_ENV, 132 | dynamoDBDataSource, 133 | graphQlApi: this.graphQlApi, 134 | }) 135 | 136 | new ProductResolver(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-ProductResolver`, { 137 | CDK_STACK_NAME, 138 | CDK_STACK_ENV, 139 | dynamoDBDataSource, 140 | graphQlApi: this.graphQlApi, 141 | }) 142 | 143 | new CartResolver(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-CartResolver`, { 144 | CDK_STACK_NAME, 145 | CDK_STACK_ENV, 146 | dynamoDBDataSource, 147 | graphQlApi: this.graphQlApi, 148 | }) 149 | 150 | new OrderResolver(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-OrderResolver`, { 151 | CDK_STACK_NAME, 152 | CDK_STACK_ENV, 153 | dynamoDBDataSource, 154 | graphQlApi: this.graphQlApi, 155 | }) 156 | 157 | new InvoiceResolver(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-InvoiceResolver`, { 158 | CDK_STACK_NAME, 159 | CDK_STACK_ENV, 160 | dynamoDBDataSource, 161 | graphQlApi: this.graphQlApi, 162 | }) 163 | 164 | new ImageResolver(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-ImageResolver`, { 165 | CDK_STACK_NAME, 166 | CDK_STACK_ENV, 167 | dynamoDBDataSource, 168 | graphQlApi: this.graphQlApi, 169 | }) 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /packages/infrastructure/graphql/datasources.js: -------------------------------------------------------------------------------- 1 | const { Construct, RemovalPolicy } = require('@aws-cdk/core') 2 | const { CfnDataSource } = require('@aws-cdk/aws-appsync') 3 | const { Table, AttributeType, BillingMode } = require('@aws-cdk/aws-dynamodb') 4 | 5 | module.exports = class DynamoDBDataSource extends Construct { 6 | constructor(scope, id, props) { 7 | super(scope, id) 8 | 9 | const { 10 | graphQlApi, 11 | appSyncServiceRole, 12 | deliveryPublishLambda, 13 | CDK_STACK_NAME, 14 | CDK_STACK_ENV, 15 | CDK_AWS_REGION, 16 | } = props 17 | 18 | const dynamoDBTable = new Table(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-DynamoDB-Table`, { 19 | partitionKey: { name: 'id', type: AttributeType.STRING }, 20 | sortKey: { name: 'entity', type: AttributeType.STRING }, 21 | tableName: `${CDK_STACK_NAME}-Table-${CDK_STACK_ENV}`, 22 | billingMode: BillingMode.PAY_PER_REQUEST, 23 | removalPolicy: RemovalPolicy.DESTROY, 24 | timeToLiveAttribute: 'expire', 25 | }) 26 | 27 | this.dynamoDBDataSource = new CfnDataSource(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-DynamoDB-DataSource`, { 28 | name: 'DynamoDB', 29 | type: 'AMAZON_DYNAMODB', 30 | apiId: graphQlApi.attrApiId, 31 | serviceRoleArn: appSyncServiceRole.roleArn, 32 | dynamoDbConfig: { 33 | tableName: dynamoDBTable.tableName, 34 | awsRegion: CDK_AWS_REGION, 35 | useCallerCredentials: false, 36 | }, 37 | }) 38 | 39 | this.parentDataSource = new CfnDataSource(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-Parent-DataSource`, { 40 | name: 'Parent', 41 | type: 'NONE', 42 | apiId: graphQlApi.attrApiId, 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/infrastructure/graphql/resolvers/cart.js: -------------------------------------------------------------------------------- 1 | const { Construct } = require('@aws-cdk/core') 2 | const { CfnResolver } = require('@aws-cdk/aws-appsync') 3 | 4 | module.exports = class CartResolver extends Construct { 5 | constructor(scope, id, props) { 6 | super(scope, id) 7 | 8 | const { graphQlApi, dynamoDBDataSource, CDK_STACK_NAME, CDK_STACK_ENV } = props 9 | 10 | new CfnResolver(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-CartGetResolver`, { 11 | dataSourceName: dynamoDBDataSource.attrName, 12 | apiId: graphQlApi.attrApiId, 13 | fieldName: 'cartGet', 14 | typeName: 'Query', 15 | requestMappingTemplate: ` 16 | #set($entity = "Cart") 17 | 18 | { 19 | "version": "2017-02-28", 20 | "operation": "GetItem", 21 | "key" : { "id": $util.dynamodb.toDynamoDBJson($ctx.args.id), "entity": $util.dynamodb.toDynamoDBJson($entity) }, 22 | } 23 | `, 24 | responseMappingTemplate: `$util.toJson($ctx.result)`, 25 | }) 26 | 27 | new CfnResolver(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-CartMeResolver`, { 28 | dataSourceName: dynamoDBDataSource.attrName, 29 | apiId: graphQlApi.attrApiId, 30 | fieldName: 'cart', 31 | typeName: 'Me', 32 | requestMappingTemplate: ` 33 | { 34 | "version": "2017-02-28", 35 | "operation": "GetItem", 36 | "key": { 37 | "id": $util.dynamodb.toDynamoDBJson($ctx.identity.username), 38 | "entity": $util.dynamodb.toDynamoDBJson("Cart"), 39 | }, 40 | } 41 | `, 42 | responseMappingTemplate: `$util.toJson($ctx.result)`, 43 | }) 44 | 45 | new CfnResolver(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-CartProductsResolver`, { 46 | dataSourceName: dynamoDBDataSource.attrName, 47 | apiId: graphQlApi.attrApiId, 48 | fieldName: 'products', 49 | typeName: 'Cart', 50 | requestMappingTemplate: ` 51 | #set($ids = []) 52 | 53 | #foreach($id in \${ctx.source.productIds}) 54 | #set($map = {}) 55 | $util.qr($map.put("id", $util.dynamodb.toString($id))) 56 | $util.qr($map.put("entity", $util.dynamodb.toString("Product"))) 57 | $util.qr($ids.add($map)) 58 | #end 59 | 60 | { 61 | "version" : "2018-05-29", 62 | "operation" : "BatchGetItem", 63 | "tables" : { 64 | "${CDK_STACK_NAME}-Table-${CDK_STACK_ENV}": { 65 | "keys": #if($ctx.source.productIds.size() != 0) $util.toJson($ids) #else $util.toJson([{"id": $util.dynamodb.toString("undefined")}]) #end, 66 | "consistentRead": true 67 | } 68 | } 69 | } 70 | `, 71 | responseMappingTemplate: ` 72 | { 73 | "items": $util.toJson($ctx.result.data["${CDK_STACK_NAME}-Table-${CDK_STACK_ENV}"]), 74 | "nextToken": $util.toJson($util.defaultIfNullOrBlank($context.result.nextToken, null)) 75 | } 76 | `, 77 | }) 78 | 79 | new CfnResolver(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-CartUpsertMutationResolver`, { 80 | dataSourceName: dynamoDBDataSource.attrName, 81 | apiId: graphQlApi.attrApiId, 82 | fieldName: 'cartUpsert', 83 | typeName: 'Mutation', 84 | requestMappingTemplate: ` 85 | { 86 | "version" : "2017-02-28", 87 | "operation" : "PutItem", 88 | "key" : { "id": $util.dynamodb.toDynamoDBJson($ctx.identity.username), "entity": $util.dynamodb.toDynamoDBJson("Cart") }, 89 | "attributeValues" : $util.dynamodb.toMapValuesJson($ctx.args.input) 90 | } 91 | `, 92 | responseMappingTemplate: `$util.toJson($ctx.result)`, 93 | }) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /packages/infrastructure/graphql/resolvers/image.js: -------------------------------------------------------------------------------- 1 | const { Construct } = require('@aws-cdk/core') 2 | const { CfnResolver } = require('@aws-cdk/aws-appsync') 3 | 4 | module.exports = class ImageResolver extends Construct { 5 | constructor(scope, id, props) { 6 | super(scope, id) 7 | 8 | const { graphQlApi, dynamoDBDataSource, CDK_STACK_NAME, CDK_STACK_ENV } = props 9 | 10 | new CfnResolver(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-ImageListResolver`, { 11 | dataSourceName: dynamoDBDataSource.attrName, 12 | apiId: graphQlApi.attrApiId, 13 | fieldName: 'imageList', 14 | typeName: 'Query', 15 | requestMappingTemplate: ` 16 | #set($entity = "Image") 17 | #set($ctx.args.filter.entity = { "eq": $entity }) 18 | #set($userFilter = { "entity": { "eq": $entity } }) 19 | 20 | { 21 | "version": "2017-02-28", 22 | "operation": "Scan", 23 | "filter": #if($context.args.filter) $util.transform.toDynamoDBFilterExpression($ctx.args.filter) #else $util.transform.toDynamoDBFilterExpression($userFilter) #end, 24 | "limit": #if($ctx.args.limit) $ctx.args.limit #else null #end, 25 | "nextToken": $util.toJson($util.defaultIfNullOrEmpty($ctx.args.nextToken, null)), 26 | } 27 | `, 28 | responseMappingTemplate: ` 29 | { 30 | "items": $util.toJson($ctx.result.items), 31 | "nextToken": $util.toJson($util.defaultIfNullOrBlank($context.result.nextToken, null)) 32 | } 33 | `, 34 | }) 35 | 36 | new CfnResolver(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-ImageGetResolver`, { 37 | dataSourceName: dynamoDBDataSource.attrName, 38 | apiId: graphQlApi.attrApiId, 39 | fieldName: 'imageGet', 40 | typeName: 'Query', 41 | requestMappingTemplate: ` 42 | #set($entity = "Image") 43 | 44 | { 45 | "version": "2017-02-28", 46 | "operation": "GetItem", 47 | "key" : { "id": $util.dynamodb.toDynamoDBJson($ctx.args.id), "entity": $util.dynamodb.toDynamoDBJson($entity) }, 48 | }`, 49 | responseMappingTemplate: `$util.toJson($ctx.result)`, 50 | }) 51 | 52 | new CfnResolver(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-ImageUpsertMutationResolver`, { 53 | dataSourceName: dynamoDBDataSource.attrName, 54 | apiId: graphQlApi.attrApiId, 55 | fieldName: 'imageUpsert', 56 | typeName: 'Mutation', 57 | requestMappingTemplate: ` 58 | #if($ctx.args.input.id) #set($id = $ctx.args.input.id) #else #set($id = $util.autoId()) #end 59 | #set($entity = "Image") 60 | 61 | { 62 | "version" : "2017-02-28", 63 | "operation" : "PutItem", 64 | "key" : { "id": $util.dynamodb.toDynamoDBJson($id), "entity": $util.dynamodb.toDynamoDBJson($entity) }, 65 | "attributeValues" : $util.dynamodb.toMapValuesJson($ctx.args.input) 66 | } 67 | `, 68 | responseMappingTemplate: `$util.toJson($ctx.result)`, 69 | }) 70 | 71 | new CfnResolver(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-ImageDeleteMutationResolver`, { 72 | dataSourceName: dynamoDBDataSource.attrName, 73 | apiId: graphQlApi.attrApiId, 74 | fieldName: 'imageDelete', 75 | typeName: 'Mutation', 76 | requestMappingTemplate: ` 77 | #set($entity = "Image") 78 | 79 | { 80 | "version" : "2017-02-28", 81 | "operation" : "DeleteItem", 82 | "key" : { "id" : $util.dynamodb.toDynamoDBJson($ctx.args.id), "entity": $util.dynamodb.toDynamoDBJson($entity) } 83 | } 84 | `, 85 | responseMappingTemplate: `$util.toJson($ctx.result)`, 86 | }) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /packages/infrastructure/graphql/resolvers/invoice.js: -------------------------------------------------------------------------------- 1 | const { Construct } = require('@aws-cdk/core') 2 | const { CfnResolver } = require('@aws-cdk/aws-appsync') 3 | 4 | module.exports = class InvoiceResolver extends Construct { 5 | constructor(scope, id, props) { 6 | super(scope, id) 7 | 8 | const { graphQlApi, dynamoDBDataSource, CDK_STACK_NAME, CDK_STACK_ENV } = props 9 | 10 | new CfnResolver(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-InvoiceGetResolver`, { 11 | dataSourceName: dynamoDBDataSource.attrName, 12 | apiId: graphQlApi.attrApiId, 13 | fieldName: 'invoiceGet', 14 | typeName: 'Query', 15 | requestMappingTemplate: ` 16 | #set($entity = "Invoice") 17 | 18 | { 19 | "version": "2017-02-28", 20 | "operation": "GetItem", 21 | "key" : { "id": $util.dynamodb.toDynamoDBJson($ctx.args.id), "entity": $util.dynamodb.toDynamoDBJson($entity) }, 22 | } 23 | `, 24 | responseMappingTemplate: `$util.toJson($ctx.result)`, 25 | }) 26 | 27 | new CfnResolver(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-InvoiceForOrderMutationResolver`, { 28 | dataSourceName: dynamoDBDataSource.attrName, 29 | apiId: graphQlApi.attrApiId, 30 | fieldName: 'invoiceForOrder', 31 | typeName: 'Mutation', 32 | requestMappingTemplate: ` 33 | #if($ctx.args.input.id) #set($id = $ctx.args.input.id) #else #set($id = $util.autoId()) #end 34 | #set($entity = "Invoice") 35 | 36 | { 37 | "version" : "2017-02-28", 38 | "operation" : "PutItem", 39 | "key" : { "id": $util.dynamodb.toDynamoDBJson($id), "entity": $util.dynamodb.toDynamoDBJson($entity) }, 40 | "attributeValues" : $util.dynamodb.toMapValuesJson($ctx.args.input) 41 | } 42 | `, 43 | responseMappingTemplate: `$util.toJson($ctx.result)`, 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/infrastructure/graphql/resolvers/me.js: -------------------------------------------------------------------------------- 1 | const { Construct } = require('@aws-cdk/core') 2 | const { CfnResolver } = require('@aws-cdk/aws-appsync') 3 | 4 | module.exports = class MeResolver extends Construct { 5 | constructor(scope, id, props) { 6 | super(scope, id) 7 | 8 | const { graphQlApi, parentDataSource, CDK_STACK_NAME, CDK_STACK_ENV } = props 9 | 10 | new CfnResolver(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-MeGetResolver`, { 11 | dataSourceName: parentDataSource.attrName, 12 | apiId: graphQlApi.attrApiId, 13 | fieldName: 'me', 14 | typeName: 'Query', 15 | requestMappingTemplate: `{"version": "2017-02-28", "payload": {}}`, 16 | responseMappingTemplate: `$util.toJson({})`, 17 | }) 18 | 19 | new CfnResolver(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-MeUserResolver`, { 20 | dataSourceName: parentDataSource.attrName, 21 | apiId: graphQlApi.attrApiId, 22 | fieldName: 'user', 23 | typeName: 'Me', 24 | requestMappingTemplate: `{"version": "2017-02-28", "payload": {}}`, 25 | responseMappingTemplate: ` 26 | #set($result = { 27 | "username": $ctx.identity.username, 28 | "id": $ctx.identity.username 29 | }) 30 | 31 | $util.toJson($result) 32 | `, 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/infrastructure/graphql/resolvers/order.js: -------------------------------------------------------------------------------- 1 | const { Construct } = require('@aws-cdk/core') 2 | const { CfnResolver } = require('@aws-cdk/aws-appsync') 3 | 4 | module.exports = class OrderResolver extends Construct { 5 | constructor(scope, id, props) { 6 | super(scope, id) 7 | 8 | const { graphQlApi, dynamoDBDataSource, CDK_STACK_NAME, CDK_STACK_ENV } = props 9 | 10 | new CfnResolver(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-OrderGetResolver`, { 11 | dataSourceName: dynamoDBDataSource.attrName, 12 | apiId: graphQlApi.attrApiId, 13 | fieldName: 'orderGet', 14 | typeName: 'Query', 15 | requestMappingTemplate: ` 16 | #set($entity = "Order") 17 | 18 | { 19 | "version": "2017-02-28", 20 | "operation": "GetItem", 21 | "key" : { "id": $util.dynamodb.toDynamoDBJson($ctx.args.id), "entity": $util.dynamodb.toDynamoDBJson($entity) }, 22 | } 23 | `, 24 | responseMappingTemplate: `$util.toJson($ctx.result)`, 25 | }) 26 | 27 | new CfnResolver(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-OrderForCartMutationResolver`, { 28 | dataSourceName: dynamoDBDataSource.attrName, 29 | apiId: graphQlApi.attrApiId, 30 | fieldName: 'orderForCart', 31 | typeName: 'Mutation', 32 | requestMappingTemplate: ` 33 | #if($ctx.args.input.id) #set($id = $ctx.args.input.id) #else #set($id = $util.autoId()) #end 34 | #set($entity = "Order") 35 | 36 | { 37 | "version" : "2017-02-28", 38 | "operation" : "PutItem", 39 | "key" : { "id": $util.dynamodb.toDynamoDBJson($id), "entity": $util.dynamodb.toDynamoDBJson($entity) }, 40 | "attributeValues" : $util.dynamodb.toMapValuesJson($ctx.args.input) 41 | } 42 | `, 43 | responseMappingTemplate: `$util.toJson($ctx.result)`, 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/infrastructure/graphql/resolvers/product.js: -------------------------------------------------------------------------------- 1 | const { Construct } = require('@aws-cdk/core') 2 | const { CfnResolver } = require('@aws-cdk/aws-appsync') 3 | 4 | module.exports = class ProductResolver extends Construct { 5 | constructor(scope, id, props) { 6 | super(scope, id) 7 | 8 | const { graphQlApi, dynamoDBDataSource, CDK_STACK_NAME, CDK_STACK_ENV } = props 9 | 10 | new CfnResolver(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-ProductListResolver`, { 11 | dataSourceName: dynamoDBDataSource.attrName, 12 | apiId: graphQlApi.attrApiId, 13 | fieldName: 'productList', 14 | typeName: 'Query', 15 | requestMappingTemplate: ` 16 | #set($entity = "Product") 17 | #set($ctx.args.filter.entity = { "eq": $entity }) 18 | #set($userFilter = { "entity": { "eq": $entity } }) 19 | 20 | { 21 | "version": "2017-02-28", 22 | "operation": "Scan", 23 | "filter": #if($context.args.filter) $util.transform.toDynamoDBFilterExpression($ctx.args.filter) #else $util.transform.toDynamoDBFilterExpression($userFilter) #end, 24 | "limit": #if($ctx.args.limit) $ctx.args.limit #else null #end, 25 | "nextToken": $util.toJson($util.defaultIfNullOrEmpty($ctx.args.nextToken, null)), 26 | } 27 | `, 28 | responseMappingTemplate: ` 29 | { 30 | "items": $util.toJson($ctx.result.items), 31 | "nextToken": $util.toJson($util.defaultIfNullOrBlank($context.result.nextToken, null)) 32 | } 33 | `, 34 | }) 35 | 36 | new CfnResolver(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-ProductGetResolver`, { 37 | dataSourceName: dynamoDBDataSource.attrName, 38 | apiId: graphQlApi.attrApiId, 39 | fieldName: 'productGet', 40 | typeName: 'Query', 41 | requestMappingTemplate: ` 42 | #set($entity = "Product") 43 | 44 | { 45 | "version": "2017-02-28", 46 | "operation": "GetItem", 47 | "key" : { "id": $util.dynamodb.toDynamoDBJson($ctx.args.id), "entity": $util.dynamodb.toDynamoDBJson($entity) }, 48 | } 49 | `, 50 | responseMappingTemplate: `$util.toJson($ctx.result)`, 51 | }) 52 | 53 | new CfnResolver(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-ProductUpsertMutationResolver`, { 54 | dataSourceName: dynamoDBDataSource.attrName, 55 | apiId: graphQlApi.attrApiId, 56 | fieldName: 'productUpsert', 57 | typeName: 'Mutation', 58 | requestMappingTemplate: ` 59 | #if($ctx.args.input.id) #set($id = $ctx.args.input.id) #else #set($id = $util.autoId()) #end 60 | #set($entity = "Product") 61 | 62 | { 63 | "version" : "2017-02-28", 64 | "operation" : "PutItem", 65 | "key" : { "id": $util.dynamodb.toDynamoDBJson($id), "entity": $util.dynamodb.toDynamoDBJson($entity) }, 66 | "attributeValues" : $util.dynamodb.toMapValuesJson($ctx.args.input) 67 | } 68 | `, 69 | responseMappingTemplate: `$util.toJson($ctx.result)`, 70 | }) 71 | 72 | new CfnResolver(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-ProductDeleteMutationResolver`, { 73 | dataSourceName: dynamoDBDataSource.attrName, 74 | apiId: graphQlApi.attrApiId, 75 | fieldName: 'productDelete', 76 | typeName: 'Mutation', 77 | requestMappingTemplate: ` 78 | #set($entity = "Product") 79 | 80 | { 81 | "version" : "2017-02-28", 82 | "operation" : "DeleteItem", 83 | "key" : { "id" : $util.dynamodb.toDynamoDBJson($ctx.args.id), "entity": $util.dynamodb.toDynamoDBJson($entity) } 84 | } 85 | `, 86 | responseMappingTemplate: `$util.toJson($ctx.result)`, 87 | }) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /packages/infrastructure/graphql/resolvers/profile.js: -------------------------------------------------------------------------------- 1 | const { Construct } = require('@aws-cdk/core') 2 | const { CfnResolver } = require('@aws-cdk/aws-appsync') 3 | 4 | module.exports = class ProfileResolver extends Construct { 5 | constructor(scope, id, props) { 6 | super(scope, id) 7 | 8 | const { graphQlApi, dynamoDBDataSource, CDK_STACK_NAME, CDK_STACK_ENV } = props 9 | 10 | new CfnResolver(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-ProfileMeResolver`, { 11 | dataSourceName: dynamoDBDataSource.attrName, 12 | apiId: graphQlApi.attrApiId, 13 | fieldName: 'profile', 14 | typeName: 'Me', 15 | requestMappingTemplate: ` 16 | { 17 | "version": "2017-02-28", 18 | "operation": "GetItem", 19 | "key": { 20 | "id": $util.dynamodb.toDynamoDBJson($ctx.identity.username), 21 | "entity": $util.dynamodb.toDynamoDBJson("Profile"), 22 | }, 23 | } 24 | `, 25 | responseMappingTemplate: `$util.toJson($ctx.result)`, 26 | }) 27 | 28 | new CfnResolver(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-ProfileUpsertMutationResolver`, { 29 | dataSourceName: dynamoDBDataSource.attrName, 30 | apiId: graphQlApi.attrApiId, 31 | fieldName: 'profileUpsert', 32 | typeName: 'Mutation', 33 | requestMappingTemplate: ` 34 | { 35 | "version" : "2017-02-28", 36 | "operation" : "PutItem", 37 | "key" : { "id": $util.dynamodb.toDynamoDBJson($ctx.identity.username), "entity": $util.dynamodb.toDynamoDBJson("Profile") }, 38 | "attributeValues" : $util.dynamodb.toMapValuesJson($ctx.args.input) 39 | } 40 | `, 41 | responseMappingTemplate: `$util.toJson($ctx.result)`, 42 | }) 43 | 44 | new CfnResolver(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-ProfileDeleteMutationResolver`, { 45 | dataSourceName: dynamoDBDataSource.attrName, 46 | apiId: graphQlApi.attrApiId, 47 | fieldName: 'profileDelete', 48 | typeName: 'Mutation', 49 | requestMappingTemplate: ` 50 | { 51 | "version" : "2017-02-28", 52 | "operation" : "DeleteItem", 53 | "key" : { "id" : $util.dynamodb.toDynamoDBJson($ctx.identity.username), "entity": $util.dynamodb.toDynamoDBJson("Profile") } 54 | } 55 | `, 56 | responseMappingTemplate: `$util.toJson($ctx.result)`, 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/infrastructure/graphql/schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | me: Me @aws_cognito_user_pools 3 | productGet(id: ID!): Product @aws_api_key @aws_cognito_user_pools 4 | productList(filter: ProductFilterInput, limit: Int, nextToken: String): ProductConnection 5 | @aws_api_key 6 | @aws_cognito_user_pools 7 | imageGet(id: ID!): Image @aws_api_key @aws_cognito_user_pools 8 | imageList(filter: ImageFilterInput, limit: Int, nextToken: String): ImageConnection 9 | @aws_api_key 10 | @aws_cognito_user_pools 11 | cartGet(id: ID!): Cart @aws_api_key @aws_cognito_user_pools 12 | orderGet(id: ID!): Order @aws_api_key @aws_cognito_user_pools 13 | invoiceGet(id: ID!): Invoice @aws_api_key @aws_cognito_user_pools 14 | } 15 | 16 | type Me @aws_cognito_user_pools { 17 | user: User 18 | profile: Profile 19 | cart: Cart 20 | } 21 | 22 | type User @aws_api_key @aws_cognito_user_pools { 23 | id: ID! 24 | username: ID 25 | } 26 | 27 | type Image @aws_api_key @aws_cognito_user_pools { 28 | id: ID! 29 | url: AWSURL 30 | title: String 31 | type: String 32 | } 33 | 34 | type ImageConnection @aws_api_key @aws_cognito_user_pools { 35 | items: [Image] 36 | nextToken: String 37 | } 38 | 39 | type Product @aws_api_key @aws_cognito_user_pools { 40 | id: ID! 41 | title: String 42 | description: String 43 | logoUrl: String 44 | price: Float 45 | } 46 | 47 | type Cart @aws_api_key @aws_cognito_user_pools { 48 | id: ID! 49 | products: ProductConnection 50 | user: User 51 | } 52 | 53 | type Order @aws_api_key @aws_cognito_user_pools { 54 | id: ID! 55 | cart: Cart 56 | user: User 57 | } 58 | 59 | type Invoice @aws_api_key @aws_cognito_user_pools { 60 | id: ID! 61 | order: Order 62 | user: User 63 | } 64 | 65 | type ProductConnection @aws_api_key @aws_cognito_user_pools { 66 | items: [Product] 67 | nextToken: String 68 | } 69 | 70 | type Profile @aws_api_key @aws_cognito_user_pools { 71 | id: ID! 72 | firstName: String 73 | lastName: String 74 | address: String 75 | zip: String 76 | city: String 77 | } 78 | 79 | input ProductFilterInput { 80 | id: IDFilterInput 81 | title: StringFilterInput 82 | } 83 | 84 | input ImageFilterInput { 85 | id: IDFilterInput 86 | } 87 | 88 | input BooleanFilterInput { 89 | ne: Boolean 90 | eq: Boolean 91 | } 92 | 93 | input FloatFilterInput { 94 | ne: Float 95 | eq: Float 96 | le: Float 97 | lt: Float 98 | ge: Float 99 | gt: Float 100 | contains: Float 101 | notContains: Float 102 | between: [Float] 103 | } 104 | 105 | input IDFilterInput { 106 | ne: ID 107 | eq: ID 108 | le: ID 109 | lt: ID 110 | ge: ID 111 | gt: ID 112 | contains: ID 113 | notContains: ID 114 | between: [ID] 115 | beginsWith: ID 116 | } 117 | 118 | input IntFilterInput { 119 | ne: Int 120 | eq: Int 121 | le: Int 122 | lt: Int 123 | ge: Int 124 | gt: Int 125 | contains: Int 126 | notContains: Int 127 | between: [Int] 128 | } 129 | 130 | input StringFilterInput { 131 | ne: String 132 | eq: String 133 | le: String 134 | lt: String 135 | ge: String 136 | gt: String 137 | contains: String 138 | notContains: String 139 | between: [String] 140 | beginsWith: String 141 | } 142 | 143 | type Mutation @aws_api_key @aws_cognito_user_pools { 144 | imageUpsert(input: ImageInput!): Image 145 | imageDelete(id: ID!): Image 146 | profileUpsert(input: ProfileInput!): Profile 147 | profileDelete(id: ID!): Profile 148 | productUpsert(input: ProductInput!): Product 149 | productDelete(id: ID!): Product 150 | cartUpsert(input: CartUpsertInput!): Cart 151 | orderForCart(input: OrderForCartInput!): Order 152 | invoiceForOrder(input: InvoiceForOrderInput!): Invoice 153 | } 154 | 155 | input OrderForCartInput { 156 | id: ID 157 | cartId: ID 158 | } 159 | 160 | input InvoiceForOrderInput { 161 | id: ID 162 | orderId: ID 163 | } 164 | 165 | input CartUpsertInput { 166 | id: ID 167 | productIds: [ID] 168 | expire: Int 169 | } 170 | 171 | input ImageInput { 172 | id: ID 173 | } 174 | 175 | input ProductInput { 176 | id: ID 177 | title: String 178 | price: Float 179 | description: String 180 | logoUrl: String 181 | } 182 | 183 | input ProfileInput { 184 | id: ID 185 | firstName: String 186 | lastName: String 187 | address: String 188 | zip: String 189 | city: String 190 | } 191 | 192 | type Subscription @aws_api_key @aws_cognito_user_pools { 193 | upsertedProduct: Product @aws_subscribe(mutations: ["productUpsert"]) 194 | deletedProduct: Product @aws_subscribe(mutations: ["productDelete"]) 195 | } 196 | -------------------------------------------------------------------------------- /packages/infrastructure/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@serverless-aws-cdk-ecommerce/infrastructure", 4 | "version": "1.0.0", 5 | "description": "Serverless AWS-CDK Infrastructure as Code", 6 | "scripts": { 7 | "clean": "rimraf cdk.out", 8 | "deploy:configs": "cdk deploy --require-approval never --app deploy-configs.js", 9 | "deploy:backends": "cdk deploy --require-approval never --app deploy-backends.js", 10 | "deploy:frontends": "cdk deploy --require-approval never --app deploy-frontends.js", 11 | "destroy:configs": "cdk destroy --require-approval never --app deploy-configs.js", 12 | "destroy:backends": "cdk destroy --require-approval never --app deploy-backends.js", 13 | "destroy:frontends": "cdk destroy --require-approval never --app deploy-frontends.js", 14 | "format": "prettier --write \"**/*.{js,jsx}\"" 15 | }, 16 | "devDependencies": { 17 | "prettier": "1.18.2", 18 | "rimraf": "3.0.0" 19 | }, 20 | "dependencies": { 21 | "@aws-cdk/aws-appsync": "1.13.1", 22 | "@aws-cdk/aws-cognito": "1.13.1", 23 | "@aws-cdk/aws-dynamodb": "1.13.1", 24 | "@aws-cdk/aws-iam": "1.13.1", 25 | "@aws-cdk/aws-lambda": "1.13.1", 26 | "@aws-cdk/aws-route53": "1.13.1", 27 | "@aws-cdk/aws-route53-targets": "1.13.1", 28 | "@aws-cdk/aws-s3": "1.13.1", 29 | "@aws-cdk/aws-s3-deployment": "1.13.1", 30 | "@aws-cdk/aws-ssm": "1.13.1", 31 | "@aws-cdk/core": "1.13.1", 32 | "aws-cdk": "1.13.1", 33 | "dotenv-cli": "3.0.0", 34 | "json-dynamo-putrequest": "1.0.0", 35 | "rimraf": "3.0.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/infrastructure/sales-app-stack.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path') 2 | const { Stack, RemovalPolicy, CfnOutput } = require('@aws-cdk/core') 3 | const { Bucket } = require('@aws-cdk/aws-s3') 4 | const { BucketDeployment, Source } = require('@aws-cdk/aws-s3-deployment') 5 | const { HostedZone, ARecord, AddressRecordTarget } = require('@aws-cdk/aws-route53') 6 | const { CloudFrontTarget } = require('@aws-cdk/aws-route53-targets') 7 | const { 8 | CloudFrontWebDistribution, 9 | ViewerProtocolPolicy, 10 | PriceClass, 11 | OriginProtocolPolicy, 12 | } = require('@aws-cdk/aws-cloudfront') 13 | 14 | module.exports = class SalesApp extends Stack { 15 | constructor(parent, id, props) { 16 | super(parent, id, props) 17 | 18 | const { 19 | CDK_STACK_NAME, 20 | CDK_STACK_ENV, 21 | CDK_SALES_APP_DOMAIN, 22 | CDK_SALES_APP_HOSTNAME, 23 | CDK_AWS_REGION, 24 | CDK_AWS_ROUTE53_HOSTED_ZONEID, 25 | CDK_AWS_CLOUDFRONT_CERTIFICATE_ARN, 26 | } = props 27 | 28 | this.salesAppBucket = new Bucket(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-SalesApp-Bucket`, { 29 | bucketName: `${CDK_SALES_APP_HOSTNAME}.${CDK_SALES_APP_DOMAIN}`, 30 | websiteIndexDocument: 'index.html', 31 | websiteErrorDocument: 'index.html', 32 | publicReadAccess: true, 33 | removalPolicy: RemovalPolicy.DESTROY, 34 | retainOnDelete: false, 35 | }) 36 | 37 | new CfnOutput(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-SalesApp-BucketWebSiteUrl`, { 38 | value: this.salesAppBucket.bucketWebsiteUrl, 39 | }) 40 | 41 | const deployment = new BucketDeployment(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-SalesApp-Deployment`, { 42 | sources: [Source.asset(join(__dirname, '../sales-app/dist'))], 43 | destinationBucket: this.salesAppBucket, 44 | retainOnDelete: false, 45 | }) 46 | 47 | const distribution = new CloudFrontWebDistribution( 48 | this, 49 | `${CDK_STACK_NAME}-${CDK_STACK_ENV}-SalesApp-Distribution`, 50 | { 51 | aliasConfiguration: { 52 | names: [`${CDK_SALES_APP_HOSTNAME}.${CDK_SALES_APP_DOMAIN}`], 53 | acmCertRef: CDK_AWS_CLOUDFRONT_CERTIFICATE_ARN, 54 | }, 55 | viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, 56 | priceClass: PriceClass.PRICE_CLASS_100, 57 | originConfigs: [ 58 | { 59 | customOriginSource: { 60 | originProtocolPolicy: OriginProtocolPolicy.HTTP_ONLY, 61 | domainName: `${this.salesAppBucket.bucketName}.s3-website.${CDK_AWS_REGION}.amazonaws.com`, 62 | }, 63 | behaviors: [ 64 | { 65 | forwardedValues: { 66 | headers: ['*'], 67 | queryString: true, 68 | cookies: { 69 | forward: 'all', 70 | }, 71 | }, 72 | isDefaultBehavior: true, 73 | compress: true, 74 | minTtlSeconds: 0, 75 | maxTtlSeconds: 31536000, 76 | defaultTtlSeconds: 86400, 77 | }, 78 | ], 79 | }, 80 | ], 81 | } 82 | ) 83 | 84 | const zone = HostedZone.fromHostedZoneAttributes(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-SalesApp-DNSZone`, { 85 | hostedZoneId: CDK_AWS_ROUTE53_HOSTED_ZONEID, 86 | zoneName: CDK_SALES_APP_DOMAIN, 87 | }) 88 | 89 | const aRecord = new ARecord(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-SalesApp-DNSAlias`, { 90 | zone, 91 | recordName: `${CDK_SALES_APP_HOSTNAME}.${CDK_SALES_APP_DOMAIN}`, 92 | target: AddressRecordTarget.fromAlias(new CloudFrontTarget(distribution)), 93 | }) 94 | 95 | new CfnOutput(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-SalesApp-Url`, { 96 | value: `https://${aRecord.domainName}`, 97 | }) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /packages/infrastructure/shop-app-stack.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path') 2 | const { Stack, RemovalPolicy, CfnOutput } = require('@aws-cdk/core') 3 | const { Bucket } = require('@aws-cdk/aws-s3') 4 | const { BucketDeployment, Source } = require('@aws-cdk/aws-s3-deployment') 5 | const { HostedZone, ARecord, AddressRecordTarget } = require('@aws-cdk/aws-route53') 6 | const { CloudFrontTarget } = require('@aws-cdk/aws-route53-targets') 7 | const { 8 | CloudFrontWebDistribution, 9 | ViewerProtocolPolicy, 10 | PriceClass, 11 | OriginProtocolPolicy, 12 | } = require('@aws-cdk/aws-cloudfront') 13 | 14 | module.exports = class ShopApp extends Stack { 15 | constructor(parent, id, props) { 16 | super(parent, id, props) 17 | 18 | const { 19 | CDK_STACK_NAME, 20 | CDK_STACK_ENV, 21 | CDK_SHOP_APP_DOMAIN, 22 | CDK_SHOP_APP_HOSTNAME, 23 | CDK_AWS_REGION, 24 | CDK_AWS_ROUTE53_HOSTED_ZONEID, 25 | CDK_AWS_CLOUDFRONT_CERTIFICATE_ARN, 26 | } = props 27 | 28 | this.shopAppBucket = new Bucket(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-ShopApp-Bucket`, { 29 | bucketName: `${CDK_SHOP_APP_HOSTNAME}.${CDK_SHOP_APP_DOMAIN}`, 30 | websiteIndexDocument: 'index.html', 31 | websiteErrorDocument: 'index.html', 32 | publicReadAccess: true, 33 | removalPolicy: RemovalPolicy.DESTROY, 34 | retainOnDelete: false, 35 | }) 36 | 37 | new CfnOutput(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-ShopApp-BucketWebSiteUrl`, { 38 | value: this.shopAppBucket.bucketWebsiteUrl, 39 | }) 40 | 41 | const deployment = new BucketDeployment(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-ShopApp-Deployment`, { 42 | sources: [Source.asset(join(__dirname, '../shop-app/public'))], 43 | destinationBucket: this.shopAppBucket, 44 | retainOnDelete: false, 45 | }) 46 | 47 | const distribution = new CloudFrontWebDistribution( 48 | this, 49 | `${CDK_STACK_NAME}-${CDK_STACK_ENV}-ShopApp-Distribution`, 50 | { 51 | aliasConfiguration: { 52 | names: [`${CDK_SHOP_APP_HOSTNAME}.${CDK_SHOP_APP_DOMAIN}`], 53 | acmCertRef: CDK_AWS_CLOUDFRONT_CERTIFICATE_ARN, 54 | }, 55 | viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, 56 | priceClass: PriceClass.PRICE_CLASS_100, 57 | originConfigs: [ 58 | { 59 | customOriginSource: { 60 | originProtocolPolicy: OriginProtocolPolicy.HTTP_ONLY, 61 | domainName: `${this.shopAppBucket.bucketName}.s3-website.${CDK_AWS_REGION}.amazonaws.com`, 62 | }, 63 | behaviors: [ 64 | { 65 | forwardedValues: { 66 | headers: ['*'], 67 | queryString: true, 68 | cookies: { 69 | forward: 'all', 70 | }, 71 | }, 72 | isDefaultBehavior: true, 73 | compress: true, 74 | minTtlSeconds: 0, 75 | maxTtlSeconds: 31536000, 76 | defaultTtlSeconds: 86400, 77 | }, 78 | ], 79 | }, 80 | ], 81 | } 82 | ) 83 | 84 | const zone = HostedZone.fromHostedZoneAttributes(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-ShopApp-DNSZone`, { 85 | hostedZoneId: CDK_AWS_ROUTE53_HOSTED_ZONEID, 86 | zoneName: CDK_SHOP_APP_DOMAIN, 87 | }) 88 | 89 | const aRecord = new ARecord(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-ShopApp-DNSAlias`, { 90 | zone, 91 | recordName: `${CDK_SHOP_APP_HOSTNAME}.${CDK_SHOP_APP_DOMAIN}`, 92 | target: AddressRecordTarget.fromAlias(new CloudFrontTarget(distribution)), 93 | }) 94 | 95 | new CfnOutput(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-ShopApp-Url`, { 96 | value: `https://${aRecord.domainName}`, 97 | }) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /packages/infrastructure/ssm-stack.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv') 2 | const { Stack } = require('@aws-cdk/core') 3 | const { StringParameter } = require('@aws-cdk/aws-ssm') 4 | 5 | module.exports = class SSM extends Stack { 6 | constructor(parent, id, props) { 7 | super(parent, id, props) 8 | 9 | const { CDK_STACK_NAME, CDK_STACK_ENV } = props 10 | 11 | Object.keys(props) 12 | .filter(key => key.startsWith('CDK_')) 13 | .forEach(key => { 14 | new StringParameter(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-${key}-SSM-Parameter`, { 15 | parameterName: `/${CDK_STACK_NAME}/${CDK_STACK_ENV}/${key}`, 16 | stringValue: props[key], 17 | }) 18 | }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/infrastructure/storybook-app-stack.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path') 2 | const { Stack, RemovalPolicy, CfnOutput } = require('@aws-cdk/core') 3 | const { Bucket } = require('@aws-cdk/aws-s3') 4 | const { BucketDeployment, Source } = require('@aws-cdk/aws-s3-deployment') 5 | const { HostedZone, ARecord, AddressRecordTarget } = require('@aws-cdk/aws-route53') 6 | const { CloudFrontTarget } = require('@aws-cdk/aws-route53-targets') 7 | const { 8 | CloudFrontWebDistribution, 9 | ViewerProtocolPolicy, 10 | PriceClass, 11 | OriginProtocolPolicy, 12 | } = require('@aws-cdk/aws-cloudfront') 13 | 14 | module.exports = class StorybookApp extends Stack { 15 | constructor(parent, id, props) { 16 | super(parent, id, props) 17 | 18 | const { 19 | CDK_STACK_NAME, 20 | CDK_STACK_ENV, 21 | CDK_STORYBOOK_APP_DOMAIN, 22 | CDK_STORYBOOK_APP_HOSTNAME, 23 | CDK_AWS_REGION, 24 | CDK_AWS_ROUTE53_HOSTED_ZONEID, 25 | CDK_AWS_CLOUDFRONT_CERTIFICATE_ARN, 26 | } = props 27 | 28 | this.storybookAppBucket = new Bucket(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-StorybookApp-Bucket`, { 29 | bucketName: `${CDK_STORYBOOK_APP_HOSTNAME}.${CDK_STORYBOOK_APP_DOMAIN}`, 30 | websiteIndexDocument: 'index.html', 31 | websiteErrorDocument: 'index.html', 32 | publicReadAccess: true, 33 | removalPolicy: RemovalPolicy.DESTROY, 34 | retainOnDelete: false, 35 | }) 36 | 37 | new CfnOutput(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-StorybookApp-BucketWebSiteUrl`, { 38 | value: this.storybookAppBucket.bucketWebsiteUrl, 39 | }) 40 | 41 | const deployment = new BucketDeployment(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-StorybookApp-Deployment`, { 42 | sources: [Source.asset(join(__dirname, '../storybook/storybook-static'))], 43 | destinationBucket: this.storybookAppBucket, 44 | retainOnDelete: false, 45 | }) 46 | 47 | const distribution = new CloudFrontWebDistribution( 48 | this, 49 | `${CDK_STACK_NAME}-${CDK_STACK_ENV}-StorybookApp-Distribution`, 50 | { 51 | aliasConfiguration: { 52 | names: [`${CDK_STORYBOOK_APP_HOSTNAME}.${CDK_STORYBOOK_APP_DOMAIN}`], 53 | acmCertRef: CDK_AWS_CLOUDFRONT_CERTIFICATE_ARN, 54 | }, 55 | viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, 56 | priceClass: PriceClass.PRICE_CLASS_100, 57 | originConfigs: [ 58 | { 59 | customOriginSource: { 60 | originProtocolPolicy: OriginProtocolPolicy.HTTP_ONLY, 61 | domainName: `${this.storybookAppBucket.bucketName}.s3-website.${CDK_AWS_REGION}.amazonaws.com`, 62 | }, 63 | behaviors: [ 64 | { 65 | forwardedValues: { 66 | headers: ['*'], 67 | queryString: true, 68 | cookies: { 69 | forward: 'all', 70 | }, 71 | }, 72 | isDefaultBehavior: true, 73 | compress: true, 74 | minTtlSeconds: 0, 75 | maxTtlSeconds: 31536000, 76 | defaultTtlSeconds: 86400, 77 | }, 78 | ], 79 | }, 80 | ], 81 | } 82 | ) 83 | 84 | const zone = HostedZone.fromHostedZoneAttributes(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-StorybookApp-DNSZone`, { 85 | hostedZoneId: CDK_AWS_ROUTE53_HOSTED_ZONEID, 86 | zoneName: CDK_STORYBOOK_APP_DOMAIN, 87 | }) 88 | 89 | const aRecord = new ARecord(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-StorybookApp-DNSAlias`, { 90 | zone, 91 | recordName: `${CDK_STORYBOOK_APP_HOSTNAME}.${CDK_STORYBOOK_APP_DOMAIN}`, 92 | target: AddressRecordTarget.fromAlias(new CloudFrontTarget(distribution)), 93 | }) 94 | 95 | new CfnOutput(this, `${CDK_STACK_NAME}-${CDK_STACK_ENV}-StorybookApp-Url`, { 96 | value: `https://${aRecord.domainName}`, 97 | }) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /packages/react-components/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | build 4 | .cache 5 | .DS_Store 6 | node_modules/ 7 | .env 8 | .env.development 9 | .env.production 10 | 11 | # Logs 12 | logs 13 | *.log 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | 18 | .cache/ 19 | public 20 | build 21 | dist 22 | storybook-static 23 | 24 | tmp 25 | cdk.context.json 26 | .cdk.staging 27 | cdk.out -------------------------------------------------------------------------------- /packages/react-components/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "bracketSpacing": true, 6 | "jsxBracketSameLine": false, 7 | "semi": false 8 | } 9 | -------------------------------------------------------------------------------- /packages/react-components/README.md: -------------------------------------------------------------------------------- 1 | # React Components 2 | 3 | * Atomic Design 4 | * React 5 | * TypeScript 6 | -------------------------------------------------------------------------------- /packages/react-components/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@serverless-aws-cdk-ecommerce/react-components", 4 | "version": "1.0.0", 5 | "description": "Shared React Components", 6 | "main": "dist/index.js", 7 | "scripts": { 8 | "clean": "rimraf dist", 9 | "develop": "tsc --watch", 10 | "build": "npm run clean && tsc", 11 | "format": "prettier --write \"src/**/*.{ts,tsx}\"" 12 | }, 13 | "devDependencies": { 14 | "@types/react": "16.9.9", 15 | "prettier": "1.18.2", 16 | "rimraf": "3.0.0", 17 | "typescript": "3.6.4" 18 | }, 19 | "dependencies": { 20 | "@apollo/react-hooks": "3.1.3", 21 | "@material-ui/core": "4.5.1", 22 | "apollo-cache-inmemory": "1.6.3", 23 | "apollo-client": "2.6.4", 24 | "apollo-link": "1.2.13", 25 | "apollo-link-context": "1.0.19", 26 | "apollo-link-error": "1.1.12", 27 | "apollo-link-http": "1.5.16", 28 | "aws-amplify": "1.2.2", 29 | "react": "16.10.2" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/react-components/src/atomics/Loading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { makeStyles } from '@material-ui/core/styles' 3 | import { LinearProgress } from '@material-ui/core' 4 | 5 | const useStyles = makeStyles({ 6 | root: { 7 | flexGrow: 1, 8 | width: '100%', 9 | }, 10 | skeleton: { 11 | height: 4, 12 | width: '100%', 13 | }, 14 | }) 15 | 16 | export function Loading({ isLoading = false }) { 17 | const classes = useStyles() 18 | if (!isLoading) return
19 | 20 | return ( 21 |
22 | 23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /packages/react-components/src/atomics/LoadingButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { makeStyles } from '@material-ui/core/styles' 3 | import { Button, CircularProgress } from '@material-ui/core' 4 | import LoopIcon from '@material-ui/icons/Loop' 5 | 6 | interface LoadingButtonProps { 7 | type?: 'submit' | 'reset' | 'button' 8 | label?: string 9 | testId?: string 10 | isLoading?: boolean 11 | isReload?: boolean 12 | onClick?: () => void 13 | } 14 | 15 | export function LoadingButton({ 16 | type = 'button', 17 | label, 18 | isLoading, 19 | isReload, 20 | onClick = () => {}, 21 | testId, 22 | }: LoadingButtonProps) { 23 | const classes = useStyles() 24 | 25 | return ( 26 |
27 | 45 | {isLoading && } 46 |
47 | ) 48 | } 49 | 50 | const useStyles = makeStyles(theme => ({ 51 | submit: { 52 | margin: theme.spacing(3, 0, 2), 53 | }, 54 | wrapper: { 55 | position: 'relative', 56 | }, 57 | submitProgress: { 58 | position: 'absolute', 59 | top: '50%', 60 | left: '50%', 61 | marginTop: -8, 62 | marginLeft: -12, 63 | }, 64 | iconSmall: { 65 | fontSize: 20, 66 | }, 67 | })) 68 | -------------------------------------------------------------------------------- /packages/react-components/src/atomics/SearchInput.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import clsx from 'clsx' 3 | import { makeStyles } from '@material-ui/core/styles' 4 | import { IconButton, OutlinedInput } from '@material-ui/core' 5 | import SearchIcon from '@material-ui/icons/Search' 6 | 7 | export function SearchInput({ placeholderText = 'Search', onSearchClick = () => {}, className = '' }) { 8 | const classes = useStyles() 9 | 10 | return ( 11 |
12 | 13 | onSearchClick()}> 14 | 15 | 16 |
17 | ) 18 | } 19 | 20 | const useStyles = makeStyles(theme => ({ 21 | input: { 22 | flex: 1, 23 | }, 24 | search: { 25 | flex: 'auto', 26 | }, 27 | })) 28 | -------------------------------------------------------------------------------- /packages/react-components/src/hooks/useForm.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, SyntheticEvent } from 'react' 2 | 3 | interface UseFormProps { 4 | callback: () => void 5 | validate: (values: object) => [Error] 6 | } 7 | 8 | export const useForm = ({ callback, validate }: UseFormProps) => { 9 | const [values, setValues] = useState({}) 10 | const [errors, setErrors] = useState({}) 11 | const [isSubmitting, setIsSubmitting] = useState(false) 12 | 13 | useEffect(() => { 14 | if (Object.keys(errors).length === 0 && isSubmitting) { 15 | callback() 16 | } 17 | }, [errors]) 18 | 19 | const handleSubmit = (event: SyntheticEvent) => { 20 | if (event) event.preventDefault() 21 | setIsSubmitting(true) 22 | setErrors(validate(values)) 23 | } 24 | 25 | const handleChange = (event: SyntheticEvent) => { 26 | const target = event.target as HTMLInputElement 27 | event.persist() 28 | setValues(values => ({ 29 | ...values, 30 | [target.name]: target.value, 31 | })) 32 | } 33 | 34 | return { 35 | handleChange, 36 | handleSubmit, 37 | values, 38 | errors, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/react-components/src/index.tsx: -------------------------------------------------------------------------------- 1 | export { AppProvider, AppContext } from './providers/AppProvider' 2 | export { LoadingButton } from './atomics/LoadingButton' 3 | export { Loading } from './atomics/Loading' 4 | export { SearchInput } from './atomics/SearchInput' 5 | export { ConfirmDialog } from './molecules/ConfirmDialog' 6 | export { Topbar } from './molecules/Topbar' 7 | export { MediaLibrary } from './organisms/MediaLibrary' 8 | export { Layout } from './templates/Layout' 9 | export { useForm } from './hooks/useForm' 10 | -------------------------------------------------------------------------------- /packages/react-components/src/molecules/ConfirmDialog.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from '@material-ui/core' 3 | 4 | interface ConfirmDialogProps { 5 | title: string 6 | content: string 7 | isOpen: boolean 8 | onAgree: () => void 9 | onDisagree: () => void 10 | onClose: () => void 11 | } 12 | 13 | export function ConfirmDialog({ 14 | title, 15 | content, 16 | isOpen, 17 | onAgree = () => {}, 18 | onDisagree = () => {}, 19 | onClose = () => {}, 20 | }: ConfirmDialogProps): JSX.Element { 21 | return ( 22 | 29 | {title} 30 | 31 | {content} 32 | 33 | 34 | 37 | 40 | 41 | 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /packages/react-components/src/molecules/Topbar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { makeStyles } from '@material-ui/core/styles' 3 | import { Grid, Paper } from '@material-ui/core' 4 | import { SpeedDial, SpeedDialAction } from '@material-ui/lab' 5 | import AddIcon from '@material-ui/icons/Add' 6 | import PublishIcon from '@material-ui/icons/Publish' 7 | import DeleteIcon from '@material-ui/icons/Delete' 8 | import MoreIcon from '@material-ui/icons/MoreHoriz' 9 | 10 | interface TopbarProps { 11 | renderSearchInput?: () => JSX.Element 12 | renderFilterInput?: () => JSX.Element 13 | onAddClick?: () => void 14 | onPublishClick?: () => void 15 | onDeleteClick?: () => void 16 | } 17 | 18 | export function Topbar({ 19 | renderSearchInput = () =>
, 20 | renderFilterInput = () =>
, 21 | onAddClick, 22 | onPublishClick, 23 | onDeleteClick, 24 | }: TopbarProps) { 25 | const classes = useStyles() 26 | const [isOpen, setIsOpen] = useState(false) 27 | 28 | return ( 29 | 30 | 31 |
{renderSearchInput()}
32 |
{renderFilterInput()}
33 |
34 |
35 |
36 |
37 | 38 | } 43 | open={isOpen} 44 | onClick={() => setIsOpen(true)} 45 | onClose={() => setIsOpen(false)} 46 | onFocus={() => setIsOpen(true)} 47 | onBlur={() => setIsOpen(false)} 48 | onMouseEnter={() => setIsOpen(true)} 49 | onMouseLeave={() => setIsOpen(false)} 50 | direction="left" 51 | > 52 | {onDeleteClick && } tooltipTitle="Löschen" onClick={onDeleteClick} />} 53 | {onPublishClick && ( 54 | } tooltipTitle="Publizieren" onClick={onPublishClick} /> 55 | )} 56 | {onAddClick && } tooltipTitle="Anlegen" onClick={onAddClick} />} 57 | 58 | 59 |
60 | ) 61 | } 62 | 63 | const useStyles = makeStyles(theme => ({ 64 | paper: { 65 | marginBottom: theme.spacing(4), 66 | position: 'sticky', 67 | top: '80px', 68 | zIndex: 1100, 69 | padding: theme.spacing(2), 70 | }, 71 | speedDial: { 72 | margin: theme.spacing(2), 73 | position: 'absolute', 74 | }, 75 | })) 76 | -------------------------------------------------------------------------------- /packages/react-components/src/organisms/MediaLibrary.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { makeStyles } from '@material-ui/core/styles' 3 | 4 | import Container from '@material-ui/core/Container' 5 | import Dialog from '@material-ui/core/Dialog' 6 | import AppBar from '@material-ui/core/AppBar' 7 | import Toolbar from '@material-ui/core/Toolbar' 8 | import IconButton from '@material-ui/core/IconButton' 9 | import Typography from '@material-ui/core/Typography' 10 | import CloseIcon from '@material-ui/icons/Close' 11 | import GridList from '@material-ui/core/GridList' 12 | import GridListTile from '@material-ui/core/GridListTile' 13 | import GridListTileBar from '@material-ui/core/GridListTileBar' 14 | import ListSubheader from '@material-ui/core/ListSubheader' 15 | 16 | import DoneIcon from '@material-ui/icons/Done' 17 | 18 | const tileData = [ 19 | { 20 | id: '1', 21 | title: 'Image', 22 | author: 'author', 23 | url: 'https://source.unsplash.com/random/1', 24 | }, 25 | { 26 | id: '2', 27 | title: 'Image', 28 | author: 'author', 29 | url: 'https://source.unsplash.com/random/2', 30 | }, 31 | { 32 | id: '3', 33 | title: 'Image', 34 | author: 'author', 35 | url: 'https://source.unsplash.com/random/3', 36 | }, 37 | { 38 | id: '4', 39 | title: 'Image', 40 | author: 'author', 41 | url: 'https://source.unsplash.com/random/4', 42 | }, 43 | ] 44 | 45 | export function MediaLibrary({ isOpen = false, onClose = () => {}, onSelect = () => {} }) { 46 | const classes = useStyles() 47 | 48 | return ( 49 | onClose()}> 50 | 51 | 52 | 53 | Fotoalbum 54 | 55 | onClose()} aria-label="close"> 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | ) 65 | } 66 | 67 | export function TitlebarGridList({ onSelect = (_: any) => {} }) { 68 | const classes = useStyles() 69 | 70 | return ( 71 |
72 | 73 | 74 | Fotos von ... 75 | 76 | {tileData.map(tile => ( 77 | 78 | {tile.title} 79 | } 82 | actionIcon={ 83 | onSelect(tile)} 87 | > 88 | 89 | 90 | } 91 | /> 92 | 93 | ))} 94 | 95 |
96 | ) 97 | } 98 | 99 | const useStyles = makeStyles(theme => ({ 100 | contentGrid: { 101 | paddingTop: theme.spacing(2), 102 | paddingBottom: theme.spacing(4), 103 | }, 104 | appBar: { 105 | position: 'relative', 106 | }, 107 | title: { 108 | marginLeft: theme.spacing(2), 109 | flex: 1, 110 | }, 111 | root: { 112 | display: 'flex', 113 | flexWrap: 'wrap', 114 | justifyContent: 'space-around', 115 | overflow: 'hidden', 116 | backgroundColor: theme.palette.background.paper, 117 | }, 118 | gridList: {}, 119 | icon: { 120 | color: 'rgba(255, 255, 255, 0.54)', 121 | }, 122 | })) 123 | -------------------------------------------------------------------------------- /packages/react-components/src/providers/AppProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useState } from 'react' 2 | import Amplify, { Auth, AuthClass } from 'aws-amplify' 3 | import { ApolloClient } from 'apollo-client' 4 | import { InMemoryCache } from 'apollo-cache-inmemory' 5 | import { HttpLink } from 'apollo-link-http' 6 | import { onError, ErrorResponse } from 'apollo-link-error' 7 | import { ApolloLink } from 'apollo-link' 8 | import { setContext } from 'apollo-link-context' 9 | import { ApolloProvider } from '@apollo/react-hooks' 10 | import { CognitoUser } from '@aws-amplify/auth' 11 | 12 | interface AppContext { 13 | Auth: AuthClass 14 | user?: CognitoUser 15 | setUser: (user?: CognitoUser) => void 16 | appVersion?: string 17 | appEnv?: string 18 | } 19 | 20 | interface AppConfig { 21 | region: string 22 | userPoolId: string 23 | userPoolWebClientId: string 24 | graphQlUrl: string 25 | graphQlApiKey?: string 26 | appVersion?: string 27 | appEnv?: string 28 | } 29 | 30 | interface AppProviderProps { 31 | children: JSX.Element[] | JSX.Element 32 | config: AppConfig 33 | onLinkError: (error: ErrorResponse) => void 34 | } 35 | 36 | export { AppContext, AppProvider } 37 | 38 | const AppContext: React.Context = createContext({ 39 | Auth, 40 | setUser: _ => {}, 41 | }) 42 | 43 | function AppProvider({ 44 | children, 45 | config: { region, userPoolId, userPoolWebClientId, graphQlUrl, graphQlApiKey, appVersion, appEnv }, 46 | onLinkError = _ => {}, 47 | }: AppProviderProps): JSX.Element { 48 | Amplify.configure({ 49 | Auth: { 50 | region, 51 | userPoolId, 52 | userPoolWebClientId, 53 | }, 54 | }) 55 | 56 | const [user, setUser] = useState() 57 | const httpLink = new HttpLink({ uri: graphQlUrl }) 58 | const authLink = setContext((_, { headers }) => { 59 | const token = localStorage.getItem('token') 60 | return token 61 | ? { 62 | headers: { 63 | authorization: token, 64 | ...headers, 65 | }, 66 | } 67 | : { 68 | headers: { 69 | 'x-api-key': graphQlApiKey, 70 | ...headers, 71 | }, 72 | } 73 | }) 74 | const errorLink = onError(error => { 75 | console.error(error) 76 | onLinkError(error) 77 | }) 78 | 79 | const client = new ApolloClient({ 80 | link: ApolloLink.from([errorLink, authLink, httpLink]), 81 | cache: new InMemoryCache(), 82 | }) 83 | 84 | const value = { 85 | Auth, 86 | user, 87 | setUser, 88 | appVersion, 89 | appEnv, 90 | } 91 | 92 | return ( 93 | 94 | {children} 95 | 96 | ) 97 | } 98 | -------------------------------------------------------------------------------- /packages/react-components/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "ES2015" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 6 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, 11 | "declaration": true /* Generates corresponding '.d.ts' file. */, 12 | "declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */, 13 | "sourceMap": true /* Generates corresponding '.map' file. */, 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "./dist" /* Redirect output structure to the directory. */, 16 | "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true /* Enable all strict type-checking options. */, 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | } 63 | } -------------------------------------------------------------------------------- /packages/sales-app/.env-template: -------------------------------------------------------------------------------- 1 | CDK_STACK_NAME= 2 | CDK_STACK_ENV= 3 | 4 | CDK_AWS_COGNITO_USER_POOL_ID= 5 | CDK_AWS_COGNITO_USER_POOL_WEBCLIENT_ID= 6 | CDK_AWS_APPSYNC_URL= 7 | CDK_AWS_APPSYNC_APIKEY= 8 | -------------------------------------------------------------------------------- /packages/sales-app/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | build 4 | .cache 5 | .DS_Store 6 | node_modules/ 7 | .env 8 | .env.development 9 | .env.production 10 | 11 | # Logs 12 | logs 13 | *.log 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | 18 | .cache/ 19 | public 20 | build 21 | dist 22 | storybook-static 23 | 24 | tmp 25 | cdk.context.json 26 | .cdk.staging 27 | cdk.out -------------------------------------------------------------------------------- /packages/sales-app/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "bracketSpacing": true, 6 | "jsxBracketSameLine": false, 7 | "semi": false 8 | } 9 | -------------------------------------------------------------------------------- /packages/sales-app/README.md: -------------------------------------------------------------------------------- 1 | # Sales App 2 | 3 | * ParcelJS 4 | * React 5 | * Single Page Application 6 | -------------------------------------------------------------------------------- /packages/sales-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@serverless-aws-cdk-ecommerce/sales-app", 4 | "version": "1.0.2", 5 | "description": "Sales App", 6 | "scripts": { 7 | "clean": "rimraf .cache dist", 8 | "develop": "rimraf .cache && parcel src/index.html", 9 | "build": "ssmenv -- parcel build src/index.html --no-source-maps", 10 | "serve": "http-server-spa dist index.html 8081", 11 | "format": "prettier --write \"src/**/*.{js,jsx}\"" 12 | }, 13 | "devDependencies": { 14 | "@mikebild/ssmenv-cli": "^1.0.0", 15 | "http-server-spa": "1.3.0", 16 | "parcel-bundler": "1.12.4", 17 | "prettier": "1.18.2", 18 | "rimraf": "3.0.0" 19 | }, 20 | "dependencies": { 21 | "@apollo/react-hooks": "3.1.3", 22 | "@babel/polyfill": "7.6.0", 23 | "@material-ui/core": "4.5.1", 24 | "@material-ui/icons": "4.5.1", 25 | "@material-ui/lab": "4.0.0-alpha.29", 26 | "@serverless-aws-cdk-ecommerce/react-components": "^1.0.0", 27 | "apollo-cache-inmemory": "1.6.3", 28 | "apollo-client": "2.6.4", 29 | "apollo-link": "1.2.13", 30 | "apollo-link-context": "1.0.19", 31 | "apollo-link-error": "1.1.12", 32 | "apollo-link-http": "1.5.16", 33 | "aws-amplify": "1.2.2", 34 | "aws-sdk": "2.551.0", 35 | "graphql": "^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0", 36 | "graphql-tag": "2.10.1", 37 | "prop-types": "15.7.2", 38 | "react": "16.10.2", 39 | "react-dom": "16.10.2", 40 | "react-helmet": "5.2.1", 41 | "react-router-dom": "5.1.2", 42 | "roboto-fontface": "0.10.0", 43 | "uuid": "3.3.3" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/sales-app/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useContext } from 'react' 2 | import { Route, Redirect, useLocation, useHistory } from 'react-router-dom' 3 | import { AppContext, AppProvider } from '@serverless-aws-cdk-ecommerce/react-components' 4 | import { DashboardPage } from './pages/dashboard' 5 | import { ProductPage } from './pages/products' 6 | import { SignInPage } from './pages/signin' 7 | import { SignUpPage } from './pages/signup' 8 | import { PasswordPage } from './pages/password' 9 | import { ForgotPage } from './pages/forgot' 10 | 11 | export function App() { 12 | const history = useHistory() 13 | const config = { 14 | userPoolId: `${process.env.CDK_AWS_COGNITO_USER_POOL_ID}`, 15 | userPoolWebClientId: `${process.env.CDK_AWS_COGNITO_USER_POOL_WEBCLIENT_ID}`, 16 | graphQlUrl: `${process.env.CDK_AWS_APPSYNC_URL}`, 17 | appVersion: `${process.env.npm_package_version}`, 18 | appEnv: `${process.env.CDK_STACK_ENV}`, 19 | } 20 | 21 | return ( 22 | { 25 | if (networkError && networkError.statusCode === 401) { 26 | return history.push('/signin') 27 | } 28 | }} 29 | > 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | ) 59 | } 60 | 61 | function CheckAuth({ children }) { 62 | const { state } = useLocation() 63 | const hasToken = Boolean(localStorage.getItem('token')) 64 | const { user = hasToken ? {} : undefined, setUser, Auth } = useContext(AppContext) 65 | const fetchCurrentAuthenticatedUser = async () => { 66 | try { 67 | const cognitoUser = await Auth.currentAuthenticatedUser() 68 | localStorage.setItem('token', cognitoUser.signInUserSession.accessToken.jwtToken) 69 | setUser(cognitoUser) 70 | } catch (error) { 71 | localStorage.removeItem('token') 72 | setUser(undefined) 73 | } 74 | } 75 | 76 | useEffect(() => { 77 | fetchCurrentAuthenticatedUser() 78 | }, []) 79 | 80 | if (!user) { 81 | return ( 82 | 88 | ) 89 | } 90 | 91 | return children 92 | } 93 | -------------------------------------------------------------------------------- /packages/sales-app/src/components/ProductCard.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { makeStyles } from '@material-ui/core/styles' 3 | import { Typography, Grid, Card, CardActions, CardContent, CardMedia } from '@material-ui/core' 4 | import { SpeedDial, SpeedDialIcon, SpeedDialAction } from '@material-ui/lab' 5 | import DeleteIcon from '@material-ui/icons/Delete' 6 | import EditIcon from '@material-ui/icons/EditOutlined' 7 | 8 | export function ProductCard({ item = {}, onEdit = () => {}, onDelete = () => {} }) { 9 | const { 10 | id, 11 | title, 12 | description, 13 | price, 14 | logoUrl, 15 | color = '#FFF', 16 | category: { title: categoryTitle, color: categoryColor } = {}, 17 | } = item 18 | const classes = useStyles() 19 | const [isOpen, setIsOpen] = useState(false) 20 | const actions = [ 21 | { 22 | icon: , 23 | name: 'Ändern', 24 | onClick: onEdit, 25 | }, 26 | { 27 | icon: , 28 | name: 'Löschen', 29 | onClick: onDelete, 30 | }, 31 | ] 32 | 33 | return ( 34 | 35 | 36 | {logoUrl && } 37 |
44 | 45 | {categoryTitle} 46 | 47 |
48 | 49 | 50 | {title} 51 | 52 | 53 | {description} 54 | 55 | 56 | {price} € 57 | 58 | 59 | 60 | } 64 | open={isOpen} 65 | onClick={() => setIsOpen(true)} 66 | onClose={() => setIsOpen(false)} 67 | onFocus={() => setIsOpen(true)} 68 | onBlur={() => setIsOpen(false)} 69 | onMouseEnter={() => setIsOpen(true)} 70 | onMouseLeave={() => setIsOpen(false)} 71 | direction="left" 72 | > 73 | {actions.map(({ name, icon, onClick }) => ( 74 | 75 | ))} 76 | 77 | 78 |
79 |
80 | ) 81 | } 82 | 83 | const useStyles = makeStyles(theme => ({ 84 | card: {}, 85 | content: {}, 86 | cover: { 87 | height: 200, 88 | }, 89 | actions: { 90 | justifyContent: 'flex-end', 91 | paddingRight: theme.spacing(2), 92 | paddingBottom: theme.spacing(0), 93 | }, 94 | })) 95 | -------------------------------------------------------------------------------- /packages/sales-app/src/components/ProductForm.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useRef } from 'react' 2 | import { makeStyles } from '@material-ui/core/styles' 3 | import { Button, IconButton, TextField, Dialog, DialogActions, DialogContent, DialogTitle } from '@material-ui/core' 4 | import CloseIcon from '@material-ui/icons/Close' 5 | import DeleteIcon from '@material-ui/icons/Delete' 6 | import AddAPhotoIcon from '@material-ui/icons/AddAPhoto' 7 | 8 | export function ProductForm({ 9 | isOpen = false, 10 | value = {}, 11 | onClose = () => {}, 12 | onEdit = () => {}, 13 | onAdd = () => {}, 14 | onCancel = () => {}, 15 | onAddPhoto = () => {}, 16 | onDelete = () => {}, 17 | }) { 18 | const classes = useStyles() 19 | const { id, title, price, description, logoUrl, category: { id: categoryId } = {} } = value 20 | const titleRef = useRef() 21 | const priceRef = useRef() 22 | const descriptionRef = useRef() 23 | const logoUrlRef = useRef() 24 | const [newCategoryId, setNewCategoryId] = useState(categoryId) 25 | const hasId = Boolean(id) 26 | 27 | useEffect(() => { 28 | if (logoUrlRef && logoUrlRef.current) logoUrlRef.current.value = logoUrl 29 | }, [logoUrl]) 30 | 31 | return ( 32 | 33 | 34 | Produkt 35 |
36 | {hasId && ( 37 | onDelete({ ...value })}> 38 | 39 | 40 | )} 41 | 42 | 43 | 44 |
45 |
46 | 47 | 55 | 64 | 74 |
75 | 83 |
84 | 85 | 86 | 87 |
88 |
89 |
90 | 91 | 94 | {hasId ? ( 95 | 110 | ) : ( 111 | 125 | )} 126 | 127 |
128 | ) 129 | } 130 | 131 | const useStyles = makeStyles(theme => ({ 132 | addPhotoButton: { 133 | marginLeft: theme.spacing(1), 134 | marginRight: theme.spacing(1), 135 | }, 136 | wrapper: { 137 | display: 'flex', 138 | justifyContent: 'space-between', 139 | alignItems: 'center', 140 | }, 141 | headerButtons: { 142 | float: 'right', 143 | }, 144 | dialogActions: { 145 | padding: theme.spacing(3), 146 | }, 147 | })) 148 | -------------------------------------------------------------------------------- /packages/sales-app/src/graphql/product-fragment.graphql: -------------------------------------------------------------------------------- 1 | fragment ProductFragment on Product { 2 | id 3 | title 4 | price 5 | description 6 | logoUrl 7 | } 8 | -------------------------------------------------------------------------------- /packages/sales-app/src/graphql/product-get.graphql: -------------------------------------------------------------------------------- 1 | #import "./product-fragment.graphql" 2 | 3 | query ProductGet($id: ID!) { 4 | productGet(id: $id) { 5 | ...ProductFragment 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/sales-app/src/graphql/product-list.graphql: -------------------------------------------------------------------------------- 1 | #import "./product-fragment.graphql" 2 | 3 | query ProductList { 4 | productList { 5 | products: items { 6 | ...ProductFragment 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/sales-app/src/graphql/product-mutation-delete.graphql: -------------------------------------------------------------------------------- 1 | #import "./product-fragment.graphql" 2 | 3 | mutation ProductDelete($id: ID!) { 4 | productDelete(id: $id) { 5 | ...ProductFragment 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/sales-app/src/graphql/product-mutation-upsert.graphql: -------------------------------------------------------------------------------- 1 | #import "./product-fragment.graphql" 2 | 3 | mutation ProductUpsert($input: ProductInput!) { 4 | productUpsert(input: $input) { 5 | ...ProductFragment 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/sales-app/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | E-Commerce Sales 5 | 6 | 10 | 11 | 12 | 13 | 14 |
loading ...
15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/sales-app/src/index.jsx: -------------------------------------------------------------------------------- 1 | import '@babel/polyfill' 2 | import 'roboto-fontface' 3 | import React from 'react' 4 | import { render } from 'react-dom' 5 | import { BrowserRouter as Router } from 'react-router-dom' 6 | import { ThemeProvider } from '@material-ui/styles' 7 | import CssBaseline from '@material-ui/core/CssBaseline' 8 | import theme from './theme' 9 | import { App } from './App' 10 | 11 | render( 12 | 13 | 14 | 15 | 16 | 17 | , 18 | document.getElementById('root') 19 | ) 20 | -------------------------------------------------------------------------------- /packages/sales-app/src/pages/dashboard.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link, useHistory } from 'react-router-dom' 3 | import { Layout } from '@serverless-aws-cdk-ecommerce/react-components' 4 | import { makeStyles } from '@material-ui/core/styles' 5 | import { Typography, Grid, Container, ListItem, ListItemIcon, ListItemText, Divider } from '@material-ui/core' 6 | 7 | import AppsIcon from '@material-ui/icons/Apps' 8 | import EventAvailableIcon from '@material-ui/icons/EventAvailable' 9 | 10 | export function DashboardPage() { 11 | const classes = useStyles() 12 | const history = useHistory() 13 | 14 | return ( 15 | { 18 | return ( 19 | <> 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ) 35 | }} 36 | onLogout={() => history.push('/signin')} 37 | > 38 | 39 | 40 | Deine Übersicht 41 | 42 | 43 | 44 | 45 | ) 46 | } 47 | 48 | const useStyles = makeStyles(theme => ({ 49 | contentGrid: { 50 | paddingTop: theme.spacing(4), 51 | paddingBottom: theme.spacing(4), 52 | }, 53 | headLine: { 54 | paddingBottom: theme.spacing(4), 55 | }, 56 | })) 57 | -------------------------------------------------------------------------------- /packages/sales-app/src/pages/forgot.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useRef, useState } from 'react' 2 | import { Link as RouterLink, useHistory } from 'react-router-dom' 3 | import { AppContext } from '@serverless-aws-cdk-ecommerce/react-components' 4 | import { makeStyles } from '@material-ui/core/styles' 5 | import { Avatar, Button, CssBaseline, TextField, Link, Grid, Typography, Container } from '@material-ui/core' 6 | import LockOutlinedIcon from '@material-ui/icons/LockOutlined' 7 | 8 | export function ForgotPage() { 9 | const history = useHistory() 10 | const classes = useStyles() 11 | const { Auth, user } = useContext(AppContext) 12 | const [message, setMessage] = useState('') 13 | const [isConfirmVisible, setIsConfirmVisible] = useState(false) 14 | const emailRef = useRef('') 15 | const codeRef = useRef('') 16 | const newPasswordRef = useRef('') 17 | const newPasswordVerifyRef = useRef('') 18 | 19 | const handleForgotPasswordSubmit = async e => { 20 | e.preventDefault() 21 | 22 | try { 23 | await Auth.forgotPassword(emailRef.current.value) 24 | 25 | setIsConfirmVisible(true) 26 | } catch (error) { 27 | console.error(error) 28 | setMessage(error.message) 29 | } 30 | } 31 | 32 | const handleForgotPasswordConfirmSubmit = async e => { 33 | e.preventDefault() 34 | 35 | if (newPasswordRef.current.value !== newPasswordVerifyRef.current.value) { 36 | return setMessage('Die Kennwörter stimmen nicht überein.') 37 | } 38 | 39 | try { 40 | await Auth.forgotPasswordSubmit(emailRef.current.value, codeRef.current.value, newPasswordRef.current.value) 41 | 42 | history.push('/signin') 43 | } catch (error) { 44 | setMessage(error.message) 45 | } 46 | } 47 | 48 | return ( 49 | 50 | 51 |
52 | 53 | 54 | 55 | 56 | Kennwort zurücksetzen 57 | 58 | 59 | 60 | 61 | Bitte geben Sie Ihre E-Mail Adresse ein. 62 | 63 | 64 | 65 |
(isConfirmVisible ? handleForgotPasswordConfirmSubmit(e) : handleForgotPasswordSubmit(e))} 69 | > 70 | 71 | 72 | 83 | 84 | 85 | {isConfirmVisible && ( 86 | <> 87 | 88 | 97 | 98 | 99 | 109 | 110 | 111 | 121 | 122 | 123 | )} 124 | 125 | 126 | 129 | 130 | 131 |
132 | 133 | 134 | 135 | {message &&

{message}

} 136 |
137 |
138 |
139 |
140 | ) 141 | } 142 | 143 | const useStyles = makeStyles(theme => ({ 144 | '@global': { 145 | body: { 146 | backgroundColor: theme.palette.common.white, 147 | }, 148 | }, 149 | paper: { 150 | marginTop: theme.spacing(8), 151 | display: 'flex', 152 | flexDirection: 'column', 153 | alignItems: 'center', 154 | }, 155 | avatar: { 156 | margin: theme.spacing(1), 157 | backgroundColor: theme.palette.secondary.main, 158 | }, 159 | form: { 160 | width: '100%', // Fix IE 11 issue. 161 | marginTop: theme.spacing(3), 162 | }, 163 | submit: { 164 | margin: theme.spacing(3, 0, 2), 165 | }, 166 | subtitle: { 167 | marginTop: theme.spacing(2), 168 | }, 169 | })) 170 | -------------------------------------------------------------------------------- /packages/sales-app/src/pages/password.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useRef, useState } from 'react' 2 | import { Link as RouterLink, useHistory } from 'react-router-dom' 3 | import { AppContext } from '@serverless-aws-cdk-ecommerce/react-components' 4 | import { makeStyles } from '@material-ui/core/styles' 5 | import { Avatar, Button, CssBaseline, TextField, Link, Grid, Typography, Container } from '@material-ui/core' 6 | import LockOutlinedIcon from '@material-ui/icons/LockOutlined' 7 | 8 | export function PasswordPage() { 9 | const history = useHistory() 10 | const classes = useStyles() 11 | const { Auth, user } = useContext(AppContext) 12 | const [message, setMessage] = useState('') 13 | const newPasswordRef = useRef('') 14 | const newPasswordVerifyRef = useRef('') 15 | 16 | const handleSubmit = async e => { 17 | e.preventDefault() 18 | 19 | if (newPasswordRef.current.value !== newPasswordVerifyRef.current.value) { 20 | return setMessage('Kennwörter stimmen nicht überein.') 21 | } 22 | 23 | try { 24 | await Auth.completeNewPassword(user, newPasswordRef.current.value) 25 | history.push('/signin') 26 | } catch (error) { 27 | console.error(error) 28 | setMessage(error.message) 29 | } 30 | } 31 | 32 | return ( 33 | 34 | 35 |
36 | 37 | 38 | 39 | 40 | Kennwort ändern 41 | 42 | 43 | 44 | 45 | Bitte geben Sie Ihr altes und neues Kennwort ein. 46 | 47 | 48 | 49 |
handleSubmit(e)}> 50 | 51 | 52 | 62 | 63 | 64 | 74 | 75 | 76 | 79 | 80 | 81 |
82 | 83 | 84 | {message &&

{message}

} 85 |
86 |
87 |
88 |
89 | ) 90 | } 91 | 92 | const useStyles = makeStyles(theme => ({ 93 | '@global': { 94 | body: { 95 | backgroundColor: theme.palette.common.white, 96 | }, 97 | }, 98 | paper: { 99 | marginTop: theme.spacing(8), 100 | display: 'flex', 101 | flexDirection: 'column', 102 | alignItems: 'center', 103 | }, 104 | avatar: { 105 | margin: theme.spacing(1), 106 | backgroundColor: theme.palette.secondary.main, 107 | }, 108 | form: { 109 | width: '100%', // Fix IE 11 issue. 110 | marginTop: theme.spacing(3), 111 | }, 112 | submit: { 113 | margin: theme.spacing(3, 0, 2), 114 | }, 115 | subtitle: { 116 | marginTop: theme.spacing(2), 117 | }, 118 | })) 119 | -------------------------------------------------------------------------------- /packages/sales-app/src/pages/products.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext } from 'react' 2 | import { Link, useHistory } from 'react-router-dom' 3 | import { useQuery, useMutation } from '@apollo/react-hooks' 4 | import { makeStyles } from '@material-ui/core/styles' 5 | import { List, ListItem, ListItemText, ListItemIcon, Container, Grid, Divider } from '@material-ui/core' 6 | import RefreshIcon from '@material-ui/icons/Refresh' 7 | import AppsIcon from '@material-ui/icons/Apps' 8 | import EventAvailableIcon from '@material-ui/icons/EventAvailable' 9 | import { 10 | Layout, 11 | Loading, 12 | AppContext, 13 | MediaLibrary, 14 | ConfirmDialog, 15 | Topbar, 16 | SearchInput, 17 | } from '@serverless-aws-cdk-ecommerce/react-components' 18 | import { ProductForm } from '../components/ProductForm' 19 | import { ProductCard } from '../components/ProductCard' 20 | import LIST from '../graphql/product-list.graphql' 21 | import DELETE from '../graphql/product-mutation-delete.graphql' 22 | import UPSERT from '../graphql/product-mutation-upsert.graphql' 23 | 24 | export function ProductPage() { 25 | const classes = useStyles() 26 | const history = useHistory() 27 | const [isFormOpen, setIsFormOpen] = useState(false) 28 | const [isMediaLibraryOpen, setIsMediaLibraryOpen] = useState(false) 29 | const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false) 30 | const [selected, setSelected] = useState({}) 31 | 32 | const { loading, refetch, data: { productList: { products = [] } = {} } = {} } = useQuery(LIST) 33 | const [deleteEntity, {}] = useMutation(DELETE) 34 | const [upsertEntity, {}] = useMutation(UPSERT) 35 | 36 | return ( 37 | { 40 | return ( 41 | <> 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | refetch()}> 57 | 58 | 59 | 60 | 61 | 62 | 63 | ) 64 | }} 65 | onLogout={() => history.push('/signin')} 66 | > 67 | 68 | setIsConfirmDialogOpen(false)} 73 | onAgree={async () => { 74 | await deleteEntity({ variables: { id: selected.id } }) 75 | await refetch() 76 | setIsConfirmDialogOpen(false) 77 | setIsFormOpen(false) 78 | setSelected({}) 79 | }} 80 | onDisagree={() => setIsConfirmDialogOpen(false)} 81 | /> 82 | { 86 | setSelected({}) 87 | setIsFormOpen(false) 88 | }} 89 | onCancel={() => { 90 | setSelected({}) 91 | setIsFormOpen(false) 92 | }} 93 | onAddPhoto={() => setIsMediaLibraryOpen(true)} 94 | onEdit={async input => { 95 | await upsertEntity({ variables: { input } }) 96 | setIsFormOpen(false) 97 | setSelected({}) 98 | }} 99 | onAdd={async input => { 100 | await upsertEntity({ variables: { input } }) 101 | await refetch() 102 | setIsFormOpen(false) 103 | setSelected({}) 104 | }} 105 | onDelete={input => { 106 | setSelected(input) 107 | setIsConfirmDialogOpen(true) 108 | }} 109 | /> 110 | setIsMediaLibraryOpen(false)} 113 | onSelect={image => { 114 | setIsMediaLibraryOpen(false) 115 | setSelected({ 116 | ...selected, 117 | logoUrl: image.url, 118 | }) 119 | }} 120 | /> 121 | 122 | } 124 | onAddClick={() => { 125 | setSelected({}) 126 | setIsFormOpen(true) 127 | }} 128 | /> 129 | 130 | {products.map(item => ( 131 | { 135 | setSelected(item) 136 | setIsFormOpen(true) 137 | }} 138 | onDelete={() => { 139 | setSelected(item) 140 | setIsConfirmDialogOpen(true) 141 | }} 142 | /> 143 | ))} 144 | 145 | 146 | 147 | ) 148 | } 149 | 150 | const useStyles = makeStyles(theme => ({ 151 | contentGrid: { 152 | paddingTop: theme.spacing(4), 153 | paddingBottom: theme.spacing(4), 154 | }, 155 | })) 156 | -------------------------------------------------------------------------------- /packages/sales-app/src/pages/signin.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useRef, useState } from 'react' 2 | import { Link as RouterLink, useHistory, useLocation } from 'react-router-dom' 3 | import { AppContext, LoadingButton } from '@serverless-aws-cdk-ecommerce/react-components' 4 | import { makeStyles } from '@material-ui/core/styles' 5 | import { Avatar, CssBaseline, TextField, Link, Grid, Typography, Container } from '@material-ui/core' 6 | import LockOutlinedIcon from '@material-ui/icons/LockOutlined' 7 | 8 | export function SignInPage() { 9 | const history = useHistory() 10 | const { state: { from = '/' } = {} } = useLocation() 11 | const classes = useStyles() 12 | const { Auth, setUser } = useContext(AppContext) 13 | const emailRef = useRef('') 14 | const passwordRef = useRef('') 15 | const [isLoading, setIsLoading] = useState(false) 16 | const [message, setMessage] = useState('') 17 | 18 | const handleSubmit = async e => { 19 | e.preventDefault() 20 | setMessage('') 21 | setIsLoading(true) 22 | 23 | try { 24 | const user = await Auth.signIn(emailRef.current.value, passwordRef.current.value) 25 | setUser(user) 26 | 27 | const { challengeName, signInUserSession } = user 28 | if (challengeName === 'NEW_PASSWORD_REQUIRED') return history.push('/password') 29 | 30 | localStorage.setItem('token', signInUserSession.accessToken.jwtToken) 31 | history.push(from) 32 | } catch (error) { 33 | console.error(error) 34 | setIsLoading(false) 35 | setMessage(error.message) 36 | localStorage.removeItem('token') 37 | } 38 | } 39 | 40 | return ( 41 | 42 | 43 |
44 | 45 | 46 | 47 | 48 | Benutzeranmeldung 49 | 50 |
handleSubmit(e)}> 51 | 63 | 75 | 76 | 77 | 78 | 79 | Kennwort vergessen? 80 | 81 | 82 | 83 | 84 | Neues Konto? 85 | 86 | 87 | 88 |
89 | {message} 90 |
91 |
92 |
93 | 94 |
95 |
96 | ) 97 | } 98 | 99 | const useStyles = makeStyles(theme => ({ 100 | '@global': { 101 | body: { 102 | backgroundColor: theme.palette.common.white, 103 | }, 104 | }, 105 | paper: { 106 | marginTop: theme.spacing(8), 107 | display: 'flex', 108 | flexDirection: 'column', 109 | alignItems: 'center', 110 | }, 111 | avatar: { 112 | margin: theme.spacing(1), 113 | backgroundColor: theme.palette.secondary.main, 114 | }, 115 | form: { 116 | width: '100%', // Fix IE 11 issue. 117 | marginTop: theme.spacing(1), 118 | }, 119 | message: { 120 | color: 'red', 121 | margin: theme.spacing(2, 0), 122 | }, 123 | })) 124 | -------------------------------------------------------------------------------- /packages/sales-app/src/pages/signup.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useRef, useState } from 'react' 2 | import { Link as RouterLink, useHistory } from 'react-router-dom' 3 | import { AppContext } from '@serverless-aws-cdk-ecommerce/react-components' 4 | 5 | import { makeStyles } from '@material-ui/core/styles' 6 | import { Avatar, Button, CssBaseline, TextField, Link, Grid, Typography, Container } from '@material-ui/core' 7 | import LockOutlinedIcon from '@material-ui/icons/LockOutlined' 8 | 9 | export function SignUpPage() { 10 | const history = useHistory() 11 | const classes = useStyles() 12 | const { Auth } = useContext(AppContext) 13 | const [message, setMessage] = useState('') 14 | const [isConfirmVisible, setIsConfirmVisible] = useState(false) 15 | const emailRef = useRef('') 16 | const passwordRef = useRef('') 17 | const passwordVerifyRef = useRef('') 18 | const codeRef = useRef('') 19 | 20 | const handleSignupSubmit = async e => { 21 | e.preventDefault() 22 | 23 | if (passwordRef.current.value !== passwordVerifyRef.current.value) { 24 | return setMessage('Die Kennwörter stimmen nicht überein.') 25 | } 26 | 27 | try { 28 | await Auth.signUp({ 29 | username: emailRef.current.value, 30 | password: passwordRef.current.value, 31 | attributes: { 32 | email: emailRef.current.value, 33 | website: emailRef.current.value.split('@')[1], 34 | }, 35 | }) 36 | 37 | setIsConfirmVisible(true) 38 | } catch (error) { 39 | setMessage(error.message) 40 | } 41 | } 42 | 43 | const handleSignupConfigSubmit = async e => { 44 | e.preventDefault() 45 | 46 | try { 47 | await Auth.confirmSignUp(emailRef.current.value, codeRef.current.value) 48 | 49 | history.push('/signin') 50 | } catch (error) { 51 | setMessage(error.message) 52 | } 53 | } 54 | 55 | return ( 56 | 57 | 58 |
59 | 60 | 61 | 62 | 63 | Benutzerregistrierung 64 | 65 |
(isConfirmVisible ? handleSignupConfigSubmit(e) : handleSignupSubmit(e))} 69 | > 70 | 71 | 72 | 82 | 83 | 84 | 95 | 96 | 97 | 98 | 108 | 109 | 110 | {isConfirmVisible && ( 111 | 112 | 122 | 123 | )} 124 | 125 | 128 |
129 | 130 | 131 | 132 | Zur Anmeldung 133 | 134 | 135 | 136 | 137 | 138 | {message &&

{message}

} 139 |
140 |
141 |
142 |
143 | ) 144 | } 145 | 146 | const useStyles = makeStyles(theme => ({ 147 | '@global': { 148 | body: { 149 | backgroundColor: theme.palette.common.white, 150 | }, 151 | }, 152 | paper: { 153 | marginTop: theme.spacing(8), 154 | display: 'flex', 155 | flexDirection: 'column', 156 | alignItems: 'center', 157 | }, 158 | avatar: { 159 | margin: theme.spacing(1), 160 | backgroundColor: theme.palette.secondary.main, 161 | }, 162 | form: { 163 | width: '100%', // Fix IE 11 issue. 164 | marginTop: theme.spacing(3), 165 | }, 166 | submit: { 167 | margin: theme.spacing(3, 0, 2), 168 | }, 169 | })) 170 | -------------------------------------------------------------------------------- /packages/sales-app/src/theme.js: -------------------------------------------------------------------------------- 1 | import { createMuiTheme } from '@material-ui/core/styles' 2 | import { red } from '@material-ui/core/colors' 3 | 4 | const theme = createMuiTheme({ 5 | palette: { 6 | primary: { 7 | main: '#1976d2', 8 | }, 9 | secondary: { 10 | main: '#03a9f4', 11 | }, 12 | error: { 13 | main: red.A400, 14 | }, 15 | background: { 16 | default: '#fff', 17 | }, 18 | }, 19 | }) 20 | 21 | export default theme 22 | -------------------------------------------------------------------------------- /packages/shop-app/.env-template: -------------------------------------------------------------------------------- 1 | CDK_STACK_NAME= 2 | CDK_STACK_ENV= 3 | 4 | CDK_AWS_COGNITO_USER_POOL_ID= 5 | CDK_AWS_COGNITO_USER_POOL_WEBCLIENT_ID= 6 | CDK_AWS_APPSYNC_URL= 7 | CDK_AWS_APPSYNC_APIKEY= -------------------------------------------------------------------------------- /packages/shop-app/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "jest/globals": true 6 | }, 7 | "extends": ["airbnb", "prettier", "prettier/react"], 8 | "parser": "babel-eslint", 9 | "parserOptions": { 10 | "ecmaFeatures": { 11 | "experimentalObjectRestSpread": true, 12 | "jsx": true 13 | }, 14 | "sourceType": "module" 15 | }, 16 | "plugins": ["react", "prettier", "jest"], 17 | "rules": { 18 | "no-use-before-define": "off", 19 | "prettier/prettier": "error", 20 | "import/prefer-default-export": "off", 21 | "import/no-extraneous-dependencies": "off", 22 | "react/jsx-filename-extension": "off", 23 | "react/jsx-props-no-spreading": "off", 24 | "jsx-a11y/anchor-is-valid": "off", 25 | "no-console": 0, 26 | "no-underscore-dangle": [2, { "allowAfterThis": true }], 27 | "no-shadow": "off", 28 | "jsx-a11y/label-has-for": [ 29 | 2, 30 | { 31 | "required": { 32 | "every": ["id"] 33 | } 34 | } 35 | ], 36 | "react/prop-types": 0 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/shop-app/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | build 4 | .cache 5 | .DS_Store 6 | node_modules/ 7 | .env 8 | .env.development 9 | .env.production 10 | 11 | # Logs 12 | logs 13 | *.log 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | 18 | .cache/ 19 | public 20 | build 21 | dist 22 | storybook-static 23 | 24 | tmp 25 | cdk.context.json 26 | .cdk.staging 27 | cdk.out -------------------------------------------------------------------------------- /packages/shop-app/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "bracketSpacing": true, 6 | "jsxBracketSameLine": false, 7 | "semi": false 8 | } 9 | -------------------------------------------------------------------------------- /packages/shop-app/README.md: -------------------------------------------------------------------------------- 1 | # ECommerce Shop App 2 | 3 | ## Features 4 | 5 | - GraphQL API 6 | - React 16 7 | - PWA (includes manifest.webmanifest & offline support) 8 | - ES-Lint & Prettier 9 | - Material-UI 10 | - Authentication via AWS Cognito (Login and Register) 11 | -------------------------------------------------------------------------------- /packages/shop-app/gatsby-browser.js: -------------------------------------------------------------------------------- 1 | import wrapProvider from './wrap-with-provider' 2 | 3 | export const wrapRootElement = wrapProvider 4 | -------------------------------------------------------------------------------- /packages/shop-app/gatsby-config.js: -------------------------------------------------------------------------------- 1 | const fetch = require('isomorphic-fetch') 2 | const { createHttpLink } = require('apollo-link-http') 3 | 4 | require('dotenv').config({ 5 | path: `.env.${process.env.NODE_ENV}`, 6 | }) 7 | 8 | module.exports = { 9 | siteMetadata: { 10 | title: 'E-Commerce Shop', 11 | author: 'Mike Bild', 12 | description: 'A another e-commerce shop.', 13 | siteUrl: 'https://ecommerce-shop.mikebild.com', 14 | }, 15 | pathPrefix: '', 16 | 17 | plugins: [ 18 | { 19 | resolve: 'gatsby-plugin-env-variables', 20 | options: { 21 | whitelist: [ 22 | 'CDK_AWS_COGNITO_USER_POOL_ID', 23 | 'CDK_AWS_COGNITO_USER_POOL_WEBCLIENT_ID', 24 | 'CDK_AWS_APPSYNC_URL', 25 | 'CDK_AWS_APPSYNC_APIKEY', 26 | 'npm_package_version', 27 | 'CDK_STACK_ENV', 28 | ], 29 | }, 30 | }, 31 | { 32 | resolve: 'gatsby-plugin-eslint', 33 | options: { 34 | test: /\.js$|\.jsx$/, 35 | exclude: /(node_modules|cache|public)/, 36 | options: { 37 | emitWarning: true, 38 | failOnError: false, 39 | }, 40 | }, 41 | }, 42 | { 43 | resolve: 'gatsby-source-graphql', 44 | options: { 45 | typeName: 'ECommerce', 46 | fieldName: 'ecommerce', 47 | fetch, 48 | createLink: pluginOptions => { 49 | return createHttpLink({ 50 | uri: process.env.CDK_AWS_APPSYNC_URL, 51 | headers: { 52 | 'x-api-key': process.env.CDK_AWS_APPSYNC_APIKEY, 53 | }, 54 | }) 55 | }, 56 | }, 57 | }, 58 | { 59 | resolve: `gatsby-source-filesystem`, 60 | options: { 61 | path: `${__dirname}/src/pages`, 62 | name: 'pages', 63 | }, 64 | }, 65 | { 66 | resolve: 'gatsby-source-filesystem', 67 | options: { 68 | name: 'img', 69 | path: `${__dirname}/src/images`, 70 | }, 71 | }, 72 | `gatsby-transformer-sharp`, 73 | `gatsby-plugin-sharp`, 74 | { 75 | resolve: `gatsby-plugin-manifest`, 76 | options: { 77 | name: 'E-Commerce Shop App', 78 | short_name: 'E-Commerce Shop App', 79 | start_url: '/', 80 | background_color: '#ffffff', 81 | theme_color: '#ffffff', 82 | display: 'minimal-ui', 83 | icons: [ 84 | { 85 | src: `/favicons/android-chrome-512x512.png`, 86 | sizes: `512x512`, 87 | type: `image/png`, 88 | }, 89 | ], 90 | }, 91 | }, 92 | `gatsby-plugin-offline`, 93 | `gatsby-plugin-react-helmet`, 94 | ], 95 | } 96 | -------------------------------------------------------------------------------- /packages/shop-app/gatsby-node.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | exports.createPages = async ({ graphql, actions }) => { 4 | const { createPage } = actions 5 | const productPageTemplate = path.resolve('src/templates/ProductPage.jsx') 6 | const result = await graphql(` 7 | query ProductList { 8 | ecommerce { 9 | productList { 10 | products: items { 11 | ...ProductFragment 12 | } 13 | } 14 | } 15 | } 16 | 17 | fragment ProductFragment on ECommerce_Product { 18 | id 19 | title 20 | } 21 | `) 22 | 23 | if (result.errors) return console.error(result.errors) 24 | 25 | result.data.ecommerce.productList.products.forEach(({ id }) => { 26 | createPage({ 27 | path: `/products/${id}/`, 28 | component: productPageTemplate, 29 | context: { 30 | id, 31 | }, 32 | }) 33 | }) 34 | } 35 | 36 | exports.onCreateWebpackConfig = ({ actions }) => { 37 | actions.setWebpackConfig({ 38 | node: { fs: 'empty' }, 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /packages/shop-app/gatsby-ssr.js: -------------------------------------------------------------------------------- 1 | import 'localstorage-polyfill' 2 | import wrapProvider from './wrap-with-provider' 3 | 4 | export const wrapRootElement = wrapProvider 5 | -------------------------------------------------------------------------------- /packages/shop-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@serverless-aws-cdk-ecommerce/shop-app", 3 | "description": "ECommerce Shop App", 4 | "version": "1.0.1", 5 | "dependencies": { 6 | "@material-ui/core": "4.5.1", 7 | "@material-ui/icons": "4.5.1", 8 | "@serverless-aws-cdk-ecommerce/react-components": "^1.0.0", 9 | "gatsby": "2.16.4", 10 | "isomorphic-fetch": "2.2.1", 11 | "localstorage-polyfill": "1.0.1", 12 | "react": "16.10.2", 13 | "react-dom": "16.10.2", 14 | "react-helmet": "6.0.0-beta", 15 | "roboto-fontface": "0.10.0" 16 | }, 17 | "devDependencies": { 18 | "@mikebild/ssmenv-cli": "^1.0.0", 19 | "@testing-library/react": "9.3.0", 20 | "babel-eslint": "10.0.3", 21 | "dotenv": "8.2.0", 22 | "eslint": "6.5.1", 23 | "eslint-config-airbnb": "18.0.1", 24 | "eslint-config-prettier": "6.4.0", 25 | "eslint-loader": "3.0.2", 26 | "eslint-plugin-import": "2.18.2", 27 | "eslint-plugin-jest": "22.19.0", 28 | "eslint-plugin-jsx-a11y": "6.2.3", 29 | "eslint-plugin-prettier": "3.1.1", 30 | "eslint-plugin-react": "7.16.0", 31 | "eslint-plugin-react-hooks": "2.1.2", 32 | "gatsby-cli": "2.8.2", 33 | "gatsby-plugin-env-variables": "1.0.1", 34 | "gatsby-plugin-eslint": "2.0.5", 35 | "gatsby-plugin-manifest": "2.2.23", 36 | "gatsby-plugin-offline": "3.0.16", 37 | "gatsby-plugin-react-helmet": "3.1.13", 38 | "gatsby-plugin-sharp": "2.2.32", 39 | "gatsby-source-filesystem": "2.1.33", 40 | "gatsby-source-graphql": "2.1.20", 41 | "gatsby-transformer-sharp": "2.2.23", 42 | "http-server-spa": "1.3.0", 43 | "jest": "24.9.0", 44 | "prettier": "1.18.2", 45 | "rimraf": "3.0.0" 46 | }, 47 | "scripts": { 48 | "dev": "gatsby develop", 49 | "lint": "eslint 'src/**/*.{js,jsx}' --quiet", 50 | "clean": "rimraf .cache public", 51 | "develop": "rimraf .cache && gatsby develop", 52 | "build": "npm run clean && ssmenv -- gatsby build", 53 | "serve": "http-server-spa public index.html 8082", 54 | "format": "prettier --write \"src/**/*.{js,jsx}\"" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/shop-app/src/components/CartSummary.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { makeStyles } from '@material-ui/core/styles' 3 | import { 4 | Button, 5 | IconButton, 6 | Dialog, 7 | DialogActions, 8 | DialogContent, 9 | DialogTitle, 10 | Table, 11 | TableCell, 12 | TableBody, 13 | TableRow, 14 | TableHead, 15 | CardMedia, 16 | Typography, 17 | } from '@material-ui/core' 18 | import CloseIcon from '@material-ui/icons/Close' 19 | import DeleteIcon from '@material-ui/icons/Delete' 20 | 21 | export function CartSummary({ 22 | isOpen = false, 23 | value = {}, 24 | onClose = () => {}, 25 | onCartOrder = () => {}, 26 | onCancel = () => {}, 27 | onRemoveProduct = () => {}, 28 | }) { 29 | const classes = useStyles() 30 | const { products: { cartProducts = [] } = {} } = value || {} 31 | const hasProducts = cartProducts.length 32 | 33 | return ( 34 | 35 | 36 | Warenkorb 37 |
38 | 39 | 40 | 41 |
42 |
43 | 44 | {!hasProducts && ( 45 | 46 | Keine Produkte im Warenkorb 47 | 48 | )} 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | {cartProducts.filter(Boolean).map(({ id, title, logoUrl, price }) => ( 57 | 58 | 59 | 60 | 61 | {title} 62 | {price} € 63 | 64 | { 66 | onRemoveProduct({ id }) 67 | }} 68 | > 69 | 70 | 71 | 72 | 73 | ))} 74 | 75 |
76 |
77 | 78 | 81 | 84 | 85 |
86 | ) 87 | } 88 | 89 | const useStyles = makeStyles(theme => ({ 90 | wrapper: { 91 | display: 'flex', 92 | justifyContent: 'space-between', 93 | alignItems: 'center', 94 | }, 95 | headerButtons: { 96 | float: 'right', 97 | }, 98 | dialogActions: { 99 | padding: theme.spacing(3), 100 | }, 101 | cover: { 102 | width: 100, 103 | height: 100, 104 | }, 105 | table: { 106 | marginBottom: 40, 107 | }, 108 | })) 109 | -------------------------------------------------------------------------------- /packages/shop-app/src/components/ProductCard.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { makeStyles } from '@material-ui/core/styles' 3 | import { Typography, Grid, Card, CardActions, CardContent, CardMedia, IconButton } from '@material-ui/core' 4 | import AddShoppingCartIcon from '@material-ui/icons/AddShoppingCart' 5 | 6 | export function ProductCard({ item = {}, onAddToCart }) { 7 | const { 8 | title, 9 | price, 10 | description, 11 | logoUrl, 12 | color = '#FFF', 13 | category: { title: categoryTitle, color: categoryColor } = {}, 14 | } = item 15 | const classes = useStyles() 16 | 17 | return ( 18 | 19 | 20 | {logoUrl && } 21 | {categoryTitle && ( 22 |
29 | 30 | {categoryTitle} 31 | 32 |
33 | )} 34 | 35 | 36 | {title} 37 | 38 | 39 | {description} 40 | 41 | 42 | {price} € 43 | 44 | 45 | 46 | onAddToCart(item)}> 47 | 48 | 49 | 50 |
51 |
52 | ) 53 | } 54 | 55 | const useStyles = makeStyles(theme => ({ 56 | card: {}, 57 | content: {}, 58 | cover: { 59 | height: 200, 60 | }, 61 | actions: { 62 | justifyContent: 'flex-end', 63 | paddingRight: theme.spacing(2), 64 | paddingBottom: theme.spacing(0), 65 | }, 66 | })) 67 | -------------------------------------------------------------------------------- /packages/shop-app/src/components/ProfileForm.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react' 2 | import { makeStyles } from '@material-ui/core/styles' 3 | import { Button, IconButton, TextField, Dialog, DialogActions, DialogContent, DialogTitle } from '@material-ui/core' 4 | import CloseIcon from '@material-ui/icons/Close' 5 | 6 | export function ProfileForm({ 7 | isOpen = false, 8 | value = {}, 9 | onClose = () => {}, 10 | onEdit = () => {}, 11 | onCancel = () => {}, 12 | }) { 13 | const classes = useStyles() 14 | const { firstName, lastName, address, city, zip } = value || {} 15 | const firstNameRef = useRef() 16 | const lastNameRef = useRef() 17 | const addressRef = useRef() 18 | const cityRef = useRef() 19 | const zipRef = useRef() 20 | 21 | return ( 22 | 23 | 24 | Profil 25 |
26 | 27 | 28 | 29 |
30 |
31 | 32 | 40 | 48 | 58 |
59 | 60 |
61 |
62 | 70 |
71 |
72 | 73 | 76 | 90 | 91 |
92 | ) 93 | } 94 | 95 | const useStyles = makeStyles(theme => ({ 96 | wrapper: { 97 | display: 'flex', 98 | justifyContent: 'space-between', 99 | alignItems: 'center', 100 | }, 101 | headerButtons: { 102 | float: 'right', 103 | }, 104 | dialogActions: { 105 | padding: theme.spacing(3), 106 | }, 107 | })) 108 | -------------------------------------------------------------------------------- /packages/shop-app/src/components/SEO.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Helmet } from 'react-helmet' 3 | import { useStaticQuery, graphql, withPrefix } from 'gatsby' 4 | 5 | export function SEO({ description, lang = 'de', meta = [], keywords = [], title }) { 6 | const { site } = useStaticQuery( 7 | graphql` 8 | query { 9 | site { 10 | siteMetadata { 11 | title 12 | description 13 | author 14 | } 15 | } 16 | } 17 | ` 18 | ) 19 | const metaDescription = description || site.siteMetadata.description 20 | 21 | return ( 22 | 0 65 | ? { 66 | name: `keywords`, 67 | content: keywords.join(`, `), 68 | } 69 | : [] 70 | ) 71 | .concat(meta)} 72 | > 73 | 74 | 75 | 76 | 77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /packages/shop-app/src/images/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeBild/serverless-aws-cdk-ecommerce/cb08fb4ff4a58284f37f15c77cf7fec9e4eae094/packages/shop-app/src/images/header.png -------------------------------------------------------------------------------- /packages/shop-app/src/images/ill-short-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | ILOVELAMP-white-01 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /packages/shop-app/src/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | ILOVELAMP-white-01 6 | 7 | 8 | 9 | 11 | 12 | 15 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /packages/shop-app/src/images/moltin-light-hex.svg.svg: -------------------------------------------------------------------------------- 1 | light-hex -------------------------------------------------------------------------------- /packages/shop-app/src/pages/404.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link as RouterLink, navigate } from 'gatsby' 3 | import { makeStyles } from '@material-ui/core/styles' 4 | import { Container, Grid, Typography, IconButton } from '@material-ui/core' 5 | import { ShoppingCart as ShoppingCartIcon } from '@material-ui/icons' 6 | import { Layout } from '@serverless-aws-cdk-ecommerce/react-components' 7 | 8 | export default function Profile() { 9 | const classes = useStyles() 10 | 11 | return ( 12 | { 16 | return ( 17 | <> 18 | 19 | Produkte 20 | 21 | 22 | 23 | 24 | 25 | ) 26 | }} 27 | onLogout={() => navigate('/signin')} 28 | > 29 | 30 | 31 | 404 32 | 33 | 34 | 35 | 36 | ) 37 | } 38 | 39 | const useStyles = makeStyles(theme => ({ 40 | contentGrid: { 41 | paddingTop: theme.spacing(4), 42 | paddingBottom: theme.spacing(4), 43 | }, 44 | topMenuLink: { 45 | color: 'white', 46 | textDecoration: 'none', 47 | textTransform: 'uppercase', 48 | paddingLeft: theme.spacing(3), 49 | paddingRight: theme.spacing(3), 50 | '&:hover': { color: 'lightgray', textDecoration: 'none' }, 51 | }, 52 | shoppingCartLink: { 53 | color: 'white', 54 | textDecoration: 'none', 55 | }, 56 | })) 57 | -------------------------------------------------------------------------------- /packages/shop-app/src/pages/__tests__/login.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, fireEvent, cleanup } from 'react-testing-library' 3 | 4 | const Login = () => null 5 | 6 | afterEach(cleanup) 7 | 8 | test('renders', () => { 9 | const { asFragment } = render() 10 | expect(asFragment()).toMatchSnapshot() 11 | }) 12 | 13 | test('expect error messages if form is submitted with empty fields', () => { 14 | const { queryByText, getByLabelText, getByText } = render() 15 | expect(getByLabelText(/Email/i).value).toBe('') 16 | expect(getByLabelText(/Password/i).value).toBe('') 17 | fireEvent.click(getByText(/Login/i)) 18 | expect(queryByText('Email address is required')).not.toBeNull() 19 | }) 20 | -------------------------------------------------------------------------------- /packages/shop-app/src/pages/__tests__/register.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-testing-library' 3 | 4 | const Register = () => null 5 | const props = { location: { pathname: '' } } 6 | 7 | test('renders', () => { 8 | const { asFragment } = render() 9 | expect(asFragment()).toMatchSnapshot() 10 | }) 11 | 12 | test('register renders name and email', () => { 13 | const { getByLabelText } = render() 14 | expect(getByLabelText(/Email/i)) 15 | expect(getByLabelText(/name/i)) 16 | }) 17 | -------------------------------------------------------------------------------- /packages/shop-app/src/pages/confirm.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useRef, useState } from 'react' 2 | import { navigate } from 'gatsby' 3 | import { AppContext } from '@serverless-aws-cdk-ecommerce/react-components' 4 | import Avatar from '@material-ui/core/Avatar' 5 | import Button from '@material-ui/core/Button' 6 | import CssBaseline from '@material-ui/core/CssBaseline' 7 | import TextField from '@material-ui/core/TextField' 8 | import Grid from '@material-ui/core/Grid' 9 | import LockOutlinedIcon from '@material-ui/icons/LockOutlined' 10 | import Typography from '@material-ui/core/Typography' 11 | import { makeStyles } from '@material-ui/core/styles' 12 | import Container from '@material-ui/core/Container' 13 | 14 | export default function Confirm() { 15 | const classes = useStyles() 16 | const { Auth } = useContext(AppContext) 17 | const [message, setMessage] = useState('') 18 | const emailRef = useRef('') 19 | const codeRef = useRef('') 20 | 21 | const handleSubmit = async e => { 22 | e.preventDefault() 23 | 24 | try { 25 | await Auth.confirmSignUp(emailRef.current.value, codeRef.current.value) 26 | navigate('/signin') 27 | } catch (error) { 28 | setMessage(error.message) 29 | } 30 | } 31 | 32 | return ( 33 | 34 | 35 |
36 | 37 | 38 | 39 | 40 | Benutzerregistrierung bestätigen 41 | 42 | 43 | 44 | 45 | Bitte geben Sie den Registrierungscode aus Ihrer E-Mail ein. 46 | 47 | 48 | 49 |
handleSubmit(e)}> 50 | 51 | 52 | 62 | 63 | 64 | 74 | 75 | 76 | 79 | 80 | 81 |
82 | 83 | 84 | {message &&

{message}

} 85 |
86 |
87 |
88 |
89 | ) 90 | } 91 | 92 | const useStyles = makeStyles(theme => ({ 93 | '@global': { 94 | body: { 95 | backgroundColor: theme.palette.common.white, 96 | }, 97 | }, 98 | paper: { 99 | marginTop: theme.spacing(8), 100 | display: 'flex', 101 | flexDirection: 'column', 102 | alignItems: 'center', 103 | }, 104 | avatar: { 105 | margin: theme.spacing(1), 106 | backgroundColor: theme.palette.secondary.main, 107 | }, 108 | form: { 109 | width: '100%', // Fix IE 11 issue. 110 | marginTop: theme.spacing(3), 111 | }, 112 | submit: { 113 | margin: theme.spacing(3, 0, 2), 114 | }, 115 | subtitle: { 116 | marginTop: theme.spacing(2), 117 | }, 118 | })) 119 | -------------------------------------------------------------------------------- /packages/shop-app/src/pages/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useState } from 'react' 2 | import { Link as RouterLink, navigate } from 'gatsby' 3 | import { useQuery, useMutation } from '@apollo/react-hooks' 4 | import graphql from 'graphql-tag' 5 | import { makeStyles } from '@material-ui/core/styles' 6 | import { Layout, Loading, SearchInput, AppContext } from '@serverless-aws-cdk-ecommerce/react-components' 7 | import { Container, Grid, IconButton, Badge, MenuItem } from '@material-ui/core' 8 | import ShoppingCartIcon from '@material-ui/icons/ShoppingCart' 9 | import SettingsIcon from '@material-ui/icons/Settings' 10 | import { ProductCard } from '../components/ProductCard' 11 | import { CartSummary } from '../components/CartSummary' 12 | import { ProfileForm } from '../components/ProfileForm' 13 | import { SEO } from '../components/SEO' 14 | 15 | const PAGE_QUERY = graphql(` 16 | query ProductPage { 17 | productList { 18 | products: items { 19 | id 20 | title 21 | description 22 | logoUrl 23 | price 24 | } 25 | } 26 | me { 27 | user { 28 | username 29 | } 30 | profile { 31 | id 32 | firstName 33 | lastName 34 | address 35 | zip 36 | city 37 | } 38 | cart { 39 | id 40 | products { 41 | cartProducts: items { 42 | id 43 | title 44 | description 45 | logoUrl 46 | price 47 | } 48 | } 49 | } 50 | } 51 | } 52 | `) 53 | 54 | const CART_UPSERT = graphql(` 55 | mutation CartUpsert($input: CartUpsertInput!) { 56 | cartUpsert(input: $input) { 57 | id 58 | products { 59 | cartProducts: items { 60 | id 61 | title 62 | description 63 | logoUrl 64 | price 65 | } 66 | } 67 | } 68 | } 69 | `) 70 | 71 | const PROFILE_UPSERT = graphql(` 72 | mutation ProfileUpsert($input: ProfileInput!) { 73 | profileUpsert(input: $input) { 74 | id 75 | firstName 76 | lastName 77 | address 78 | zip 79 | city 80 | } 81 | } 82 | `) 83 | 84 | export default function Products() { 85 | const classes = useStyles() 86 | const { 87 | loading, 88 | data: { me: { cart = {}, profile = {} } = {}, productList: { products = [] } = {} } = {}, 89 | } = useQuery(PAGE_QUERY) 90 | const [cartUpsert] = useMutation(CART_UPSERT) 91 | const [profileUpsert] = useMutation(PROFILE_UPSERT) 92 | const [isCartOpen, setIsCartOpen] = useState(false) 93 | const [isProfileOpen, setIsProfileOpen] = useState(false) 94 | 95 | const { products: { cartProducts = [] } = {} } = cart || {} 96 | const { firstName } = profile || {} 97 | 98 | return ( 99 | 100 | 101 | { 105 | return ( 106 | <> 107 | 108 | Produkte 109 | 110 | setIsCartOpen(true)}> 111 | 112 | 113 | 114 | 115 | 116 | ) 117 | }} 118 | renderProfileMenu={({ close }) => { 119 | return ( 120 | { 122 | setIsProfileOpen(true) 123 | close() 124 | }} 125 | > 126 | 127 | Profil von {firstName} 128 | 129 | ) 130 | }} 131 | onLogout={() => navigate('/signin')} 132 | > 133 | 134 | setIsProfileOpen(false)} 138 | onClose={() => setIsProfileOpen(false)} 139 | onEdit={async profile => { 140 | await profileUpsert({ variables: { input: profile } }) 141 | setIsProfileOpen(false) 142 | }} 143 | /> 144 | setIsCartOpen(false)} 148 | onCancel={() => setIsCartOpen(false)} 149 | onCartOrder={() => {}} 150 | onRemoveProduct={async ({ id }) => { 151 | const productIds = cartProducts 152 | .filter(Boolean) 153 | .map(({ id }) => id) 154 | .filter(itm => itm !== id) 155 | await cartUpsert({ variables: { input: { productIds } } }) 156 | }} 157 | /> 158 | 159 | 160 | 161 | {products.filter(Boolean).map(item => ( 162 | { 166 | const productIds = [...new Set([...cartProducts.filter(Boolean).map(({ id }) => id), id])] 167 | await cartUpsert({ variables: { input: { productIds } } }) 168 | }} 169 | /> 170 | ))} 171 | 172 | 173 | 174 | 175 | ) 176 | } 177 | 178 | const useStyles = makeStyles(theme => ({ 179 | contentGrid: { 180 | paddingTop: theme.spacing(4), 181 | paddingBottom: theme.spacing(4), 182 | }, 183 | search: { 184 | paddingBottom: theme.spacing(4), 185 | }, 186 | topMenuLink: { 187 | color: 'white', 188 | textDecoration: 'none', 189 | textTransform: 'uppercase', 190 | paddingLeft: theme.spacing(3), 191 | paddingRight: theme.spacing(3), 192 | '&:hover': { color: 'lightgray', textDecoration: 'none' }, 193 | }, 194 | shoppingCartLink: { 195 | color: 'white', 196 | textDecoration: 'none', 197 | }, 198 | leftIcon: { 199 | marginRight: theme.spacing(1), 200 | }, 201 | })) 202 | 203 | function CheckAuth({ children, isProtected = false }) { 204 | const hasToken = Boolean(localStorage.getItem('token')) 205 | const { user = hasToken ? {} : undefined, setUser, Auth } = useContext(AppContext) 206 | const fetchCurrentAuthenticatedUser = async () => { 207 | try { 208 | const cognitoUser = await Auth.currentAuthenticatedUser() 209 | localStorage.setItem('token', cognitoUser.signInUserSession.accessToken.jwtToken) 210 | setUser(cognitoUser) 211 | } catch (error) { 212 | localStorage.removeItem('token') 213 | setUser(undefined) 214 | navigate('/signin') 215 | } 216 | } 217 | 218 | useEffect(() => { 219 | fetchCurrentAuthenticatedUser() 220 | }, []) 221 | 222 | if (isProtected && !user) { 223 | return null 224 | } 225 | 226 | return children 227 | } 228 | -------------------------------------------------------------------------------- /packages/shop-app/src/pages/password.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useRef, useState } from 'react' 2 | import { navigate } from 'gatsby' 3 | import { makeStyles } from '@material-ui/core/styles' 4 | import { AppContext } from '@serverless-aws-cdk-ecommerce/react-components' 5 | import { Avatar, Button, CssBaseline, TextField, Grid, Typography, Container } from '@material-ui/core' 6 | import LockOutlinedIcon from '@material-ui/icons/LockOutlined' 7 | 8 | export default function Password() { 9 | const classes = useStyles() 10 | const { Auth, user } = useContext(AppContext) 11 | const [message, setMessage] = useState('') 12 | const newPasswordRef = useRef('') 13 | const newPasswordVerifyRef = useRef('') 14 | 15 | const handleSubmit = async e => { 16 | e.preventDefault() 17 | 18 | if (newPasswordRef.current.value !== newPasswordVerifyRef.current.value) { 19 | return setMessage('Kennwörter stimmen nicht überein.') 20 | } 21 | 22 | try { 23 | await Auth.completeNewPassword(user, newPasswordRef.current.value) 24 | navigate('/signin') 25 | } catch (error) { 26 | console.error(error) 27 | setMessage(error.message) 28 | } 29 | 30 | return null 31 | } 32 | 33 | return ( 34 | 35 | 36 |
37 | 38 | 39 | 40 | 41 | Kennwort ändern 42 | 43 | 44 | 45 | 46 | Bitte geben Sie Ihr altes und neues Kennwort ein. 47 | 48 | 49 | 50 |
handleSubmit(e)}> 51 | 52 | 53 | 63 | 64 | 65 | 75 | 76 | 77 | 80 | 81 | 82 |
83 | 84 | 85 | {message &&

{message}

} 86 |
87 |
88 |
89 |
90 | ) 91 | } 92 | 93 | const useStyles = makeStyles(theme => ({ 94 | '@global': { 95 | body: { 96 | backgroundColor: theme.palette.common.white, 97 | }, 98 | }, 99 | paper: { 100 | marginTop: theme.spacing(8), 101 | display: 'flex', 102 | flexDirection: 'column', 103 | alignItems: 'center', 104 | }, 105 | avatar: { 106 | margin: theme.spacing(1), 107 | backgroundColor: theme.palette.secondary.main, 108 | }, 109 | form: { 110 | width: '100%', // Fix IE 11 issue. 111 | marginTop: theme.spacing(3), 112 | }, 113 | submit: { 114 | margin: theme.spacing(3, 0, 2), 115 | }, 116 | subtitle: { 117 | marginTop: theme.spacing(2), 118 | }, 119 | })) 120 | -------------------------------------------------------------------------------- /packages/shop-app/src/pages/profile.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect } from 'react' 2 | import { Link as RouterLink, navigate } from 'gatsby' 3 | import graphql from 'graphql-tag' 4 | import { useQuery } from '@apollo/react-hooks' 5 | import { makeStyles } from '@material-ui/core/styles' 6 | import { Container, Grid, Typography, IconButton } from '@material-ui/core' 7 | import { ShoppingCart as ShoppingCartIcon } from '@material-ui/icons' 8 | import { Layout, Loading, AppContext } from '@serverless-aws-cdk-ecommerce/react-components' 9 | 10 | const LIST = graphql(` 11 | query ProductList { 12 | productList { 13 | products: items { 14 | id 15 | title 16 | } 17 | } 18 | } 19 | `) 20 | 21 | export default function Profile() { 22 | const classes = useStyles() 23 | const { loading = false } = useQuery(LIST) 24 | 25 | return ( 26 | 27 | { 31 | return ( 32 | <> 33 | 34 | Produkte 35 | 36 | 37 | 38 | 39 | 40 | ) 41 | }} 42 | onLogout={() => navigate('/signin')} 43 | > 44 | 45 | 46 | 47 | Dein Benutzerprofil 48 | 49 | 50 | 51 | 52 | 53 | ) 54 | } 55 | 56 | const useStyles = makeStyles(theme => ({ 57 | contentGrid: { 58 | paddingTop: theme.spacing(4), 59 | paddingBottom: theme.spacing(4), 60 | }, 61 | topMenuLink: { 62 | color: 'white', 63 | textDecoration: 'none', 64 | textTransform: 'uppercase', 65 | paddingLeft: theme.spacing(3), 66 | paddingRight: theme.spacing(3), 67 | '&:hover': { color: 'lightgray', textDecoration: 'none' }, 68 | }, 69 | shoppingCartLink: { 70 | color: 'white', 71 | textDecoration: 'none', 72 | }, 73 | })) 74 | 75 | function CheckAuth({ children, isProtected = false }) { 76 | const hasToken = Boolean(localStorage.getItem('token')) 77 | const { user = hasToken ? {} : undefined, setUser, Auth } = useContext(AppContext) 78 | const fetchCurrentAuthenticatedUser = async () => { 79 | try { 80 | const cognitoUser = await Auth.currentAuthenticatedUser() 81 | localStorage.setItem('token', cognitoUser.signInUserSession.accessToken.jwtToken) 82 | setUser(cognitoUser) 83 | } catch (error) { 84 | localStorage.removeItem('token') 85 | setUser(undefined) 86 | navigate('/signin') 87 | } 88 | } 89 | 90 | useEffect(() => { 91 | fetchCurrentAuthenticatedUser() 92 | }, []) 93 | 94 | if (isProtected && !user) { 95 | return null 96 | } 97 | 98 | return children 99 | } 100 | -------------------------------------------------------------------------------- /packages/shop-app/src/pages/signin.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useRef, useState } from 'react' 2 | import { Link as RouterLink, navigate } from 'gatsby' 3 | import Avatar from '@material-ui/core/Avatar' 4 | import CssBaseline from '@material-ui/core/CssBaseline' 5 | import TextField from '@material-ui/core/TextField' 6 | import Link from '@material-ui/core/Link' 7 | import Grid from '@material-ui/core/Grid' 8 | import LockOutlinedIcon from '@material-ui/icons/LockOutlined' 9 | import Typography from '@material-ui/core/Typography' 10 | import { makeStyles } from '@material-ui/core/styles' 11 | import Container from '@material-ui/core/Container' 12 | import { AppContext, LoadingButton } from '@serverless-aws-cdk-ecommerce/react-components' 13 | 14 | export default function SignIn() { 15 | const classes = useStyles() 16 | const { Auth, setUser } = useContext(AppContext) 17 | const emailRef = useRef('') 18 | const passwordRef = useRef('') 19 | const [isLoading, setIsLoading] = useState(false) 20 | const [message, setMessage] = useState('') 21 | 22 | const handleSubmit = async e => { 23 | e.preventDefault() 24 | setMessage('') 25 | setIsLoading(true) 26 | 27 | try { 28 | const user = await Auth.signIn(emailRef.current.value, passwordRef.current.value) 29 | setUser(user) 30 | 31 | const { challengeName, signInUserSession } = user 32 | if (challengeName === 'NEW_PASSWORD_REQUIRED') return navigate('/password') 33 | 34 | localStorage.setItem('token', signInUserSession.accessToken.jwtToken) 35 | navigate('/') 36 | } catch (error) { 37 | console.error(error) 38 | setIsLoading(false) 39 | setMessage(error.message) 40 | localStorage.removeItem('token') 41 | } 42 | 43 | return null 44 | } 45 | 46 | return ( 47 | 48 | 49 |
50 | 51 | 52 | 53 | 54 | Benutzeranmeldung 55 | 56 |
handleSubmit(e)}> 57 | 69 | 81 | 82 | 83 | 84 | 85 | Kennwort vergessen? 86 | 87 | 88 | 89 | 90 | Neues Konto? 91 | 92 | 93 | 94 |
{message}
95 |
96 |
97 | 98 |
99 |
100 | ) 101 | } 102 | 103 | const useStyles = makeStyles(theme => ({ 104 | '@global': { 105 | body: { 106 | backgroundColor: theme.palette.common.white, 107 | }, 108 | }, 109 | paper: { 110 | marginTop: theme.spacing(8), 111 | display: 'flex', 112 | flexDirection: 'column', 113 | alignItems: 'center', 114 | }, 115 | avatar: { 116 | margin: theme.spacing(1), 117 | backgroundColor: theme.palette.secondary.main, 118 | }, 119 | form: { 120 | width: '100%', // Fix IE 11 issue. 121 | marginTop: theme.spacing(1), 122 | }, 123 | message: { 124 | color: 'red', 125 | margin: theme.spacing(2, 0), 126 | }, 127 | })) 128 | -------------------------------------------------------------------------------- /packages/shop-app/src/pages/signup.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useRef, useState } from 'react' 2 | import { Link as RouterLink, navigate } from 'gatsby' 3 | import { AppContext } from '@serverless-aws-cdk-ecommerce/react-components' 4 | import { makeStyles } from '@material-ui/core/styles' 5 | import Avatar from '@material-ui/core/Avatar' 6 | import Button from '@material-ui/core/Button' 7 | import CssBaseline from '@material-ui/core/CssBaseline' 8 | import TextField from '@material-ui/core/TextField' 9 | import Link from '@material-ui/core/Link' 10 | import Grid from '@material-ui/core/Grid' 11 | 12 | import Typography from '@material-ui/core/Typography' 13 | 14 | import Container from '@material-ui/core/Container' 15 | 16 | import LockOutlinedIcon from '@material-ui/icons/LockOutlined' 17 | 18 | export default function SignUp() { 19 | const classes = useStyles() 20 | const { Auth } = useContext(AppContext) 21 | const [message, setMessage] = useState('') 22 | const emailRef = useRef('') 23 | const passwordRef = useRef('') 24 | 25 | const handleSubmit = async e => { 26 | e.preventDefault() 27 | 28 | try { 29 | await Auth.signUp({ 30 | username: emailRef.current.value, 31 | password: passwordRef.current.value, 32 | attributes: { 33 | email: emailRef.current.value, 34 | website: emailRef.current.value.split('@')[1], 35 | }, 36 | }) 37 | 38 | navigate('/confirm') 39 | } catch (error) { 40 | setMessage(error.message) 41 | } 42 | } 43 | 44 | return ( 45 | 46 | 47 |
48 | 49 | 50 | 51 | 52 | Benutzerregistrierung 53 | 54 |
handleSubmit(e)}> 55 | 56 | 57 | 67 | 68 | 69 | 80 | 81 | 82 | 85 |
86 | 87 | 88 | 89 | Zur Anmeldung 90 | 91 | 92 | 93 | 94 | 95 | {message &&

{message}

} 96 |
97 |
98 |
99 |
100 | ) 101 | } 102 | 103 | const useStyles = makeStyles(theme => ({ 104 | '@global': { 105 | body: { 106 | backgroundColor: theme.palette.common.white, 107 | }, 108 | }, 109 | paper: { 110 | marginTop: theme.spacing(8), 111 | display: 'flex', 112 | flexDirection: 'column', 113 | alignItems: 'center', 114 | }, 115 | avatar: { 116 | margin: theme.spacing(1), 117 | backgroundColor: theme.palette.secondary.main, 118 | }, 119 | form: { 120 | width: '100%', // Fix IE 11 issue. 121 | marginTop: theme.spacing(3), 122 | }, 123 | submit: { 124 | margin: theme.spacing(3, 0, 2), 125 | }, 126 | })) 127 | -------------------------------------------------------------------------------- /packages/shop-app/src/templates/ProductPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { graphql } from 'gatsby' 3 | import { Layout } from '@serverless-aws-cdk-ecommerce/react-components' 4 | import { SEO } from '../components/SEO' 5 | 6 | export default function ProductPageTemplate() { 7 | return ( 8 | 9 | 10 | 11 | ) 12 | } 13 | 14 | export const pageQuery = graphql` 15 | query ProductsQuery($id: ID!) { 16 | ecommerce { 17 | productList(filter: { id: { eq: $id } }) { 18 | products: items { 19 | ...ProductFragment 20 | } 21 | } 22 | } 23 | } 24 | fragment ProductFragment on ECommerce_Product { 25 | id 26 | title 27 | } 28 | ` 29 | -------------------------------------------------------------------------------- /packages/shop-app/static/favicons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeBild/serverless-aws-cdk-ecommerce/cb08fb4ff4a58284f37f15c77cf7fec9e4eae094/packages/shop-app/static/favicons/android-chrome-512x512.png -------------------------------------------------------------------------------- /packages/shop-app/static/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeBild/serverless-aws-cdk-ecommerce/cb08fb4ff4a58284f37f15c77cf7fec9e4eae094/packages/shop-app/static/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /packages/shop-app/static/favicons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /packages/shop-app/static/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeBild/serverless-aws-cdk-ecommerce/cb08fb4ff4a58284f37f15c77cf7fec9e4eae094/packages/shop-app/static/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /packages/shop-app/static/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeBild/serverless-aws-cdk-ecommerce/cb08fb4ff4a58284f37f15c77cf7fec9e4eae094/packages/shop-app/static/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /packages/shop-app/static/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeBild/serverless-aws-cdk-ecommerce/cb08fb4ff4a58284f37f15c77cf7fec9e4eae094/packages/shop-app/static/favicons/favicon.ico -------------------------------------------------------------------------------- /packages/shop-app/static/favicons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeBild/serverless-aws-cdk-ecommerce/cb08fb4ff4a58284f37f15c77cf7fec9e4eae094/packages/shop-app/static/favicons/mstile-150x150.png -------------------------------------------------------------------------------- /packages/shop-app/static/favicons/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /packages/shop-app/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /packages/shop-app/theme.js: -------------------------------------------------------------------------------- 1 | import { createMuiTheme } from '@material-ui/core/styles' 2 | import { red } from '@material-ui/core/colors' 3 | 4 | const theme = createMuiTheme({ 5 | palette: { 6 | primary: { 7 | main: '#1976d2', 8 | }, 9 | secondary: { 10 | main: '#03a9f4', 11 | }, 12 | error: { 13 | main: red.A400, 14 | }, 15 | background: { 16 | default: '#fff', 17 | }, 18 | }, 19 | }) 20 | 21 | export default theme 22 | -------------------------------------------------------------------------------- /packages/shop-app/wrap-with-provider.js: -------------------------------------------------------------------------------- 1 | import 'isomorphic-fetch' 2 | import 'roboto-fontface' 3 | import React from 'react' 4 | import { ThemeProvider } from '@material-ui/styles' 5 | import CssBaseline from '@material-ui/core/CssBaseline' 6 | import { AppProvider } from '@serverless-aws-cdk-ecommerce/react-components' 7 | import theme from './theme' 8 | 9 | const config = { 10 | userPoolId: process.env.CDK_AWS_COGNITO_USER_POOL_ID, 11 | userPoolWebClientId: process.env.CDK_AWS_COGNITO_USER_POOL_WEBCLIENT_ID, 12 | graphQlUrl: process.env.CDK_AWS_APPSYNC_URL, 13 | graphQlApiKey: process.env.CDK_AWS_APPSYNC_APIKEY, 14 | appVersion: process.env.npm_package_version, 15 | appEnv: process.env.CDK_STACK_ENV, 16 | } 17 | export default ({ element }) => ( 18 | 19 | 20 | {element} 21 | 22 | ) 23 | -------------------------------------------------------------------------------- /packages/ssmenv-cli/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | build 4 | .cache 5 | .DS_Store 6 | node_modules/ 7 | .env 8 | .env.development 9 | .env.production 10 | 11 | # Logs 12 | logs 13 | *.log 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | 18 | .cache/ 19 | public 20 | build 21 | dist 22 | storybook-static 23 | 24 | tmp 25 | cdk.context.json 26 | .cdk.staging 27 | cdk.out -------------------------------------------------------------------------------- /packages/ssmenv-cli/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "bracketSpacing": true, 6 | "jsxBracketSameLine": false, 7 | "semi": false 8 | } 9 | -------------------------------------------------------------------------------- /packages/ssmenv-cli/README.md: -------------------------------------------------------------------------------- 1 | # AWS SSM Parameter Store to Environment Variables 2 | -------------------------------------------------------------------------------- /packages/ssmenv-cli/bin/ssmenv.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const { SSM } = require('aws-sdk') 3 | const spawn = require('cross-spawn') 4 | const argv = require('minimist')(process.argv.slice(2)) 5 | require('dotenv').config({ path: `.env.${process.env.NODE_ENV}` }) 6 | require('dotenv').config() 7 | 8 | const CDK_STACK_NAME = process.env.CDK_STACK_NAME 9 | const CDK_STACK_ENV = process.env.CDK_STACK_ENV 10 | 11 | if (!CDK_STACK_NAME) { 12 | console.error(`Environment Variable "CDK_STACK_NAME" is missing.`) 13 | process.exit(1) 14 | } 15 | 16 | if (!CDK_STACK_ENV) { 17 | console.error(`Environment Variable "CDK_STACK_ENV" is missing.`) 18 | process.exit(1) 19 | } 20 | 21 | main() 22 | .then(() => { 23 | spawn(argv._[0], argv._.slice(1), { stdio: 'inherit' }).on('exit', exitCode => { 24 | process.exit(exitCode) 25 | }) 26 | }) 27 | .catch(error => { 28 | console.error(error) 29 | process.exit(1) 30 | }) 31 | 32 | async function main() { 33 | const ssm = new SSM({ region: 'eu-central-1' }) 34 | const Names = await allParametersByPath(ssm) 35 | 36 | const chunkedNames = Names.reduce((state, name, i) => { 37 | if (i % 10 === 0) state.push([]) 38 | const currentArrayLength = state.length 39 | state[currentArrayLength - 1].push(name) 40 | return state 41 | }, []) 42 | 43 | const chunkedPromises = chunkedNames.map(Names => ssm.getParameters({ Names }).promise()) 44 | const allChunkedResults = await Promise.all(chunkedPromises) 45 | const allParameters = allChunkedResults.reduce((state, results) => [...state, ...results.Parameters], []) 46 | 47 | console.log(`Start mapping AWS SSM store parameters /${CDK_STACK_NAME}/${CDK_STACK_ENV}/ to environment variables.`) 48 | 49 | allParameters.forEach(({ Name, Value }) => { 50 | const key = Name.replace(`/${CDK_STACK_NAME}/${CDK_STACK_ENV}/`, '') 51 | const value = Value 52 | const hasExistingValue = Boolean(process.env[key]) 53 | 54 | if (hasExistingValue) return console.log(`Dublicate : ${key}`) 55 | 56 | console.log(`Created : ${key}`) 57 | process.env[key] = value 58 | }) 59 | } 60 | 61 | async function allParametersByPath(ssm, names = [], nextToken) { 62 | const { Parameters = [], NextToken } = await ssm 63 | .getParametersByPath({ Path: `/${CDK_STACK_NAME}/${CDK_STACK_ENV}`, NextToken: nextToken }) 64 | .promise() 65 | 66 | const Names = [...names, ...Parameters.map(({ Name }) => Name)] 67 | 68 | return NextToken ? await allParametersByPath(ssm, Names, NextToken) : Names 69 | } 70 | -------------------------------------------------------------------------------- /packages/ssmenv-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@mikebild/ssmenv-cli", 4 | "version": "1.0.0", 5 | "description": "AWS SSM Paramter Store to Environment Variables", 6 | "bin": { 7 | "ssmenv": "bin/ssmenv.js" 8 | }, 9 | "license": "MIT", 10 | "dependencies": { 11 | "aws-sdk": "2.551.0", 12 | "cross-spawn": "7.0.1", 13 | "dotenv": "8.2.0", 14 | "minimist": "1.2.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/storybook/.env.config: -------------------------------------------------------------------------------- 1 | CDK_STACK_NAME= 2 | CDK_STACK_ENV= -------------------------------------------------------------------------------- /packages/storybook/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | build 4 | .cache 5 | .DS_Store 6 | node_modules/ 7 | .env 8 | .env.development 9 | .env.production 10 | 11 | # Logs 12 | logs 13 | *.log 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | 18 | .cache/ 19 | public 20 | build 21 | dist 22 | storybook-static 23 | 24 | tmp 25 | cdk.context.json 26 | .cdk.staging 27 | cdk.out -------------------------------------------------------------------------------- /packages/storybook/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "bracketSpacing": true, 6 | "jsxBracketSameLine": false, 7 | "semi": false 8 | } 9 | -------------------------------------------------------------------------------- /packages/storybook/.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register' 2 | import '@storybook/addon-links/register' 3 | import '@storybook/addon-knobs/register' 4 | import '@storybook/addon-viewport/register' 5 | -------------------------------------------------------------------------------- /packages/storybook/.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure, addDecorator, addParameters } from '@storybook/react' 2 | import { withKnobs } from '@storybook/addon-knobs' 3 | import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport' 4 | 5 | configure(require.context('../stories', true, /\.stories\.js$/), module) 6 | addDecorator(withKnobs) 7 | 8 | addParameters({ 9 | viewport: { 10 | viewports: { 11 | ...INITIAL_VIEWPORTS, 12 | }, 13 | }, 14 | }) 15 | -------------------------------------------------------------------------------- /packages/storybook/README.md: -------------------------------------------------------------------------------- 1 | # Storybook 2 | -------------------------------------------------------------------------------- /packages/storybook/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@serverless-aws-cdk-ecommerce/storybook", 4 | "version": "1.0.0", 5 | "description": "React Component Storybook", 6 | "scripts": { 7 | "clean": "rimraf storybook-static", 8 | "develop": "start-storybook -p 6006", 9 | "build": "ssmenv -- build-storybook", 10 | "serve": "http-server-spa storybook-static index.html 8083", 11 | "format": "prettier --write \"**/*.{js,jsx}\"" 12 | }, 13 | "dependencies": {}, 14 | "devDependencies": { 15 | "@babel/core": "7.6.4", 16 | "@mikebild/ssmenv-cli": "^1.0.0", 17 | "@storybook/addon-viewport": "5.2.4", 18 | "babel-loader": "8.0.6", 19 | "http-server-spa": "1.3.0", 20 | "prettier": "1.18.2", 21 | "rimraf": "3.0.0", 22 | "@serverless-aws-cdk-ecommerce/react-components": "^1.0.0", 23 | "@storybook/addon-actions": "5.2.4", 24 | "@storybook/addon-knobs": "5.2.4", 25 | "@storybook/addon-links": "5.2.4", 26 | "@storybook/addons": "5.2.4", 27 | "@storybook/react": "5.2.4", 28 | "storybook": "5.1.11" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/storybook/stories/1-Atomics.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { boolean, text, select } from '@storybook/addon-knobs' 3 | import { action } from '@storybook/addon-actions' 4 | import { Loading, LoadingButton, SearchInput } from '@serverless-aws-cdk-ecommerce/react-components' 5 | 6 | export default { 7 | title: '1-Atomics', 8 | } 9 | 10 | export function loading() { 11 | return 12 | } 13 | 14 | export function loadingButton() { 15 | return ( 16 | 23 | ) 24 | } 25 | 26 | export function searchInput() { 27 | return ( 28 | 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /packages/storybook/stories/2-Molecules.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { boolean, text } from '@storybook/addon-knobs' 3 | import { action } from '@storybook/addon-actions' 4 | import { ConfirmDialog, Topbar } from '@serverless-aws-cdk-ecommerce/react-components' 5 | 6 | export default { 7 | title: '2-Molecules', 8 | } 9 | 10 | export function confirmDialog() { 11 | return ( 12 | 20 | ) 21 | } 22 | 23 | export function topbar() { 24 | return ( 25 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /packages/storybook/stories/3-Organisms.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { boolean } from '@storybook/addon-knobs' 3 | import { action } from '@storybook/addon-actions' 4 | import { MediaLibrary } from '@serverless-aws-cdk-ecommerce/react-components' 5 | 6 | export default { 7 | title: '3-Organisms', 8 | } 9 | 10 | export function mediaLibrary() { 11 | return 12 | } 13 | -------------------------------------------------------------------------------- /packages/storybook/stories/4-Templates.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { boolean } from '@storybook/addon-knobs' 3 | import { action } from '@storybook/addon-actions' 4 | import { Layout, AppProvider } from '@serverless-aws-cdk-ecommerce/react-components' 5 | 6 | export default { 7 | title: '4-Templates', 8 | } 9 | 10 | export function layout() { 11 | return ( 12 | 13 | 14 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /packages/storybook/stories/5-Providers.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { boolean } from '@storybook/addon-knobs' 3 | import { action } from '@storybook/addon-actions' 4 | import { AppProvider } from '@serverless-aws-cdk-ecommerce/react-components' 5 | 6 | export default { 7 | title: '5-Providers', 8 | } 9 | 10 | export function appProvider() { 11 | return 12 | } 13 | -------------------------------------------------------------------------------- /packages/storybook/stories/6-Hooks.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { boolean } from '@storybook/addon-knobs' 3 | import { action } from '@storybook/addon-actions' 4 | import { useForm } from '@serverless-aws-cdk-ecommerce/react-components' 5 | 6 | export default { 7 | title: '6-Hooks', 8 | } 9 | 10 | export function useFormHook() { 11 | return null 12 | } 13 | --------------------------------------------------------------------------------