├── frontend ├── .eslintrc.json ├── public │ └── favicon.ico ├── .env.local.example ├── next-env.d.ts ├── pages │ ├── _app.tsx │ └── index.tsx ├── config.ts ├── next.config.js ├── codegen.yml ├── styles │ ├── globals.css │ └── Home.module.css ├── .gitignore ├── graphql │ ├── operation.graphql │ └── generated.ts ├── tsconfig.json ├── README.md ├── components │ └── chime │ │ ├── context.tsx │ │ ├── MeetingControl.tsx │ │ ├── MeetingCaller.tsx │ │ └── MeetingReceiver.tsx └── package.json ├── .npmignore ├── imgs ├── gui.png ├── sequence.png └── architecture.png ├── .gitignore ├── .prettierrc ├── backend ├── .prettierrc ├── schema.graphql └── chime-resolver.ts ├── jest.config.js ├── CODE_OF_CONDUCT.md ├── test ├── chime-sdk-demo.test.ts └── __snapshots__ │ └── chime-sdk-demo.test.ts.snap ├── bin └── chime-sdk-demo.ts ├── .github └── workflows │ ├── update_snapshot.yml │ └── build.yml ├── tsconfig.json ├── package.json ├── LICENSE ├── lib ├── constructs │ ├── auth.ts │ ├── frontend.ts │ └── backend-api.ts └── chime-sdk-demo-stack.ts ├── cdk.json ├── CONTRIBUTING.md └── README.md /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /imgs/gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/chime-sdk-meetings-demo-nextjs-appsync-cognito-cdk/HEAD/imgs/gui.png -------------------------------------------------------------------------------- /imgs/sequence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/chime-sdk-meetings-demo-nextjs-appsync-cognito-cdk/HEAD/imgs/sequence.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | -------------------------------------------------------------------------------- /imgs/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/chime-sdk-meetings-demo-nextjs-appsync-cognito-cdk/HEAD/imgs/architecture.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 120 7 | } -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/chime-sdk-meetings-demo-nextjs-appsync-cognito-cdk/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /backend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true, 6 | "printWidth": 120 7 | } 8 | -------------------------------------------------------------------------------- /frontend/.env.local.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_BACKEND_API_URL= 2 | NEXT_PUBLIC_USER_POOL_ID= 3 | NEXT_PUBLIC_USER_POOL_CLIENT_ID= 4 | NEXT_PUBLIC_AWS_REGION= 5 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /frontend/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /frontend/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css'; 2 | import type { AppProps } from 'next/app'; 3 | 4 | function MyApp({ Component, pageProps }: AppProps) { 5 | 6 | return ; 7 | } 8 | 9 | export default MyApp; 10 | -------------------------------------------------------------------------------- /frontend/config.ts: -------------------------------------------------------------------------------- 1 | const config = { 2 | apiEndpoint: process.env.NEXT_PUBLIC_BACKEND_API_URL!, 3 | userPoolId: process.env.NEXT_PUBLIC_USER_POOL_ID!, 4 | awsRegion: process.env.NEXT_PUBLIC_AWS_REGION!, 5 | userPoolClientId: process.env.NEXT_PUBLIC_USER_POOL_CLIENT_ID!, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /frontend/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | output: 'export', 6 | webpack: (config, options) => { 7 | config.externals.push('mapbox-gl'); 8 | return config; 9 | }, 10 | }; 11 | 12 | module.exports = nextConfig; 13 | -------------------------------------------------------------------------------- /frontend/codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: ../backend/schema.graphql 3 | documents: graphql/operation.graphql 4 | generates: 5 | graphql/generated.ts: 6 | plugins: 7 | - typescript 8 | # - typescript-resolvers 9 | - typescript-operations 10 | # - typed-document-node 11 | - typescript-urql 12 | -------------------------------------------------------------------------------- /frontend/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | -------------------------------------------------------------------------------- /test/chime-sdk-demo.test.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "aws-cdk-lib"; 2 | import { Template } from 'aws-cdk-lib/assertions'; 3 | import { ChimeSdkDemoStack } from "../lib/chime-sdk-demo-stack"; 4 | 5 | test("Snapshot test", () => { 6 | const app = new cdk.App(); 7 | const stack = new ChimeSdkDemoStack(app, "TestStack"); 8 | const template = Template.fromStack(stack); 9 | expect(template).toMatchSnapshot(); 10 | }); 11 | -------------------------------------------------------------------------------- /bin/chime-sdk-demo.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from 'aws-cdk-lib'; 4 | import { ChimeSdkDemoStack } from '../lib/chime-sdk-demo-stack'; 5 | 6 | const app = new cdk.App(); 7 | new ChimeSdkDemoStack(app, 'ChimeSdkDemoStack', {}); 8 | 9 | // import { Aspects } from 'aws-cdk-lib'; 10 | // import { AwsSolutionsChecks } from 'cdk-nag'; 11 | // Aspects.of(app).add(new AwsSolutionsChecks()); 12 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # typescript 32 | *.tsbuildinfo 33 | 34 | !next-env.d.ts 35 | !next.config.js 36 | -------------------------------------------------------------------------------- /frontend/graphql/operation.graphql: -------------------------------------------------------------------------------- 1 | mutation CreateMeeting { 2 | createChimeMeeting { 3 | meetingResponse 4 | attendeeResponse 5 | } 6 | } 7 | 8 | mutation JoinMeeting($meetingResponse: String!) { 9 | joinMeeting(meetingResponse: $meetingResponse) { 10 | meetingResponse 11 | attendeeResponse 12 | } 13 | } 14 | 15 | mutation CreateMeetingInvitation($target: ID!, $source: ID!, $meetingResponse: String!) { 16 | createMeetingInvitation(target: $target, source: $source, meetingResponse: $meetingResponse) { 17 | source 18 | target 19 | meetingResponse 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/update_snapshot.yml: -------------------------------------------------------------------------------- 1 | name: Update snapshot 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - 'dependabot/**' 8 | 9 | jobs: 10 | update: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Use Node.js 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: "20.x" 18 | - run: npm ci 19 | - run: npm run test -- -u 20 | - name: Add & Commit 21 | uses: EndBug/add-and-commit@v7.2.0 22 | with: 23 | add: "test/__snapshots__/." 24 | message: "update snapshot" 25 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: [push, workflow_dispatch] 3 | jobs: 4 | Build-and-Test-CDK: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - name: Use Node.js 9 | uses: actions/setup-node@v1 10 | with: 11 | node-version: "20.x" 12 | - run: | 13 | npm ci 14 | npm run build 15 | npm run test 16 | Build-and-Test-Frontend: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Use Node.js 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: "20.x" 24 | - run: | 25 | npm ci 26 | npm run build 27 | working-directory: ./frontend 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2018", 7 | "dom" 8 | ], 9 | "declaration": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": false, 19 | "inlineSourceMap": true, 20 | "inlineSources": true, 21 | "experimentalDecorators": true, 22 | "strictPropertyInitialization": false, 23 | "noEmit": true, 24 | "typeRoots": [ 25 | "./node_modules/@types" 26 | ] 27 | }, 28 | "exclude": [ 29 | "node_modules", 30 | "cdk.out", 31 | "frontend" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /backend/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: Query 3 | mutation: Mutation 4 | subscription: Subscription 5 | } 6 | 7 | type ChimeMeeting @aws_cognito_user_pools { 8 | meetingResponse: String! 9 | attendeeResponse: String! 10 | } 11 | 12 | type MeetingInvitation @aws_cognito_user_pools { 13 | target: ID! 14 | source: ID! 15 | meetingResponse: String! 16 | } 17 | 18 | type Query { 19 | dummy: ChimeMeeting 20 | } 21 | 22 | type Mutation { 23 | createChimeMeeting: ChimeMeeting @aws_cognito_user_pools 24 | createMeetingInvitation( 25 | target: ID! 26 | source: ID! 27 | meetingResponse: String! 28 | ): MeetingInvitation @aws_cognito_user_pools 29 | joinMeeting(meetingResponse: String!): ChimeMeeting @aws_cognito_user_pools 30 | } 31 | 32 | type Subscription { 33 | onMeetingInvited(target: ID!): MeetingInvitation 34 | @aws_subscribe(mutations: ["createMeetingInvitation"]) 35 | @aws_cognito_user_pools 36 | } 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@aws-samples/chime-sdk-demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "bin": { 6 | "chime-sdk-demo": "bin/chime-sdk-demo.js" 7 | }, 8 | "scripts": { 9 | "build": "tsc", 10 | "watch": "tsc -w", 11 | "test": "jest", 12 | "cdk": "cdk" 13 | }, 14 | "devDependencies": { 15 | "@types/aws-lambda": "^8.10.101", 16 | "@types/jest": "^27.5.0", 17 | "@types/node": "10.17.27", 18 | "@types/prettier": "2.6.0", 19 | "@types/uuid": "^8.3.4", 20 | "aws-cdk": "^2.26.0", 21 | "jest": "^27.5.1", 22 | "ts-jest": "^27.1.4", 23 | "ts-node": "^10.7.0", 24 | "typescript": "^4.6.3", 25 | "uuid": "^8.3.2" 26 | }, 27 | "dependencies": { 28 | "@aws-sdk/client-chime": "^3.592.0", 29 | "aws-cdk-lib": "^2.26.0", 30 | "cdk-nag": "^2.14.29", 31 | "constructs": "^10.0.0", 32 | "deploy-time-build": "^0.3.1", 33 | "source-map-support": "^0.5.21" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /lib/constructs/auth.ts: -------------------------------------------------------------------------------- 1 | import { CfnOutput, Duration, RemovalPolicy } from 'aws-cdk-lib'; 2 | import { UserPool, UserPoolClient } from 'aws-cdk-lib/aws-cognito'; 3 | import { Construct } from 'constructs'; 4 | 5 | export interface AuthProps {} 6 | 7 | export class Auth extends Construct { 8 | readonly userPool: UserPool; 9 | readonly client: UserPoolClient; 10 | constructor(scope: Construct, id: string, props?: AuthProps) { 11 | super(scope, id); 12 | 13 | const userPool = new UserPool(this, 'UserPool', { 14 | passwordPolicy: { 15 | requireUppercase: true, 16 | requireSymbols: true, 17 | requireDigits: true, 18 | minLength: 8, 19 | }, 20 | selfSignUpEnabled: true, 21 | signInAliases: { 22 | username: false, 23 | email: true, 24 | }, 25 | removalPolicy: RemovalPolicy.DESTROY, 26 | }); 27 | 28 | const client = userPool.addClient(`Client`, { 29 | idTokenValidity: Duration.days(1), 30 | }); 31 | 32 | this.client = client; 33 | this.userPool = userPool; 34 | 35 | new CfnOutput(this, "UserPoolId", {value: userPool.userPoolId}); 36 | new CfnOutput(this, "UserPoolClientId", {value: client.userPoolClientId}); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Frontend 2 | This is a single page application frontend powered by Next.js. 3 | 4 | ## Getting Started 5 | To run this frontend locally, you must first set environment variables in `.env.local`. You can get those values from the deployed stack outputs. 6 | 7 | ```sh 8 | cp .env.local.example .env.local 9 | # set the variables below: 10 | # NEXT_PUBLIC_BACKEND_API_URL: AppSync endpoint 11 | # NEXT_PUBLIC_USER_POOL_ID: Cognito user pool ID 12 | # NEXT_PUBLIC_USER_POOL_CLIENT_ID: Cognito user pool client ID 13 | # NEXT_PUBLIC_AWS_REGION: AWS region for AppSync and Cognito 14 | ``` 15 | 16 | Then, run the development server: 17 | 18 | ```bash 19 | npm run dev 20 | ``` 21 | 22 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 23 | 24 | ## Generate GraphQL interface 25 | We use [GraphQL Code Generator](https://www.graphql-code-generator.com/) to generate AppSync interfaces for React from GraphQL schema. Although the generated schema is already commited to git, if you make changes in GraphQL schema, you can re-generate the interfaces by the following command: 26 | 27 | ```sh 28 | npm run schema 29 | ``` 30 | 31 | The generated files will be placed in [graphql/generated.ts](./graphql/generated.ts). 32 | -------------------------------------------------------------------------------- /frontend/components/chime/context.tsx: -------------------------------------------------------------------------------- 1 | import React, { Dispatch, SetStateAction, useContext, useState } from 'react' 2 | 3 | // Chime Meetingは同時に複数の通話をすると扱いが複雑になるので、それを防ぐために 4 | // 各コンポーネント間で通話状態を共有するContextを作る 5 | export type ChimeMeetingState = { 6 | isOnCall: boolean 7 | meetingResponse?: any 8 | } 9 | 10 | const initialState: ChimeMeetingState = { 11 | isOnCall: false, 12 | } 13 | 14 | const ChimeMeetingStateContext = 15 | React.createContext(initialState) 16 | 17 | const SetChimeMeetingStateContext = React.createContext< 18 | Dispatch> 19 | >(() => {}) 20 | 21 | export const useChimeMeetingState = () => { 22 | return { 23 | state: useContext(ChimeMeetingStateContext), 24 | setState: useContext(SetChimeMeetingStateContext), 25 | } 26 | } 27 | 28 | export const ChimeMeetingStateProvider = (props: { 29 | children: React.ReactNode 30 | }) => { 31 | const [state, setState] = useState(initialState) 32 | return ( 33 | 34 | 35 | {props.children} 36 | 37 | 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /lib/chime-sdk-demo-stack.ts: -------------------------------------------------------------------------------- 1 | import { CfnOutput, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib'; 2 | import { BlockPublicAccess, Bucket, BucketEncryption, ObjectOwnership } from 'aws-cdk-lib/aws-s3'; 3 | import { Construct } from 'constructs'; 4 | import { Auth } from './constructs/auth'; 5 | import { BackendApi } from './constructs/backend-api'; 6 | import { Frontend } from './constructs/frontend'; 7 | 8 | export class ChimeSdkDemoStack extends Stack { 9 | constructor(scope: Construct, id: string, props?: StackProps) { 10 | super(scope, id, props); 11 | 12 | const accessLogBucket = new Bucket(this, 'AccessLogBucket', { 13 | encryption: BucketEncryption.S3_MANAGED, 14 | blockPublicAccess: BlockPublicAccess.BLOCK_ALL, 15 | enforceSSL: true, 16 | removalPolicy: RemovalPolicy.DESTROY, 17 | objectOwnership: ObjectOwnership.OBJECT_WRITER, 18 | autoDeleteObjects: true, 19 | }); 20 | 21 | const auth = new Auth(this, 'Auth'); 22 | 23 | const backend = new BackendApi(this, 'BackendApi', { 24 | auth, 25 | }); 26 | 27 | const frontend = new Frontend(this, 'Frontend', { 28 | backendApiUrl: backend.api.graphqlUrl, 29 | auth, 30 | accessLogBucket, 31 | }); 32 | 33 | new CfnOutput(this, 'FrontendDomainName', { 34 | value: `https://${frontend.cloudFrontWebDistribution.distributionDomainName}`, 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@aws-samples/frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "schema": "graphql-codegen" 11 | }, 12 | "dependencies": { 13 | "@aws-amplify/ui-react": "^5.0.0", 14 | "@emotion/react": "^11.9.0", 15 | "@emotion/styled": "^11.8.1", 16 | "@mui/material": "^5.8.2", 17 | "amazon-chime-sdk-component-library-react": "^3.2.0", 18 | "amazon-chime-sdk-js": "^3.5.0", 19 | "aws-amplify": "^5.3.12", 20 | "graphql": "^16.8.1", 21 | "next": "14.1.1", 22 | "react": "^18", 23 | "react-dom": "^18", 24 | "react-is": "^18", 25 | "styled-components": "^5.3.5", 26 | "styled-system": "^5.1.5", 27 | "urql": "^2.2.2", 28 | "urql-exchange-async-headers": "^0.0.2" 29 | }, 30 | "devDependencies": { 31 | "@graphql-codegen/cli": "^5.0.2", 32 | "@graphql-codegen/introspection": "^4.0.3", 33 | "@graphql-codegen/typed-document-node": "^5.0.7", 34 | "@graphql-codegen/typescript": "^4.0.7", 35 | "@graphql-codegen/typescript-operations": "^4.2.1", 36 | "@graphql-codegen/typescript-resolvers": "^4.1.0", 37 | "@graphql-codegen/typescript-urql": "^4.0.0", 38 | "@types/node": "^18", 39 | "@types/react": "^18", 40 | "@types/react-dom": "^18", 41 | "@types/styled-components": "^5.1.25", 42 | "eslint": "8.19.0", 43 | "eslint-config-next": "^14.0.0", 44 | "typescript": "^5" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /backend/chime-resolver.ts: -------------------------------------------------------------------------------- 1 | import { v4 } from 'uuid' 2 | import { AppSyncResolverHandler } from 'aws-lambda' 3 | import { Chime } from '@aws-sdk/client-chime' 4 | 5 | type EmptyArgument = {} 6 | 7 | type JoinMeetingArgument = { 8 | meetingResponse: string 9 | } 10 | 11 | type CreateChimeMeetingResult = { 12 | attendeeResponse: string 13 | meetingResponse: string 14 | } 15 | 16 | type JoinMeetingResult = { 17 | attendeeResponse: any 18 | meetingResponse: string 19 | } 20 | 21 | type Result = CreateChimeMeetingResult | JoinMeetingResult 22 | type Argument = EmptyArgument | JoinMeetingArgument 23 | 24 | // You must use "us-east-1" as the region for Chime API and set the endpoint. 25 | // https://docs.aws.amazon.com/chime-sdk/latest/dg/configure-sdk-invoke.html 26 | const chime = new Chime({ region: 'us-east-1', endpoint: 'https://service.chime.aws.amazon.com' }) 27 | 28 | export const handler: AppSyncResolverHandler = async (event, context) => { 29 | console.log(event) 30 | switch (event.info.fieldName) { 31 | case 'createChimeMeeting': 32 | return await createChimeMeeting() 33 | case 'joinMeeting': 34 | return await joinMeeting(event.arguments as JoinMeetingArgument) 35 | default: 36 | throw new Error('invalid event field!') 37 | } 38 | } 39 | 40 | const createChimeMeeting = async (): Promise => { 41 | const meetingResponse = await chime.createMeeting({ 42 | ClientRequestToken: v4(), 43 | MediaRegion: 'ap-northeast-1', // Specify the region in which to create the meeting. 44 | }) 45 | 46 | if (meetingResponse?.Meeting?.MeetingId == null) { 47 | throw Error('empty MeetingId!') 48 | } 49 | 50 | return await joinMeeting({ meetingResponse: JSON.stringify(meetingResponse) }) 51 | } 52 | 53 | const joinMeeting = async (request: JoinMeetingArgument): Promise => { 54 | const meeting = JSON.parse(request.meetingResponse) 55 | const attendeeResponse = await chime.createAttendee({ 56 | MeetingId: meeting.Meeting.MeetingId, 57 | ExternalUserId: v4(), 58 | }) 59 | 60 | return { 61 | attendeeResponse: JSON.stringify(attendeeResponse), 62 | ...request, 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/chime-sdk-demo.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 33 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 34 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 35 | "@aws-cdk/core:enablePartitionLiterals": true, 36 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 37 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 38 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 39 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 40 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 41 | "@aws-cdk/aws-route53-patters:useCertificate": true, 42 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 43 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 44 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 45 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 46 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 47 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 48 | "@aws-cdk/aws-redshift:columnId": true, 49 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /frontend/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | } 4 | 5 | .main { 6 | min-height: 100vh; 7 | padding: 4rem 0; 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .footer { 16 | display: flex; 17 | flex: 1; 18 | padding: 2rem 0; 19 | border-top: 1px solid #eaeaea; 20 | justify-content: center; 21 | align-items: center; 22 | } 23 | 24 | .footer a { 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | flex-grow: 1; 29 | } 30 | 31 | .title a { 32 | color: #0070f3; 33 | text-decoration: none; 34 | } 35 | 36 | .title a:hover, 37 | .title a:focus, 38 | .title a:active { 39 | text-decoration: underline; 40 | } 41 | 42 | .title { 43 | margin: 0; 44 | line-height: 1.15; 45 | font-size: 4rem; 46 | } 47 | 48 | .title, 49 | .description { 50 | text-align: center; 51 | } 52 | 53 | .description { 54 | margin: 4rem 0; 55 | line-height: 1.5; 56 | font-size: 1.5rem; 57 | } 58 | 59 | .code { 60 | background: #fafafa; 61 | border-radius: 5px; 62 | padding: 0.75rem; 63 | font-size: 1.1rem; 64 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 65 | Bitstream Vera Sans Mono, Courier New, monospace; 66 | } 67 | 68 | .grid { 69 | display: flex; 70 | align-items: center; 71 | justify-content: center; 72 | flex-wrap: wrap; 73 | max-width: 800px; 74 | } 75 | 76 | .card { 77 | margin: 1rem; 78 | padding: 1.5rem; 79 | text-align: left; 80 | color: inherit; 81 | text-decoration: none; 82 | border: 1px solid #eaeaea; 83 | border-radius: 10px; 84 | transition: color 0.15s ease, border-color 0.15s ease; 85 | max-width: 300px; 86 | } 87 | 88 | .card:hover, 89 | .card:focus, 90 | .card:active { 91 | color: #0070f3; 92 | border-color: #0070f3; 93 | } 94 | 95 | .card h2 { 96 | margin: 0 0 1rem 0; 97 | font-size: 1.5rem; 98 | } 99 | 100 | .card p { 101 | margin: 0; 102 | font-size: 1.25rem; 103 | line-height: 1.5; 104 | } 105 | 106 | .logo { 107 | height: 1em; 108 | margin-left: 0.5rem; 109 | } 110 | 111 | @media (max-width: 600px) { 112 | .grid { 113 | width: 100%; 114 | flex-direction: column; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /frontend/components/chime/MeetingControl.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useMeetingManager, 3 | lightTheme, 4 | ControlBar, 5 | ControlBarButton, 6 | AudioOutputControl, 7 | AudioInputControl, 8 | Phone, 9 | VoiceFocusProvider, 10 | useMeetingEvent, 11 | } from 'amazon-chime-sdk-component-library-react' 12 | import { VoiceFocusModelName } from 'amazon-chime-sdk-js' 13 | import { useEffect } from 'react' 14 | import { ThemeProvider } from 'styled-components' 15 | import { useChimeMeetingState } from './context' 16 | 17 | const MeetingControl = () => { 18 | const meetingManager = useMeetingManager() 19 | const { state, setState } = useChimeMeetingState() 20 | const meetingResponse = state.meetingResponse 21 | const meetingEvent = useMeetingEvent() 22 | 23 | useEffect(() => { 24 | if (meetingEvent == null) return 25 | console.log(meetingEvent) 26 | switch (meetingEvent.name) { 27 | case 'meetingEnded': 28 | setState({ isOnCall: false }) 29 | break 30 | } 31 | }, [meetingEvent]) 32 | 33 | const hangUpButtonProps = { 34 | icon: , 35 | onClick: () => meetingManager.leave(), 36 | label: 'End', 37 | } 38 | 39 | // https://aws.github.io/amazon-chime-sdk-component-library-react/?path=/docs/sdk-components-meetingcontrols-audioinputvfcontrol--page 40 | const voiceFocusName = (name: string): VoiceFocusModelName => { 41 | if (name && ['default', 'ns_es'].includes(name)) { 42 | return name as VoiceFocusModelName 43 | } 44 | return 'default' 45 | } 46 | 47 | const getVoiceFocusSpecName = (): VoiceFocusModelName => { 48 | if ( 49 | meetingResponse && 50 | meetingResponse.Meeting?.MeetingFeatures?.Audio?.EchoReduction === 51 | 'AVAILABLE' 52 | ) { 53 | return voiceFocusName('ns_es') 54 | } 55 | return voiceFocusName('default') 56 | } 57 | 58 | const vfConfigValue = { 59 | spec: { name: getVoiceFocusSpecName() }, 60 | createMeetingResponse: meetingResponse, 61 | } 62 | 63 | return ( 64 | 65 | 66 | {state.isOnCall ? ( 67 | 68 | 69 | 70 | 71 | 72 | ) : ( 73 | <> 74 | )} 75 | 76 | 77 | ) 78 | } 79 | 80 | export default MeetingControl 81 | -------------------------------------------------------------------------------- /frontend/components/chime/MeetingCaller.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import Grid from '@mui/material/Grid'; 3 | import { DeviceLabels, useMeetingManager, lightTheme } from 'amazon-chime-sdk-component-library-react'; 4 | import { MeetingSessionConfiguration } from 'amazon-chime-sdk-js'; 5 | import { ThemeProvider } from 'styled-components'; 6 | import { Button, Stack, TextField } from '@mui/material'; 7 | import { useChimeMeetingState } from './context'; 8 | import { useCreateMeetingInvitationMutation, useCreateMeetingMutation } from '../../graphql/generated'; 9 | 10 | type Props = { 11 | myId: string; 12 | }; 13 | 14 | const ChimeMeeting = (props: Props) => { 15 | const [_, createMeeting] = useCreateMeetingMutation(); 16 | const [__, createMeetingInvitation] = useCreateMeetingInvitationMutation(); 17 | const [targetId, setTargetId] = useState(''); 18 | const [calling, setCalling] = useState(false); 19 | const { state, setState } = useChimeMeetingState(); 20 | 21 | const { myId } = props; 22 | const meetingManager = useMeetingManager(); 23 | 24 | const onTargetIdEdit = (e: any) => setTargetId(e.target.value); 25 | const onStartCall = async () => { 26 | setCalling(true); 27 | const response = await createMeeting(); 28 | const meeting = response.data?.createChimeMeeting; 29 | if (meeting == null) { 30 | console.error('failed to create a meeting'); 31 | setCalling(false); 32 | return; 33 | } 34 | await startMeetingSession(meeting.meetingResponse, meeting.attendeeResponse); 35 | await createMeetingInvitation({ 36 | target: targetId, 37 | source: myId, 38 | meetingResponse: meeting.meetingResponse, 39 | }); 40 | setCalling(false); 41 | }; 42 | 43 | const startMeetingSession = async (meetingResponse: string, attendeeResponse: string) => { 44 | const meetingSessionConfiguration = new MeetingSessionConfiguration( 45 | JSON.parse(meetingResponse), 46 | JSON.parse(attendeeResponse), 47 | ); 48 | 49 | await meetingManager.join(meetingSessionConfiguration, { 50 | deviceLabels: DeviceLabels.Audio, 51 | enableWebAudio: true, 52 | }); 53 | 54 | await meetingManager.start(); 55 | setState({ isOnCall: true, meetingResponse: JSON.parse(meetingResponse) }); 56 | }; 57 | 58 | return ( 59 | 60 | 61 | 68 | 75 | 76 | 77 | ); 78 | }; 79 | 80 | export default ChimeMeeting; 81 | -------------------------------------------------------------------------------- /lib/constructs/frontend.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import { RemovalPolicy, Stack } from 'aws-cdk-lib'; 3 | import { BlockPublicAccess, Bucket, BucketEncryption, IBucket } from 'aws-cdk-lib/aws-s3'; 4 | import { CloudFrontWebDistribution, OriginAccessIdentity } from 'aws-cdk-lib/aws-cloudfront'; 5 | import { NodejsBuild } from 'deploy-time-build'; 6 | import { Auth } from './auth'; 7 | 8 | export interface FrontendProps { 9 | readonly backendApiUrl: string; 10 | readonly auth: Auth; 11 | readonly accessLogBucket: IBucket; 12 | } 13 | 14 | export class Frontend extends Construct { 15 | readonly cloudFrontWebDistribution: CloudFrontWebDistribution; 16 | constructor(scope: Construct, id: string, props: FrontendProps) { 17 | super(scope, id); 18 | 19 | const assetBucket = new Bucket(this, 'AssetBucket', { 20 | encryption: BucketEncryption.S3_MANAGED, 21 | blockPublicAccess: BlockPublicAccess.BLOCK_ALL, 22 | enforceSSL: true, 23 | removalPolicy: RemovalPolicy.DESTROY, 24 | autoDeleteObjects: true, 25 | }); 26 | 27 | const originAccessIdentity = new OriginAccessIdentity(this, 'OriginAccessIdentity'); 28 | const distribution = new CloudFrontWebDistribution(this, 'Distribution', { 29 | originConfigs: [ 30 | { 31 | s3OriginSource: { 32 | s3BucketSource: assetBucket, 33 | originAccessIdentity, 34 | }, 35 | behaviors: [ 36 | { 37 | isDefaultBehavior: true, 38 | }, 39 | ], 40 | }, 41 | ], 42 | errorConfigurations: [ 43 | { 44 | errorCode: 404, 45 | errorCachingMinTtl: 0, 46 | responseCode: 200, 47 | responsePagePath: '/', 48 | }, 49 | { 50 | errorCode: 403, 51 | errorCachingMinTtl: 0, 52 | responseCode: 200, 53 | responsePagePath: '/', 54 | }, 55 | ], 56 | loggingConfig: { 57 | bucket: props.accessLogBucket, 58 | prefix: 'Frontend/', 59 | }, 60 | }); 61 | 62 | new NodejsBuild(this, 'ReactBuild', { 63 | assets: [ 64 | { 65 | path: 'frontend', 66 | exclude: ['node_modules', 'out', '.env.local*', '.next'], 67 | commands: ['npm ci'], 68 | // prevent too frequent frontend deployment, for temporary use 69 | // assetHash: 'frontend_asset', 70 | }, 71 | ], 72 | buildCommands: ['npm run build'], 73 | buildEnvironment: { 74 | NEXT_PUBLIC_BACKEND_API_URL: props.backendApiUrl, 75 | NEXT_PUBLIC_USER_POOL_ID: props.auth.userPool.userPoolId, 76 | NEXT_PUBLIC_USER_POOL_CLIENT_ID: props.auth.client.userPoolClientId, 77 | NEXT_PUBLIC_AWS_REGION: Stack.of(props.auth.userPool).region, 78 | }, 79 | destinationBucket: assetBucket, 80 | distribution, 81 | outputSourceDirectory: 'out', 82 | }); 83 | 84 | this.cloudFrontWebDistribution = distribution; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /frontend/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import Head from 'next/head' 3 | import { Amplify, Auth } from 'aws-amplify'; 4 | import config from '../config'; 5 | import '@aws-amplify/ui-react/styles.css'; 6 | import { Authenticator, Button } from '@aws-amplify/ui-react'; 7 | import { AppBar, Grid, TextField, Stack, Toolbar, Typography } from '@mui/material'; 8 | import { ChimeMeetingStateProvider } from '../components/chime/context'; 9 | import { MeetingProvider } from 'amazon-chime-sdk-component-library-react'; 10 | import MeetingCaller from '../components/chime/MeetingCaller'; 11 | import MeetingControl from '../components/chime/MeetingControl'; 12 | import MeetingReceiver from '../components/chime/MeetingReceiver'; 13 | import { createClient, defaultExchanges, Provider } from 'urql'; 14 | import { asyncHeaderExchange } from 'urql-exchange-async-headers'; 15 | 16 | const Home: NextPage = () => { 17 | const amplifyConfig = { 18 | aws_appsync_graphqlEndpoint: config.apiEndpoint, 19 | aws_appsync_region: config.awsRegion, 20 | aws_appsync_authenticationType: 'AMAZON_COGNITO_USER_POOLS', 21 | Auth: { 22 | region: config.awsRegion, 23 | userPoolId: config.userPoolId, 24 | userPoolWebClientId: config.userPoolClientId, 25 | }, 26 | }; 27 | Amplify.configure(amplifyConfig); 28 | 29 | const client = createClient({ 30 | url: config.apiEndpoint, 31 | exchanges: [ 32 | // https://github.com/FormidableLabs/urql/issues/234 33 | asyncHeaderExchange(async () => { 34 | const currentSession = await Auth.currentSession(); 35 | return { 36 | authorization: currentSession.getAccessToken().getJwtToken(), 37 | }; 38 | }), 39 | ...defaultExchanges, 40 | ], 41 | }); 42 | 43 | return ( 44 | 45 | {({ signOut, user }) => ( 46 | <> 47 | 48 | Chime SDK meetings demo 49 | 50 | 51 | 52 | 53 | Chime SDK meetings demo 54 | 55 | 58 | 59 | 60 |
61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 |
78 | 79 | )} 80 |
81 | ); 82 | }; 83 | 84 | export default Home; 85 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /frontend/components/chime/MeetingReceiver.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { DeviceLabels, useMeetingManager, lightTheme } from 'amazon-chime-sdk-component-library-react'; 3 | import { MeetingSessionConfiguration } from 'amazon-chime-sdk-js'; 4 | import { ThemeProvider } from 'styled-components'; 5 | import { API } from 'aws-amplify'; 6 | import { Button, Stack } from '@mui/material'; 7 | import { useChimeMeetingState } from './context'; 8 | import { MeetingInvitation, useJoinMeetingMutation } from '../../graphql/generated'; 9 | 10 | type Props = { 11 | myId: string; 12 | }; 13 | 14 | const MeetingReceiver = (props: Props) => { 15 | const [_, joinMeeting] = useJoinMeetingMutation(); 16 | const [invitations, setInvitations] = useState<{ 17 | [key: string]: MeetingInvitation; 18 | }>({}); 19 | const { state, setState } = useChimeMeetingState(); 20 | 21 | let subscription: any; 22 | const { myId } = props; 23 | const meetingManager = useMeetingManager(); 24 | 25 | useEffect(() => { 26 | beginSubscription(); 27 | return () => { 28 | if (subscription) { 29 | // Unsubscribe on unmounting this component 30 | subscription.unsubscribe(); 31 | } 32 | }; 33 | }, [myId]); 34 | 35 | const onReceiveCall = async (invitation: MeetingInvitation) => { 36 | const response = await joinMeeting({ 37 | meetingResponse: invitation.meetingResponse, 38 | }); 39 | const meeting = response.data?.joinMeeting; 40 | if (meeting == null) { 41 | console.error('failed to join a meeting'); 42 | return; 43 | } 44 | 45 | await startMeetingSession(meeting.meetingResponse, meeting.attendeeResponse); 46 | setInvitations({}); 47 | }; 48 | 49 | const beginSubscription = async () => { 50 | const sub = API.graphql({ 51 | query: ` 52 | subscription InviteSubscription($target: ID!) { 53 | onMeetingInvited(target: $target) { 54 | meetingResponse 55 | target 56 | source 57 | } 58 | } 59 | `, 60 | variables: { target: myId }, 61 | }); 62 | 63 | if ('subscribe' in sub) { 64 | subscription = sub.subscribe({ 65 | next: (event: any) => { 66 | const inv = event.value.data.onMeetingInvited as MeetingInvitation; 67 | setInvitations((prev) => ({ ...prev, [inv.source]: inv })); 68 | }, 69 | }); 70 | } 71 | }; 72 | 73 | const startMeetingSession = async (meetingResponse: string, attendeeResponse: string) => { 74 | const meetingSessionConfiguration = new MeetingSessionConfiguration( 75 | JSON.parse(meetingResponse), 76 | JSON.parse(attendeeResponse), 77 | ); 78 | 79 | await meetingManager.join(meetingSessionConfiguration, { 80 | deviceLabels: DeviceLabels.Audio, 81 | enableWebAudio: true, 82 | }); 83 | 84 | await meetingManager.start(); 85 | setState({ isOnCall: true, meetingResponse: JSON.parse(meetingResponse) }); 86 | }; 87 | 88 | return ( 89 | 90 | 91 | {Object.entries(invitations).map(([k, v], i) => ( 92 | 100 | ))} 101 | 102 | 103 | ); 104 | }; 105 | 106 | export default MeetingReceiver; 107 | -------------------------------------------------------------------------------- /lib/constructs/backend-api.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import { CfnOutput, Duration } from 'aws-cdk-lib'; 3 | import { Auth } from './auth'; 4 | import * as appsync from 'aws-cdk-lib/aws-appsync'; 5 | import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; 6 | import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'; 7 | import { Runtime } from 'aws-cdk-lib/aws-lambda'; 8 | 9 | export interface BackendApiProps { 10 | readonly auth: Auth; 11 | } 12 | 13 | export class BackendApi extends Construct { 14 | readonly api: appsync.GraphqlApi; 15 | 16 | constructor(scope: Construct, id: string, props: BackendApiProps) { 17 | super(scope, id); 18 | 19 | const chimeResolverFunction = new NodejsFunction(this, 'ChimeResolverFunction', { 20 | entry: 'backend/chime-resolver.ts', 21 | timeout: Duration.seconds(30), 22 | runtime: Runtime.NODEJS_20_X, 23 | }); 24 | 25 | chimeResolverFunction.addToRolePolicy( 26 | new PolicyStatement({ 27 | effect: Effect.ALLOW, 28 | actions: ['chime:createAttendee', 'chime:createMeeting'], 29 | resources: ['*'], 30 | }), 31 | ); 32 | 33 | const api = new appsync.GraphqlApi(this, 'Api', { 34 | name: 'Api', 35 | definition: appsync.Definition.fromFile('backend/schema.graphql'), 36 | authorizationConfig: { 37 | defaultAuthorization: { 38 | authorizationType: appsync.AuthorizationType.USER_POOL, 39 | userPoolConfig: { 40 | userPool: props.auth.userPool, 41 | }, 42 | }, 43 | }, 44 | logConfig: { 45 | fieldLogLevel: appsync.FieldLogLevel.ALL, 46 | }, 47 | }); 48 | 49 | const chimeDataSource = api.addLambdaDataSource('ChimeDataSource', chimeResolverFunction); 50 | 51 | chimeDataSource.createResolver('CreateChimeMeeting', { 52 | typeName: 'Mutation', 53 | fieldName: 'createChimeMeeting', 54 | requestMappingTemplate: appsync.MappingTemplate.lambdaRequest(), 55 | responseMappingTemplate: appsync.MappingTemplate.lambdaResult(), 56 | }); 57 | 58 | chimeDataSource.createResolver('JoinMeeting', { 59 | typeName: 'Mutation', 60 | fieldName: 'joinMeeting', 61 | requestMappingTemplate: appsync.MappingTemplate.lambdaRequest(), 62 | responseMappingTemplate: appsync.MappingTemplate.lambdaResult(), 63 | }); 64 | 65 | const dummyDataSource = new appsync.NoneDataSource(this, 'DummyDataSource', { 66 | api, 67 | }); 68 | 69 | dummyDataSource.createResolver('CreateMeetingInvitation', { 70 | typeName: 'Mutation', 71 | fieldName: 'createMeetingInvitation', 72 | // only authorize a request when source == user 73 | requestMappingTemplate: appsync.MappingTemplate.fromString(` 74 | #if ($context.identity.sub != $context.arguments.source) 75 | $util.unauthorized() 76 | #end 77 | { 78 | "version": "2018-05-29", 79 | "payload": $util.toJson($context.arguments) 80 | } 81 | `), 82 | responseMappingTemplate: appsync.MappingTemplate.fromString('$util.toJson($context.result)'), 83 | }); 84 | 85 | dummyDataSource.createResolver('OnMeetingInvited', { 86 | typeName: 'Subscription', 87 | fieldName: 'onMeetingInvited', 88 | // only authorize a request when target == user 89 | requestMappingTemplate: appsync.MappingTemplate.fromString(` 90 | #if ($context.identity.sub != $context.arguments.target) 91 | $util.unauthorized() 92 | #end 93 | { 94 | "version": "2018-05-29", 95 | "payload": {} 96 | } 97 | `), 98 | responseMappingTemplate: appsync.MappingTemplate.fromString('$util.toJson(null)'), 99 | }); 100 | 101 | this.api = api; 102 | new CfnOutput(this, 'BackendApiUrl', { value: api.graphqlUrl }); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chime SDK Meetings 1on1 call demo with Next.js / AppSync / CDK 2 | [![Build](https://github.com/aws-samples/chime-sdk-meetings-demo-nextjs-appsync-cognito-cdk/actions/workflows/build.yml/badge.svg)](https://github.com/aws-samples/chime-sdk-meetings-demo-nextjs-appsync-cognito-cdk/actions/workflows/build.yml) 3 | 4 | This is a sample project to demonstrate [Chime SDK meetings](https://docs.aws.amazon.com/chime-sdk/latest/dg/mtgs-sdk-mtgs.html) for one-on-one call with [Next.js](https://nextjs.org/), [AWS AppSync](https://aws.amazon.com/appsync/), [Amazon Cognito](https://aws.amazon.com/cognito/) and [AWS Cloud Development Kit (CDK)](https://github.com/aws/aws-cdk). 5 | 6 | This sample includes: 7 | 8 | * 1on1 call with authN/Z leveraging Chime SDK meetings 9 | * GraphQL endpoint to call Chime SDK APIs and subscribe calling events 10 | * Secure your backend API with JWT authentication 11 | * Next.js frontend assets distribution via [CloudFront](https://aws.amazon.com/cloudfront/) 12 | * Instant deployment of the entire app by a single command 13 | 14 | ## Architecture Overview 15 | 16 | ![architecture](imgs/architecture.png) 17 | 18 | We use AppSync, a managed GraphQL service for backend API, enabling a realtime notification when a user is called, and other serverless services to reduce heavy lifting of managing servers. 19 | 20 | ### How to use 21 | When you open this web app, you need first to sign up a Cognito user. After signing in to the user, a page like the below image will be shown. 22 | 23 | ![gui](imgs/gui.png) 24 | 25 | The top text box `MyID` shows your Cognito user ID, which is used like a "phone number" in this case. When you want to make a call to a user, you must specify a target's user ID in the next text field `Target ID`. 26 | When you input the target user ID and click the `CALL` button, a meeting is created and the target user is notified via AppSync subscription. Note that the receiver must open the page before a call is made to receive a notfication; otherwise the call is discarded. 27 | 28 | If the target user clicked the `RECEIVE A CALL` button, they will also join the meeting and can start a one-on-one call. 29 | 30 | You can also specify audio devices via the Chime control panel. 31 | 32 | ## Deploy 33 | You need the following tools to deploy this sample: 34 | 35 | * [Node.js](https://nodejs.org/en/download/) (>= v16) 36 | * [Docker](https://docs.docker.com/get-docker/) 37 | * [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) and a configured IAM profile 38 | 39 | Then run the following commands: 40 | 41 | ```sh 42 | npm ci 43 | npx cdk bootstrap 44 | npx cdk deploy 45 | ``` 46 | 47 | Initial deployment usually takes about 10 minutes. You can also use `npx cdk deploy` command to deploy your changes. 48 | 49 | After a successful deployment, you will get a CLI output like the below: 50 | 51 | ``` 52 | ✅ ChimeSdkDemoStack 53 | 54 | ✨ Deployment time: 324.9s 55 | 56 | Outputs: 57 | ChimeSdkDemoStack.AuthUserPoolClientId8216BF9A = xxxxxxxxxxxxxxx 58 | ChimeSdkDemoStack.AuthUserPoolIdC0605E59 = ap-northeast-1_yyyyyy 59 | ChimeSdkDemoStack.BackendApiBackendApiUrl4A0A7879 = https://zzzzzz.appsync-api.ap-northeast-1.amazonaws.com/graphql 60 | ChimeSdkDemoStack.FrontendDomainName = https://vvvvvvvv.cloudfront.net 61 | Stack ARN: 62 | arn:aws:cloudformation:ap-northeast-1:123456789012:stack/ChimeSdkDemoStack/1111 63 | ``` 64 | 65 | Opening the URL in `FrontendDomainName` output, you can now try the sample app on your browser. 66 | 67 | ## How it works 68 | Here is a sequence diagram for starting a call. You can refer to [the official documents](https://docs.aws.amazon.com/chime-sdk/latest/dg/create-mtgs.html) for further details. 69 | 70 | ![sequence](imgs/sequence.png) 71 | 72 | ## Clean up 73 | To avoid incurring future charges, clean up the resources you created. 74 | 75 | You can remove all the AWS resources deployed by this sample running the following command: 76 | 77 | ```sh 78 | npx cdk destroy --force 79 | ``` 80 | 81 | ## Security 82 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 83 | 84 | ## License 85 | This library is licensed under the MIT-0 License. See the LICENSE file. 86 | -------------------------------------------------------------------------------- /frontend/graphql/generated.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | import * as Urql from 'urql'; 3 | export type Maybe = T | null; 4 | export type InputMaybe = Maybe; 5 | export type Exact = { [K in keyof T]: T[K] }; 6 | export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; 7 | export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; 8 | export type MakeEmpty = { [_ in K]?: never }; 9 | export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; 10 | export type Omit = Pick>; 11 | /** All built-in and custom scalars, mapped to their actual values */ 12 | export type Scalars = { 13 | ID: { input: string; output: string; } 14 | String: { input: string; output: string; } 15 | Boolean: { input: boolean; output: boolean; } 16 | Int: { input: number; output: number; } 17 | Float: { input: number; output: number; } 18 | }; 19 | 20 | export type ChimeMeeting = { 21 | __typename?: 'ChimeMeeting'; 22 | attendeeResponse: Scalars['String']['output']; 23 | meetingResponse: Scalars['String']['output']; 24 | }; 25 | 26 | export type MeetingInvitation = { 27 | __typename?: 'MeetingInvitation'; 28 | meetingResponse: Scalars['String']['output']; 29 | source: Scalars['ID']['output']; 30 | target: Scalars['ID']['output']; 31 | }; 32 | 33 | export type Mutation = { 34 | __typename?: 'Mutation'; 35 | createChimeMeeting?: Maybe; 36 | createMeetingInvitation?: Maybe; 37 | joinMeeting?: Maybe; 38 | }; 39 | 40 | 41 | export type MutationCreateMeetingInvitationArgs = { 42 | meetingResponse: Scalars['String']['input']; 43 | source: Scalars['ID']['input']; 44 | target: Scalars['ID']['input']; 45 | }; 46 | 47 | 48 | export type MutationJoinMeetingArgs = { 49 | meetingResponse: Scalars['String']['input']; 50 | }; 51 | 52 | export type Query = { 53 | __typename?: 'Query'; 54 | dummy?: Maybe; 55 | }; 56 | 57 | export type Subscription = { 58 | __typename?: 'Subscription'; 59 | onMeetingInvited?: Maybe; 60 | }; 61 | 62 | 63 | export type SubscriptionOnMeetingInvitedArgs = { 64 | target: Scalars['ID']['input']; 65 | }; 66 | 67 | export type CreateMeetingMutationVariables = Exact<{ [key: string]: never; }>; 68 | 69 | 70 | export type CreateMeetingMutation = { __typename?: 'Mutation', createChimeMeeting?: { __typename?: 'ChimeMeeting', meetingResponse: string, attendeeResponse: string } | null }; 71 | 72 | export type JoinMeetingMutationVariables = Exact<{ 73 | meetingResponse: Scalars['String']['input']; 74 | }>; 75 | 76 | 77 | export type JoinMeetingMutation = { __typename?: 'Mutation', joinMeeting?: { __typename?: 'ChimeMeeting', meetingResponse: string, attendeeResponse: string } | null }; 78 | 79 | export type CreateMeetingInvitationMutationVariables = Exact<{ 80 | target: Scalars['ID']['input']; 81 | source: Scalars['ID']['input']; 82 | meetingResponse: Scalars['String']['input']; 83 | }>; 84 | 85 | 86 | export type CreateMeetingInvitationMutation = { __typename?: 'Mutation', createMeetingInvitation?: { __typename?: 'MeetingInvitation', source: string, target: string, meetingResponse: string } | null }; 87 | 88 | 89 | export const CreateMeetingDocument = gql` 90 | mutation CreateMeeting { 91 | createChimeMeeting { 92 | meetingResponse 93 | attendeeResponse 94 | } 95 | } 96 | `; 97 | 98 | export function useCreateMeetingMutation() { 99 | return Urql.useMutation(CreateMeetingDocument); 100 | }; 101 | export const JoinMeetingDocument = gql` 102 | mutation JoinMeeting($meetingResponse: String!) { 103 | joinMeeting(meetingResponse: $meetingResponse) { 104 | meetingResponse 105 | attendeeResponse 106 | } 107 | } 108 | `; 109 | 110 | export function useJoinMeetingMutation() { 111 | return Urql.useMutation(JoinMeetingDocument); 112 | }; 113 | export const CreateMeetingInvitationDocument = gql` 114 | mutation CreateMeetingInvitation($target: ID!, $source: ID!, $meetingResponse: String!) { 115 | createMeetingInvitation( 116 | target: $target 117 | source: $source 118 | meetingResponse: $meetingResponse 119 | ) { 120 | source 121 | target 122 | meetingResponse 123 | } 124 | } 125 | `; 126 | 127 | export function useCreateMeetingInvitationMutation() { 128 | return Urql.useMutation(CreateMeetingInvitationDocument); 129 | }; -------------------------------------------------------------------------------- /test/__snapshots__/chime-sdk-demo.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Snapshot test 1`] = ` 4 | Object { 5 | "Mappings": Object { 6 | "LatestNodeRuntimeMap": Object { 7 | "af-south-1": Object { 8 | "value": "nodejs20.x", 9 | }, 10 | "ap-east-1": Object { 11 | "value": "nodejs20.x", 12 | }, 13 | "ap-northeast-1": Object { 14 | "value": "nodejs20.x", 15 | }, 16 | "ap-northeast-2": Object { 17 | "value": "nodejs20.x", 18 | }, 19 | "ap-northeast-3": Object { 20 | "value": "nodejs20.x", 21 | }, 22 | "ap-south-1": Object { 23 | "value": "nodejs20.x", 24 | }, 25 | "ap-south-2": Object { 26 | "value": "nodejs20.x", 27 | }, 28 | "ap-southeast-1": Object { 29 | "value": "nodejs20.x", 30 | }, 31 | "ap-southeast-2": Object { 32 | "value": "nodejs20.x", 33 | }, 34 | "ap-southeast-3": Object { 35 | "value": "nodejs20.x", 36 | }, 37 | "ap-southeast-4": Object { 38 | "value": "nodejs20.x", 39 | }, 40 | "ap-southeast-5": Object { 41 | "value": "nodejs20.x", 42 | }, 43 | "ap-southeast-7": Object { 44 | "value": "nodejs20.x", 45 | }, 46 | "ca-central-1": Object { 47 | "value": "nodejs20.x", 48 | }, 49 | "ca-west-1": Object { 50 | "value": "nodejs20.x", 51 | }, 52 | "cn-north-1": Object { 53 | "value": "nodejs18.x", 54 | }, 55 | "cn-northwest-1": Object { 56 | "value": "nodejs18.x", 57 | }, 58 | "eu-central-1": Object { 59 | "value": "nodejs20.x", 60 | }, 61 | "eu-central-2": Object { 62 | "value": "nodejs20.x", 63 | }, 64 | "eu-isoe-west-1": Object { 65 | "value": "nodejs18.x", 66 | }, 67 | "eu-north-1": Object { 68 | "value": "nodejs20.x", 69 | }, 70 | "eu-south-1": Object { 71 | "value": "nodejs20.x", 72 | }, 73 | "eu-south-2": Object { 74 | "value": "nodejs20.x", 75 | }, 76 | "eu-west-1": Object { 77 | "value": "nodejs20.x", 78 | }, 79 | "eu-west-2": Object { 80 | "value": "nodejs20.x", 81 | }, 82 | "eu-west-3": Object { 83 | "value": "nodejs20.x", 84 | }, 85 | "il-central-1": Object { 86 | "value": "nodejs20.x", 87 | }, 88 | "me-central-1": Object { 89 | "value": "nodejs20.x", 90 | }, 91 | "me-south-1": Object { 92 | "value": "nodejs20.x", 93 | }, 94 | "mx-central-1": Object { 95 | "value": "nodejs20.x", 96 | }, 97 | "sa-east-1": Object { 98 | "value": "nodejs20.x", 99 | }, 100 | "us-east-1": Object { 101 | "value": "nodejs20.x", 102 | }, 103 | "us-east-2": Object { 104 | "value": "nodejs20.x", 105 | }, 106 | "us-gov-east-1": Object { 107 | "value": "nodejs18.x", 108 | }, 109 | "us-gov-west-1": Object { 110 | "value": "nodejs18.x", 111 | }, 112 | "us-iso-east-1": Object { 113 | "value": "nodejs18.x", 114 | }, 115 | "us-iso-west-1": Object { 116 | "value": "nodejs18.x", 117 | }, 118 | "us-isob-east-1": Object { 119 | "value": "nodejs18.x", 120 | }, 121 | "us-west-1": Object { 122 | "value": "nodejs20.x", 123 | }, 124 | "us-west-2": Object { 125 | "value": "nodejs20.x", 126 | }, 127 | }, 128 | }, 129 | "Outputs": Object { 130 | "AuthUserPoolClientId8216BF9A": Object { 131 | "Value": Object { 132 | "Ref": "AuthUserPoolClientC635291F", 133 | }, 134 | }, 135 | "AuthUserPoolIdC0605E59": Object { 136 | "Value": Object { 137 | "Ref": "AuthUserPool8115E87F", 138 | }, 139 | }, 140 | "BackendApiBackendApiUrl4A0A7879": Object { 141 | "Value": Object { 142 | "Fn::GetAtt": Array [ 143 | "BackendApiC9FA6A88", 144 | "GraphQLUrl", 145 | ], 146 | }, 147 | }, 148 | "FrontendDomainName": Object { 149 | "Value": Object { 150 | "Fn::Join": Array [ 151 | "", 152 | Array [ 153 | "https://", 154 | Object { 155 | "Fn::GetAtt": Array [ 156 | "FrontendDistributionCFDistributionAE9BA647", 157 | "DomainName", 158 | ], 159 | }, 160 | ], 161 | ], 162 | }, 163 | }, 164 | }, 165 | "Parameters": Object { 166 | "BootstrapVersion": Object { 167 | "Default": "/cdk-bootstrap/hnb659fds/version", 168 | "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]", 169 | "Type": "AWS::SSM::Parameter::Value", 170 | }, 171 | }, 172 | "Resources": Object { 173 | "AccessLogBucketAutoDeleteObjectsCustomResource01AB31E8": Object { 174 | "DeletionPolicy": "Delete", 175 | "DependsOn": Array [ 176 | "AccessLogBucketPolicyF52D2D01", 177 | ], 178 | "Properties": Object { 179 | "BucketName": Object { 180 | "Ref": "AccessLogBucketDA470295", 181 | }, 182 | "ServiceToken": Object { 183 | "Fn::GetAtt": Array [ 184 | "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F", 185 | "Arn", 186 | ], 187 | }, 188 | }, 189 | "Type": "Custom::S3AutoDeleteObjects", 190 | "UpdateReplacePolicy": "Delete", 191 | }, 192 | "AccessLogBucketDA470295": Object { 193 | "DeletionPolicy": "Delete", 194 | "Properties": Object { 195 | "BucketEncryption": Object { 196 | "ServerSideEncryptionConfiguration": Array [ 197 | Object { 198 | "ServerSideEncryptionByDefault": Object { 199 | "SSEAlgorithm": "AES256", 200 | }, 201 | }, 202 | ], 203 | }, 204 | "OwnershipControls": Object { 205 | "Rules": Array [ 206 | Object { 207 | "ObjectOwnership": "ObjectWriter", 208 | }, 209 | ], 210 | }, 211 | "PublicAccessBlockConfiguration": Object { 212 | "BlockPublicAcls": true, 213 | "BlockPublicPolicy": true, 214 | "IgnorePublicAcls": true, 215 | "RestrictPublicBuckets": true, 216 | }, 217 | "Tags": Array [ 218 | Object { 219 | "Key": "aws-cdk:auto-delete-objects", 220 | "Value": "true", 221 | }, 222 | ], 223 | }, 224 | "Type": "AWS::S3::Bucket", 225 | "UpdateReplacePolicy": "Delete", 226 | }, 227 | "AccessLogBucketPolicyF52D2D01": Object { 228 | "Properties": Object { 229 | "Bucket": Object { 230 | "Ref": "AccessLogBucketDA470295", 231 | }, 232 | "PolicyDocument": Object { 233 | "Statement": Array [ 234 | Object { 235 | "Action": "s3:*", 236 | "Condition": Object { 237 | "Bool": Object { 238 | "aws:SecureTransport": "false", 239 | }, 240 | }, 241 | "Effect": "Deny", 242 | "Principal": Object { 243 | "AWS": "*", 244 | }, 245 | "Resource": Array [ 246 | Object { 247 | "Fn::GetAtt": Array [ 248 | "AccessLogBucketDA470295", 249 | "Arn", 250 | ], 251 | }, 252 | Object { 253 | "Fn::Join": Array [ 254 | "", 255 | Array [ 256 | Object { 257 | "Fn::GetAtt": Array [ 258 | "AccessLogBucketDA470295", 259 | "Arn", 260 | ], 261 | }, 262 | "/*", 263 | ], 264 | ], 265 | }, 266 | ], 267 | }, 268 | Object { 269 | "Action": Array [ 270 | "s3:PutBucketPolicy", 271 | "s3:GetBucket*", 272 | "s3:List*", 273 | "s3:DeleteObject*", 274 | ], 275 | "Effect": "Allow", 276 | "Principal": Object { 277 | "AWS": Object { 278 | "Fn::GetAtt": Array [ 279 | "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", 280 | "Arn", 281 | ], 282 | }, 283 | }, 284 | "Resource": Array [ 285 | Object { 286 | "Fn::GetAtt": Array [ 287 | "AccessLogBucketDA470295", 288 | "Arn", 289 | ], 290 | }, 291 | Object { 292 | "Fn::Join": Array [ 293 | "", 294 | Array [ 295 | Object { 296 | "Fn::GetAtt": Array [ 297 | "AccessLogBucketDA470295", 298 | "Arn", 299 | ], 300 | }, 301 | "/*", 302 | ], 303 | ], 304 | }, 305 | ], 306 | }, 307 | ], 308 | "Version": "2012-10-17", 309 | }, 310 | }, 311 | "Type": "AWS::S3::BucketPolicy", 312 | }, 313 | "AuthUserPool8115E87F": Object { 314 | "DeletionPolicy": "Delete", 315 | "Properties": Object { 316 | "AccountRecoverySetting": Object { 317 | "RecoveryMechanisms": Array [ 318 | Object { 319 | "Name": "verified_phone_number", 320 | "Priority": 1, 321 | }, 322 | Object { 323 | "Name": "verified_email", 324 | "Priority": 2, 325 | }, 326 | ], 327 | }, 328 | "AdminCreateUserConfig": Object { 329 | "AllowAdminCreateUserOnly": false, 330 | }, 331 | "AutoVerifiedAttributes": Array [ 332 | "email", 333 | ], 334 | "EmailVerificationMessage": "The verification code to your new account is {####}", 335 | "EmailVerificationSubject": "Verify your new account", 336 | "Policies": Object { 337 | "PasswordPolicy": Object { 338 | "MinimumLength": 8, 339 | "RequireNumbers": true, 340 | "RequireSymbols": true, 341 | "RequireUppercase": true, 342 | }, 343 | }, 344 | "SmsVerificationMessage": "The verification code to your new account is {####}", 345 | "UsernameAttributes": Array [ 346 | "email", 347 | ], 348 | "VerificationMessageTemplate": Object { 349 | "DefaultEmailOption": "CONFIRM_WITH_CODE", 350 | "EmailMessage": "The verification code to your new account is {####}", 351 | "EmailSubject": "Verify your new account", 352 | "SmsMessage": "The verification code to your new account is {####}", 353 | }, 354 | }, 355 | "Type": "AWS::Cognito::UserPool", 356 | "UpdateReplacePolicy": "Delete", 357 | }, 358 | "AuthUserPoolClientC635291F": Object { 359 | "Properties": Object { 360 | "AllowedOAuthFlows": Array [ 361 | "implicit", 362 | "code", 363 | ], 364 | "AllowedOAuthFlowsUserPoolClient": true, 365 | "AllowedOAuthScopes": Array [ 366 | "profile", 367 | "phone", 368 | "email", 369 | "openid", 370 | "aws.cognito.signin.user.admin", 371 | ], 372 | "CallbackURLs": Array [ 373 | "https://example.com", 374 | ], 375 | "IdTokenValidity": 1440, 376 | "SupportedIdentityProviders": Array [ 377 | "COGNITO", 378 | ], 379 | "TokenValidityUnits": Object { 380 | "IdToken": "minutes", 381 | }, 382 | "UserPoolId": Object { 383 | "Ref": "AuthUserPool8115E87F", 384 | }, 385 | }, 386 | "Type": "AWS::Cognito::UserPoolClient", 387 | }, 388 | "BackendApiApiLogsRoleEB9B6309": Object { 389 | "Properties": Object { 390 | "AssumeRolePolicyDocument": Object { 391 | "Statement": Array [ 392 | Object { 393 | "Action": "sts:AssumeRole", 394 | "Effect": "Allow", 395 | "Principal": Object { 396 | "Service": "appsync.amazonaws.com", 397 | }, 398 | }, 399 | ], 400 | "Version": "2012-10-17", 401 | }, 402 | "ManagedPolicyArns": Array [ 403 | Object { 404 | "Fn::Join": Array [ 405 | "", 406 | Array [ 407 | "arn:", 408 | Object { 409 | "Ref": "AWS::Partition", 410 | }, 411 | ":iam::aws:policy/service-role/AWSAppSyncPushToCloudWatchLogs", 412 | ], 413 | ], 414 | }, 415 | ], 416 | }, 417 | "Type": "AWS::IAM::Role", 418 | }, 419 | "BackendApiC9FA6A88": Object { 420 | "Properties": Object { 421 | "AuthenticationType": "AMAZON_COGNITO_USER_POOLS", 422 | "LogConfig": Object { 423 | "CloudWatchLogsRoleArn": Object { 424 | "Fn::GetAtt": Array [ 425 | "BackendApiApiLogsRoleEB9B6309", 426 | "Arn", 427 | ], 428 | }, 429 | "FieldLogLevel": "ALL", 430 | }, 431 | "Name": "Api", 432 | "UserPoolConfig": Object { 433 | "AwsRegion": Object { 434 | "Ref": "AWS::Region", 435 | }, 436 | "DefaultAction": "ALLOW", 437 | "UserPoolId": Object { 438 | "Ref": "AuthUserPool8115E87F", 439 | }, 440 | }, 441 | }, 442 | "Type": "AWS::AppSync::GraphQLApi", 443 | }, 444 | "BackendApiChimeDataSourceDB0CB8BE": Object { 445 | "Properties": Object { 446 | "ApiId": Object { 447 | "Fn::GetAtt": Array [ 448 | "BackendApiC9FA6A88", 449 | "ApiId", 450 | ], 451 | }, 452 | "LambdaConfig": Object { 453 | "LambdaFunctionArn": Object { 454 | "Fn::GetAtt": Array [ 455 | "BackendApiChimeResolverFunctionF715C39D", 456 | "Arn", 457 | ], 458 | }, 459 | }, 460 | "Name": "ChimeDataSource", 461 | "ServiceRoleArn": Object { 462 | "Fn::GetAtt": Array [ 463 | "BackendApiChimeDataSourceServiceRole1862C0F4", 464 | "Arn", 465 | ], 466 | }, 467 | "Type": "AWS_LAMBDA", 468 | }, 469 | "Type": "AWS::AppSync::DataSource", 470 | }, 471 | "BackendApiChimeDataSourceServiceRole1862C0F4": Object { 472 | "Properties": Object { 473 | "AssumeRolePolicyDocument": Object { 474 | "Statement": Array [ 475 | Object { 476 | "Action": "sts:AssumeRole", 477 | "Effect": "Allow", 478 | "Principal": Object { 479 | "Service": "appsync.amazonaws.com", 480 | }, 481 | }, 482 | ], 483 | "Version": "2012-10-17", 484 | }, 485 | }, 486 | "Type": "AWS::IAM::Role", 487 | }, 488 | "BackendApiChimeDataSourceServiceRoleDefaultPolicyD1300BB8": Object { 489 | "Properties": Object { 490 | "PolicyDocument": Object { 491 | "Statement": Array [ 492 | Object { 493 | "Action": "lambda:InvokeFunction", 494 | "Effect": "Allow", 495 | "Resource": Array [ 496 | Object { 497 | "Fn::GetAtt": Array [ 498 | "BackendApiChimeResolverFunctionF715C39D", 499 | "Arn", 500 | ], 501 | }, 502 | Object { 503 | "Fn::Join": Array [ 504 | "", 505 | Array [ 506 | Object { 507 | "Fn::GetAtt": Array [ 508 | "BackendApiChimeResolverFunctionF715C39D", 509 | "Arn", 510 | ], 511 | }, 512 | ":*", 513 | ], 514 | ], 515 | }, 516 | ], 517 | }, 518 | ], 519 | "Version": "2012-10-17", 520 | }, 521 | "PolicyName": "BackendApiChimeDataSourceServiceRoleDefaultPolicyD1300BB8", 522 | "Roles": Array [ 523 | Object { 524 | "Ref": "BackendApiChimeDataSourceServiceRole1862C0F4", 525 | }, 526 | ], 527 | }, 528 | "Type": "AWS::IAM::Policy", 529 | }, 530 | "BackendApiChimeResolverFunctionF715C39D": Object { 531 | "DependsOn": Array [ 532 | "BackendApiChimeResolverFunctionServiceRoleDefaultPolicy27CD3C5E", 533 | "BackendApiChimeResolverFunctionServiceRole1EDDA251", 534 | ], 535 | "Properties": Object { 536 | "Code": Object { 537 | "S3Bucket": Object { 538 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 539 | }, 540 | "S3Key": "1f44bb10de9aaafe8d858c22a5377e98e78ff9b8cd7cc4a050110944d75f21ba.zip", 541 | }, 542 | "Handler": "index.handler", 543 | "Role": Object { 544 | "Fn::GetAtt": Array [ 545 | "BackendApiChimeResolverFunctionServiceRole1EDDA251", 546 | "Arn", 547 | ], 548 | }, 549 | "Runtime": "nodejs20.x", 550 | "Timeout": 30, 551 | }, 552 | "Type": "AWS::Lambda::Function", 553 | }, 554 | "BackendApiChimeResolverFunctionServiceRole1EDDA251": Object { 555 | "Properties": Object { 556 | "AssumeRolePolicyDocument": Object { 557 | "Statement": Array [ 558 | Object { 559 | "Action": "sts:AssumeRole", 560 | "Effect": "Allow", 561 | "Principal": Object { 562 | "Service": "lambda.amazonaws.com", 563 | }, 564 | }, 565 | ], 566 | "Version": "2012-10-17", 567 | }, 568 | "ManagedPolicyArns": Array [ 569 | Object { 570 | "Fn::Join": Array [ 571 | "", 572 | Array [ 573 | "arn:", 574 | Object { 575 | "Ref": "AWS::Partition", 576 | }, 577 | ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", 578 | ], 579 | ], 580 | }, 581 | ], 582 | }, 583 | "Type": "AWS::IAM::Role", 584 | }, 585 | "BackendApiChimeResolverFunctionServiceRoleDefaultPolicy27CD3C5E": Object { 586 | "Properties": Object { 587 | "PolicyDocument": Object { 588 | "Statement": Array [ 589 | Object { 590 | "Action": Array [ 591 | "chime:createAttendee", 592 | "chime:createMeeting", 593 | ], 594 | "Effect": "Allow", 595 | "Resource": "*", 596 | }, 597 | ], 598 | "Version": "2012-10-17", 599 | }, 600 | "PolicyName": "BackendApiChimeResolverFunctionServiceRoleDefaultPolicy27CD3C5E", 601 | "Roles": Array [ 602 | Object { 603 | "Ref": "BackendApiChimeResolverFunctionServiceRole1EDDA251", 604 | }, 605 | ], 606 | }, 607 | "Type": "AWS::IAM::Policy", 608 | }, 609 | "BackendApiCreateChimeMeeting4EE28BC4": Object { 610 | "DependsOn": Array [ 611 | "BackendApiChimeDataSourceDB0CB8BE", 612 | "BackendApiSchema3E54B1EF", 613 | ], 614 | "Properties": Object { 615 | "ApiId": Object { 616 | "Fn::GetAtt": Array [ 617 | "BackendApiC9FA6A88", 618 | "ApiId", 619 | ], 620 | }, 621 | "DataSourceName": "ChimeDataSource", 622 | "FieldName": "createChimeMeeting", 623 | "Kind": "UNIT", 624 | "RequestMappingTemplate": "{\\"version\\": \\"2017-02-28\\", \\"operation\\": \\"Invoke\\", \\"payload\\": $util.toJson($ctx)}", 625 | "ResponseMappingTemplate": "$util.toJson($ctx.result)", 626 | "TypeName": "Mutation", 627 | }, 628 | "Type": "AWS::AppSync::Resolver", 629 | }, 630 | "BackendApiCreateMeetingInvitationA97C9EB0": Object { 631 | "DependsOn": Array [ 632 | "BackendApiSchema3E54B1EF", 633 | "BackendApiDummyDataSource541C491E", 634 | ], 635 | "Properties": Object { 636 | "ApiId": Object { 637 | "Fn::GetAtt": Array [ 638 | "BackendApiC9FA6A88", 639 | "ApiId", 640 | ], 641 | }, 642 | "DataSourceName": "DummyDataSource", 643 | "FieldName": "createMeetingInvitation", 644 | "Kind": "UNIT", 645 | "RequestMappingTemplate": " 646 | #if ($context.identity.sub != $context.arguments.source) 647 | $util.unauthorized() 648 | #end 649 | { 650 | \\"version\\": \\"2018-05-29\\", 651 | \\"payload\\": $util.toJson($context.arguments) 652 | } 653 | ", 654 | "ResponseMappingTemplate": "$util.toJson($context.result)", 655 | "TypeName": "Mutation", 656 | }, 657 | "Type": "AWS::AppSync::Resolver", 658 | }, 659 | "BackendApiDummyDataSource541C491E": Object { 660 | "Properties": Object { 661 | "ApiId": Object { 662 | "Fn::GetAtt": Array [ 663 | "BackendApiC9FA6A88", 664 | "ApiId", 665 | ], 666 | }, 667 | "Name": "DummyDataSource", 668 | "Type": "NONE", 669 | }, 670 | "Type": "AWS::AppSync::DataSource", 671 | }, 672 | "BackendApiJoinMeetingEF98A0CD": Object { 673 | "DependsOn": Array [ 674 | "BackendApiChimeDataSourceDB0CB8BE", 675 | "BackendApiSchema3E54B1EF", 676 | ], 677 | "Properties": Object { 678 | "ApiId": Object { 679 | "Fn::GetAtt": Array [ 680 | "BackendApiC9FA6A88", 681 | "ApiId", 682 | ], 683 | }, 684 | "DataSourceName": "ChimeDataSource", 685 | "FieldName": "joinMeeting", 686 | "Kind": "UNIT", 687 | "RequestMappingTemplate": "{\\"version\\": \\"2017-02-28\\", \\"operation\\": \\"Invoke\\", \\"payload\\": $util.toJson($ctx)}", 688 | "ResponseMappingTemplate": "$util.toJson($ctx.result)", 689 | "TypeName": "Mutation", 690 | }, 691 | "Type": "AWS::AppSync::Resolver", 692 | }, 693 | "BackendApiLogRetention12C9E417": Object { 694 | "Properties": Object { 695 | "LogGroupName": Object { 696 | "Fn::Join": Array [ 697 | "", 698 | Array [ 699 | "/aws/appsync/apis/", 700 | Object { 701 | "Fn::GetAtt": Array [ 702 | "BackendApiC9FA6A88", 703 | "ApiId", 704 | ], 705 | }, 706 | ], 707 | ], 708 | }, 709 | "ServiceToken": Object { 710 | "Fn::GetAtt": Array [ 711 | "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A", 712 | "Arn", 713 | ], 714 | }, 715 | }, 716 | "Type": "Custom::LogRetention", 717 | }, 718 | "BackendApiOnMeetingInvited4F125712": Object { 719 | "DependsOn": Array [ 720 | "BackendApiSchema3E54B1EF", 721 | "BackendApiDummyDataSource541C491E", 722 | ], 723 | "Properties": Object { 724 | "ApiId": Object { 725 | "Fn::GetAtt": Array [ 726 | "BackendApiC9FA6A88", 727 | "ApiId", 728 | ], 729 | }, 730 | "DataSourceName": "DummyDataSource", 731 | "FieldName": "onMeetingInvited", 732 | "Kind": "UNIT", 733 | "RequestMappingTemplate": " 734 | #if ($context.identity.sub != $context.arguments.target) 735 | $util.unauthorized() 736 | #end 737 | { 738 | \\"version\\": \\"2018-05-29\\", 739 | \\"payload\\": {} 740 | } 741 | ", 742 | "ResponseMappingTemplate": "$util.toJson(null)", 743 | "TypeName": "Subscription", 744 | }, 745 | "Type": "AWS::AppSync::Resolver", 746 | }, 747 | "BackendApiSchema3E54B1EF": Object { 748 | "Properties": Object { 749 | "ApiId": Object { 750 | "Fn::GetAtt": Array [ 751 | "BackendApiC9FA6A88", 752 | "ApiId", 753 | ], 754 | }, 755 | "Definition": "schema { 756 | query: Query 757 | mutation: Mutation 758 | subscription: Subscription 759 | } 760 | 761 | type ChimeMeeting @aws_cognito_user_pools { 762 | meetingResponse: String! 763 | attendeeResponse: String! 764 | } 765 | 766 | type MeetingInvitation @aws_cognito_user_pools { 767 | target: ID! 768 | source: ID! 769 | meetingResponse: String! 770 | } 771 | 772 | type Query { 773 | dummy: ChimeMeeting 774 | } 775 | 776 | type Mutation { 777 | createChimeMeeting: ChimeMeeting @aws_cognito_user_pools 778 | createMeetingInvitation( 779 | target: ID! 780 | source: ID! 781 | meetingResponse: String! 782 | ): MeetingInvitation @aws_cognito_user_pools 783 | joinMeeting(meetingResponse: String!): ChimeMeeting @aws_cognito_user_pools 784 | } 785 | 786 | type Subscription { 787 | onMeetingInvited(target: ID!): MeetingInvitation 788 | @aws_subscribe(mutations: [\\"createMeetingInvitation\\"]) 789 | @aws_cognito_user_pools 790 | } 791 | ", 792 | }, 793 | "Type": "AWS::AppSync::GraphQLSchema", 794 | }, 795 | "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536": Object { 796 | "DependsOn": Array [ 797 | "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF", 798 | "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265", 799 | ], 800 | "Properties": Object { 801 | "Code": Object { 802 | "S3Bucket": Object { 803 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 804 | }, 805 | "S3Key": "0158f40002a8c211635388a87874fd4dcc3d68f525fe08a0fe0f014069ae539c.zip", 806 | }, 807 | "Environment": Object { 808 | "Variables": Object { 809 | "AWS_CA_BUNDLE": "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", 810 | }, 811 | }, 812 | "Handler": "index.handler", 813 | "Layers": Array [ 814 | Object { 815 | "Ref": "FrontendReactBuildDeployAwsCliLayer6AE670F6", 816 | }, 817 | ], 818 | "Role": Object { 819 | "Fn::GetAtt": Array [ 820 | "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265", 821 | "Arn", 822 | ], 823 | }, 824 | "Runtime": "python3.9", 825 | "Timeout": 900, 826 | }, 827 | "Type": "AWS::Lambda::Function", 828 | }, 829 | "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265": Object { 830 | "Properties": Object { 831 | "AssumeRolePolicyDocument": Object { 832 | "Statement": Array [ 833 | Object { 834 | "Action": "sts:AssumeRole", 835 | "Effect": "Allow", 836 | "Principal": Object { 837 | "Service": "lambda.amazonaws.com", 838 | }, 839 | }, 840 | ], 841 | "Version": "2012-10-17", 842 | }, 843 | "ManagedPolicyArns": Array [ 844 | Object { 845 | "Fn::Join": Array [ 846 | "", 847 | Array [ 848 | "arn:", 849 | Object { 850 | "Ref": "AWS::Partition", 851 | }, 852 | ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", 853 | ], 854 | ], 855 | }, 856 | ], 857 | }, 858 | "Type": "AWS::IAM::Role", 859 | }, 860 | "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF": Object { 861 | "Properties": Object { 862 | "PolicyDocument": Object { 863 | "Statement": Array [ 864 | Object { 865 | "Action": Array [ 866 | "s3:GetObject*", 867 | "s3:GetBucket*", 868 | "s3:List*", 869 | ], 870 | "Effect": "Allow", 871 | "Resource": Array [ 872 | Object { 873 | "Fn::Join": Array [ 874 | "", 875 | Array [ 876 | "arn:", 877 | Object { 878 | "Ref": "AWS::Partition", 879 | }, 880 | ":s3:::", 881 | Object { 882 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 883 | }, 884 | ], 885 | ], 886 | }, 887 | Object { 888 | "Fn::Join": Array [ 889 | "", 890 | Array [ 891 | "arn:", 892 | Object { 893 | "Ref": "AWS::Partition", 894 | }, 895 | ":s3:::", 896 | Object { 897 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 898 | }, 899 | "/*", 900 | ], 901 | ], 902 | }, 903 | ], 904 | }, 905 | Object { 906 | "Action": Array [ 907 | "s3:GetObject*", 908 | "s3:GetBucket*", 909 | "s3:List*", 910 | "s3:DeleteObject*", 911 | "s3:PutObject", 912 | "s3:PutObjectLegalHold", 913 | "s3:PutObjectRetention", 914 | "s3:PutObjectTagging", 915 | "s3:PutObjectVersionTagging", 916 | "s3:Abort*", 917 | ], 918 | "Effect": "Allow", 919 | "Resource": Array [ 920 | Object { 921 | "Fn::GetAtt": Array [ 922 | "FrontendAssetBucket3FA96E62", 923 | "Arn", 924 | ], 925 | }, 926 | Object { 927 | "Fn::Join": Array [ 928 | "", 929 | Array [ 930 | Object { 931 | "Fn::GetAtt": Array [ 932 | "FrontendAssetBucket3FA96E62", 933 | "Arn", 934 | ], 935 | }, 936 | "/*", 937 | ], 938 | ], 939 | }, 940 | ], 941 | }, 942 | Object { 943 | "Action": Array [ 944 | "cloudfront:GetInvalidation", 945 | "cloudfront:CreateInvalidation", 946 | ], 947 | "Effect": "Allow", 948 | "Resource": "*", 949 | }, 950 | ], 951 | "Version": "2012-10-17", 952 | }, 953 | "PolicyName": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF", 954 | "Roles": Array [ 955 | Object { 956 | "Ref": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265", 957 | }, 958 | ], 959 | }, 960 | "Type": "AWS::IAM::Policy", 961 | }, 962 | "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F": Object { 963 | "DependsOn": Array [ 964 | "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", 965 | ], 966 | "Properties": Object { 967 | "Code": Object { 968 | "S3Bucket": Object { 969 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 970 | }, 971 | "S3Key": "faa95a81ae7d7373f3e1f242268f904eb748d8d0fdd306e8a6fe515a1905a7d6.zip", 972 | }, 973 | "Description": Object { 974 | "Fn::Join": Array [ 975 | "", 976 | Array [ 977 | "Lambda function for auto-deleting objects in ", 978 | Object { 979 | "Ref": "AccessLogBucketDA470295", 980 | }, 981 | " S3 bucket.", 982 | ], 983 | ], 984 | }, 985 | "Handler": "index.handler", 986 | "MemorySize": 128, 987 | "Role": Object { 988 | "Fn::GetAtt": Array [ 989 | "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", 990 | "Arn", 991 | ], 992 | }, 993 | "Runtime": Object { 994 | "Fn::FindInMap": Array [ 995 | "LatestNodeRuntimeMap", 996 | Object { 997 | "Ref": "AWS::Region", 998 | }, 999 | "value", 1000 | ], 1001 | }, 1002 | "Timeout": 900, 1003 | }, 1004 | "Type": "AWS::Lambda::Function", 1005 | }, 1006 | "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092": Object { 1007 | "Properties": Object { 1008 | "AssumeRolePolicyDocument": Object { 1009 | "Statement": Array [ 1010 | Object { 1011 | "Action": "sts:AssumeRole", 1012 | "Effect": "Allow", 1013 | "Principal": Object { 1014 | "Service": "lambda.amazonaws.com", 1015 | }, 1016 | }, 1017 | ], 1018 | "Version": "2012-10-17", 1019 | }, 1020 | "ManagedPolicyArns": Array [ 1021 | Object { 1022 | "Fn::Sub": "arn:\${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", 1023 | }, 1024 | ], 1025 | }, 1026 | "Type": "AWS::IAM::Role", 1027 | }, 1028 | "FrontendAssetBucket3FA96E62": Object { 1029 | "DeletionPolicy": "Delete", 1030 | "Properties": Object { 1031 | "BucketEncryption": Object { 1032 | "ServerSideEncryptionConfiguration": Array [ 1033 | Object { 1034 | "ServerSideEncryptionByDefault": Object { 1035 | "SSEAlgorithm": "AES256", 1036 | }, 1037 | }, 1038 | ], 1039 | }, 1040 | "PublicAccessBlockConfiguration": Object { 1041 | "BlockPublicAcls": true, 1042 | "BlockPublicPolicy": true, 1043 | "IgnorePublicAcls": true, 1044 | "RestrictPublicBuckets": true, 1045 | }, 1046 | "Tags": Array [ 1047 | Object { 1048 | "Key": "aws-cdk:auto-delete-objects", 1049 | "Value": "true", 1050 | }, 1051 | Object { 1052 | "Key": "aws-cdk:cr-owned:e1ed62f7", 1053 | "Value": "true", 1054 | }, 1055 | ], 1056 | }, 1057 | "Type": "AWS::S3::Bucket", 1058 | "UpdateReplacePolicy": "Delete", 1059 | }, 1060 | "FrontendAssetBucketAutoDeleteObjectsCustomResource8D0834F0": Object { 1061 | "DeletionPolicy": "Delete", 1062 | "DependsOn": Array [ 1063 | "FrontendAssetBucketPolicyE9D0C32D", 1064 | ], 1065 | "Properties": Object { 1066 | "BucketName": Object { 1067 | "Ref": "FrontendAssetBucket3FA96E62", 1068 | }, 1069 | "ServiceToken": Object { 1070 | "Fn::GetAtt": Array [ 1071 | "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F", 1072 | "Arn", 1073 | ], 1074 | }, 1075 | }, 1076 | "Type": "Custom::S3AutoDeleteObjects", 1077 | "UpdateReplacePolicy": "Delete", 1078 | }, 1079 | "FrontendAssetBucketPolicyE9D0C32D": Object { 1080 | "Properties": Object { 1081 | "Bucket": Object { 1082 | "Ref": "FrontendAssetBucket3FA96E62", 1083 | }, 1084 | "PolicyDocument": Object { 1085 | "Statement": Array [ 1086 | Object { 1087 | "Action": "s3:*", 1088 | "Condition": Object { 1089 | "Bool": Object { 1090 | "aws:SecureTransport": "false", 1091 | }, 1092 | }, 1093 | "Effect": "Deny", 1094 | "Principal": Object { 1095 | "AWS": "*", 1096 | }, 1097 | "Resource": Array [ 1098 | Object { 1099 | "Fn::GetAtt": Array [ 1100 | "FrontendAssetBucket3FA96E62", 1101 | "Arn", 1102 | ], 1103 | }, 1104 | Object { 1105 | "Fn::Join": Array [ 1106 | "", 1107 | Array [ 1108 | Object { 1109 | "Fn::GetAtt": Array [ 1110 | "FrontendAssetBucket3FA96E62", 1111 | "Arn", 1112 | ], 1113 | }, 1114 | "/*", 1115 | ], 1116 | ], 1117 | }, 1118 | ], 1119 | }, 1120 | Object { 1121 | "Action": Array [ 1122 | "s3:PutBucketPolicy", 1123 | "s3:GetBucket*", 1124 | "s3:List*", 1125 | "s3:DeleteObject*", 1126 | ], 1127 | "Effect": "Allow", 1128 | "Principal": Object { 1129 | "AWS": Object { 1130 | "Fn::GetAtt": Array [ 1131 | "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", 1132 | "Arn", 1133 | ], 1134 | }, 1135 | }, 1136 | "Resource": Array [ 1137 | Object { 1138 | "Fn::GetAtt": Array [ 1139 | "FrontendAssetBucket3FA96E62", 1140 | "Arn", 1141 | ], 1142 | }, 1143 | Object { 1144 | "Fn::Join": Array [ 1145 | "", 1146 | Array [ 1147 | Object { 1148 | "Fn::GetAtt": Array [ 1149 | "FrontendAssetBucket3FA96E62", 1150 | "Arn", 1151 | ], 1152 | }, 1153 | "/*", 1154 | ], 1155 | ], 1156 | }, 1157 | ], 1158 | }, 1159 | Object { 1160 | "Action": "s3:GetObject", 1161 | "Effect": "Allow", 1162 | "Principal": Object { 1163 | "CanonicalUser": Object { 1164 | "Fn::GetAtt": Array [ 1165 | "FrontendOriginAccessIdentity15749265", 1166 | "S3CanonicalUserId", 1167 | ], 1168 | }, 1169 | }, 1170 | "Resource": Object { 1171 | "Fn::Join": Array [ 1172 | "", 1173 | Array [ 1174 | Object { 1175 | "Fn::GetAtt": Array [ 1176 | "FrontendAssetBucket3FA96E62", 1177 | "Arn", 1178 | ], 1179 | }, 1180 | "/*", 1181 | ], 1182 | ], 1183 | }, 1184 | }, 1185 | ], 1186 | "Version": "2012-10-17", 1187 | }, 1188 | }, 1189 | "Type": "AWS::S3::BucketPolicy", 1190 | }, 1191 | "FrontendDistributionCFDistributionAE9BA647": Object { 1192 | "Properties": Object { 1193 | "DistributionConfig": Object { 1194 | "CustomErrorResponses": Array [ 1195 | Object { 1196 | "ErrorCachingMinTTL": 0, 1197 | "ErrorCode": 404, 1198 | "ResponseCode": 200, 1199 | "ResponsePagePath": "/", 1200 | }, 1201 | Object { 1202 | "ErrorCachingMinTTL": 0, 1203 | "ErrorCode": 403, 1204 | "ResponseCode": 200, 1205 | "ResponsePagePath": "/", 1206 | }, 1207 | ], 1208 | "DefaultCacheBehavior": Object { 1209 | "AllowedMethods": Array [ 1210 | "GET", 1211 | "HEAD", 1212 | ], 1213 | "CachedMethods": Array [ 1214 | "GET", 1215 | "HEAD", 1216 | ], 1217 | "Compress": true, 1218 | "ForwardedValues": Object { 1219 | "Cookies": Object { 1220 | "Forward": "none", 1221 | }, 1222 | "QueryString": false, 1223 | }, 1224 | "TargetOriginId": "origin1", 1225 | "ViewerProtocolPolicy": "redirect-to-https", 1226 | }, 1227 | "DefaultRootObject": "index.html", 1228 | "Enabled": true, 1229 | "HttpVersion": "http2", 1230 | "IPV6Enabled": true, 1231 | "Logging": Object { 1232 | "Bucket": Object { 1233 | "Fn::GetAtt": Array [ 1234 | "AccessLogBucketDA470295", 1235 | "RegionalDomainName", 1236 | ], 1237 | }, 1238 | "IncludeCookies": false, 1239 | "Prefix": "Frontend/", 1240 | }, 1241 | "Origins": Array [ 1242 | Object { 1243 | "ConnectionAttempts": 3, 1244 | "ConnectionTimeout": 10, 1245 | "DomainName": Object { 1246 | "Fn::GetAtt": Array [ 1247 | "FrontendAssetBucket3FA96E62", 1248 | "RegionalDomainName", 1249 | ], 1250 | }, 1251 | "Id": "origin1", 1252 | "S3OriginConfig": Object { 1253 | "OriginAccessIdentity": Object { 1254 | "Fn::Join": Array [ 1255 | "", 1256 | Array [ 1257 | "origin-access-identity/cloudfront/", 1258 | Object { 1259 | "Ref": "FrontendOriginAccessIdentity15749265", 1260 | }, 1261 | ], 1262 | ], 1263 | }, 1264 | }, 1265 | }, 1266 | ], 1267 | "PriceClass": "PriceClass_100", 1268 | "ViewerCertificate": Object { 1269 | "CloudFrontDefaultCertificate": true, 1270 | }, 1271 | }, 1272 | }, 1273 | "Type": "AWS::CloudFront::Distribution", 1274 | }, 1275 | "FrontendOriginAccessIdentity15749265": Object { 1276 | "Properties": Object { 1277 | "CloudFrontOriginAccessIdentityConfig": Object { 1278 | "Comment": "Allows CloudFront to reach the bucket", 1279 | }, 1280 | }, 1281 | "Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity", 1282 | }, 1283 | "FrontendReactBuildDeployAwsCliLayer6AE670F6": Object { 1284 | "DependsOn": Array [ 1285 | "FrontendReactBuildE02FF220", 1286 | ], 1287 | "Properties": Object { 1288 | "Content": Object { 1289 | "S3Bucket": Object { 1290 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 1291 | }, 1292 | "S3Key": "3322b7049fb0ed2b7cbb644a2ada8d1116ff80c32dca89e6ada846b5de26f961.zip", 1293 | }, 1294 | "Description": "/opt/awscli/aws", 1295 | }, 1296 | "Type": "AWS::Lambda::LayerVersion", 1297 | }, 1298 | "FrontendReactBuildDeployCustomResourceA2CE2F1C": Object { 1299 | "DeletionPolicy": "Delete", 1300 | "DependsOn": Array [ 1301 | "FrontendReactBuildE02FF220", 1302 | ], 1303 | "Properties": Object { 1304 | "DestinationBucketName": Object { 1305 | "Ref": "FrontendAssetBucket3FA96E62", 1306 | }, 1307 | "DistributionId": Object { 1308 | "Ref": "FrontendDistributionCFDistributionAE9BA647", 1309 | }, 1310 | "Prune": true, 1311 | "ServiceToken": Object { 1312 | "Fn::GetAtt": Array [ 1313 | "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536", 1314 | "Arn", 1315 | ], 1316 | }, 1317 | "SourceBucketNames": Array [ 1318 | Object { 1319 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 1320 | }, 1321 | ], 1322 | "SourceObjectKeys": Array [ 1323 | Object { 1324 | "Fn::GetAtt": Array [ 1325 | "FrontendReactBuildE02FF220", 1326 | "destinationObjectKey", 1327 | ], 1328 | }, 1329 | ], 1330 | }, 1331 | "Type": "Custom::CDKBucketDeployment", 1332 | "UpdateReplacePolicy": "Delete", 1333 | }, 1334 | "FrontendReactBuildE02FF220": Object { 1335 | "DeletionPolicy": "Delete", 1336 | "Properties": Object { 1337 | "ServiceToken": Object { 1338 | "Fn::GetAtt": Array [ 1339 | "NodejsBuildCustomResourceHandler25648b212c404f09aa65b6bbb0c446591C4101F8", 1340 | "Arn", 1341 | ], 1342 | }, 1343 | "buildCommands": Array [ 1344 | "npm run build", 1345 | ], 1346 | "codeBuildProjectName": Object { 1347 | "Ref": "FrontendReactBuildProject48A6230E", 1348 | }, 1349 | "destinationBucketName": Object { 1350 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 1351 | }, 1352 | "environment": Object { 1353 | "NEXT_PUBLIC_AWS_REGION": Object { 1354 | "Ref": "AWS::Region", 1355 | }, 1356 | "NEXT_PUBLIC_BACKEND_API_URL": Object { 1357 | "Fn::GetAtt": Array [ 1358 | "BackendApiC9FA6A88", 1359 | "GraphQLUrl", 1360 | ], 1361 | }, 1362 | "NEXT_PUBLIC_USER_POOL_CLIENT_ID": Object { 1363 | "Ref": "AuthUserPoolClientC635291F", 1364 | }, 1365 | "NEXT_PUBLIC_USER_POOL_ID": Object { 1366 | "Ref": "AuthUserPool8115E87F", 1367 | }, 1368 | }, 1369 | "outputEnvFile": false, 1370 | "outputSourceDirectory": "frontend/out", 1371 | "sources": Array [ 1372 | Object { 1373 | "commands": Array [ 1374 | "npm ci", 1375 | ], 1376 | "extractPath": "frontend", 1377 | "sourceBucketName": Object { 1378 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 1379 | }, 1380 | "sourceObjectKey": "97ced61e4601ad57fd901dd78f8c03e50fc030819dda73cfa354410ffdbd99c4.zip", 1381 | }, 1382 | ], 1383 | "type": "NodejsBuild", 1384 | "workingDirectory": "frontend", 1385 | }, 1386 | "Type": "Custom::CDKNodejsBuild", 1387 | "UpdateReplacePolicy": "Delete", 1388 | }, 1389 | "FrontendReactBuildProject48A6230E": Object { 1390 | "Properties": Object { 1391 | "Artifacts": Object { 1392 | "Type": "NO_ARTIFACTS", 1393 | }, 1394 | "Cache": Object { 1395 | "Type": "NO_CACHE", 1396 | }, 1397 | "EncryptionKey": "alias/aws/s3", 1398 | "Environment": Object { 1399 | "ComputeType": "BUILD_GENERAL1_SMALL", 1400 | "Image": "aws/codebuild/standard:7.0", 1401 | "ImagePullCredentialsType": "CODEBUILD", 1402 | "PrivilegedMode": false, 1403 | "Type": "LINUX_CONTAINER", 1404 | }, 1405 | "ServiceRole": Object { 1406 | "Fn::GetAtt": Array [ 1407 | "FrontendReactBuildProjectRole67F82260", 1408 | "Arn", 1409 | ], 1410 | }, 1411 | "Source": Object { 1412 | "BuildSpec": "{ 1413 | \\"version\\": \\"0.2\\", 1414 | \\"env\\": { 1415 | \\"shell\\": \\"bash\\" 1416 | }, 1417 | \\"phases\\": { 1418 | \\"install\\": { 1419 | \\"runtime-versions\\": { 1420 | \\"nodejs\\": 18 1421 | } 1422 | }, 1423 | \\"build\\": { 1424 | \\"commands\\": [ 1425 | \\"current_dir=$(pwd)\\", 1426 | \\"\\\\necho \\\\\\"$input\\\\\\"\\\\nfor obj in $(echo \\\\\\"$input\\\\\\" | jq -r '.[] | @base64'); do\\\\n decoded=$(echo \\\\\\"$obj\\\\\\" | base64 --decode)\\\\n assetUrl=$(echo \\\\\\"$decoded\\\\\\" | jq -r '.assetUrl')\\\\n extractPath=$(echo \\\\\\"$decoded\\\\\\" | jq -r '.extractPath')\\\\n commands=$(echo \\\\\\"$decoded\\\\\\" | jq -r '.commands')\\\\n\\\\n # Download the zip file\\\\n aws s3 cp \\\\\\"$assetUrl\\\\\\" temp.zip\\\\n\\\\n # Extract the zip file to the extractPath directory\\\\n mkdir -p \\\\\\"$extractPath\\\\\\"\\\\n unzip temp.zip -d \\\\\\"$extractPath\\\\\\"\\\\n\\\\n # Remove the zip file\\\\n rm temp.zip\\\\n\\\\n # Run the specified commands in the extractPath directory\\\\n cd \\\\\\"$extractPath\\\\\\"\\\\n ls -la\\\\n eval \\\\\\"$commands\\\\\\"\\\\n cd \\\\\\"$current_dir\\\\\\"\\\\n ls -la\\\\ndone\\\\n \\", 1427 | \\"ls -la\\", 1428 | \\"cd \\\\\\"$workingDirectory\\\\\\"\\", 1429 | \\"eval \\\\\\"$buildCommands\\\\\\"\\", 1430 | \\"ls -la\\", 1431 | \\"cd \\\\\\"$current_dir\\\\\\"\\", 1432 | \\"cd \\\\\\"$outputSourceDirectory\\\\\\"\\", 1433 | \\"zip -r output.zip ./\\", 1434 | \\"aws s3 cp output.zip \\\\\\"s3://$destinationBucketName/$destinationObjectKey\\\\\\"\\", 1435 | \\"\\\\nif [[ $outputEnvFile == \\\\\\"true\\\\\\" ]]\\\\nthen\\\\n # Split the comma-separated string into an array\\\\n for var_name in \${envNames//,/ }\\\\n do\\\\n echo \\\\\\"Element: $var_name\\\\\\"\\\\n var_value=\\\\\\"\${!var_name}\\\\\\"\\\\n echo \\\\\\"$var_name=$var_value\\\\\\" >> tmp.env\\\\n done\\\\n\\\\n aws s3 cp tmp.env \\\\\\"s3://$destinationBucketName/$envFileKey\\\\\\"\\\\nfi\\\\n \\" 1436 | ] 1437 | }, 1438 | \\"post_build\\": { 1439 | \\"commands\\": [ 1440 | \\"echo Build completed on \`date\`\\", 1441 | \\"\\\\nSTATUS='SUCCESS'\\\\nif [ $CODEBUILD_BUILD_SUCCEEDING -ne 1 ] # Test if the build is failing\\\\nthen\\\\nSTATUS='FAILED'\\\\nREASON=\\\\\\"NodejsBuild failed. See CloudWatch Log stream for the detailed reason: \\\\nhttps://$AWS_REGION.console.aws.amazon.com/cloudwatch/home?region=$AWS_REGION#logsV2:log-groups/log-group/\\\\\\\\$252Faws\\\\\\\\$252Fcodebuild\\\\\\\\$252F$projectName/log-events/$CODEBUILD_LOG_PATH\\\\\\"\\\\nfi\\\\ncat < payload.json\\\\n{\\\\n \\\\\\"StackId\\\\\\": \\\\\\"$stackId\\\\\\",\\\\n \\\\\\"RequestId\\\\\\": \\\\\\"$requestId\\\\\\",\\\\n \\\\\\"LogicalResourceId\\\\\\":\\\\\\"$logicalResourceId\\\\\\",\\\\n \\\\\\"PhysicalResourceId\\\\\\": \\\\\\"$logicalResourceId\\\\\\",\\\\n \\\\\\"Status\\\\\\": \\\\\\"$STATUS\\\\\\",\\\\n \\\\\\"Reason\\\\\\": \\\\\\"$REASON\\\\\\",\\\\n \\\\\\"Data\\\\\\": {\\\\n \\\\\\"destinationObjectKey\\\\\\": \\\\\\"$destinationObjectKey\\\\\\",\\\\n \\\\\\"envFileKey\\\\\\": \\\\\\"$envFileKey\\\\\\"\\\\n }\\\\n}\\\\nEOF\\\\ncurl -v -i -X PUT -H 'Content-Type:' -d \\\\\\"@payload.json\\\\\\" \\\\\\"$responseURL\\\\\\"\\\\n \\" 1442 | ] 1443 | } 1444 | } 1445 | }", 1446 | "Type": "NO_SOURCE", 1447 | }, 1448 | }, 1449 | "Type": "AWS::CodeBuild::Project", 1450 | }, 1451 | "FrontendReactBuildProjectRole67F82260": Object { 1452 | "Properties": Object { 1453 | "AssumeRolePolicyDocument": Object { 1454 | "Statement": Array [ 1455 | Object { 1456 | "Action": "sts:AssumeRole", 1457 | "Effect": "Allow", 1458 | "Principal": Object { 1459 | "Service": "codebuild.amazonaws.com", 1460 | }, 1461 | }, 1462 | ], 1463 | "Version": "2012-10-17", 1464 | }, 1465 | }, 1466 | "Type": "AWS::IAM::Role", 1467 | }, 1468 | "FrontendReactBuildProjectRoleDefaultPolicyDFEE9CCE": Object { 1469 | "Properties": Object { 1470 | "PolicyDocument": Object { 1471 | "Statement": Array [ 1472 | Object { 1473 | "Action": Array [ 1474 | "logs:CreateLogGroup", 1475 | "logs:CreateLogStream", 1476 | "logs:PutLogEvents", 1477 | ], 1478 | "Effect": "Allow", 1479 | "Resource": Array [ 1480 | Object { 1481 | "Fn::Join": Array [ 1482 | "", 1483 | Array [ 1484 | "arn:", 1485 | Object { 1486 | "Ref": "AWS::Partition", 1487 | }, 1488 | ":logs:", 1489 | Object { 1490 | "Ref": "AWS::Region", 1491 | }, 1492 | ":", 1493 | Object { 1494 | "Ref": "AWS::AccountId", 1495 | }, 1496 | ":log-group:/aws/codebuild/", 1497 | Object { 1498 | "Ref": "FrontendReactBuildProject48A6230E", 1499 | }, 1500 | ], 1501 | ], 1502 | }, 1503 | Object { 1504 | "Fn::Join": Array [ 1505 | "", 1506 | Array [ 1507 | "arn:", 1508 | Object { 1509 | "Ref": "AWS::Partition", 1510 | }, 1511 | ":logs:", 1512 | Object { 1513 | "Ref": "AWS::Region", 1514 | }, 1515 | ":", 1516 | Object { 1517 | "Ref": "AWS::AccountId", 1518 | }, 1519 | ":log-group:/aws/codebuild/", 1520 | Object { 1521 | "Ref": "FrontendReactBuildProject48A6230E", 1522 | }, 1523 | ":*", 1524 | ], 1525 | ], 1526 | }, 1527 | ], 1528 | }, 1529 | Object { 1530 | "Action": Array [ 1531 | "codebuild:CreateReportGroup", 1532 | "codebuild:CreateReport", 1533 | "codebuild:UpdateReport", 1534 | "codebuild:BatchPutTestCases", 1535 | "codebuild:BatchPutCodeCoverages", 1536 | ], 1537 | "Effect": "Allow", 1538 | "Resource": Object { 1539 | "Fn::Join": Array [ 1540 | "", 1541 | Array [ 1542 | "arn:", 1543 | Object { 1544 | "Ref": "AWS::Partition", 1545 | }, 1546 | ":codebuild:", 1547 | Object { 1548 | "Ref": "AWS::Region", 1549 | }, 1550 | ":", 1551 | Object { 1552 | "Ref": "AWS::AccountId", 1553 | }, 1554 | ":report-group/", 1555 | Object { 1556 | "Ref": "FrontendReactBuildProject48A6230E", 1557 | }, 1558 | "-*", 1559 | ], 1560 | ], 1561 | }, 1562 | }, 1563 | Object { 1564 | "Action": Array [ 1565 | "s3:GetObject*", 1566 | "s3:GetBucket*", 1567 | "s3:List*", 1568 | ], 1569 | "Effect": "Allow", 1570 | "Resource": Array [ 1571 | Object { 1572 | "Fn::Join": Array [ 1573 | "", 1574 | Array [ 1575 | "arn:", 1576 | Object { 1577 | "Ref": "AWS::Partition", 1578 | }, 1579 | ":s3:::", 1580 | Object { 1581 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 1582 | }, 1583 | ], 1584 | ], 1585 | }, 1586 | Object { 1587 | "Fn::Join": Array [ 1588 | "", 1589 | Array [ 1590 | "arn:", 1591 | Object { 1592 | "Ref": "AWS::Partition", 1593 | }, 1594 | ":s3:::", 1595 | Object { 1596 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 1597 | }, 1598 | "/*", 1599 | ], 1600 | ], 1601 | }, 1602 | ], 1603 | }, 1604 | Object { 1605 | "Action": Array [ 1606 | "s3:DeleteObject*", 1607 | "s3:PutObject", 1608 | "s3:PutObjectLegalHold", 1609 | "s3:PutObjectRetention", 1610 | "s3:PutObjectTagging", 1611 | "s3:PutObjectVersionTagging", 1612 | "s3:Abort*", 1613 | ], 1614 | "Effect": "Allow", 1615 | "Resource": Array [ 1616 | Object { 1617 | "Fn::Join": Array [ 1618 | "", 1619 | Array [ 1620 | "arn:", 1621 | Object { 1622 | "Ref": "AWS::Partition", 1623 | }, 1624 | ":s3:::", 1625 | Object { 1626 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 1627 | }, 1628 | ], 1629 | ], 1630 | }, 1631 | Object { 1632 | "Fn::Join": Array [ 1633 | "", 1634 | Array [ 1635 | "arn:", 1636 | Object { 1637 | "Ref": "AWS::Partition", 1638 | }, 1639 | ":s3:::", 1640 | Object { 1641 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 1642 | }, 1643 | "/*", 1644 | ], 1645 | ], 1646 | }, 1647 | ], 1648 | }, 1649 | ], 1650 | "Version": "2012-10-17", 1651 | }, 1652 | "PolicyName": "FrontendReactBuildProjectRoleDefaultPolicyDFEE9CCE", 1653 | "Roles": Array [ 1654 | Object { 1655 | "Ref": "FrontendReactBuildProjectRole67F82260", 1656 | }, 1657 | ], 1658 | }, 1659 | "Type": "AWS::IAM::Policy", 1660 | }, 1661 | "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A": Object { 1662 | "DependsOn": Array [ 1663 | "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB", 1664 | "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB", 1665 | ], 1666 | "Properties": Object { 1667 | "Code": Object { 1668 | "S3Bucket": Object { 1669 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 1670 | }, 1671 | "S3Key": "4e26bf2d0a26f2097fb2b261f22bb51e3f6b4b52635777b1e54edbd8e2d58c35.zip", 1672 | }, 1673 | "Handler": "index.handler", 1674 | "Role": Object { 1675 | "Fn::GetAtt": Array [ 1676 | "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB", 1677 | "Arn", 1678 | ], 1679 | }, 1680 | "Runtime": Object { 1681 | "Fn::FindInMap": Array [ 1682 | "LatestNodeRuntimeMap", 1683 | Object { 1684 | "Ref": "AWS::Region", 1685 | }, 1686 | "value", 1687 | ], 1688 | }, 1689 | "Timeout": 900, 1690 | }, 1691 | "Type": "AWS::Lambda::Function", 1692 | }, 1693 | "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB": Object { 1694 | "Properties": Object { 1695 | "AssumeRolePolicyDocument": Object { 1696 | "Statement": Array [ 1697 | Object { 1698 | "Action": "sts:AssumeRole", 1699 | "Effect": "Allow", 1700 | "Principal": Object { 1701 | "Service": "lambda.amazonaws.com", 1702 | }, 1703 | }, 1704 | ], 1705 | "Version": "2012-10-17", 1706 | }, 1707 | "ManagedPolicyArns": Array [ 1708 | Object { 1709 | "Fn::Join": Array [ 1710 | "", 1711 | Array [ 1712 | "arn:", 1713 | Object { 1714 | "Ref": "AWS::Partition", 1715 | }, 1716 | ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", 1717 | ], 1718 | ], 1719 | }, 1720 | ], 1721 | }, 1722 | "Type": "AWS::IAM::Role", 1723 | }, 1724 | "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB": Object { 1725 | "Properties": Object { 1726 | "PolicyDocument": Object { 1727 | "Statement": Array [ 1728 | Object { 1729 | "Action": Array [ 1730 | "logs:PutRetentionPolicy", 1731 | "logs:DeleteRetentionPolicy", 1732 | ], 1733 | "Effect": "Allow", 1734 | "Resource": "*", 1735 | }, 1736 | ], 1737 | "Version": "2012-10-17", 1738 | }, 1739 | "PolicyName": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB", 1740 | "Roles": Array [ 1741 | Object { 1742 | "Ref": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB", 1743 | }, 1744 | ], 1745 | }, 1746 | "Type": "AWS::IAM::Policy", 1747 | }, 1748 | "NodejsBuildCustomResourceHandler25648b212c404f09aa65b6bbb0c446591C4101F8": Object { 1749 | "DependsOn": Array [ 1750 | "NodejsBuildCustomResourceHandler25648b212c404f09aa65b6bbb0c44659ServiceRoleDefaultPolicyCF8879D3", 1751 | "NodejsBuildCustomResourceHandler25648b212c404f09aa65b6bbb0c44659ServiceRoleCB01FBE6", 1752 | ], 1753 | "Properties": Object { 1754 | "Code": Object { 1755 | "S3Bucket": Object { 1756 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 1757 | }, 1758 | "S3Key": "aa47254059d94cff79a1f8a5a97c4e8a0e14a3f105d2b089464c0beeeb6cfe8d.zip", 1759 | }, 1760 | "Handler": "index.handler", 1761 | "Role": Object { 1762 | "Fn::GetAtt": Array [ 1763 | "NodejsBuildCustomResourceHandler25648b212c404f09aa65b6bbb0c44659ServiceRoleCB01FBE6", 1764 | "Arn", 1765 | ], 1766 | }, 1767 | "Runtime": "nodejs18.x", 1768 | "Timeout": 300, 1769 | }, 1770 | "Type": "AWS::Lambda::Function", 1771 | }, 1772 | "NodejsBuildCustomResourceHandler25648b212c404f09aa65b6bbb0c44659ServiceRoleCB01FBE6": Object { 1773 | "Properties": Object { 1774 | "AssumeRolePolicyDocument": Object { 1775 | "Statement": Array [ 1776 | Object { 1777 | "Action": "sts:AssumeRole", 1778 | "Effect": "Allow", 1779 | "Principal": Object { 1780 | "Service": "lambda.amazonaws.com", 1781 | }, 1782 | }, 1783 | ], 1784 | "Version": "2012-10-17", 1785 | }, 1786 | "ManagedPolicyArns": Array [ 1787 | Object { 1788 | "Fn::Join": Array [ 1789 | "", 1790 | Array [ 1791 | "arn:", 1792 | Object { 1793 | "Ref": "AWS::Partition", 1794 | }, 1795 | ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", 1796 | ], 1797 | ], 1798 | }, 1799 | ], 1800 | }, 1801 | "Type": "AWS::IAM::Role", 1802 | }, 1803 | "NodejsBuildCustomResourceHandler25648b212c404f09aa65b6bbb0c44659ServiceRoleDefaultPolicyCF8879D3": Object { 1804 | "Properties": Object { 1805 | "PolicyDocument": Object { 1806 | "Statement": Array [ 1807 | Object { 1808 | "Action": "codebuild:StartBuild", 1809 | "Effect": "Allow", 1810 | "Resource": Object { 1811 | "Fn::GetAtt": Array [ 1812 | "FrontendReactBuildProject48A6230E", 1813 | "Arn", 1814 | ], 1815 | }, 1816 | }, 1817 | ], 1818 | "Version": "2012-10-17", 1819 | }, 1820 | "PolicyName": "NodejsBuildCustomResourceHandler25648b212c404f09aa65b6bbb0c44659ServiceRoleDefaultPolicyCF8879D3", 1821 | "Roles": Array [ 1822 | Object { 1823 | "Ref": "NodejsBuildCustomResourceHandler25648b212c404f09aa65b6bbb0c44659ServiceRoleCB01FBE6", 1824 | }, 1825 | ], 1826 | }, 1827 | "Type": "AWS::IAM::Policy", 1828 | }, 1829 | }, 1830 | "Rules": Object { 1831 | "CheckBootstrapVersion": Object { 1832 | "Assertions": Array [ 1833 | Object { 1834 | "Assert": Object { 1835 | "Fn::Not": Array [ 1836 | Object { 1837 | "Fn::Contains": Array [ 1838 | Array [ 1839 | "1", 1840 | "2", 1841 | "3", 1842 | "4", 1843 | "5", 1844 | ], 1845 | Object { 1846 | "Ref": "BootstrapVersion", 1847 | }, 1848 | ], 1849 | }, 1850 | ], 1851 | }, 1852 | "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.", 1853 | }, 1854 | ], 1855 | }, 1856 | }, 1857 | } 1858 | `; 1859 | --------------------------------------------------------------------------------