feat(auth): add generic OIDC login with mandatory groups

This commit is contained in:
2026-08-17 05:44:10 +02:00
parent 202822f3ba
commit 4fe51cbeb1
20 changed files with 1144 additions and 40 deletions
+20 -4
View File
@@ -80,6 +80,8 @@ export function AppShell() {
} as CSSProperties;
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
const activeSessionIdRef = useRef<string | null>(null);
const activeSessionEpochRef = useRef(0);
const newSessionOperationRef = useRef<{ target: string | null; epoch: number } | null>(null);
const resumeInvocationRef = useRef(0);
const latestResumeIntentRef = useRef<{ token: number; id: string } | null>(null);
const resumeInFlightRef = useRef(new Map<string, {
@@ -165,6 +167,7 @@ export function AppShell() {
function selectActiveSession(id: string | null) {
// Keep async Resume completions synchronized before React commits the state update.
if (activeSessionIdRef.current !== id) activeSessionEpochRef.current += 1;
activeSessionIdRef.current = id;
setActiveSessionId(id);
}
@@ -505,6 +508,7 @@ export function AppShell() {
function startNewSession() {
invalidateResumeIntent();
newSessionOperationRef.current = null;
resetSession();
// Starting a new question closes any open session detail panel: the reader is
// moving away from that session, so its left-hand box must not linger.
@@ -519,11 +523,18 @@ export function AppShell() {
}
function beginSessionCreation() {
newSessionOperationRef.current = {
target: activeSessionIdRef.current,
epoch: activeSessionEpochRef.current,
};
setAwaitingQuestion(false);
setCreatingSession(true);
}
function finishSessionCreation(id: string) {
const operation = newSessionOperationRef.current;
newSessionOperationRef.current = null;
if (!operation || operation.target !== activeSessionIdRef.current || operation.epoch !== activeSessionEpochRef.current) return;
// React batches these updates, preserving the provisional session view
// while useSessionStream opens the durable session's SSE channel.
selectActiveSession(id);
@@ -533,21 +544,25 @@ export function AppShell() {
}
function failSessionCreation(message?: string) {
const operation = newSessionOperationRef.current;
newSessionOperationRef.current = null;
if (!operation || operation.target !== activeSessionIdRef.current || operation.epoch !== activeSessionEpochRef.current) return;
setCreatingSession(false);
resetSession();
toast.error(message ?? "Failed to create session. Your question is ready to retry.");
}
async function stopSession() {
if (!activeSessionId) return;
const id = activeSessionId;
const id = activeSessionIdRef.current;
if (!id) return;
const guard = captureAuthOperation({ sessionId: id, disposalEpoch: operationEpochRef.current });
if (!guard) return;
invalidateResumeIntent();
try {
await closeSession(id);
} finally {
if (!isAuthOperationCurrent(guard, { sessionId: id, disposalEpoch: operationEpochRef.current })) return;
if (!isAuthOperationCurrent(guard, { sessionId: id, disposalEpoch: operationEpochRef.current })
|| activeSessionIdRef.current !== id) return;
resetSession();
selectActiveSession(null);
setAwaitingQuestion(false);
@@ -643,7 +658,7 @@ export function AppShell() {
</header>
)}
<CentralStatus working={working} />
{activeSessionId && <WidgetHost sessionId={activeSessionId} />}
{activeSessionId && <WidgetHost key={`widget:${activeSessionId}:${activeSessionEpochRef.current}`} sessionId={activeSessionId} />}
{finalized && !agentActive && (
<div className="rounded-2xl border border-border/80 bg-card p-5 text-center shadow-md">
<p className="text-sm text-muted-foreground">
@@ -668,6 +683,7 @@ export function AppShell() {
<div className="rounded-2xl border border-border/80 bg-card shadow-md transition-colors focus-within:border-primary/50 focus-within:ring-3 focus-within:ring-ring/15">
<div className="px-2.5 py-2">
<SteerInput
key={`steer:${activeSessionId ?? "new"}:${activeSessionEpochRef.current}`}
sessionId={activeSessionId}
onSessionCreating={beginSessionCreation}
onSessionCreated={finishSessionCreation}