diff --git a/apps/backend/lambdas/expenditures/services/expenditures.ts b/apps/backend/lambdas/expenditures/services/expenditures.ts index cf4645fc..9de79f78 100644 --- a/apps/backend/lambdas/expenditures/services/expenditures.ts +++ b/apps/backend/lambdas/expenditures/services/expenditures.ts @@ -9,7 +9,8 @@ import { applyExpenditureScope, type ExpenditureScope } from './scope'; const REGION = process.env.AWS_REGION ?? 'us-east-2'; const BUCKET = process.env.REPORTS_BUCKET_NAME ?? ''; -const s3 = new S3Client({ region: REGION }); +// SDK default signs a checksum of the presigner's empty body; real bytes then fail the PUT. +const s3 = new S3Client({ region: REGION, requestChecksumCalculation: 'WHEN_REQUIRED' }); // Receipts are PDFs only, matching the dropzone in AddExpenseModal. export const RECEIPT_CONTENT_TYPE = 'application/pdf'; diff --git a/apps/backend/lambdas/reports/controllers/reports.ts b/apps/backend/lambdas/reports/controllers/reports.ts index 712e9f29..4530b247 100644 --- a/apps/backend/lambdas/reports/controllers/reports.ts +++ b/apps/backend/lambdas/reports/controllers/reports.ts @@ -15,7 +15,10 @@ import { getObjectSize, } from '../report-service'; -const s3 = new S3Client({ region: process.env.AWS_REGION ?? 'us-east-2' }); +const s3 = new S3Client({ + region: process.env.AWS_REGION ?? 'us-east-2', + requestChecksumCalculation: 'WHEN_REQUIRED', +}); const BUCKET = process.env.REPORTS_BUCKET_NAME ?? ''; const ALLOWED_EXTENSIONS = ['pdf', 'docx'] as const; diff --git a/apps/backend/lambdas/users/photos.ts b/apps/backend/lambdas/users/photos.ts index 3fde94be..7da133ed 100644 --- a/apps/backend/lambdas/users/photos.ts +++ b/apps/backend/lambdas/users/photos.ts @@ -4,7 +4,7 @@ import { reportError } from '@branch/lambda-http'; const REGION = process.env.AWS_REGION ?? 'us-east-2'; const BUCKET = process.env.REPORTS_BUCKET_NAME ?? ''; -const s3 = new S3Client({ region: REGION }); +const s3 = new S3Client({ region: REGION, requestChecksumCalculation: 'WHEN_REQUIRED' }); /** Profile photos share the reports bucket, under their own prefix. */ export const AVATAR_PREFIX = 'avatars/'; diff --git a/infrastructure/aws/README.md b/infrastructure/aws/README.md index 6f23dbab..72c06d7d 100644 --- a/infrastructure/aws/README.md +++ b/infrastructure/aws/README.md @@ -88,6 +88,7 @@ | [aws_s3_bucket.frontend](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/s3_bucket) | resource | | [aws_s3_bucket.lambda_deployments](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/s3_bucket) | resource | | [aws_s3_bucket.reports_bucket](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/s3_bucket) | resource | +| [aws_s3_bucket_cors_configuration.reports_bucket_cors](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/s3_bucket_cors_configuration) | resource | | [aws_s3_bucket_policy.frontend](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/s3_bucket_policy) | resource | | [aws_s3_bucket_public_access_block.frontend](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/s3_bucket_public_access_block) | resource | | [aws_s3_bucket_public_access_block.reports_bucket_public_access](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/s3_bucket_public_access_block) | resource | diff --git a/infrastructure/aws/s3.tf b/infrastructure/aws/s3.tf index b3e01c52..b796a852 100644 --- a/infrastructure/aws/s3.tf +++ b/infrastructure/aws/s3.tf @@ -11,6 +11,21 @@ resource "aws_s3_bucket_public_access_block" "reports_bucket_public_access" { restrict_public_buckets = true } +resource "aws_s3_bucket_cors_configuration" "reports_bucket_cors" { + bucket = aws_s3_bucket.reports_bucket.id + + cors_rule { + allowed_methods = ["PUT"] + allowed_headers = ["content-type"] + max_age_seconds = 7200 + + allowed_origins = compact([ + "https://${aws_cloudfront_distribution.frontend.domain_name}", + local.attach_dns ? "https://${var.app_domain}" : "", + ]) + } +} + output "reports_bucket_name" { description = "Name of the S3 bucket for generated reports" value = aws_s3_bucket.reports_bucket.id