AWSTemplateFormatVersion: "2010-09-09"
Description: Amazon Connect Experience Builder

Parameters:
    S3BucketName:
        Type: String
        Description: "The S3 Bucket containing the deployment package (multiple .zip)"
    EmailAddress:
        Type: String
        Description: "Email address for the frontend user account"

Resources:
    UsersDDBTable:
        Type: AWS::DynamoDB::Table
        Properties:
            AttributeDefinitions:
                - AttributeName: apiKey
                  AttributeType: S
            KeySchema:
                - AttributeName: apiKey
                  KeyType: HASH
            ProvisionedThroughput:
                ReadCapacityUnits: 1
                WriteCapacityUnits: 1

    # S3 bucket policy
    FrontEndS3BucketPolicy:
        Type: AWS::S3::BucketPolicy
        Properties:
            Bucket: !Ref FrontEndS3Bucket
            PolicyDocument:
                Version: "2008-10-17"
                Id: "PolicyForCloudFrontPrivateContent"
                Statement:
                    - Action:
                          - "s3:GetObject"
                      Effect: Allow
                      Resource: !Join ["", [!GetAtt FrontEndS3Bucket.Arn, "/*"]]
                      Principal:
                          CanonicalUser: !GetAtt [CDNOriginIdentity, S3CanonicalUserId]
    # S3 buckets
    FrontEndS3Bucket:
        Type: AWS::S3::Bucket
        Properties:
            PublicAccessBlockConfiguration:
                BlockPublicAcls: true
                BlockPublicPolicy: true
                IgnorePublicAcls: true
                RestrictPublicBuckets: true
            LoggingConfiguration:
                DestinationBucketName: !Ref LoggingS3Bucket
                LogFilePrefix: "frontend-logs/"

    LoggingS3Bucket:
        Type: AWS::S3::Bucket
        Properties:
            PublicAccessBlockConfiguration:
                BlockPublicAcls: true
                BlockPublicPolicy: true
                IgnorePublicAcls: true
                RestrictPublicBuckets: true

    # Lambda layer
    LambdaAWSLayer:
        Type: AWS::Lambda::LayerVersion
        Properties:
            CompatibleArchitectures:
                - x86_64
            CompatibleRuntimes:
                - nodejs14.x
                - nodejs16.x
            Content:
                S3Bucket: !Ref S3BucketName
                S3Key: "nodejs.zip"
            LayerName: aws-2-1-1152-0-layer

    # Lambda role
    LambdaRole:
        Type: AWS::IAM::Role
        Properties:
            AssumeRolePolicyDocument:
                Version: "2012-10-17"
                Statement:
                    - Effect: Allow
                      Principal:
                          Service: lambda.amazonaws.com
                      Action: sts:AssumeRole
            ManagedPolicyArns:
                - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
            Policies:
                - PolicyName: InlineConnectPolicy
                  PolicyDocument:
                      Version: "2012-10-17"
                      Statement:
                          - Effect: Allow
                            Action:
                                - connect:ListInstances
                                - connect:ListInstanceAttributes
                                - connect:ListPhoneNumbers
                                - connect:ListPhoneNumbersV2
                                - connect:ListQueues
                                - connect:SearchQueues
                                - connect:ClaimPhoneNumber
                                - connect:SearchAvailablePhoneNumbers
                                - connect:CreateContactFlow
                                - connect:CreateContactFlowModule
                                - connect:CreateHoursOfOperation
                                - connect:CreateQueue
                                - connect:AssociatePhoneNumberContactFlow
                                - ds:DescribeDirectories
                            Resource: "*"
                - PolicyName: InlineStatesPolicy
                  PolicyDocument:
                      Version: "2012-10-17"
                      Statement:
                          - Effect: Allow
                            Action:
                                - states:StartExecution
                                - states:DescribeExecution
                            Resource: "*"
                - PolicyName: InlineDDBPolicy
                  PolicyDocument:
                      Version: "2012-10-17"
                      Statement:
                          - Effect: Allow
                            Action:
                                - dynamodb:GetItem
                            Resource:
                                - !GetAtt UsersDDBTable.Arn

    # State machine role
    StatesExecutionRole:
        Type: "AWS::IAM::Role"
        Properties:
            AssumeRolePolicyDocument:
                Version: "2012-10-17"
                Statement:
                    - Effect: "Allow"
                      Principal:
                          Service: !Sub "states.${AWS::Region}.amazonaws.com"
                      Action: "sts:AssumeRole"
            Path: "/"
            Policies:
                - PolicyName: StatesExecutionPolicy
                  PolicyDocument:
                      Version: "2012-10-17"
                      Statement:
                          - Effect: Allow
                            Action:
                                - "lambda:InvokeFunction"
                            Resource: "*"

    ###### LAMBDAS ######
    LambdaAuthenticate:
        Type: AWS::Lambda::Function
        Properties:
            Description: "Authenticate frontend users"
            Handler: index.handler
            Runtime: nodejs16.x
            Role: !GetAtt LambdaRole.Arn
            Timeout: 12
            Code:
                S3Bucket: !Ref S3BucketName
                S3Key: "authenticate.zip"
            Environment:
                Variables:
                    TABLE: !Ref UsersDDBTable

    LambdaAuthorize:
        Type: AWS::Lambda::Function
        Properties:
            Description: "Authorize API calls"
            Handler: index.handler
            Runtime: nodejs16.x
            Role: !GetAtt LambdaRole.Arn
            Timeout: 12
            Code:
                S3Bucket: !Ref S3BucketName
                S3Key: "authorizer.zip"
            Environment:
                Variables:
                    TABLE: !Ref UsersDDBTable
                    API:
                        !Join [
                            "",
                            [
                                "arn:",
                                !Ref AWS::Partition,
                                ":execute-api:",
                                !Ref AWS::Region,
                                ":",
                                !Ref AWS::AccountId,
                                ":",
                                !Ref ApiGateway,
                            ],
                        ]

    LambdaListConnectInstances:
        Type: AWS::Lambda::Function
        DependsOn:
            - LambdaAWSLayer
        Properties:
            Description: "List connect instances in region for account"
            Handler: index.handler
            Runtime: nodejs16.x
            Role: !GetAtt LambdaRole.Arn
            Timeout: 12
            Layers:
                - !Ref LambdaAWSLayer
            Code:
                S3Bucket: !Ref S3BucketName
                S3Key: "list-instances.zip"

    LambdaStartStateMachine:
        Type: AWS::Lambda::Function
        DependsOn:
            - LambdaAWSLayer
        Properties:
            Description: "Initiate the orchestration"
            Handler: index.handler
            Runtime: nodejs16.x
            Role: !GetAtt LambdaRole.Arn
            Timeout: 12
            Layers:
                - !Ref LambdaAWSLayer
            Code:
                S3Bucket: !Ref S3BucketName
                S3Key: "start-state-machine.zip"
            Environment:
                Variables:
                    STATE_MACHINE_ARN: !Ref ExperienceStateMachine

    LambdaListPhoneNumbers:
        Type: AWS::Lambda::Function
        DependsOn:
            - LambdaAWSLayer
        Properties:
            Description: "List phone numbers available to an instance"
            Handler: index.handler
            Runtime: nodejs16.x
            Role: !GetAtt LambdaRole.Arn
            Timeout: 20
            Layers:
                - !Ref LambdaAWSLayer
            Code:
                S3Bucket: !Ref S3BucketName
                S3Key: "list-phone-numbers.zip"

    LambdaGetInstanceResources:
        Type: AWS::Lambda::Function
        DependsOn:
            - LambdaAWSLayer
        Properties:
            Description: "List relevant instance resources"
            Handler: index.handler
            Runtime: nodejs16.x
            Role: !GetAtt LambdaRole.Arn
            Timeout: 20
            Layers:
                - !Ref LambdaAWSLayer
            Code:
                S3Bucket: !Ref S3BucketName
                S3Key: "list-resources.zip"

    LambdaExperienceProgress:
        Type: AWS::Lambda::Function
        DependsOn:
            - LambdaAWSLayer
        Properties:
            Description: "Retrieve the progress of a creation state machine"
            Handler: index.handler
            Runtime: nodejs16.x
            Role: !GetAtt LambdaRole.Arn
            Timeout: 20
            Layers:
                - !Ref LambdaAWSLayer
            Code:
                S3Bucket: !Ref S3BucketName
                S3Key: "progress.zip"

    LambdaCreateQueues:
        Type: AWS::Lambda::Function
        DependsOn:
            - LambdaAWSLayer
        Properties:
            Description: "Create queues in the instance"
            Handler: index.handler
            Runtime: nodejs16.x
            Role: !GetAtt LambdaRole.Arn
            Timeout: 60
            Layers:
                - !Ref LambdaAWSLayer
            Code:
                S3Bucket: !Ref S3BucketName
                S3Key: "create-queues.zip"

    LambdaCreateHoursOfOperation:
        Type: AWS::Lambda::Function
        DependsOn:
            - LambdaAWSLayer
        Properties:
            Description: "Create the hours of operation in instance"
            Handler: index.handler
            Runtime: nodejs16.x
            Role: !GetAtt LambdaRole.Arn
            Timeout: 60
            Layers:
                - !Ref LambdaAWSLayer
            Code:
                S3Bucket: !Ref S3BucketName
                S3Key: "create-hours-of-operation.zip"

    LambdaCreateContactFlows:
        Type: AWS::Lambda::Function
        DependsOn:
            - LambdaAWSLayer
        Properties:
            Description: "Create the contact flows in instance"
            Handler: index.handler
            Runtime: nodejs16.x
            Role: !GetAtt LambdaRole.Arn
            Timeout: 300
            Layers:
                - !Ref LambdaAWSLayer
            Code:
                S3Bucket: !Ref S3BucketName
                S3Key: "create-contact-flows.zip"

    LambdaClaimNumber:
        Type: AWS::Lambda::Function
        DependsOn:
            - LambdaAWSLayer
        Properties:
            Description: "Claim a number and associates it with a contact flow"
            Handler: index.handler
            Runtime: nodejs16.x
            Role: !GetAtt LambdaRole.Arn
            Timeout: 12
            Layers:
                - !Ref LambdaAWSLayer
            Code:
                S3Bucket: !Ref S3BucketName
                S3Key: "claim-number.zip"

    # State Machine
    ExperienceStateMachine:
        Type: AWS::StepFunctions::StateMachine
        Properties:
            DefinitionS3Location:
                Bucket: !Ref S3BucketName
                Key: state-machine.asl.json
            RoleArn: !GetAtt StatesExecutionRole.Arn
            DefinitionSubstitutions:
                createHoursOfOperation: !GetAtt LambdaCreateHoursOfOperation.Arn
                createQueues: !GetAtt LambdaCreateQueues.Arn
                createContactFlows: !GetAtt LambdaCreateContactFlows.Arn
                claimPhoneNumber: !GetAtt LambdaClaimNumber.Arn

    # API Gateway
    ApiGateway:
        Type: AWS::ApiGateway::RestApi
        Properties:
            Name: !Sub "${AWS::StackName}-ceb-api"

    Authorizer:
        Type: "AWS::ApiGateway::Authorizer"
        Properties:
            AuthorizerResultTtlInSeconds: "300"
            AuthorizerUri:
                !Join [
                    "",
                    [
                        "arn:aws:apigateway:",
                        !Ref "AWS::Region",
                        ":lambda:path/2015-03-31/functions/",
                        !GetAtt LambdaAuthorize.Arn,
                        "/invocations",
                    ],
                ]
            Type: REQUEST
            IdentitySource: method.request.header.Authorization
            Name: DefaultAuthorizer
            RestApiId: !Ref ApiGateway

    # API Deployment
    ApiDeployment:
        Type: AWS::ApiGateway::Deployment
        DependsOn:
            - ApiAuthOptionsMethod
            - ApiAuthPostMethod
            - ApiExperienceOptionsMethod
            - ApiExperiencePutMethod
            - ApiExperiencePostMethod
            - ApiHelloOptionsMethod
            - ApiHelloGetMethod
            - ApiInstancesOptionsMethod
            - ApiInstancesGetMethod
            - ApiPhoneNumbersOptionsMethod
            - ApiPhoneNumbersPostMethod
            - ApiResourcesOptionsMethod
            - ApiResourcesPostMethod
        Properties:
            RestApiId: !Ref ApiGateway
            StageName: "DummyStage"

    # API Stage
    ApiStage:
        Type: AWS::ApiGateway::Stage
        DependsOn:
            - ApiGateway
        Properties:
            RestApiId: !Ref ApiGateway
            StageName: dev
            DeploymentId: !Ref ApiDeployment
            MethodSettings:
                - DataTraceEnabled: false
                  HttpMethod: "*"
                  ResourcePath: "/*"

    ###### API RESOURCES AND METHODS ######
    ApiAuthResource:
        Type: AWS::ApiGateway::Resource
        Properties:
            RestApiId: !Ref ApiGateway
            ParentId: !GetAtt ApiGateway.RootResourceId
            PathPart: "auth"

    ApiAuthPostMethod:
        Type: AWS::ApiGateway::Method
        Properties:
            AuthorizationType: "NONE"
            HttpMethod: "POST"
            ResourceId: !Ref ApiAuthResource
            RestApiId: !Ref ApiGateway
            Integration:
                Type: "AWS_PROXY"
                IntegrationHttpMethod: "POST"
                Uri:
                    !Join [
                        "",
                        [
                            "arn:aws:apigateway:",
                            !Ref AWS::Region,
                            ":lambda:path/2015-03-31/functions/",
                            !GetAtt LambdaAuthenticate.Arn,
                            "/invocations",
                        ],
                    ]
            MethodResponses:
                - StatusCode: 200
                  ResponseModels:
                      application/json: "Empty"
                  ResponseParameters:
                      method.response.header.Access-Control-Allow-Headers: false
                      method.response.header.Access-Control-Allow-Methods: false
                      method.response.header.Access-Control-Allow-Origin: false

    ApiAuthOptionsMethod:
        Type: AWS::ApiGateway::Method
        Properties:
            AuthorizationType: NONE
            RestApiId: !Ref ApiGateway
            ResourceId: !Ref ApiAuthResource
            HttpMethod: OPTIONS
            Integration:
                IntegrationResponses:
                    - StatusCode: 200
                      ResponseParameters:
                          method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
                          method.response.header.Access-Control-Allow-Methods: "'PUT,POST,GET,OPTIONS'"
                          method.response.header.Access-Control-Allow-Origin: "'*'"
                      ResponseTemplates:
                          application/json: ""
                PassthroughBehavior: WHEN_NO_MATCH
                RequestTemplates:
                    application/json: '{"statusCode": 200}'
                Type: MOCK
            MethodResponses:
                - StatusCode: 200
                  ResponseModels:
                      application/json: "Empty"
                  ResponseParameters:
                      method.response.header.Access-Control-Allow-Headers: false
                      method.response.header.Access-Control-Allow-Methods: false
                      method.response.header.Access-Control-Allow-Origin: false

    ApiExperienceResource:
        Type: AWS::ApiGateway::Resource
        Properties:
            RestApiId: !Ref ApiGateway
            ParentId: !GetAtt ApiGateway.RootResourceId
            PathPart: "experience"

    ApiExperiencePutMethod:
        Type: AWS::ApiGateway::Method
        Properties:
            AuthorizationType: "CUSTOM"
            AuthorizerId: !GetAtt Authorizer.AuthorizerId
            HttpMethod: "PUT"
            ResourceId: !Ref ApiExperienceResource
            RestApiId: !Ref ApiGateway
            Integration:
                Type: "AWS_PROXY"
                IntegrationHttpMethod: "POST"
                Uri:
                    !Join [
                        "",
                        [
                            "arn:aws:apigateway:",
                            !Ref AWS::Region,
                            ":lambda:path/2015-03-31/functions/",
                            !GetAtt LambdaStartStateMachine.Arn,
                            "/invocations",
                        ],
                    ]
            MethodResponses:
                - StatusCode: 200
                  ResponseModels:
                      application/json: "Empty"
                  ResponseParameters:
                      method.response.header.Access-Control-Allow-Headers: false
                      method.response.header.Access-Control-Allow-Methods: false
                      method.response.header.Access-Control-Allow-Origin: false

    ApiExperiencePostMethod:
        Type: AWS::ApiGateway::Method
        Properties:
            AuthorizationType: "CUSTOM"
            AuthorizerId: !GetAtt Authorizer.AuthorizerId
            HttpMethod: "POST"
            ResourceId: !Ref ApiExperienceResource
            RestApiId: !Ref ApiGateway
            Integration:
                Type: "AWS_PROXY"
                IntegrationHttpMethod: "POST"
                Uri:
                    !Join [
                        "",
                        [
                            "arn:aws:apigateway:",
                            !Ref AWS::Region,
                            ":lambda:path/2015-03-31/functions/",
                            !GetAtt LambdaExperienceProgress.Arn,
                            "/invocations",
                        ],
                    ]
            MethodResponses:
                - StatusCode: 200
                  ResponseModels:
                      application/json: "Empty"
                  ResponseParameters:
                      method.response.header.Access-Control-Allow-Headers: false
                      method.response.header.Access-Control-Allow-Methods: false
                      method.response.header.Access-Control-Allow-Origin: false

    ApiExperienceOptionsMethod:
        Type: AWS::ApiGateway::Method
        Properties:
            AuthorizationType: NONE
            RestApiId: !Ref ApiGateway
            ResourceId: !Ref ApiExperienceResource
            HttpMethod: OPTIONS
            Integration:
                IntegrationResponses:
                    - StatusCode: 200
                      ResponseParameters:
                          method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
                          method.response.header.Access-Control-Allow-Methods: "'PUT,POST,GET,OPTIONS'"
                          method.response.header.Access-Control-Allow-Origin: "'*'"
                      ResponseTemplates:
                          application/json: ""
                PassthroughBehavior: WHEN_NO_MATCH
                RequestTemplates:
                    application/json: '{"statusCode": 200}'
                Type: MOCK
            MethodResponses:
                - StatusCode: 200
                  ResponseModels:
                      application/json: "Empty"
                  ResponseParameters:
                      method.response.header.Access-Control-Allow-Headers: false
                      method.response.header.Access-Control-Allow-Methods: false
                      method.response.header.Access-Control-Allow-Origin: false

    ApiHelloResource:
        Type: AWS::ApiGateway::Resource
        Properties:
            RestApiId: !Ref ApiGateway
            ParentId: !GetAtt ApiGateway.RootResourceId
            PathPart: "hello"

    ApiHelloGetMethod:
        Type: AWS::ApiGateway::Method
        Properties:
            AuthorizationType: "NONE"
            HttpMethod: "GET"
            ResourceId: !Ref ApiHelloResource
            RestApiId: !Ref ApiGateway
            Integration:
                IntegrationResponses:
                    - StatusCode: 200
                      ResponseParameters:
                          method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
                          method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS'"
                          method.response.header.Access-Control-Allow-Origin: "'*'"
                      ResponseTemplates:
                          application/json: ""
                PassthroughBehavior: WHEN_NO_MATCH
                RequestTemplates:
                    application/json: '{"statusCode": 200}'
                Type: MOCK
            MethodResponses:
                - StatusCode: 200
                  ResponseModels:
                      application/json: "Empty"
                  ResponseParameters:
                      method.response.header.Access-Control-Allow-Headers: false
                      method.response.header.Access-Control-Allow-Methods: false
                      method.response.header.Access-Control-Allow-Origin: false

    ApiInstancesOptionsMethod:
        Type: AWS::ApiGateway::Method
        Properties:
            AuthorizationType: NONE
            RestApiId: !Ref ApiGateway
            ResourceId: !Ref ApiInstancesResource
            HttpMethod: OPTIONS
            Integration:
                IntegrationResponses:
                    - StatusCode: 200
                      ResponseParameters:
                          method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
                          method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS'"
                          method.response.header.Access-Control-Allow-Origin: "'*'"
                      ResponseTemplates:
                          application/json: ""
                PassthroughBehavior: WHEN_NO_MATCH
                RequestTemplates:
                    application/json: '{"statusCode": 200}'
                Type: MOCK
            MethodResponses:
                - StatusCode: 200
                  ResponseModels:
                      application/json: "Empty"
                  ResponseParameters:
                      method.response.header.Access-Control-Allow-Headers: false
                      method.response.header.Access-Control-Allow-Methods: false
                      method.response.header.Access-Control-Allow-Origin: false

    ApiInstancesResource:
        Type: AWS::ApiGateway::Resource
        Properties:
            RestApiId: !Ref ApiGateway
            ParentId: !GetAtt ApiGateway.RootResourceId
            PathPart: "instances"

    ApiInstancesGetMethod:
        Type: AWS::ApiGateway::Method
        Properties:
            AuthorizationType: "CUSTOM"
            AuthorizerId: !GetAtt Authorizer.AuthorizerId
            HttpMethod: "GET"
            ResourceId: !Ref ApiInstancesResource
            RestApiId: !Ref ApiGateway
            Integration:
                Type: "AWS_PROXY"
                IntegrationHttpMethod: "POST"
                Uri:
                    !Join [
                        "",
                        [
                            "arn:aws:apigateway:",
                            !Ref AWS::Region,
                            ":lambda:path/2015-03-31/functions/",
                            !GetAtt LambdaListConnectInstances.Arn,
                            "/invocations",
                        ],
                    ]
            MethodResponses:
                - StatusCode: 200
                  ResponseModels:
                      application/json: "Empty"
                  ResponseParameters:
                      method.response.header.Access-Control-Allow-Headers: false
                      method.response.header.Access-Control-Allow-Methods: false
                      method.response.header.Access-Control-Allow-Origin: false

    ApiHelloOptionsMethod:
        Type: AWS::ApiGateway::Method
        Properties:
            AuthorizationType: NONE
            RestApiId: !Ref ApiGateway
            ResourceId: !Ref ApiHelloResource
            HttpMethod: OPTIONS
            Integration:
                IntegrationResponses:
                    - StatusCode: 200
                      ResponseParameters:
                          method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
                          method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS'"
                          method.response.header.Access-Control-Allow-Origin: "'*'"
                      ResponseTemplates:
                          application/json: ""
                PassthroughBehavior: WHEN_NO_MATCH
                RequestTemplates:
                    application/json: '{"statusCode": 200}'
                Type: MOCK
            MethodResponses:
                - StatusCode: 200
                  ResponseModels:
                      application/json: "Empty"
                  ResponseParameters:
                      method.response.header.Access-Control-Allow-Headers: false
                      method.response.header.Access-Control-Allow-Methods: false
                      method.response.header.Access-Control-Allow-Origin: false

    ApiPhoneNumbersResource:
        Type: AWS::ApiGateway::Resource
        Properties:
            RestApiId: !Ref ApiGateway
            ParentId: !GetAtt ApiGateway.RootResourceId
            PathPart: "phone-numbers"

    ApiPhoneNumbersPostMethod:
        Type: AWS::ApiGateway::Method
        Properties:
            AuthorizationType: "CUSTOM"
            AuthorizerId: !GetAtt Authorizer.AuthorizerId
            HttpMethod: "POST"
            ResourceId: !Ref ApiPhoneNumbersResource
            RestApiId: !Ref ApiGateway
            Integration:
                Type: "AWS_PROXY"
                IntegrationHttpMethod: "POST"
                Uri:
                    !Join [
                        "",
                        [
                            "arn:aws:apigateway:",
                            !Ref AWS::Region,
                            ":lambda:path/2015-03-31/functions/",
                            !GetAtt LambdaListPhoneNumbers.Arn,
                            "/invocations",
                        ],
                    ]
            MethodResponses:
                - StatusCode: 200
                  ResponseModels:
                      application/json: "Empty"
                  ResponseParameters:
                      method.response.header.Access-Control-Allow-Headers: false
                      method.response.header.Access-Control-Allow-Methods: false
                      method.response.header.Access-Control-Allow-Origin: false

    ApiPhoneNumbersOptionsMethod:
        Type: AWS::ApiGateway::Method
        Properties:
            AuthorizationType: NONE
            RestApiId: !Ref ApiGateway
            ResourceId: !Ref ApiPhoneNumbersResource
            HttpMethod: OPTIONS
            Integration:
                IntegrationResponses:
                    - StatusCode: 200
                      ResponseParameters:
                          method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
                          method.response.header.Access-Control-Allow-Methods: "'PUT,POST,GET,OPTIONS'"
                          method.response.header.Access-Control-Allow-Origin: "'*'"
                      ResponseTemplates:
                          application/json: ""
                PassthroughBehavior: WHEN_NO_MATCH
                RequestTemplates:
                    application/json: '{"statusCode": 200}'
                Type: MOCK
            MethodResponses:
                - StatusCode: 200
                  ResponseModels:
                      application/json: "Empty"
                  ResponseParameters:
                      method.response.header.Access-Control-Allow-Headers: false
                      method.response.header.Access-Control-Allow-Methods: false
                      method.response.header.Access-Control-Allow-Origin: false

    ApiResourcesResource:
        Type: AWS::ApiGateway::Resource
        Properties:
            RestApiId: !Ref ApiGateway
            ParentId: !GetAtt ApiGateway.RootResourceId
            PathPart: "resources"

    ApiResourcesPostMethod:
        Type: AWS::ApiGateway::Method
        Properties:
            AuthorizationType: "CUSTOM"
            AuthorizerId: !GetAtt Authorizer.AuthorizerId
            HttpMethod: "POST"
            ResourceId: !Ref ApiResourcesResource
            RestApiId: !Ref ApiGateway
            Integration:
                Type: "AWS_PROXY"
                IntegrationHttpMethod: "POST"
                Uri:
                    !Join [
                        "",
                        [
                            "arn:aws:apigateway:",
                            !Ref AWS::Region,
                            ":lambda:path/2015-03-31/functions/",
                            !GetAtt LambdaGetInstanceResources.Arn,
                            "/invocations",
                        ],
                    ]
            MethodResponses:
                - StatusCode: 200
                  ResponseModels:
                      application/json: "Empty"
                  ResponseParameters:
                      method.response.header.Access-Control-Allow-Headers: false
                      method.response.header.Access-Control-Allow-Methods: false
                      method.response.header.Access-Control-Allow-Origin: false

    ApiResourcesOptionsMethod:
        Type: AWS::ApiGateway::Method
        Properties:
            AuthorizationType: NONE
            RestApiId: !Ref ApiGateway
            ResourceId: !Ref ApiResourcesResource
            HttpMethod: OPTIONS
            Integration:
                IntegrationResponses:
                    - StatusCode: 200
                      ResponseParameters:
                          method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
                          method.response.header.Access-Control-Allow-Methods: "'PUT,POST,GET,OPTIONS'"
                          method.response.header.Access-Control-Allow-Origin: "'*'"
                      ResponseTemplates:
                          application/json: ""
                PassthroughBehavior: WHEN_NO_MATCH
                RequestTemplates:
                    application/json: '{"statusCode": 200}'
                Type: MOCK
            MethodResponses:
                - StatusCode: 200
                  ResponseModels:
                      application/json: "Empty"
                  ResponseParameters:
                      method.response.header.Access-Control-Allow-Headers: false
                      method.response.header.Access-Control-Allow-Methods: false
                      method.response.header.Access-Control-Allow-Origin: false

    # API Lambda Permission
    LambdaApiPermissionAuthorize:
        Type: AWS::Lambda::Permission
        Properties:
            Action: "lambda:invokeFunction"
            FunctionName: !GetAtt LambdaAuthorize.Arn
            Principal: "apigateway.amazonaws.com"
            SourceArn:
                !Join [
                    "",
                    ["arn:aws:execute-api:", !Ref AWS::Region, ":", !Ref AWS::AccountId, ":", !Ref ApiGateway, "/*"],
                ]

    LambdaApiPermissionResourcesPost:
        Type: AWS::Lambda::Permission
        Properties:
            Action: "lambda:invokeFunction"
            FunctionName: !GetAtt LambdaGetInstanceResources.Arn
            Principal: "apigateway.amazonaws.com"
            SourceArn:
                !Join [
                    "",
                    ["arn:aws:execute-api:", !Ref AWS::Region, ":", !Ref AWS::AccountId, ":", !Ref ApiGateway, "/*"],
                ]

    LambdaApiPermissionAuthenticatePost:
        Type: AWS::Lambda::Permission
        Properties:
            Action: "lambda:invokeFunction"
            FunctionName: !GetAtt LambdaAuthenticate.Arn
            Principal: "apigateway.amazonaws.com"
            SourceArn:
                !Join [
                    "",
                    ["arn:aws:execute-api:", !Ref AWS::Region, ":", !Ref AWS::AccountId, ":", !Ref ApiGateway, "/*"],
                ]

    LambdaApiPermissionPhoneNumbersPost:
        Type: AWS::Lambda::Permission
        Properties:
            Action: "lambda:invokeFunction"
            FunctionName: !GetAtt LambdaListPhoneNumbers.Arn
            Principal: "apigateway.amazonaws.com"
            SourceArn:
                !Join [
                    "",
                    ["arn:aws:execute-api:", !Ref AWS::Region, ":", !Ref AWS::AccountId, ":", !Ref ApiGateway, "/*"],
                ]

    LambdaApiPermissionInstancesGet:
        Type: AWS::Lambda::Permission
        Properties:
            Action: "lambda:invokeFunction"
            FunctionName: !GetAtt LambdaListConnectInstances.Arn
            Principal: "apigateway.amazonaws.com"
            SourceArn:
                !Join [
                    "",
                    ["arn:aws:execute-api:", !Ref AWS::Region, ":", !Ref AWS::AccountId, ":", !Ref ApiGateway, "/*"],
                ]

    LambdaApiPermissionExperiencePost:
        Type: AWS::Lambda::Permission
        Properties:
            Action: "lambda:invokeFunction"
            FunctionName: !GetAtt LambdaExperienceProgress.Arn
            Principal: "apigateway.amazonaws.com"
            SourceArn:
                !Join [
                    "",
                    ["arn:aws:execute-api:", !Ref AWS::Region, ":", !Ref AWS::AccountId, ":", !Ref ApiGateway, "/*"],
                ]

    LambdaApiPermissionExperiencePut:
        Type: AWS::Lambda::Permission
        Properties:
            Action: "lambda:invokeFunction"
            FunctionName: !GetAtt LambdaStartStateMachine.Arn
            Principal: "apigateway.amazonaws.com"
            SourceArn:
                !Join [
                    "",
                    ["arn:aws:execute-api:", !Ref AWS::Region, ":", !Ref AWS::AccountId, ":", !Ref ApiGateway, "/*"],
                ]

    # CDN Origin Identity
    CDNOriginIdentity:
        Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
        Properties:
            CloudFrontOriginAccessIdentityConfig:
                Comment: !Sub "Cloudfront Origin identity"

    CustomCachePolicy:
        Type: AWS::CloudFront::CachePolicy
        Properties:
            CachePolicyConfig:
                Comment: String
                DefaultTTL: 3600
                MaxTTL: 86400
                MinTTL: 500
                Name: !Sub "CustomCachePolicy_${AWS::StackName}"
                ParametersInCacheKeyAndForwardedToOrigin:
                    CookiesConfig:
                        CookieBehavior: none
                    EnableAcceptEncodingBrotli: true
                    EnableAcceptEncodingGzip: true
                    HeadersConfig:
                        HeaderBehavior: whitelist
                        Headers:
                            - Authorization
                    QueryStringsConfig:
                        QueryStringBehavior: none

    CustomOriginRequestPolicy:
        Type: AWS::CloudFront::OriginRequestPolicy
        Properties:
            OriginRequestPolicyConfig:
                Name: !Sub "CustomOriginRequestPolicy_${AWS::StackName}"
                CookiesConfig:
                    CookieBehavior: all
                HeadersConfig:
                    HeaderBehavior: none
                QueryStringsConfig:
                    QueryStringBehavior: all

    # CDN distribution
    CloudFrontDistribution:
        Type: "AWS::CloudFront::Distribution"
        Properties:
            DistributionConfig:
                Logging:
                    Bucket: !GetAtt LoggingS3Bucket.DomainName
                    Prefix: "distribution/"
                CustomErrorResponses:
                    - ErrorCode: 404
                      ResponseCode: 200
                      ResponsePagePath: "/index.html"
                    - ErrorCode: 403
                      ResponseCode: 200
                      ResponsePagePath: "/index.html"
                DefaultCacheBehavior:
                    CachePolicyId: "4135ea2d-6df8-44a3-9df3-4b5a84be39ad"
                    AllowedMethods:
                        - GET
                        - HEAD
                        - OPTIONS
                    ForwardedValues:
                        Cookies:
                            Forward: none
                        QueryString: false
                    TargetOriginId: !Sub "S3-origin-${FrontEndS3Bucket}"
                    ViewerProtocolPolicy: redirect-to-https
                DefaultRootObject: index.html
                Enabled: True
                HttpVersion: http2
                Origins:
                    - DomainName: !GetAtt FrontEndS3Bucket.RegionalDomainName
                      Id: !Sub "S3-origin-${FrontEndS3Bucket}"
                      S3OriginConfig:
                          OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/${CDNOriginIdentity}"
                    - DomainName: !Sub "${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com"
                      Id: !Sub "API-origin-${ApiGateway}"
                      CustomOriginConfig:
                          OriginSSLProtocols:
                              - "TLSv1.2"
                          OriginProtocolPolicy: https-only
                CacheBehaviors:
                    - TargetOriginId: !Sub "API-origin-${ApiGateway}"
                      CachePolicyId: !Ref CustomCachePolicy
                      Compress: true
                      PathPattern: "dev/*"
                      ViewerProtocolPolicy: "redirect-to-https"
                      OriginRequestPolicyId: !Ref CustomOriginRequestPolicy
                      AllowedMethods:
                          - HEAD
                          - DELETE
                          - POST
                          - GET
                          - OPTIONS
                          - PUT
                          - PATCH
                PriceClass: PriceClass_All

    S3CustomResource:
        Type: Custom::S3CustomResource
        Properties:
            ServiceToken: !GetAtt CustomResourceLambdaFunction.Arn
            SourceBucket: !Ref S3BucketName
            DestinationBucket: !Ref FrontEndS3Bucket
    CustomResourceLambdaFunction:
        Type: "AWS::Lambda::Function"
        Properties:
            Handler: index.handler
            Role: !GetAtt CustomResourceLambdaExecutionRole.Arn
            Timeout: 360
            Runtime: nodejs16.x
            Code:
                ZipFile: |
                    const AWS = require("aws-sdk");
                    var response = require('cfn-response');
                    const s3 = new AWS.S3();

                    exports.handler = (event, context) => {

                        const request = event.RequestType;

                        switch (request) {
                            case "Create":
                                var params = {
                                    Bucket: event.ResourceProperties.SourceBucket,
                                    Prefix: "frontend"
                                }
                                getBucketContent(params).then((data) => {

                                        copyObjects(data).then((data) => {
                                                console.log("Object copied successfully");
                                                response.send(event, context, response.SUCCESS, {});
                                            })
                                            .catch((e) => {
                                                console.log(e);
                                                response.send(event, context, response.FAILED, {});
                                            })
                                    })
                                    .catch(e => {
                                        console.log(e);
                                    })

                                break;
                            case "Update":
                                var params = {
                                    Bucket: event.ResourceProperties.SourceBucket,
                                    Prefix: "frontend"
                                }
                                getBucketContent(params).then((data) => {

                                        copyObjects(data).then((data) => {
                                                console.log("Object copied successfully");
                                                response.send(event, context, response.SUCCESS, {});
                                            })
                                            .catch((e) => {
                                                console.log(e);
                                                response.send(event, context, response.FAILED, {});
                                            })
                                    })
                                    .catch(e => {
                                        console.log(e);
                                        response.send(event, context, response.FAILED, {});
                                    })

                                break;
                            case "Delete":
                                var params = {
                                    Bucket: event.ResourceProperties.DestinationBucket,
                                    Prefix: ""
                                }
                                getBucketContent(params).then((data) => {

                                        deleteObjects(data).then((data) => {
                                                console.log("Objects deleted successfully");
                                                response.send(event, context, response.SUCCESS, {});
                                            })
                                            .catch((e) => {
                                                console.log(e);
                                                response.send(event, context, response.FAILED, {});
                                            })
                                    })
                                    .catch(e => {
                                        console.log(e);
                                        response.send(event, context, response.FAILED, {});
                                    })

                                break;
                            default:
                                console.log("Unsupported operation.");
                                response.send(event, context, response.FAILED, {});
                                console.log("Sending FAILED from default");
                        }

                        function getBucketContent(params) {
                            return new Promise(async (res, rej) => {
                                try {
                                    const list = await s3.listObjectsV2(params).promise();

                                    console.log("1. Listed content");
                                    res(list);
                                }
                                catch (e) {
                                    rej(e);
                                }

                            });
                        }

                        function copyObjects(data) {
                            return new Promise(async (res, rej) => {
                                for (let index in data.Contents) {
                                    var params = {
                                        Bucket: event.ResourceProperties.DestinationBucket,
                                        CopySource: event.ResourceProperties.SourceBucket + "/" + data.Contents[index].Key,
                                        Key: data.Contents[index].Key.replace("frontend/", "")
                                    }

                                    try {
                                        const res = await s3.copyObject(params).promise();
                                    }
                                    catch (e) {
                                        rej(e);
                                    }
                                }
                                console.log("1. Finished copying");

                                res(true);
                            });
                        }

                        function deleteObjects(data) {
                            return new Promise(async (res, rej) => {
                                for (let index in data.Contents) {
                                    var params = {
                                        Bucket: event.ResourceProperties.DestinationBucket,
                                        Key: data.Contents[index].Key
                                    }

                                    try {
                                        console.log(params);
                                        const res = await s3.deleteObject(params).promise();
                                        console.log(res)
                                    }
                                    catch (e) {
                                        rej(e);
                                    }
                                }
                                console.log("1. Finished deleting");

                                res(true);
                            });
                        }
                    };

    CustomResourceLambdaExecutionRole:
        Type: AWS::IAM::Role
        Properties:
            AssumeRolePolicyDocument:
                Statement:
                    - Action:
                          - sts:AssumeRole
                      Effect: Allow
                      Principal:
                          Service:
                              - lambda.amazonaws.com
                Version: "2012-10-17"
            Path: "/"
            Policies:
                - PolicyDocument:
                      Statement:
                          - Action:
                                - logs:CreateLogGroup
                                - logs:CreateLogStream
                                - logs:PutLogEvents
                            Effect: Allow
                            Resource: arn:aws:logs:*:*:*
                      Version: "2012-10-17"
                  PolicyName: !Sub ${AWS::StackName}-CustomResource-CW
                - PolicyDocument:
                      Statement:
                          - Action:
                                - s3:DeleteObject
                                - s3:List*
                                - s3:GetObject
                                - s3:PutObject
                            Effect: Allow
                            Resource:
                                - !Sub arn:aws:s3:::${S3BucketName}/*
                                - !Sub arn:aws:s3:::${S3BucketName}
                                - !Sub arn:aws:s3:::${FrontEndS3Bucket}/*
                                - !Sub arn:aws:s3:::${FrontEndS3Bucket}
                      Version: "2012-10-17"
                  PolicyName: !Sub ${AWS::StackName}-CustomResourceLambda-S3

    DDBCustomResource:
        Type: Custom::S3CustomResource
        Properties:
            ServiceToken: !GetAtt CustomResourceDDBLambdaFunction.Arn
            EmailAddress: !Ref EmailAddress
            UsersTable: !Ref UsersDDBTable

    CustomResourceDDBLambdaFunction:
        Type: "AWS::Lambda::Function"
        Properties:
            Handler: index.handler
            Role: !GetAtt CustomResourceDDBLambdaExecutionRole.Arn
            Timeout: 10
            Runtime: nodejs16.x
            Environment:
                Variables:
                    TABLE: !Ref UsersDDBTable
            Code:
                ZipFile: |
                    var response = require('cfn-response');
                    const AWS = require('aws-sdk');
                    const ddb = new AWS.DynamoDB();

                    String.prototype.hashCode = function() {
                        var hash = 0,
                            i, chr;
                        if (this.length === 0) return hash;
                        for (i = 0; i < this.length; i++) {
                            chr = this.charCodeAt(i);
                            hash = ((hash << 5) - hash) + chr;
                            hash |= 0; // Convert to 32bit integer
                        }
                        return Math.abs(hash);
                    }

                    exports.handler = (event, context) => {
                        const email = event.ResourceProperties.EmailAddress;
                        const apiKey = (email + Date.now()).hashCode();

                        const request = event.RequestType;

                        if (request == "Create" || request == "Update") {
                            let params = {
                                TableName: process.env.TABLE,
                                Item: {
                                    'email': { S: email },
                                    'apiKey': { S: apiKey.toString() }
                                }
                            }

                            ddb.putItem(params, function(err, data) {
                                if (err) {
                                    response.send(event, context, response.FAILED, {});
                                } else {
                                    response.send(event, context, response.SUCCESS, { "ApiKey": apiKey.toString() });
                                }
                            });        
                        } else {
                            response.send(event, context, response.SUCCESS, {});
                        }
                    };

    CustomResourceDDBLambdaExecutionRole:
        Type: AWS::IAM::Role
        Properties:
            AssumeRolePolicyDocument:
                Statement:
                    - Action:
                          - sts:AssumeRole
                      Effect: Allow
                      Principal:
                          Service:
                              - lambda.amazonaws.com
                Version: "2012-10-17"
            Path: "/"
            Policies:
                - PolicyDocument:
                      Statement:
                          - Action:
                                - logs:CreateLogGroup
                                - logs:CreateLogStream
                                - logs:PutLogEvents
                            Effect: Allow
                            Resource: arn:aws:logs:*:*:*
                      Version: "2012-10-17"
                  PolicyName: !Sub ${AWS::StackName}-CRDDBLambda-logs
                - PolicyDocument:
                      Statement:
                          - Action:
                                - dynamodb:PutItem
                            Effect: Allow
                            Resource:
                                - !GetAtt UsersDDBTable.Arn
                      Version: "2012-10-17"
                  PolicyName: !Sub ${AWS::StackName}-CRDDBLambda-ddb

Outputs:
    WebclientURL:
        Description: The URL to access the webclient after deployment.
        Value: !GetAtt CloudFrontDistribution.DomainName
    ApiKey:
        Description: The API key that will be required by the frontend to authenticate the user
        Value: !GetAtt DDBCustomResource.ApiKey