From d429f0914a72be3405374b57e98f729538bb9721 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sun, 6 Jul 2025 11:21:11 -0500 Subject: [PATCH 01/14] Use S3 bucket for swagger UI and json --- Makefile | 19 ++- cloudformation/main.yml | 45 ++++-- src/api/createLambdaPackage.js | 2 - src/api/createSwagger.ts | 56 +++++++ src/api/index.ts | 266 +++++++++++++++++---------------- src/api/package.json | 6 +- 6 files changed, 244 insertions(+), 150 deletions(-) create mode 100644 src/api/createSwagger.ts diff --git a/Makefile b/Makefile index 9d35f7a3..afe83c62 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,7 @@ current_aws_account := $(shell aws sts get-caller-identity --query Account --out src_directory_root = src/ dist_ui_directory_root = dist_ui/ +dist_docs_directory_root = dist/swagger/ integration_test_directory_root = tests/live_integration/ # CHANGE ME (as needed) @@ -28,6 +29,8 @@ common_params = --no-confirm-changeset \ s3_bucket_prefix = "$(current_aws_account)-$(region)-$(application_key)" ui_s3_bucket = "$(s3_bucket_prefix)-ui" +docs_s3_bucket = "$(s3_bucket_prefix)-docs" + GIT_HASH := $(shell git rev-parse --short HEAD) @@ -66,19 +69,23 @@ build: src/ cloudformation/ local: VITE_BUILD_HASH=$(GIT_HASH) yarn run dev + +postdeploy: + @echo "Syncing S3 UI bucket..." + aws s3 sync $(dist_ui_directory_root) s3://$(ui_s3_bucket)/ --delete + @echo "Syncing S3 Docs bucket..." + aws s3 sync $(dist_docs_directory_root) s3://$(docs_s3_bucket)/ --delete + make invalidate_cloudfront + deploy_prod: check_account_prod @echo "Deploying CloudFormation stack..." sam deploy $(common_params) --parameter-overrides $(run_env)=prod $(set_application_prefix)=$(application_key) $(set_application_name)="$(application_name)" S3BucketPrefix="$(s3_bucket_prefix)" - @echo "Syncing S3 bucket..." - aws s3 sync $(dist_ui_directory_root) s3://$(ui_s3_bucket)/ --delete - make invalidate_cloudfront + make postdeploy deploy_dev: check_account_dev @echo "Deploying CloudFormation stack..." sam deploy $(common_params) --parameter-overrides $(run_env)=dev $(set_application_prefix)=$(application_key) $(set_application_name)="$(application_name)" S3BucketPrefix="$(s3_bucket_prefix)" - @echo "Syncing S3 bucket..." - aws s3 sync $(dist_ui_directory_root) s3://$(ui_s3_bucket)/ --delete - make invalidate_cloudfront + make postdeploy invalidate_cloudfront: @echo "Creating CloudFront invalidation..." diff --git a/cloudformation/main.yml b/cloudformation/main.yml index 93cce72a..dd8eff6d 100644 --- a/cloudformation/main.yml +++ b/cloudformation/main.yml @@ -611,11 +611,18 @@ Resources: WebsiteConfiguration: IndexDocument: index.html + AppDocsS3Bucket: + Type: AWS::S3::Bucket + Properties: + BucketName: !Sub ${S3BucketPrefix}-docs + WebsiteConfiguration: + IndexDocument: index.html + CloudFrontOriginAccessIdentity: Type: AWS::CloudFront::CloudFrontOriginAccessIdentity Properties: CloudFrontOriginAccessIdentityConfig: - Comment: !Sub "Access identity for ${AppFrontendS3Bucket}" + Comment: !Sub "Access identity for ${AppFrontendS3Bucket} and ${AppDocsS3Bucket}" AppFrontendCloudfrontDistribution: Type: AWS::CloudFront::Distribution @@ -627,6 +634,10 @@ Resources: DomainName: !GetAtt AppFrontendS3Bucket.RegionalDomainName S3OriginConfig: OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}" + - Id: S3DocsOrigin + DomainName: !GetAtt AppDocsS3Bucket.RegionalDomainName + S3OriginConfig: + OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}" - Id: LambdaOrigin DomainName: !Select [0, !Split ['/', !Select [1, !Split ['https://', !GetAtt AppLambdaUrl.FunctionUrl]]]] CustomOriginConfig: @@ -698,22 +709,23 @@ Resources: OriginRequestPolicyId: b689b0a8-53d0-40ab-baf2-68738e2966ac Compress: true - PathPattern: "/api/documentation*" - TargetOriginId: LambdaOrigin + Compress: true + TargetOriginId: S3DocsOrigin ViewerProtocolPolicy: redirect-to-https AllowedMethods: - GET - HEAD - - OPTIONS - - PUT - - POST - - DELETE - - PATCH CachedMethods: - GET - HEAD - CachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6" - OriginRequestPolicyId: b689b0a8-53d0-40ab-baf2-68738e2966ac - Compress: true + ForwardedValues: + QueryString: true + Cookies: + Forward: none + CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 # caching-optimized + # LambdaFunctionAssociations: + # - EventType: origin-request + # LambdaFunctionARN: !Ref AppFrontendEdgeLambdaVersion - PathPattern: "/api/*" TargetOriginId: LambdaOrigin ViewerProtocolPolicy: redirect-to-https @@ -754,6 +766,19 @@ Resources: Action: s3:GetObject Resource: !Sub "${AppFrontendS3Bucket.Arn}/*" + AppDocsS3BucketPolicy: + Type: AWS::S3::BucketPolicy + Properties: + Bucket: !Ref AppDocsS3Bucket + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId + Action: s3:GetObject + Resource: !Sub "${AppDocsS3Bucket.Arn}/*" + CloudfrontNoCachePolicy: Type: AWS::CloudFront::CachePolicy Properties: diff --git a/src/api/createLambdaPackage.js b/src/api/createLambdaPackage.js index 82d7f546..d59f1fca 100644 --- a/src/api/createLambdaPackage.js +++ b/src/api/createLambdaPackage.js @@ -13,8 +13,6 @@ export const packagesToTransfer = [ "moment-timezone", "passkit-generator", "fastify", - "@fastify/swagger", - "@fastify/swagger-ui", "zod", "argon2", "ioredis", diff --git a/src/api/createSwagger.ts b/src/api/createSwagger.ts new file mode 100644 index 00000000..7f538c87 --- /dev/null +++ b/src/api/createSwagger.ts @@ -0,0 +1,56 @@ +import { fileURLToPath } from "url"; +import path from "node:path"; +import { writeFile, mkdir } from "fs/promises"; +import init from "./index.js"; // Assuming this is your Fastify app initializer + +const html = ` + + + + + + + ACM @ UIUC Core API + + + +
+ + + + +`; +/** + * Generates and saves Swagger/OpenAPI specification files. + */ +async function createSwaggerFiles() { + try { + const app = await init(false, false); + await app.ready(); + console.log("App is ready. Generating specs..."); + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + const outputDir = path.resolve(__dirname, "..", "..", "dist", "swagger"); + await mkdir(outputDir, { recursive: true }); + const jsonSpec = JSON.stringify(app.swagger(), null, 2); + const yamlSpec = app.swagger({ yaml: true }); + await writeFile(path.join(outputDir, "openapi.json"), jsonSpec); + await writeFile(path.join(outputDir, "openapi.yaml"), yamlSpec); + await writeFile(path.join(outputDir, "index.html"), html); + + console.log(`✅ Swagger files successfully generated in ${outputDir}`); + await app.close(); + } catch (err) { + console.error("❌ Failed to generate Swagger files:", err); + process.exit(1); + } +} + +createSwaggerFiles(); diff --git a/src/api/index.ts b/src/api/index.ts index 48ec22b0..160fb465 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -15,7 +15,6 @@ import { environmentConfig, genericConfig, SecretConfig, - SecretTesting, } from "../common/config.js"; import organizationsPlugin from "./routes/organizations.js"; import authorizeFromSchemaPlugin from "./plugins/authorizeFromSchema.js"; @@ -36,8 +35,7 @@ import membershipPlugin from "./routes/membership.js"; import path from "path"; // eslint-disable-line import/no-nodejs-modules import roomRequestRoutes from "./routes/roomRequests.js"; import logsPlugin from "./routes/logs.js"; -import fastifySwagger from "@fastify/swagger"; -import fastifySwaggerUI from "@fastify/swagger-ui"; + import { fastifyZodOpenApiPlugin, fastifyZodOpenApiTransform, @@ -50,19 +48,17 @@ import { withTags } from "./components/index.js"; import apiKeyRoute from "./routes/apiKey.js"; import clearSessionRoute from "./routes/clearSession.js"; import RedisModule from "ioredis"; +import { fileURLToPath } from "url"; // eslint-disable-line import/no-nodejs-modules +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); dotenv.config(); const now = () => Date.now(); -async function init(prettyPrint: boolean = false) { - const dynamoClient = new DynamoDBClient({ - region: genericConfig.AwsRegion, - }); - - const secretsManagerClient = new SecretsManagerClient({ - region: genericConfig.AwsRegion, - }); +async function init(prettyPrint: boolean = false, initClients: boolean = true) { + const isRunningInLambda = + process.env.LAMBDA_TASK_ROOT || process.env.AWS_LAMBDA_FUNCTION_NAME; const transport = prettyPrint ? { target: "pino-pretty", @@ -98,111 +94,118 @@ async function init(prettyPrint: boolean = false) { await app.register(evaluatePoliciesPlugin); await app.register(errorHandlerPlugin); await app.register(fastifyZodOpenApiPlugin); - await app.register(fastifySwagger, { - openapi: { - info: { - title: "ACM @ UIUC Core API", - description: "ACM @ UIUC Core Management Platform", - version: "1.0.0", - contact: { - name: "ACM @ UIUC Infrastructure Team", - email: "infra@acm.illinois.edu", - url: "infra.acm.illinois.edu", - }, - license: { - name: "BSD 3-Clause", - identifier: "BSD-3-Clause", - url: "https://github.com/acm-uiuc/core/blob/main/LICENSE", - }, - termsOfService: "https://core.acm.illinois.edu/tos", - }, - servers: [ - { - url: "https://core.acm.illinois.edu", - description: "Production API server", - }, - { - url: "https://core.aws.qa.acmuiuc.org", - description: "QA API server", - }, - ], - - tags: [ - { - name: "Events", - description: - "Retrieve ACM @ UIUC-wide and organization-specific calendars and event metadata.", - }, - { - name: "Generic", - description: "Retrieve metadata about a user or ACM @ UIUC .", - }, - { - name: "iCalendar Integration", - description: - "Retrieve Events calendars in iCalendar format (for integration with external calendar clients).", - }, - { - name: "IAM", - description: "Identity and Access Management for internal services.", - }, - { name: "Linkry", description: "Link Shortener." }, - { - name: "Logging", - description: "View audit logs for various services.", - }, - { - name: "Membership", - description: "Purchasing or checking ACM @ UIUC membership.", - }, - { - name: "Tickets/Merchandise", - description: "Handling the tickets and merchandise lifecycle.", - }, - { - name: "Mobile Wallet", - description: "Issuing Apple/Google Wallet passes.", - }, - { - name: "Stripe", - description: - "Collecting payments for ACM @ UIUC invoices and other services.", - }, - { - name: "Room Requests", - description: - "Creating room reservation requests for ACM @ UIUC within University buildings.", - }, - { - name: "API Keys", - description: "Manage the lifecycle of API keys.", + try { + const fastifySwagger = import("@fastify/swagger"); + const fastifySwaggerUI = import("@fastify/swagger-ui"); + await app.register(fastifySwagger, { + openapi: { + info: { + title: "ACM @ UIUC Core API", + description: "ACM @ UIUC Core Management Platform", + version: "1.0.0", + contact: { + name: "ACM @ UIUC Infrastructure Team", + email: "infra@acm.illinois.edu", + url: "infra.acm.illinois.edu", + }, + license: { + name: "BSD 3-Clause", + identifier: "BSD-3-Clause", + url: "https://github.com/acm-uiuc/core/blob/main/LICENSE", + }, + termsOfService: "https://core.acm.illinois.edu/tos", }, - ], + servers: [ + { + url: "https://core.acm.illinois.edu", + description: "Production API server", + }, + { + url: "https://core.aws.qa.acmuiuc.org", + description: "QA API server", + }, + ], - openapi: "3.1.0" satisfies ZodOpenApiVersion, // If this is not specified, it will default to 3.1.0 - components: { - securitySchemes: { - bearerAuth: { - type: "http", - scheme: "bearer", - bearerFormat: "JWT", + tags: [ + { + name: "Events", + description: + "Retrieve ACM @ UIUC-wide and organization-specific calendars and event metadata.", + }, + { + name: "Generic", + description: "Retrieve metadata about a user or ACM @ UIUC .", + }, + { + name: "iCalendar Integration", + description: + "Retrieve Events calendars in iCalendar format (for integration with external calendar clients).", + }, + { + name: "IAM", description: - "Authorization: Bearer {token}\n\nThis API uses JWT tokens issued by Entra ID (Azure AD) with the Core API audience. Tokens must be included in the Authorization header as a Bearer token for all protected endpoints.", + "Identity and Access Management for internal services.", + }, + { name: "Linkry", description: "Link Shortener." }, + { + name: "Logging", + description: "View audit logs for various services.", + }, + { + name: "Membership", + description: "Purchasing or checking ACM @ UIUC membership.", + }, + { + name: "Tickets/Merchandise", + description: "Handling the tickets and merchandise lifecycle.", + }, + { + name: "Mobile Wallet", + description: "Issuing Apple/Google Wallet passes.", + }, + { + name: "Stripe", + description: + "Collecting payments for ACM @ UIUC invoices and other services.", + }, + { + name: "Room Requests", + description: + "Creating room reservation requests for ACM @ UIUC within University buildings.", + }, + { + name: "API Keys", + description: "Manage the lifecycle of API keys.", }, - apiKeyAuth: { - type: "apiKey", - in: "header", - name: "X-Api-Key", + ], + + openapi: "3.1.0" satisfies ZodOpenApiVersion, // If this is not specified, it will default to 3.1.0 + components: { + securitySchemes: { + bearerAuth: { + type: "http", + scheme: "bearer", + bearerFormat: "JWT", + description: + "Authorization: Bearer {token}\n\nThis API uses JWT tokens issued by Entra ID (Azure AD) with the Core API audience. Tokens must be included in the Authorization header as a Bearer token for all protected endpoints.", + }, + apiKeyAuth: { + type: "apiKey", + in: "header", + name: "X-Api-Key", + }, }, }, }, - }, - transform: fastifyZodOpenApiTransform, - transformObject: fastifyZodOpenApiTransformObject, - }); - await app.register(fastifySwaggerUI, { - routePrefix: "/api/documentation", - }); + transform: fastifyZodOpenApiTransform, + transformObject: fastifyZodOpenApiTransformObject, + }); + await app.register(fastifySwaggerUI, { + routePrefix: "/api/documentation", + }); + } catch (e) { + app.log.warn("Fastify Swagger not created!"); + } await app.register(fastifyStatic, { root: path.join(__dirname, "public"), prefix: "/", @@ -222,7 +225,7 @@ async function init(prettyPrint: boolean = false) { message: `Audit log can only be disabled if the run environment is "dev"!`, }); } - if (process.env.LAMBDA_TASK_ROOT || process.env.AWS_LAMBDA_FUNCTION_NAME) { + if (isRunningInLambda) { throw new InternalServerError({ message: `Audit log cannot be disabled when running in AWS Lambda environment!`, }); @@ -235,24 +238,30 @@ async function init(prettyPrint: boolean = false) { app.environmentConfig = environmentConfig[app.runEnvironment as RunEnvironment]; app.nodeCache = new NodeCache({ checkperiod: 30 }); - app.dynamoClient = dynamoClient; - app.secretsManagerClient = secretsManagerClient; - app.refreshSecretConfig = async () => { - app.log.debug( - `Getting secrets: ${JSON.stringify(app.environmentConfig.ConfigurationSecretIds)}.`, - ); - const allSecrets = await Promise.all( - app.environmentConfig.ConfigurationSecretIds.map((secretName) => - getSecretValue(app.secretsManagerClient, secretName), - ), - ); - app.secretConfig = allSecrets.reduce( - (acc, currentSecret) => ({ ...acc, ...currentSecret }), - {}, - ) as SecretConfig; - }; - await app.refreshSecretConfig(); - app.redisClient = new RedisModule.default(app.secretConfig.redis_url); + if (initClients) { + app.dynamoClient = new DynamoDBClient({ + region: genericConfig.AwsRegion, + }); + app.secretsManagerClient = new SecretsManagerClient({ + region: genericConfig.AwsRegion, + }); + app.refreshSecretConfig = async () => { + app.log.debug( + `Getting secrets: ${JSON.stringify(app.environmentConfig.ConfigurationSecretIds)}.`, + ); + const allSecrets = await Promise.all( + app.environmentConfig.ConfigurationSecretIds.map((secretName) => + getSecretValue(app.secretsManagerClient, secretName), + ), + ); + app.secretConfig = allSecrets.reduce( + (acc, currentSecret) => ({ ...acc, ...currentSecret }), + {}, + ) as SecretConfig; + }; + await app.refreshSecretConfig(); + app.redisClient = new RedisModule.default(app.secretConfig.redis_url); + } app.addHook("onRequest", (req, _, done) => { req.startTime = now(); const hostname = req.hostname; @@ -260,7 +269,6 @@ async function init(prettyPrint: boolean = false) { req.log.info({ hostname, url, method: req.method }, "received request"); done(); }); - app.addHook("onResponse", (req, reply, done) => { req.log.info( { diff --git a/src/api/package.json b/src/api/package.json index 44f945b9..5052fabe 100644 --- a/src/api/package.json +++ b/src/api/package.json @@ -7,7 +7,7 @@ "license": "BSD-3-Clause", "type": "module", "scripts": { - "build": "tsc && node build.js", + "build": "tsc && node build.js && tsx createSwagger.ts", "dev": "cross-env DISABLE_AUDIT_LOG=true cross-env LOG_LEVEL=debug concurrently --names 'esbuild,server' 'node esbuild.config.js --watch' 'cd ../../dist_devel && nodemon index.js'", "typecheck": "tsc --noEmit", "lint": "eslint . --ext .ts --cache", @@ -30,8 +30,6 @@ "@fastify/caching": "^9.0.1", "@fastify/cors": "^11.0.1", "@fastify/static": "^8.1.1", - "@fastify/swagger": "^9.5.0", - "@fastify/swagger-ui": "^5.2.2", "@middy/core": "^6.1.6", "@middy/event-normalizer": "^6.1.6", "@middy/sqs-partial-batch-failure": "^6.1.6", @@ -64,6 +62,8 @@ "zod-validation-error": "^3.3.1" }, "devDependencies": { + "@fastify/swagger": "^9.5.0", + "@fastify/swagger-ui": "^5.2.2", "@tsconfig/node22": "^22.0.1", "@types/aws-lambda": "^8.10.149", "@types/qrcode": "^1.5.5", From 643295878b1e565aa5ca2f5a6d63c8b95d4a8a09 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sun, 6 Jul 2025 11:37:04 -0500 Subject: [PATCH 02/14] use website urls for buckets --- cloudformation/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloudformation/main.yml b/cloudformation/main.yml index dd8eff6d..e82bd81e 100644 --- a/cloudformation/main.yml +++ b/cloudformation/main.yml @@ -631,11 +631,11 @@ Resources: HttpVersion: 'http2and3' Origins: - Id: S3WebsiteOrigin - DomainName: !GetAtt AppFrontendS3Bucket.RegionalDomainName + DomainName: !Select [0, !Split ['/', !Select [1, !Split ['http://', !GetAtt AppFrontendS3Bucket.WebsiteURL]]]] S3OriginConfig: OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}" - Id: S3DocsOrigin - DomainName: !GetAtt AppDocsS3Bucket.RegionalDomainName + DomainName: !Select [0, !Split ['/', !Select [1, !Split ['http://', !GetAtt AppDocsS3Bucket.WebsiteURL]]]] S3OriginConfig: OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}" - Id: LambdaOrigin From f40272ae90e124036aff2627244034b7f08145aa Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sun, 6 Jul 2025 11:46:31 -0500 Subject: [PATCH 03/14] Revert "use website urls for buckets" This reverts commit 643295878b1e565aa5ca2f5a6d63c8b95d4a8a09. --- cloudformation/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloudformation/main.yml b/cloudformation/main.yml index e82bd81e..dd8eff6d 100644 --- a/cloudformation/main.yml +++ b/cloudformation/main.yml @@ -631,11 +631,11 @@ Resources: HttpVersion: 'http2and3' Origins: - Id: S3WebsiteOrigin - DomainName: !Select [0, !Split ['/', !Select [1, !Split ['http://', !GetAtt AppFrontendS3Bucket.WebsiteURL]]]] + DomainName: !GetAtt AppFrontendS3Bucket.RegionalDomainName S3OriginConfig: OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}" - Id: S3DocsOrigin - DomainName: !Select [0, !Split ['/', !Select [1, !Split ['http://', !GetAtt AppDocsS3Bucket.WebsiteURL]]]] + DomainName: !GetAtt AppDocsS3Bucket.RegionalDomainName S3OriginConfig: OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}" - Id: LambdaOrigin From d7ebf3e59587662f106405c6f8146f3428a807db Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sun, 6 Jul 2025 12:02:39 -0500 Subject: [PATCH 04/14] Use S3 OAC instead of OAI --- cloudformation/main.yml | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/cloudformation/main.yml b/cloudformation/main.yml index dd8eff6d..b681926e 100644 --- a/cloudformation/main.yml +++ b/cloudformation/main.yml @@ -618,11 +618,14 @@ Resources: WebsiteConfiguration: IndexDocument: index.html - CloudFrontOriginAccessIdentity: - Type: AWS::CloudFront::CloudFrontOriginAccessIdentity + AppCloudfrontS3OAC: + Type: AWS::CloudFront::OriginAccessControl Properties: - CloudFrontOriginAccessIdentityConfig: - Comment: !Sub "Access identity for ${AppFrontendS3Bucket} and ${AppDocsS3Bucket}" + OriginAccessControlConfig: + Name: InfraCoreApi OAC + OriginAccessControlOriginType: s3 + SigningBehavior: always + SigningProtocol: sigv4 AppFrontendCloudfrontDistribution: Type: AWS::CloudFront::Distribution @@ -633,11 +636,13 @@ Resources: - Id: S3WebsiteOrigin DomainName: !GetAtt AppFrontendS3Bucket.RegionalDomainName S3OriginConfig: - OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}" + OriginAccessIdentity: '' + OriginAccessControlId: !GetAtt AppCloudfrontS3OAC.Id - Id: S3DocsOrigin DomainName: !GetAtt AppDocsS3Bucket.RegionalDomainName S3OriginConfig: - OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}" + OriginAccessIdentity: '' + OriginAccessControlId: !GetAtt AppCloudfrontS3OAC.Id - Id: LambdaOrigin DomainName: !Select [0, !Split ['/', !Select [1, !Split ['https://', !GetAtt AppLambdaUrl.FunctionUrl]]]] CustomOriginConfig: @@ -762,9 +767,12 @@ Resources: Statement: - Effect: Allow Principal: - CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId + Service: cloudfront.amazonaws.com Action: s3:GetObject Resource: !Sub "${AppFrontendS3Bucket.Arn}/*" + Condition: + StringEquals: + AWS:SourceArn: !Sub "arn:aws:cloudfront::${AWS::AccountId}:distribution/${AppFrontendCloudfrontDistribution}" AppDocsS3BucketPolicy: Type: AWS::S3::BucketPolicy @@ -775,9 +783,13 @@ Resources: Statement: - Effect: Allow Principal: - CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId + Service: cloudfront.amazonaws.com Action: s3:GetObject Resource: !Sub "${AppDocsS3Bucket.Arn}/*" + Condition: + StringEquals: + AWS:SourceArn: !Sub "arn:aws:cloudfront::${AWS::AccountId}:distribution/${AppFrontendCloudfrontDistribution}" + CloudfrontNoCachePolicy: Type: AWS::CloudFront::CachePolicy From 4ace6a1209cee902862048ffa48aa7b5a344ebcd Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sun, 6 Jul 2025 12:03:51 -0500 Subject: [PATCH 05/14] Remove website configuration --- cloudformation/main.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cloudformation/main.yml b/cloudformation/main.yml index b681926e..bfa2b16d 100644 --- a/cloudformation/main.yml +++ b/cloudformation/main.yml @@ -608,15 +608,11 @@ Resources: Type: AWS::S3::Bucket Properties: BucketName: !Sub ${S3BucketPrefix}-ui - WebsiteConfiguration: - IndexDocument: index.html AppDocsS3Bucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub ${S3BucketPrefix}-docs - WebsiteConfiguration: - IndexDocument: index.html AppCloudfrontS3OAC: Type: AWS::CloudFront::OriginAccessControl From b0d0e70ce119695074b81dfbc95243df8a158d62 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sun, 6 Jul 2025 12:05:11 -0500 Subject: [PATCH 06/14] Fix cfn --- cloudformation/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloudformation/main.yml b/cloudformation/main.yml index bfa2b16d..68dbebc5 100644 --- a/cloudformation/main.yml +++ b/cloudformation/main.yml @@ -633,12 +633,12 @@ Resources: DomainName: !GetAtt AppFrontendS3Bucket.RegionalDomainName S3OriginConfig: OriginAccessIdentity: '' - OriginAccessControlId: !GetAtt AppCloudfrontS3OAC.Id + OriginAccessControlId: !GetAtt AppCloudfrontS3OAC.Id - Id: S3DocsOrigin DomainName: !GetAtt AppDocsS3Bucket.RegionalDomainName S3OriginConfig: OriginAccessIdentity: '' - OriginAccessControlId: !GetAtt AppCloudfrontS3OAC.Id + OriginAccessControlId: !GetAtt AppCloudfrontS3OAC.Id - Id: LambdaOrigin DomainName: !Select [0, !Split ['/', !Select [1, !Split ['https://', !GetAtt AppLambdaUrl.FunctionUrl]]]] CustomOriginConfig: From 4896cd805238d1a76e27a9d9d1c77256c52350a3 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sun, 6 Jul 2025 12:15:27 -0500 Subject: [PATCH 07/14] use same s3 bucket for frontend and swagger --- Makefile | 2 -- cloudformation/main.yml | 30 ++---------------------------- src/api/createSwagger.ts | 2 +- 3 files changed, 3 insertions(+), 31 deletions(-) diff --git a/Makefile b/Makefile index afe83c62..f7211783 100644 --- a/Makefile +++ b/Makefile @@ -73,8 +73,6 @@ local: postdeploy: @echo "Syncing S3 UI bucket..." aws s3 sync $(dist_ui_directory_root) s3://$(ui_s3_bucket)/ --delete - @echo "Syncing S3 Docs bucket..." - aws s3 sync $(dist_docs_directory_root) s3://$(docs_s3_bucket)/ --delete make invalidate_cloudfront deploy_prod: check_account_prod diff --git a/cloudformation/main.yml b/cloudformation/main.yml index 68dbebc5..99601ed9 100644 --- a/cloudformation/main.yml +++ b/cloudformation/main.yml @@ -609,11 +609,6 @@ Resources: Properties: BucketName: !Sub ${S3BucketPrefix}-ui - AppDocsS3Bucket: - Type: AWS::S3::Bucket - Properties: - BucketName: !Sub ${S3BucketPrefix}-docs - AppCloudfrontS3OAC: Type: AWS::CloudFront::OriginAccessControl Properties: @@ -634,11 +629,6 @@ Resources: S3OriginConfig: OriginAccessIdentity: '' OriginAccessControlId: !GetAtt AppCloudfrontS3OAC.Id - - Id: S3DocsOrigin - DomainName: !GetAtt AppDocsS3Bucket.RegionalDomainName - S3OriginConfig: - OriginAccessIdentity: '' - OriginAccessControlId: !GetAtt AppCloudfrontS3OAC.Id - Id: LambdaOrigin DomainName: !Select [0, !Split ['/', !Select [1, !Split ['https://', !GetAtt AppLambdaUrl.FunctionUrl]]]] CustomOriginConfig: @@ -711,7 +701,8 @@ Resources: Compress: true - PathPattern: "/api/documentation*" Compress: true - TargetOriginId: S3DocsOrigin + OriginPath: "/swagger" + TargetOriginId: S3WebsiteOrigin ViewerProtocolPolicy: redirect-to-https AllowedMethods: - GET @@ -770,23 +761,6 @@ Resources: StringEquals: AWS:SourceArn: !Sub "arn:aws:cloudfront::${AWS::AccountId}:distribution/${AppFrontendCloudfrontDistribution}" - AppDocsS3BucketPolicy: - Type: AWS::S3::BucketPolicy - Properties: - Bucket: !Ref AppDocsS3Bucket - PolicyDocument: - Version: "2012-10-17" - Statement: - - Effect: Allow - Principal: - Service: cloudfront.amazonaws.com - Action: s3:GetObject - Resource: !Sub "${AppDocsS3Bucket.Arn}/*" - Condition: - StringEquals: - AWS:SourceArn: !Sub "arn:aws:cloudfront::${AWS::AccountId}:distribution/${AppFrontendCloudfrontDistribution}" - - CloudfrontNoCachePolicy: Type: AWS::CloudFront::CachePolicy Properties: diff --git a/src/api/createSwagger.ts b/src/api/createSwagger.ts index 7f538c87..317b0de6 100644 --- a/src/api/createSwagger.ts +++ b/src/api/createSwagger.ts @@ -37,7 +37,7 @@ async function createSwaggerFiles() { console.log("App is ready. Generating specs..."); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); - const outputDir = path.resolve(__dirname, "..", "..", "dist", "swagger"); + const outputDir = path.resolve(__dirname, "..", "..", "dist_ui", "swagger"); await mkdir(outputDir, { recursive: true }); const jsonSpec = JSON.stringify(app.swagger(), null, 2); const yamlSpec = app.swagger({ yaml: true }); From 61d01eac46f82c1e4dd0aa46ae8ac688e2ed1477 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sun, 6 Jul 2025 12:17:01 -0500 Subject: [PATCH 08/14] fix cfn --- cloudformation/main.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/cloudformation/main.yml b/cloudformation/main.yml index 99601ed9..55af377a 100644 --- a/cloudformation/main.yml +++ b/cloudformation/main.yml @@ -629,6 +629,12 @@ Resources: S3OriginConfig: OriginAccessIdentity: '' OriginAccessControlId: !GetAtt AppCloudfrontS3OAC.Id + - Id: S3DocsOrigin + DomainName: !GetAtt AppFrontendS3Bucket.RegionalDomainName + S3OriginConfig: + OriginAccessIdentity: '' + OriginAccessControlId: !GetAtt AppCloudfrontS3OAC.Id + OriginPath: "/swagger" - Id: LambdaOrigin DomainName: !Select [0, !Split ['/', !Select [1, !Split ['https://', !GetAtt AppLambdaUrl.FunctionUrl]]]] CustomOriginConfig: @@ -701,8 +707,7 @@ Resources: Compress: true - PathPattern: "/api/documentation*" Compress: true - OriginPath: "/swagger" - TargetOriginId: S3WebsiteOrigin + TargetOriginId: S3DocsOrigin ViewerProtocolPolicy: redirect-to-https AllowedMethods: - GET From 5d1b31841f49f905513aa5916973b8cb4d54428b Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sun, 6 Jul 2025 12:31:40 -0500 Subject: [PATCH 09/14] do the redirect at api server level, use directly from s3 --- cloudformation/main.yml | 24 ------------------------ src/api/createSwagger.ts | 2 +- src/api/index.ts | 15 ++++++++++++++- 3 files changed, 15 insertions(+), 26 deletions(-) diff --git a/cloudformation/main.yml b/cloudformation/main.yml index 55af377a..81002091 100644 --- a/cloudformation/main.yml +++ b/cloudformation/main.yml @@ -629,12 +629,6 @@ Resources: S3OriginConfig: OriginAccessIdentity: '' OriginAccessControlId: !GetAtt AppCloudfrontS3OAC.Id - - Id: S3DocsOrigin - DomainName: !GetAtt AppFrontendS3Bucket.RegionalDomainName - S3OriginConfig: - OriginAccessIdentity: '' - OriginAccessControlId: !GetAtt AppCloudfrontS3OAC.Id - OriginPath: "/swagger" - Id: LambdaOrigin DomainName: !Select [0, !Split ['/', !Select [1, !Split ['https://', !GetAtt AppLambdaUrl.FunctionUrl]]]] CustomOriginConfig: @@ -705,24 +699,6 @@ Resources: CachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6" OriginRequestPolicyId: b689b0a8-53d0-40ab-baf2-68738e2966ac Compress: true - - PathPattern: "/api/documentation*" - Compress: true - TargetOriginId: S3DocsOrigin - ViewerProtocolPolicy: redirect-to-https - AllowedMethods: - - GET - - HEAD - CachedMethods: - - GET - - HEAD - ForwardedValues: - QueryString: true - Cookies: - Forward: none - CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 # caching-optimized - # LambdaFunctionAssociations: - # - EventType: origin-request - # LambdaFunctionARN: !Ref AppFrontendEdgeLambdaVersion - PathPattern: "/api/*" TargetOriginId: LambdaOrigin ViewerProtocolPolicy: redirect-to-https diff --git a/src/api/createSwagger.ts b/src/api/createSwagger.ts index 317b0de6..70515d92 100644 --- a/src/api/createSwagger.ts +++ b/src/api/createSwagger.ts @@ -19,7 +19,7 @@ const html = `