mirror of
https://github.com/Sea-Haven-Industries/proposal-system.git
synced 2026-09-30 06:33:13 +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)
|
var allowedRedirectUris = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
"https://proposals.seahaven.com/callback",
|
"https://proposals.seahaven.com/callback",
|
||||||
|
"https://d2yevct5e5uuz5.cloudfront.net/callback",
|
||||||
};
|
};
|
||||||
if (_config.GetValue<bool>("Auth:DevMode"))
|
if (_config.GetValue<bool>("Auth:DevMode"))
|
||||||
allowedRedirectUris.Add("http://localhost:5173/callback");
|
allowedRedirectUris.Add("http://localhost:5173/callback");
|
||||||
|
|
@ -136,7 +137,7 @@ public class AuthController : ControllerBase
|
||||||
user.Email,
|
user.Email,
|
||||||
user.DisplayName,
|
user.DisplayName,
|
||||||
user.Role.ToString(),
|
user.Role.ToString(),
|
||||||
tokenResponse.AccessToken
|
tokenResponse.IdToken
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,7 @@ builder.Services.AddCors(options =>
|
||||||
{
|
{
|
||||||
options.AddDefaultPolicy(policy =>
|
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())
|
if (builder.Environment.IsDevelopment())
|
||||||
origins.Add("http://localhost:5173");
|
origins.Add("http://localhost:5173");
|
||||||
policy.WithOrigins(origins.ToArray())
|
policy.WithOrigins(origins.ToArray())
|
||||||
|
|
@ -221,11 +221,8 @@ var app = builder.Build();
|
||||||
|
|
||||||
app.UseMiddleware<GlobalExceptionHandler>();
|
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.UseCors();
|
||||||
app.UseMiddleware<InternalApiKeyMiddleware>();
|
app.UseMiddleware<InternalApiKeyMiddleware>();
|
||||||
|
|
@ -243,4 +240,10 @@ app.Use(async (context, next) =>
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
app.MapHealthChecks("/api/health");
|
app.MapHealthChecks("/api/health");
|
||||||
|
|
||||||
|
using (var scope = app.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<ProposalDbContext>();
|
||||||
|
db.Database.Migrate();
|
||||||
|
}
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
|
||||||
|
|
@ -307,6 +307,7 @@ export class ComputeStack extends cdk.Stack {
|
||||||
corsPreflight: {
|
corsPreflight: {
|
||||||
allowOrigins: [
|
allowOrigins: [
|
||||||
'https://proposals.seahaven.com',
|
'https://proposals.seahaven.com',
|
||||||
|
'https://d2yevct5e5uuz5.cloudfront.net',
|
||||||
'http://localhost:5173',
|
'http://localhost:5173',
|
||||||
],
|
],
|
||||||
allowMethods: [
|
allowMethods: [
|
||||||
|
|
@ -364,13 +365,31 @@ export class ComputeStack extends cdk.Stack {
|
||||||
|
|
||||||
httpApi.addRoutes({
|
httpApi.addRoutes({
|
||||||
path: '/api/auth/{proxy+}',
|
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,
|
integration: apiIntegration,
|
||||||
});
|
});
|
||||||
|
|
||||||
httpApi.addRoutes({
|
httpApi.addRoutes({
|
||||||
path: '/{proxy+}',
|
path: '/{proxy+}',
|
||||||
methods: [apigatewayv2.HttpMethod.ANY],
|
methods: [
|
||||||
|
apigatewayv2.HttpMethod.GET,
|
||||||
|
apigatewayv2.HttpMethod.POST,
|
||||||
|
apigatewayv2.HttpMethod.PUT,
|
||||||
|
apigatewayv2.HttpMethod.DELETE,
|
||||||
|
apigatewayv2.HttpMethod.PATCH,
|
||||||
|
],
|
||||||
integration: apiIntegration,
|
integration: apiIntegration,
|
||||||
authorizer: jwtAuthorizer,
|
authorizer: jwtAuthorizer,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { useParams, useNavigate, useBlocker } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { useQuery, useMutation } from '@tanstack/react-query';
|
import { useQuery, useMutation } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
|
|
@ -109,13 +109,17 @@ export default function AdminWorkspace() {
|
||||||
return () => window.removeEventListener('beforeunload', handler);
|
return () => window.removeEventListener('beforeunload', handler);
|
||||||
}, [dirty]);
|
}, [dirty]);
|
||||||
|
|
||||||
const blocker = useBlocker(
|
const [navBlocked, setNavBlocked] = useState(false);
|
||||||
useCallback(
|
const pendingNavRef = useRef<string | null>(null);
|
||||||
({ currentLocation, nextLocation }: { currentLocation: { pathname: string }; nextLocation: { pathname: string } }) =>
|
|
||||||
dirty && currentLocation.pathname !== nextLocation.pathname,
|
const guardedNavigate = useCallback((to: string) => {
|
||||||
[dirty],
|
if (dirty) {
|
||||||
),
|
pendingNavRef.current = to;
|
||||||
);
|
setNavBlocked(true);
|
||||||
|
} else {
|
||||||
|
navigate(to);
|
||||||
|
}
|
||||||
|
}, [dirty, navigate]);
|
||||||
|
|
||||||
const saveMutation = useMutation({
|
const saveMutation = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
|
|
@ -258,7 +262,7 @@ export default function AdminWorkspace() {
|
||||||
>
|
>
|
||||||
Failed to load proposal{proposalError instanceof Error ? `: ${proposalError.message}` : '.'}
|
Failed to load proposal{proposalError instanceof Error ? `: ${proposalError.message}` : '.'}
|
||||||
</Alert>
|
</Alert>
|
||||||
<Button startIcon={<ArrowBackIcon />} onClick={() => navigate('/admin')} sx={{ mt: 2 }}>
|
<Button startIcon={<ArrowBackIcon />} onClick={() => guardedNavigate('/admin')} sx={{ mt: 2 }}>
|
||||||
Back to Dashboard
|
Back to Dashboard
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -269,7 +273,7 @@ export default function AdminWorkspace() {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Typography color="error">Proposal not found</Typography>
|
<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
|
Back to Dashboard
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -305,7 +309,7 @@ export default function AdminWorkspace() {
|
||||||
<Link
|
<Link
|
||||||
component="button"
|
component="button"
|
||||||
underline="hover"
|
underline="hover"
|
||||||
onClick={() => navigate('/admin')}
|
onClick={() => guardedNavigate('/admin')}
|
||||||
sx={{ fontSize: 14, color: '#0B5A73', cursor: 'pointer' }}
|
sx={{ fontSize: 14, color: '#0B5A73', cursor: 'pointer' }}
|
||||||
>
|
>
|
||||||
Admin Queue
|
Admin Queue
|
||||||
|
|
@ -550,14 +554,19 @@ export default function AdminWorkspace() {
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Unsaved Changes Navigation Guard */}
|
{/* Unsaved Changes Navigation Guard */}
|
||||||
<Dialog open={blocker.state === 'blocked'} onClose={() => blocker.reset?.()}>
|
<Dialog open={navBlocked} onClose={() => setNavBlocked(false)}>
|
||||||
<DialogTitle>Unsaved Changes</DialogTitle>
|
<DialogTitle>Unsaved Changes</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Typography>You have unsaved changes. Are you sure you want to leave?</Typography>
|
<Typography>You have unsaved changes. Are you sure you want to leave?</Typography>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={() => blocker.reset?.()}>Stay</Button>
|
<Button onClick={() => setNavBlocked(false)}>Stay</Button>
|
||||||
<Button variant="contained" color="error" onClick={() => blocker.proceed?.()}>
|
<Button variant="contained" color="error" onClick={() => {
|
||||||
|
setNavBlocked(false);
|
||||||
|
const dest = pendingNavRef.current ?? '/admin';
|
||||||
|
pendingNavRef.current = null;
|
||||||
|
navigate(dest);
|
||||||
|
}}>
|
||||||
Discard & Leave
|
Discard & Leave
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ function buildLoginUrl(): string {
|
||||||
response_type: 'code',
|
response_type: 'code',
|
||||||
scope: 'openid email profile',
|
scope: 'openid email profile',
|
||||||
redirect_uri: REDIRECT_URI,
|
redirect_uri: REDIRECT_URI,
|
||||||
identity_provider: 'Google',
|
|
||||||
});
|
});
|
||||||
return `https://${COGNITO_DOMAIN}/oauth2/authorize?${params.toString()}`;
|
return `https://${COGNITO_DOMAIN}/oauth2/authorize?${params.toString()}`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue