Phase 19: Local Auth (No-OIDC Mode) #23

Merged
luckberg merged 78 commits from gsd/phase-19-local-auth-no-oidc-mode into main 2026-06-18 06:25:00 -04:00
2 changed files with 17 additions and 7 deletions
Showing only changes of commit 1688f229e0 - Show all commits
+9 -5
View File
@@ -219,11 +219,15 @@ export async function fetchAdminResetPassword(
/** /**
* POST /api/me/link-oidc — initiate the OIDC-link flow for the current local user (Surface 13). * POST /api/me/link-oidc — initiate the OIDC-link flow for the current local user (Surface 13).
* *
* The server returns a redirect URL to begin the OIDC authorization-code flow with a * The server returns the OIDC authorization endpoint URL (with a signed `state` parameter
* state parameter encoding the linkUserId claim. The caller should follow the redirect * encoding the linkUserId claim) to begin the authorization-code flow. The caller should
* via top-level navigation (window.location.href = result.redirectUrl). * follow it via top-level navigation (window.location.href = authorizationUrl) when present.
*
* authorizationUrl is null when OIDC is not configured in env (the server cannot build the
* URL); callers MUST handle that case and surface an error instead of navigating to null.
* The server contract is { signedState, authorizationUrl } (see apps/api/src/routes/me.ts).
*/ */
export async function fetchLinkOidc(): Promise<{ redirectUrl: string }> { export async function fetchLinkOidc(): Promise<{ authorizationUrl: string | null }> {
const res = await fetch('/api/me/link-oidc', { const res = await fetch('/api/me/link-oidc', {
method: 'POST', method: 'POST',
credentials: 'include', credentials: 'include',
@@ -234,7 +238,7 @@ export async function fetchLinkOidc(): Promise<{ redirectUrl: string }> {
if (!res.ok) { if (!res.ok) {
throw new Error(`fetchLinkOidc failed: ${res.status}`); throw new Error(`fetchLinkOidc failed: ${res.status}`);
} }
return res.json() as Promise<{ redirectUrl: string }>; return res.json() as Promise<{ signedState: string; authorizationUrl: string | null }>;
} }
// ── /api/me ──────────────────────────────────────────────────────────────── // ── /api/me ────────────────────────────────────────────────────────────────
+8 -2
View File
@@ -859,9 +859,15 @@ function LinkOidcSheet({ isOpen, onClose }: LinkOidcSheetProps) {
const linkMutation = useMutation({ const linkMutation = useMutation({
mutationFn: fetchLinkOidc, mutationFn: fetchLinkOidc,
onSuccess: (data) => { onSuccess: (data) => {
// Close the sheet and initiate OIDC link flow // authorizationUrl is null when OIDC is not configured in env (server could not
// build the URL). Do NOT navigate to null — surface an error and keep the sheet open.
if (!data.authorizationUrl) {
setError('Something went wrong. Please try again.');
return;
}
// Close the sheet and initiate the OIDC link flow via top-level navigation.
onClose(); onClose();
window.location.href = data.redirectUrl; window.location.href = data.authorizationUrl;
}, },
onError: () => { onError: () => {
setError('Something went wrong. Please try again.'); setError('Something went wrong. Please try again.');