├── .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 | 
4 | 
5 | 
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------
/packages/shop-app/src/images/logo.svg:
--------------------------------------------------------------------------------
1 |
19 |
--------------------------------------------------------------------------------
/packages/shop-app/src/images/moltin-light-hex.svg.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------