From ae3ad9d82390ef01ee1d70aaed6cad3dda868c3f Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Wed, 27 May 2026 19:20:29 -0400 Subject: [PATCH] fix: CORS, JWT auth, useBlocker crash, and auto-migration for production deploy - Add CloudFront origin to API Gateway CORS preflight and .NET CORS policy - Replace HttpMethod.ANY with explicit methods so OPTIONS preflight doesn't hit the JWT authorizer (was causing 403 on all API calls) - Return Cognito ID token instead of access token from auth callback (access tokens lack the aud claim required by API Gateway JWT authorizer) - Add CloudFront callback URI to allowed redirect list - Remove identity_provider=Google from login URL to show Cognito hosted UI - Replace useBlocker (requires data router) with state-based navigation guard to fix crash on AdminWorkspace with BrowserRouter - Add auto-migration on Lambda cold start - Enable Swagger in production --- .../Controllers/AuthController.cs | 3 +- api/src/ProposalSystem.Api/Program.cs | 15 ++++--- infra/lib/compute-stack.ts | 23 ++++++++++- .../pages/admin/workspace/AdminWorkspace.tsx | 39 ++++++++++++------- web/src/pages/auth/LoginPage.tsx | 1 - 5 files changed, 56 insertions(+), 25 deletions(-) diff --git a/api/src/ProposalSystem.Api/Controllers/AuthController.cs b/api/src/ProposalSystem.Api/Controllers/AuthController.cs index 6f48aa4..afdd9b8 100644 --- a/api/src/ProposalSystem.Api/Controllers/AuthController.cs +++ b/api/src/ProposalSystem.Api/Controllers/AuthController.cs @@ -37,6 +37,7 @@ public class AuthController : ControllerBase var allowedRedirectUris = new HashSet(StringComparer.OrdinalIgnoreCase) { "https://proposals.seahaven.com/callback", + "https://d2yevct5e5uuz5.cloudfront.net/callback", }; if (_config.GetValue("Auth:DevMode")) allowedRedirectUris.Add("http://localhost:5173/callback"); @@ -136,7 +137,7 @@ public class AuthController : ControllerBase user.Email, user.DisplayName, user.Role.ToString(), - tokenResponse.AccessToken + tokenResponse.IdToken )); } diff --git a/api/src/ProposalSystem.Api/Program.cs b/api/src/ProposalSystem.Api/Program.cs index 535099f..3db61dc 100644 --- a/api/src/ProposalSystem.Api/Program.cs +++ b/api/src/ProposalSystem.Api/Program.cs @@ -205,7 +205,7 @@ builder.Services.AddCors(options => { options.AddDefaultPolicy(policy => { - var origins = new List { "https://proposals.seahaven.com" }; + var origins = new List { "https://proposals.seahaven.com", "https://d2yevct5e5uuz5.cloudfront.net" }; if (builder.Environment.IsDevelopment()) origins.Add("http://localhost:5173"); policy.WithOrigins(origins.ToArray()) @@ -221,11 +221,8 @@ var app = builder.Build(); app.UseMiddleware(); -if (!app.Environment.IsProduction()) -{ - app.UseSwagger(); - app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Proposal System API v1")); -} +app.UseSwagger(); +app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Proposal System API v1")); app.UseCors(); app.UseMiddleware(); @@ -243,4 +240,10 @@ app.Use(async (context, next) => app.MapControllers(); app.MapHealthChecks("/api/health"); +using (var scope = app.Services.CreateScope()) +{ + var db = scope.ServiceProvider.GetRequiredService(); + db.Database.Migrate(); +} + app.Run(); diff --git a/infra/lib/compute-stack.ts b/infra/lib/compute-stack.ts index 900636c..9c33a09 100644 --- a/infra/lib/compute-stack.ts +++ b/infra/lib/compute-stack.ts @@ -307,6 +307,7 @@ export class ComputeStack extends cdk.Stack { corsPreflight: { allowOrigins: [ 'https://proposals.seahaven.com', + 'https://d2yevct5e5uuz5.cloudfront.net', 'http://localhost:5173', ], allowMethods: [ @@ -364,13 +365,31 @@ export class ComputeStack extends cdk.Stack { httpApi.addRoutes({ path: '/api/auth/{proxy+}', - methods: [apigatewayv2.HttpMethod.POST], + methods: [apigatewayv2.HttpMethod.POST, apigatewayv2.HttpMethod.OPTIONS], + integration: apiIntegration, + }); + + httpApi.addRoutes({ + path: '/swagger/{proxy+}', + methods: [apigatewayv2.HttpMethod.GET], + integration: apiIntegration, + }); + + httpApi.addRoutes({ + path: '/swagger', + methods: [apigatewayv2.HttpMethod.GET], integration: apiIntegration, }); httpApi.addRoutes({ path: '/{proxy+}', - methods: [apigatewayv2.HttpMethod.ANY], + methods: [ + apigatewayv2.HttpMethod.GET, + apigatewayv2.HttpMethod.POST, + apigatewayv2.HttpMethod.PUT, + apigatewayv2.HttpMethod.DELETE, + apigatewayv2.HttpMethod.PATCH, + ], integration: apiIntegration, authorizer: jwtAuthorizer, }); diff --git a/web/src/pages/admin/workspace/AdminWorkspace.tsx b/web/src/pages/admin/workspace/AdminWorkspace.tsx index 3d6d8bf..20d75f9 100644 --- a/web/src/pages/admin/workspace/AdminWorkspace.tsx +++ b/web/src/pages/admin/workspace/AdminWorkspace.tsx @@ -1,5 +1,5 @@ -import { useState, useEffect, useCallback } from 'react'; -import { useParams, useNavigate, useBlocker } from 'react-router-dom'; +import { useState, useEffect, useCallback, useRef } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; import { useQuery, useMutation } from '@tanstack/react-query'; import { Box, @@ -109,13 +109,17 @@ export default function AdminWorkspace() { return () => window.removeEventListener('beforeunload', handler); }, [dirty]); - const blocker = useBlocker( - useCallback( - ({ currentLocation, nextLocation }: { currentLocation: { pathname: string }; nextLocation: { pathname: string } }) => - dirty && currentLocation.pathname !== nextLocation.pathname, - [dirty], - ), - ); + const [navBlocked, setNavBlocked] = useState(false); + const pendingNavRef = useRef(null); + + const guardedNavigate = useCallback((to: string) => { + if (dirty) { + pendingNavRef.current = to; + setNavBlocked(true); + } else { + navigate(to); + } + }, [dirty, navigate]); const saveMutation = useMutation({ mutationFn: async () => { @@ -258,7 +262,7 @@ export default function AdminWorkspace() { > Failed to load proposal{proposalError instanceof Error ? `: ${proposalError.message}` : '.'} - @@ -269,7 +273,7 @@ export default function AdminWorkspace() { return ( Proposal not found - @@ -305,7 +309,7 @@ export default function AdminWorkspace() { navigate('/admin')} + onClick={() => guardedNavigate('/admin')} sx={{ fontSize: 14, color: '#0B5A73', cursor: 'pointer' }} > Admin Queue @@ -550,14 +554,19 @@ export default function AdminWorkspace() { {/* Unsaved Changes Navigation Guard */} - blocker.reset?.()}> + setNavBlocked(false)}> Unsaved Changes You have unsaved changes. Are you sure you want to leave? - - + diff --git a/web/src/pages/auth/LoginPage.tsx b/web/src/pages/auth/LoginPage.tsx index 7ed131a..20cc749 100644 --- a/web/src/pages/auth/LoginPage.tsx +++ b/web/src/pages/auth/LoginPage.tsx @@ -20,7 +20,6 @@ function buildLoginUrl(): string { response_type: 'code', scope: 'openid email profile', redirect_uri: REDIRECT_URI, - identity_provider: 'Google', }); return `https://${COGNITO_DOMAIN}/oauth2/authorize?${params.toString()}`; }