mirror of
https://github.com/Sea-Haven-Industries/proposal-system.git
synced 2026-09-30 05:23:14 +00:00
fix: CORS, JWT auth, useBlocker crash, and auto-migration for production deploy
Some checks failed
Deploy / Deploy to AWS (push) Has been cancelled
Some checks failed
Deploy / Deploy to AWS (push) Has been cancelled
- 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
This commit is contained in:
parent
da783d48cd
commit
ae3ad9d823
5 changed files with 56 additions and 25 deletions
|
|
@ -37,6 +37,7 @@ public class AuthController : ControllerBase
|
|||
var allowedRedirectUris = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"https://proposals.seahaven.com/callback",
|
||||
"https://d2yevct5e5uuz5.cloudfront.net/callback",
|
||||
};
|
||||
if (_config.GetValue<bool>("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
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -205,7 +205,7 @@ builder.Services.AddCors(options =>
|
|||
{
|
||||
options.AddDefaultPolicy(policy =>
|
||||
{
|
||||
var origins = new List<string> { "https://proposals.seahaven.com" };
|
||||
var origins = new List<string> { "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<GlobalExceptionHandler>();
|
||||
|
||||
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<InternalApiKeyMiddleware>();
|
||||
|
|
@ -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<ProposalDbContext>();
|
||||
db.Database.Migrate();
|
||||
}
|
||||
|
||||
app.Run();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<string | null>(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}` : '.'}
|
||||
</Alert>
|
||||
<Button startIcon={<ArrowBackIcon />} onClick={() => navigate('/admin')} sx={{ mt: 2 }}>
|
||||
<Button startIcon={<ArrowBackIcon />} onClick={() => guardedNavigate('/admin')} sx={{ mt: 2 }}>
|
||||
Back to Dashboard
|
||||
</Button>
|
||||
</Box>
|
||||
|
|
@ -269,7 +273,7 @@ export default function AdminWorkspace() {
|
|||
return (
|
||||
<Box>
|
||||
<Typography color="error">Proposal not found</Typography>
|
||||
<Button startIcon={<ArrowBackIcon />} onClick={() => navigate('/admin')} sx={{ mt: 2 }}>
|
||||
<Button startIcon={<ArrowBackIcon />} onClick={() => guardedNavigate('/admin')} sx={{ mt: 2 }}>
|
||||
Back to Dashboard
|
||||
</Button>
|
||||
</Box>
|
||||
|
|
@ -305,7 +309,7 @@ export default function AdminWorkspace() {
|
|||
<Link
|
||||
component="button"
|
||||
underline="hover"
|
||||
onClick={() => navigate('/admin')}
|
||||
onClick={() => guardedNavigate('/admin')}
|
||||
sx={{ fontSize: 14, color: '#0B5A73', cursor: 'pointer' }}
|
||||
>
|
||||
Admin Queue
|
||||
|
|
@ -550,14 +554,19 @@ export default function AdminWorkspace() {
|
|||
</Card>
|
||||
|
||||
{/* Unsaved Changes Navigation Guard */}
|
||||
<Dialog open={blocker.state === 'blocked'} onClose={() => blocker.reset?.()}>
|
||||
<Dialog open={navBlocked} onClose={() => setNavBlocked(false)}>
|
||||
<DialogTitle>Unsaved Changes</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>You have unsaved changes. Are you sure you want to leave?</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => blocker.reset?.()}>Stay</Button>
|
||||
<Button variant="contained" color="error" onClick={() => blocker.proceed?.()}>
|
||||
<Button onClick={() => setNavBlocked(false)}>Stay</Button>
|
||||
<Button variant="contained" color="error" onClick={() => {
|
||||
setNavBlocked(false);
|
||||
const dest = pendingNavRef.current ?? '/admin';
|
||||
pendingNavRef.current = null;
|
||||
navigate(dest);
|
||||
}}>
|
||||
Discard & Leave
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
|
|
|||
|
|
@ -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()}`;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue