
service: web # Important: this must be unique in AWS Cloudformation!
frameworkVersion: ">=1.14.0 <2.0.0"

provider:
  name: aws
  runtime: nodejs12.x
  region: us-west-2
  stage: ${env:SLS_STAGE, self:custom.stageDefault}
  # Let's be conservative and prevent any unexpected changes
  # to the stack. See:
  # https://github.com/gladly-team/tab/issues/582
  # Example to update policy:
  #   aws cloudformation set-stack-policy --stack-name web-dev --region us-west-2 --stack-policy-body \
  #   '{"Statement":[
  #     {"Effect":"Allow","Principal":"*","Action":"Update:Modify","Resource":"LogicalResourceId/WebAppCloudFrontDistribution"}
  #   ]}'
  stackPolicy:
    - Effect: Deny
      Principal: "*"
      Action: "Update:*"
      Resource: "*"

custom:
  stageDefault: dev
  stage: ${env:SLS_STAGE, self:custom.stageDefault}
  newTabAppS3Bucket: ${env:DEPLOYMENT_WEB_APP_S3_BUCKET_NAME}
  newTabAppLambdaEdgeFunctionARN: arn:aws:lambda:us-east-1:${env:AWS_ACCOUNT_ID}:function:lambda-edge-${self:provider.stage}-newtabAppLambdaEdge:${env:DEPLOYMENT_WEB_APP_LAMBDA_EDGE_FUNCTION_VERSION}
  searchAppS3Bucket: ${env:DEPLOYMENT_SEARCH_APP_S3_BUCKET_NAME}
  searchAppLambdaEdgeFunctionARN: arn:aws:lambda:us-east-1:${env:AWS_ACCOUNT_ID}:function:lambda-edge-${self:provider.stage}-searchAppLambdaEdge:${env:DEPLOYMENT_SEARCH_APP_LAMBDA_EDGE_FUNCTION_VERSION}
  homepage404LambdaEdgeFunctionARN: arn:aws:lambda:us-east-1:${env:AWS_ACCOUNT_ID}:function:lambda-edge-${self:provider.stage}-homepage404LambdaEdge:${env:DEPLOYMENT_HOMEPAGE_404_LAMBDA_EDGE_FUNCTION_VERSION}
  graphQLProxyDomain: ${env:DEPLOYMENT_GRAPHQL_DOMAIN}
  graphQLProxyPath: ${env:DEPLOYMENT_GRAPHQL_PATH}
  webAppName: Tab2017
  searchAppName: SearchForACause
  graphQLAppName: TabGraphQL
  cloudfrontDomainAlias: ${env:DEPLOYMENT_WEB_APP_CLOUDFRONT_DOMAIN_ALIAS}
  SSLCertificateARN: ${env:SSL_CERTIFICATE_ARN}
  websiteOrigin: ${env:DEPLOYMENT_LANDING_PAGE_DOMAIN}
  websiteAppName: TabLandingPage
  tabV4Host: ${env:LAMBDA_TAB_V4_HOST}
  searchAPIOriginDomain: ${env:DEPLOYMENT_SEARCH_API_ORIGIN_DOMAIN}

resources:
  Resources:
    # New tab app S3 bucket
    TabS3Bucket:
      Type: AWS::S3::Bucket
      DeletionPolicy: Retain
      UpdateReplacePolicy: Retain
      Properties:
        BucketName: ${self:custom.newTabAppS3Bucket}
        AccessControl: PublicRead
        WebsiteConfiguration:
          IndexDocument: index.html
          # FYI: this origin server will always return a 200 response because
          # it's used as a single page app.
          ErrorDocument: newtab/index.html
    TabS3BucketPolicy:
      Type: AWS::S3::BucketPolicy
      DeletionPolicy: Retain
      UpdateReplacePolicy: Retain
      Properties:
        Bucket:
          Ref: TabS3Bucket
        PolicyDocument:
          Statement:
            - Sid: PublicReadGetObject
              Effect: Allow
              Principal: "*"
              Action:
                - s3:GetObject
              Resource: arn:aws:s3:::${self:custom.newTabAppS3Bucket}/*

    # Search app S3 bucket
    SearchAppS3Bucket:
      Type: AWS::S3::Bucket
      DeletionPolicy: Retain
      UpdateReplacePolicy: Retain
      Properties:
        BucketName: ${self:custom.searchAppS3Bucket}
        AccessControl: PublicRead
        WebsiteConfiguration:
          IndexDocument: index.html
          ErrorDocument: search/index.html
    SearchAppS3BucketPolicy:
      Type: AWS::S3::BucketPolicy
      DeletionPolicy: Retain
      UpdateReplacePolicy: Retain
      Properties:
        Bucket:
          Ref: SearchAppS3Bucket
        PolicyDocument:
          Statement:
            - Sid: PublicReadGetObject
              Effect: Allow
              Principal: "*"
              Action:
                - s3:GetObject
              Resource: arn:aws:s3:::${self:custom.searchAppS3Bucket}/*

    # Serve the app through Cloudfront.
    WebAppCloudFrontDistribution:
      Type: AWS::CloudFront::Distribution
      DeletionPolicy: Retain
      UpdateReplacePolicy: Retain
      Properties:
        # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-distributionconfig.html
        DistributionConfig:
          Origins:
            # Web S3 bucket.
            - DomainName: ${self:custom.newTabAppS3Bucket}.s3.amazonaws.com
              # An identifier for the origin. This must be unique within the distribution.
              Id: ${self:custom.webAppName}
              CustomOriginConfig:
                HTTPPort: 80
                HTTPSPort: 443
                # HTTP only is required for S3 buckets that are configured as
                # website endpoints:
                # https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html#DownloadDistValuesOriginProtocolPolicy 
                OriginProtocolPolicy: http-only
            # Search for a Cause S3 bucket.
            - DomainName: ${self:custom.searchAppS3Bucket}.s3.amazonaws.com
              # An identifier for the origin. This must be unique within the distribution.
              Id: ${self:custom.searchAppName}
              CustomOriginConfig:
                HTTPPort: 80
                HTTPSPort: 443
                # HTTP only is required for S3 buckets that are configured as
                # website endpoints:
                # https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html#DownloadDistValuesOriginProtocolPolicy 
                OriginProtocolPolicy: http-only
              OriginCustomHeaders:
                # We use custom headers as a workaround for the fact that
                # Edge functions don't support environment variables. See:
                # https://stackoverflow.com/a/58101487/1332513
                - HeaderName: X-Tab-Stage
                  HeaderValue: ${self:custom.stage}
            # GraphQL service.
            - DomainName: ${self:custom.graphQLProxyDomain}
              OriginPath: ${self:custom.graphQLProxyPath}
              Id: ${self:custom.graphQLAppName}
              CustomOriginConfig:
                HTTPPort: 80
                HTTPSPort: 443
                OriginProtocolPolicy: https-only
            # Homepage / landing page.
            - DomainName: ${self:custom.websiteOrigin}
              Id: ${self:custom.websiteAppName}
              CustomOriginConfig:
                HTTPPort: 80
                HTTPSPort: 443
                OriginProtocolPolicy: http-only
            # Next.js app on Vercel.
            - DomainName: ${self:custom.tabV4Host}
              # An identifier for the origin. This must be unique within the distribution.
              Id: nextjs-tab-web.now.sh
              CustomOriginConfig:
                HTTPPort: 80
                HTTPSPort: 443
                OriginProtocolPolicy: https-only
            # Search API service.
            - DomainName: ${self:custom.searchAPIOriginDomain}
              Id: SearchAPI
              CustomOriginConfig:
                HTTPPort: 80
                HTTPSPort: 443
                OriginProtocolPolicy: https-only
          Enabled: true
          Comment: ${self:custom.stage} new tab app, search app, and homepage
          # No default root object; our website (landing page) app handles it.
          # DefaultRootObject: index.html
          Aliases:
            - ${self:custom.cloudfrontDomainAlias}
          # Show the homepage's 404 page if any origin returns 404. (It will
          # call the homepage origin server because it's used in the
          # catch-all cache behavior.)
          # https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/HTTPStatusCodes.html#HTTPStatusCodes-custom-error-pages
          CustomErrorResponses:
            - ErrorCode: 404
              ResponseCode: 404
              ResponsePagePath: /404.html
              ErrorCachingMinTTL: 60
          # How CloudFront determines cache behavior:
          #   https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Expiration.html#ExpirationDownloadDist
          # CloudFront guide to CacheBehavior:
          #   https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-cachebehavior.html
          # We rely on Cache-Control headers set on the origin S3 buckets
          # to set long-lived CloudFront caching files at edge locations.
          # We enforce a minimum, short CF cache for objects that aren't
          # browser-cached.
          CacheBehaviors:
            # New tab app V4 (Next.js app on Vercel) static files. The
            # app is hosted on the /newtab/ subpath.
            - PathPattern: '/newtab/_next/*'
              MinTTL: 0
              DefaultTTL: 86400
              MaxTTL: 63115200 # two years
              AllowedMethods:
                - HEAD
                - GET
                - OPTIONS
              TargetOriginId: nextjs-tab-web.now.sh
              ForwardedValues:
                QueryString: false
                Cookies:
                  Forward: none
              ViewerProtocolPolicy: redirect-to-https
            # Next.js API on Vercel.
            - PathPattern: 'newtab/api/*'
              MinTTL: 0
              DefaultTTL: 0
              # Allow the origin to set long-lived caching on files.
              MaxTTL: 63115200 # two years
              AllowedMethods:
                - HEAD
                - DELETE
                - POST
                - GET
                - OPTIONS
                - PUT
                - PATCH
              TargetOriginId: nextjs-tab-web.now.sh
              ForwardedValues:
                # Cannot use '*', as that sends the Host header:
                # https://stackoverflow.com/a/32825414 
                Headers:
                  - Authorization
                QueryString: true
                Cookies:
                  Forward: all
              ViewerProtocolPolicy: redirect-to-https
            # New tab app
            - PathPattern: 'newtab*'
              MinTTL: 0
              DefaultTTL: 86400
              # Allow the origin to set long-lived caching on files.
              MaxTTL: 63115200 # two years
              # Including methods needed for the Next.js app API at `/newtab/api/`.
              AllowedMethods:
                - HEAD
                - DELETE
                - POST
                - GET
                - OPTIONS
                - PUT
                - PATCH
              TargetOriginId: ${self:custom.webAppName}
              ForwardedValues:
                QueryString: false
                Cookies:
                  # Send the cookies required to load /newtab/ from the
                  # beta Next.js app.
                  Forward: whitelist
                  WhitelistedNames:
                    - TabAuth.AuthUser
                    - TabAuth.AuthUser.sig
                    - TabAuth.AuthUserTokens
                    - TabAuth.AuthUserTokens.sig
                    - tabV4OptIn
                    - tabV4OptIn.sig
                Headers:
                  # The referer header is necessary for conditional auth page
                  # routing (via Lambda@Edge) between Tab v4 and Tab legacy.
                  - Referer
              Compress: true
              ViewerProtocolPolicy: redirect-to-https
              # When CloudFront calls our origin, this function rewrites all
              # HTML requests to the root index.html to serve the single-page
              # app. We don't want to rely on setting S3's error document to the
              # index.html file, because then S3 will return a 404 to Cloudfront,
              # which will call the 404 error document; CloudFront only allows
              # one custom 404 error document, which means this wouldn't work
              # for two single-page apps on the same CF distribution.
              LambdaFunctionAssociations:
                # "Executes on a cache miss, before a request is forwarded to
                # the origin"
                # https://aws.amazon.com/blogs/networking-and-content-delivery/lambdaedge-design-best-practices/
                - EventType: origin-request
                  LambdaFunctionARN: ${self:custom.newTabAppLambdaEdgeFunctionARN}
            # Search API: routing through the same domain avoids
            # preflight requests.
            - PathPattern: 'search/api*'
              MinTTL: 0
              MaxTTL: 0
              DefaultTTL: 0
              AllowedMethods:
                - HEAD
                - GET
                - OPTIONS
              TargetOriginId: SearchAPI
              ForwardedValues:
                # Cannot use '*', as that sends the Host header:
                # https://stackoverflow.com/a/32825414 
                Headers:
                  - Accept
                  - Accept-Charset
                  - Access-Control-Request-Headers
                  - Access-Control-Request-Method
                  - CloudFront-Forwarded-Proto
                  - Origin
                  - Referer
                  - User-Agent
                QueryString: true
                Cookies:
                  Forward: none
              ViewerProtocolPolicy: redirect-to-https
            # Search app
            - PathPattern: 'search*'
              MinTTL: 0
              DefaultTTL: 0
              MaxTTL: 0
              AllowedMethods:
                - HEAD
                - GET
                - OPTIONS
              # The actual origin server isn't used currently because the
              # associated Lambda@Edge function always returns a 307 redirect.
              # However, we do rely on the OriginCustomHeaders defined on the
              # origin.
              TargetOriginId: ${self:custom.searchAppName}
              ForwardedValues:
                # Required to forward search query.
                QueryString: true
                Cookies:
                  Forward: whitelist
                  WhitelistedNames:
                    # TabAuth cookies are used to pass the user's ID token to
                    # SNS for search event logging.
                    - TabAuth.AuthUserTokens
                    - TabAuth.AuthUserTokens.sig
                Headers:
                  # Required to forward the user to a localized SERP.
                  - Accept-Language
                  - CloudFront-Viewer-Country
              Compress: true
              ViewerProtocolPolicy: redirect-to-https
              LambdaFunctionAssociations:
                # We can't use viewer-request with CloudFront-Viewer-Country.
                # "CloudFront adds the CloudFront-Viewer-Country header after the viewer
                # request event. To use this example, you must create a trigger for the
                # origin request event."
                # https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-examples.html#lambda-examples-redirect-based-on-country
                - EventType: origin-request
                  LambdaFunctionARN: ${self:custom.searchAppLambdaEdgeFunctionARN}
            # GraphQL
            - PathPattern: 'graphql*'
              MinTTL: 0
              MaxTTL: 0
              DefaultTTL: 0
              AllowedMethods:
                - HEAD
                - DELETE
                - POST
                - GET
                - OPTIONS
                - PUT
                - PATCH
              TargetOriginId: ${self:custom.graphQLAppName}
              ForwardedValues:
                # Cannot use '*', as that sends the Host header:
                # https://stackoverflow.com/a/32825414 
                Headers:
                  - Authorization
                QueryString: false
                Cookies:
                  Forward: none
              ViewerProtocolPolicy: redirect-to-https
            # Next.js app on Vercel.
            - PathPattern: 'v4*'
              MinTTL: 0
              DefaultTTL: 0
              # Allow the origin to set long-lived caching on files.
              MaxTTL: 63115200 # two years
              AllowedMethods:
                - HEAD
                - DELETE
                - POST
                - GET
                - OPTIONS
                - PUT
                - PATCH
              TargetOriginId: nextjs-tab-web.now.sh
              ForwardedValues:
                # Cannot use '*', as that sends the Host header:
                # https://stackoverflow.com/a/32825414 
                Headers:
                  - Authorization
                QueryString: true
                Cookies:
                  Forward: all
              ViewerProtocolPolicy: redirect-to-https
            # Send all remaining traffic to our homepage
            - PathPattern: '*'
              MinTTL: 0
              DefaultTTL: 0
              # Allow the origin to set long-lived caching on files.
              MaxTTL: 63115200 # two years
              AllowedMethods:
                - HEAD
                - GET
                - OPTIONS
              TargetOriginId: ${self:custom.websiteAppName}
              ForwardedValues:
                QueryString: false
                Cookies:
                  Forward: none
              # https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/ServingCompressedFiles.html
              Compress: true
              ViewerProtocolPolicy: redirect-to-https
              LambdaFunctionAssociations:
                # Manually set the 404 error code when the origin returns
                # the 404 document.
                - EventType: origin-response
                  LambdaFunctionARN: ${self:custom.homepage404LambdaEdgeFunctionARN}
          DefaultCacheBehavior:
            AllowedMethods:
              - HEAD
              - GET
              - OPTIONS
            TargetOriginId: ${self:custom.webAppName}
            ForwardedValues:
              QueryString: false
              Cookies:
                Forward: none
            # The protocol that users can use to access the files in the origin.
            # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-defaultcachebehavior.html#cfn-cloudfront-defaultcachebehavior-viewerprotocolpolicy
            ViewerProtocolPolicy: redirect-to-https
          # The certificate to use when viewers use HTTPS to request objects.
          # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-distributionconfig-viewercertificate.html
          ViewerCertificate:
            AcmCertificateArn: ${self:custom.SSLCertificateARN}
            SslSupportMethod: sni-only
  
  # In order to print out the hosted domain via `serverless info`,
  # we need to define the DomainName output for CloudFormation
  Outputs:
    WebAppCloudFrontDistributionOutput:
      Value: ${self:custom.newTabAppS3Bucket}.s3.amazonaws.com
