publishing: unfenced vault accessor for the amplify path
createPublishingCredentialAccessor is execution-fenced (Temporal contexts only) — the API-process amplify route threw EXECUTION_CREDENTIAL_FENCE. Use the established PgConnectionStore+vault accessor instead (same trust level as the archive amplify route's bot-token path).
This commit is contained in:
parent
6145281b3f
commit
96e5a63dd8
11 changed files with 1501 additions and 8 deletions
94
apps/web/src/app/oidc/consent/page.tsx
Normal file
94
apps/web/src/app/oidc/consent/page.tsx
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { Card } from "../../../components/ui/primitives";
|
||||
import { LoadingPanel } from "../../../components/brand/loading";
|
||||
import { api, ApiError, errorMessage } from "../../../lib/api-client";
|
||||
import { getSessionToken } from "../../../lib/session-token";
|
||||
|
||||
/**
|
||||
* /oidc/consent — the Hub half of files (oCIS) single sign-on.
|
||||
*
|
||||
* The API's GET /oidc/authorize validates oCIS's request and sends the browser
|
||||
* here, because only this origin holds the Hub session (a Bearer token in
|
||||
* localStorage — there is no session cookie the API could read). We relay the
|
||||
* authorize params to POST /oidc/authorize/consent with that session; the API
|
||||
* re-validates everything, issues a single-use code and answers with the oCIS
|
||||
* callback URL, which we navigate to. client_id=web is first-party, so there
|
||||
* is no consent screen: the user only ever sees a brief "Signing in…".
|
||||
*/
|
||||
|
||||
const AUTHORIZE_PARAMS = [
|
||||
"response_type", "client_id", "redirect_uri", "scope", "state", "nonce", "code_challenge", "code_challenge_method", "prompt",
|
||||
] as const;
|
||||
|
||||
type Phase = { kind: "working" } | { kind: "signin" } | { kind: "error"; message: string };
|
||||
|
||||
function authorizeParams(search: string): Record<string, string> {
|
||||
const query = new URLSearchParams(search);
|
||||
const params: Record<string, string> = {};
|
||||
for (const name of AUTHORIZE_PARAMS) {
|
||||
const value = query.get(name);
|
||||
if (value !== null) params[name] = value;
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
export default function OidcConsentPage(): React.ReactElement {
|
||||
const [phase, setPhase] = useState<Phase>({ kind: "working" });
|
||||
// Each run issues a single-use code; never mint twice (StrictMode double effects).
|
||||
const started = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (started.current) return;
|
||||
started.current = true;
|
||||
if (getSessionToken() === null) {
|
||||
setPhase({ kind: "signin" });
|
||||
return;
|
||||
}
|
||||
api
|
||||
.post<{ redirectTo: string }>("/oidc/authorize/consent", authorizeParams(window.location.search), { redirectOn401: false })
|
||||
.then(({ redirectTo }) => {
|
||||
const target = new URL(redirectTo);
|
||||
if (target.protocol !== "https:" && target.protocol !== "http:") throw new Error("Unexpected sign-in redirect");
|
||||
// replace(): Back must not return here and re-run a spent authorization.
|
||||
window.location.replace(target.toString());
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
if (err instanceof ApiError && err.status === 401) setPhase({ kind: "signin" });
|
||||
else setPhase({ kind: "error", message: errorMessage(err) });
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (phase.kind === "working") {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-bg">
|
||||
<LoadingPanel label="Signing in to files…" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full items-center justify-center bg-bg p-6">
|
||||
<Card className="w-[420px] max-w-full p-6 text-center">
|
||||
<h1 className="text-xl font-semibold tracking-tight">Files sign-in</h1>
|
||||
{phase.kind === "signin" ? (
|
||||
<>
|
||||
<p className="mt-2 text-xs text-text-faint">Sign in to the Hub first, then open Drive again.</p>
|
||||
<Link href="/login" className="mt-6 inline-block text-xs text-text underline">
|
||||
Sign in
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="mt-2 text-xs text-danger">{phase.message}</p>
|
||||
<Link href="/files/drive" className="mt-6 inline-block text-xs text-text-faint underline hover:text-text">
|
||||
Back to Drive
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue