supabase
This commit is contained in:
parent
f8570411d1
commit
51eeb30a92
@ -59,6 +59,7 @@ let VariablePlayground: any;
|
||||
let I18nPlayground: any;
|
||||
let PlaygroundChat: any;
|
||||
let PlaygroundVfs: any;
|
||||
let PlaygroundAuth: any;
|
||||
let GridSearch: any;
|
||||
let LocationDetail: any;
|
||||
let Tetris: any;
|
||||
@ -84,6 +85,7 @@ if (enablePlaygrounds) {
|
||||
I18nPlayground = React.lazy(() => import("./components/playground/I18nPlayground"));
|
||||
PlaygroundChat = React.lazy(() => import("./pages/PlaygroundChat"));
|
||||
PlaygroundVfs = React.lazy(() => import("./pages/PlaygroundVfs"));
|
||||
PlaygroundAuth = React.lazy(() => import("./playground/auth"));
|
||||
SupportChat = React.lazy(() => import("./pages/SupportChat"));
|
||||
}
|
||||
|
||||
@ -212,6 +214,7 @@ const AppWrapper = () => {
|
||||
<Route path="/playground/i18n" element={<React.Suspense fallback={<div>Loading...</div>}><I18nPlayground /></React.Suspense>} />
|
||||
<Route path="/playground/chat" element={<React.Suspense fallback={<div>Loading...</div>}><PlaygroundChat /></React.Suspense>} />
|
||||
<Route path="/playground/vfs" element={<React.Suspense fallback={<div>Loading...</div>}><PlaygroundVfs /></React.Suspense>} />
|
||||
<Route path="/playground/auth" element={<React.Suspense fallback={<div>Loading...</div>}><PlaygroundAuth /></React.Suspense>} />
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -275,7 +278,8 @@ const App = () => {
|
||||
authority: "https://auth.polymech.info",
|
||||
client_id: "367440527605432321",
|
||||
redirect_uri: window.location.origin + "/authz", // Where Zitadel sends the code back to
|
||||
post_logout_redirect_uri: window.location.origin,
|
||||
// Must appear in Zitadel app → Post Logout URIs (same allowlist pattern as redirect_uri; bare origin alone often fails).
|
||||
post_logout_redirect_uri: window.location.origin + "/authz",
|
||||
response_type: "code",
|
||||
scope: "openid profile email",
|
||||
loadUserInfo: true, // Specifically instruct the client to fetch /userinfo
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Shield, ArrowRight } from 'lucide-react';
|
||||
import { Shield, LogOut } from 'lucide-react';
|
||||
import { T, translate } from '@/i18n';
|
||||
import { useAuth } from 'react-oidc-context';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
@ -10,6 +10,7 @@ const AuthZ = () => {
|
||||
const auth = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [signingOut, setSigningOut] = useState(false);
|
||||
|
||||
// Monitor auth state changes and log them for debugging
|
||||
useEffect(() => {
|
||||
@ -21,15 +22,11 @@ const AuthZ = () => {
|
||||
});
|
||||
|
||||
if (auth.user) {
|
||||
// `profile.email` is the same claim the API reads from the Bearer JWT (`getUserCached` → `user.email`).
|
||||
console.log("🛡️ [AuthZ] Identity Token Profile:", auth.user.profile);
|
||||
console.log("🛡️ [AuthZ] Access Token:", auth.user.access_token);
|
||||
}
|
||||
|
||||
if (auth.isAuthenticated) {
|
||||
console.log(`🛡️ [AuthZ] Successfully logged in as: ${auth.user?.profile.email || 'Unknown'}. Redirecting to /...`);
|
||||
navigate('/');
|
||||
}
|
||||
}, [auth.isAuthenticated, auth.isLoading, auth.error, auth.user, navigate]);
|
||||
}, [auth.isAuthenticated, auth.isLoading, auth.error, auth.user]);
|
||||
|
||||
const handleZitadelLogin = async () => {
|
||||
try {
|
||||
@ -41,6 +38,52 @@ const AuthZ = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
setSigningOut(true);
|
||||
await auth.signoutRedirect();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setSigningOut(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (auth.isAuthenticated && auth.user && !auth.isLoading) {
|
||||
const email = auth.user.profile.email ?? auth.user.profile.preferred_username ?? '';
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-background via-secondary/20 to-accent/20 flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md glass-morphism border-white/20">
|
||||
<CardHeader className="text-center">
|
||||
<div className="mx-auto bg-primary/10 w-12 h-12 rounded-full justify-center items-center flex mb-4">
|
||||
<Shield className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<CardTitle className="text-2xl font-bold bg-gradient-primary bg-clip-text text-transparent">
|
||||
<T>Signed in</T>
|
||||
</CardTitle>
|
||||
<CardDescription className="break-all">
|
||||
{email || <T>Session active</T>}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<Button className="w-full" size="lg" onClick={() => navigate('/')}>
|
||||
<T>Continue to app</T>
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="outline"
|
||||
size="lg"
|
||||
onClick={() => void handleLogout()}
|
||||
disabled={signingOut}
|
||||
>
|
||||
<LogOut className="w-4 h-4 mr-2" />
|
||||
{signingOut ? translate('Signing out…') : translate('Sign out')}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-background via-secondary/20 to-accent/20 flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md glass-morphism border-white/20">
|
||||
|
||||
510
packages/ui/src/playground/auth.tsx
Normal file
510
packages/ui/src/playground/auth.tsx
Normal file
@ -0,0 +1,510 @@
|
||||
import { useCallback, useMemo, useState, type ReactNode } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useAuth } from 'react-oidc-context';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Shield, RefreshCw, ExternalLink, Copy, Check } from 'lucide-react';
|
||||
|
||||
type AuthzDebugResponse = {
|
||||
verified: boolean;
|
||||
sub?: string;
|
||||
email?: string | null;
|
||||
appAdmin?: boolean;
|
||||
resolvedUserId?: string | null;
|
||||
userSecrets?: unknown;
|
||||
dbUsersNext?: { poolOk: boolean; database: string; profileCount: number };
|
||||
error?: string;
|
||||
hint?: string;
|
||||
};
|
||||
|
||||
function looksLikeCompactJwt(t: string): boolean {
|
||||
const p = t.split('.');
|
||||
return p.length === 3 && p.every((x) => x.length > 0);
|
||||
}
|
||||
|
||||
function decodeJwtPayload(t: string): Record<string, unknown> | null {
|
||||
if (!looksLikeCompactJwt(t)) return null;
|
||||
try {
|
||||
const b64 = t.split('.')[1];
|
||||
if (!b64) return null;
|
||||
const json = atob(b64.replace(/-/g, '+').replace(/_/g, '/'));
|
||||
return JSON.parse(json) as Record<string, unknown>;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function tokenFingerprint(t: string | undefined): {
|
||||
present: boolean;
|
||||
length: number;
|
||||
dots: number;
|
||||
kind: 'missing' | 'opaque' | 'jwt';
|
||||
preview: string;
|
||||
} {
|
||||
if (!t) {
|
||||
return { present: false, length: 0, dots: 0, kind: 'missing', preview: '—' };
|
||||
}
|
||||
const dots = (t.match(/\./g) ?? []).length;
|
||||
const jwt = looksLikeCompactJwt(t);
|
||||
const preview =
|
||||
t.length <= 56 ? t : `${t.slice(0, 28)}…${t.slice(-20)}`;
|
||||
return {
|
||||
present: true,
|
||||
length: t.length,
|
||||
dots,
|
||||
kind: jwt ? 'jwt' : 'opaque',
|
||||
preview,
|
||||
};
|
||||
}
|
||||
|
||||
function formatExp(exp: unknown): string {
|
||||
if (typeof exp !== 'number') return '—';
|
||||
const ms = exp * 1000;
|
||||
const d = new Date(ms);
|
||||
const left = ms - Date.now();
|
||||
const rel =
|
||||
left > 0
|
||||
? `in ${Math.round(left / 60000)} min`
|
||||
: `expired ${Math.round(-left / 60000)} min ago`;
|
||||
return `${d.toISOString()} (${rel})`;
|
||||
}
|
||||
|
||||
/** Zitadel often returns an opaque `access_token`; JWKS verification needs a JWT — prefer `id_token` when AT is opaque. */
|
||||
function pickBearerJwtForApi(user: { access_token: string; id_token?: string } | undefined): {
|
||||
token: string;
|
||||
source: 'access_token' | 'id_token';
|
||||
} | null {
|
||||
if (!user?.access_token) return null;
|
||||
if (looksLikeCompactJwt(user.access_token)) {
|
||||
return { token: user.access_token, source: 'access_token' };
|
||||
}
|
||||
if (user.id_token && looksLikeCompactJwt(user.id_token)) {
|
||||
return { token: user.id_token, source: 'id_token' };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function Row({
|
||||
label,
|
||||
children,
|
||||
}: {
|
||||
label: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="grid grid-cols-[8.5rem_1fr] gap-x-2 gap-y-1 text-xs items-start">
|
||||
<span className="text-muted-foreground font-medium shrink-0">{label}</span>
|
||||
<div className="min-w-0 break-all font-mono">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dev playground: exercises the new Zitadel/JWKS path (`commons/zitadel.ts` + `DATABASE_URL_NEXT` pool).
|
||||
* Calls like `GET /api/admin/authz/debug` and `GET /api/admin/authz/experiment/users` are for
|
||||
* integration testing only — not production policy.
|
||||
*/
|
||||
export default function PlaygroundAuth() {
|
||||
const auth = useAuth();
|
||||
const base = import.meta.env.VITE_SERVER_IMAGE_API_URL || '';
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [last, setLast] = useState<AuthzDebugResponse | null>(null);
|
||||
const [fetchError, setFetchError] = useState<string | null>(null);
|
||||
const [lastHttp, setLastHttp] = useState<{ status: number; ok: boolean; url: string } | null>(null);
|
||||
const [lastDbNext, setLastDbNext] = useState<AuthzDebugResponse | null>(null);
|
||||
const [lastDbNextHttp, setLastDbNextHttp] = useState<{ status: number; ok: boolean; url: string } | null>(null);
|
||||
const [loadingDbNext, setLoadingDbNext] = useState(false);
|
||||
const [fetchErrorDbNext, setFetchErrorDbNext] = useState<string | null>(null);
|
||||
/** `/api/admin/authz/experiment/users` — admin-gated user list for new-auth experiments only */
|
||||
const [loadingExpUsers, setLoadingExpUsers] = useState(false);
|
||||
const [lastExpUsers, setLastExpUsers] = useState<unknown>(null);
|
||||
const [lastExpUsersHttp, setLastExpUsersHttp] = useState<{ status: number; ok: boolean; url: string } | null>(
|
||||
null,
|
||||
);
|
||||
const [fetchErrorExpUsers, setFetchErrorExpUsers] = useState<string | null>(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const jwtPick = auth.user ? pickBearerJwtForApi(auth.user) : null;
|
||||
|
||||
const atFp = useMemo(
|
||||
() => tokenFingerprint(auth.user?.access_token),
|
||||
[auth.user?.access_token],
|
||||
);
|
||||
const idFp = useMemo(() => tokenFingerprint(auth.user?.id_token), [auth.user?.id_token]);
|
||||
|
||||
const atPayload = useMemo(
|
||||
() => (auth.user?.access_token ? decodeJwtPayload(auth.user.access_token) : null),
|
||||
[auth.user?.access_token],
|
||||
);
|
||||
const idPayload = useMemo(
|
||||
() => (auth.user?.id_token ? decodeJwtPayload(auth.user.id_token) : null),
|
||||
[auth.user?.id_token],
|
||||
);
|
||||
const pickedPayload = useMemo(
|
||||
() => (jwtPick ? decodeJwtPayload(jwtPick.token) : null),
|
||||
[jwtPick],
|
||||
);
|
||||
|
||||
const oidcUser = auth.user as
|
||||
| (typeof auth.user & { scope?: string; session_state?: string; expires_at?: number })
|
||||
| undefined;
|
||||
|
||||
const runCheck = useCallback(async () => {
|
||||
const picked = pickBearerJwtForApi(auth.user ?? undefined);
|
||||
if (!picked) {
|
||||
setLastHttp(null);
|
||||
if (!auth.user?.access_token) {
|
||||
setLast({ verified: false, error: 'missing_token', hint: 'Sign in via /authz first.' });
|
||||
} else {
|
||||
setLast({
|
||||
verified: false,
|
||||
error: 'opaque_or_non_jwt_token',
|
||||
hint:
|
||||
'Zitadel returned an opaque access_token and no JWT id_token. Ensure scope includes openid and the OIDC app returns id_token.',
|
||||
});
|
||||
}
|
||||
setFetchError(null);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setFetchError(null);
|
||||
const url = `${base.replace(/\/$/, '')}/api/admin/authz/debug`;
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: { Authorization: `Bearer ${picked.token}` },
|
||||
});
|
||||
setLastHttp({ status: res.status, ok: res.ok, url });
|
||||
const body = (await res.json()) as AuthzDebugResponse;
|
||||
setLast(body);
|
||||
} catch (e) {
|
||||
setLastHttp(null);
|
||||
setFetchError(e instanceof Error ? e.message : String(e));
|
||||
setLast(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [auth.user, base]);
|
||||
|
||||
/** `GET /api/admin/authz/db-next` — same JWT + `authzDbNextPing()` via `pool` (`DATABASE_URL_NEXT`, `db-users-next.ts`). */
|
||||
const runDbNextCheck = useCallback(async () => {
|
||||
const picked = pickBearerJwtForApi(auth.user ?? undefined);
|
||||
if (!picked) {
|
||||
setLastDbNext({
|
||||
verified: false,
|
||||
error: 'opaque_or_non_jwt_token',
|
||||
hint: 'Need a JWT id_token (or JWT access_token).',
|
||||
});
|
||||
setFetchErrorDbNext(null);
|
||||
setLastDbNextHttp(null);
|
||||
return;
|
||||
}
|
||||
setLoadingDbNext(true);
|
||||
setFetchErrorDbNext(null);
|
||||
const url = `${base.replace(/\/$/, '')}/api/admin/authz/db-next`;
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: { Authorization: `Bearer ${picked.token}` },
|
||||
});
|
||||
setLastDbNextHttp({ status: res.status, ok: res.ok, url });
|
||||
const body = (await res.json()) as AuthzDebugResponse;
|
||||
setLastDbNext(body);
|
||||
} catch (e) {
|
||||
setLastDbNextHttp(null);
|
||||
setFetchErrorDbNext(e instanceof Error ? e.message : String(e));
|
||||
setLastDbNext(null);
|
||||
} finally {
|
||||
setLoadingDbNext(false);
|
||||
}
|
||||
}, [auth.user, base]);
|
||||
|
||||
/** Experiment route: Zitadel JWT + app admin, then list users (not production `/api/admin/users`). */
|
||||
const runExperimentUsers = useCallback(async () => {
|
||||
setFetchErrorExpUsers(null);
|
||||
setLastExpUsers(null);
|
||||
setLastExpUsersHttp(null);
|
||||
if (!base) {
|
||||
setFetchErrorExpUsers('VITE_SERVER_IMAGE_API_URL is not set');
|
||||
return;
|
||||
}
|
||||
const picked = pickBearerJwtForApi(auth.user);
|
||||
if (!picked) {
|
||||
setFetchErrorExpUsers('No JWT-shaped token (use id_token when access_token is opaque)');
|
||||
return;
|
||||
}
|
||||
setLoadingExpUsers(true);
|
||||
try {
|
||||
const url = `${base.replace(/\/$/, '')}/api/admin/authz/experiment/users`;
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: { Authorization: `Bearer ${picked.token}` },
|
||||
});
|
||||
setLastExpUsersHttp({ status: res.status, ok: res.ok, url });
|
||||
const body = (await res.json()) as unknown;
|
||||
setLastExpUsers(body);
|
||||
if (!res.ok) {
|
||||
setFetchErrorExpUsers(`HTTP ${res.status}`);
|
||||
}
|
||||
} catch (e) {
|
||||
setLastExpUsersHttp(null);
|
||||
setFetchErrorExpUsers(e instanceof Error ? e.message : String(e));
|
||||
setLastExpUsers(null);
|
||||
} finally {
|
||||
setLoadingExpUsers(false);
|
||||
}
|
||||
}, [auth.user, base]);
|
||||
|
||||
const copyPickedJwt = useCallback(async () => {
|
||||
if (!jwtPick?.token) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(jwtPick.token);
|
||||
setCopied(true);
|
||||
window.setTimeout(() => setCopied(false), 2000);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}, [jwtPick?.token]);
|
||||
|
||||
return (
|
||||
<div className="min-h-[60vh] flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-3xl">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="h-5 w-5 text-primary" />
|
||||
<CardTitle>Auth / Zitadel debug</CardTitle>
|
||||
</div>
|
||||
<CardDescription>
|
||||
Same flow as <code className="text-xs">pages/AuthZ.tsx</code>. The API verifies JWTs (JWKS); if Zitadel
|
||||
gives an <strong>opaque</strong> access token, this page sends the OIDC <strong>id_token</strong> instead.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link to="/authz" className="inline-flex items-center gap-1">
|
||||
Open AuthZ <ExternalLink className="h-3.5 w-3.5" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button size="sm" onClick={() => void runCheck()} disabled={loading || !base}>
|
||||
<RefreshCw className={`h-3.5 w-3.5 mr-1 ${loading ? 'animate-spin' : ''}`} />
|
||||
{loading ? 'Checking…' : 'Authz debug'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => void runDbNextCheck()}
|
||||
disabled={loadingDbNext || !base}
|
||||
>
|
||||
<RefreshCw className={`h-3.5 w-3.5 mr-1 ${loadingDbNext ? 'animate-spin' : ''}`} />
|
||||
{loadingDbNext ? 'Pool…' : 'db-users-next (pool)'}
|
||||
</Button>
|
||||
{/* New-auth experiment only — see server `authzExperimentUsersRoute` */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => void runExperimentUsers()}
|
||||
disabled={loadingExpUsers || !base}
|
||||
>
|
||||
<RefreshCw className={`h-3.5 w-3.5 mr-1 ${loadingExpUsers ? 'animate-spin' : ''}`} />
|
||||
{loadingExpUsers ? 'Users…' : 'Experiment: all users'}
|
||||
</Button>
|
||||
{jwtPick && (
|
||||
<Button variant="secondary" size="sm" type="button" onClick={() => void copyPickedJwt()}>
|
||||
{copied ? <Check className="h-3.5 w-3.5 mr-1" /> : <Copy className="h-3.5 w-3.5 mr-1" />}
|
||||
{copied ? 'Copied' : `Copy ${jwtPick.source} (curl)`}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<section className="space-y-2">
|
||||
<h3 className="text-sm font-semibold">Environment</h3>
|
||||
<Row label="API base">
|
||||
{base || <span className="text-amber-600 dark:text-amber-400">VITE_SERVER_IMAGE_API_URL unset</span>}
|
||||
</Row>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
<section className="space-y-2">
|
||||
<h3 className="text-sm font-semibold">OIDC client session</h3>
|
||||
<Row label="isLoading">{String(auth.isLoading)}</Row>
|
||||
<Row label="isAuthenticated">{String(auth.isAuthenticated)}</Row>
|
||||
<Row label="activeNavigator">{auth.activeNavigator ?? '—'}</Row>
|
||||
<Row label="error">{auth.error?.message ?? '—'}</Row>
|
||||
<Row label="scope">{oidcUser?.scope ?? '—'}</Row>
|
||||
<Row label="session_state">{oidcUser?.session_state ?? '—'}</Row>
|
||||
<Row label="expires_at">
|
||||
{oidcUser?.expires_at != null
|
||||
? `${new Date(oidcUser.expires_at * 1000).toISOString()} (oidc-client session)`
|
||||
: '—'}
|
||||
</Row>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
<section className="space-y-3">
|
||||
<h3 className="text-sm font-semibold">Tokens (client storage)</h3>
|
||||
<div className="rounded-md border bg-muted/30 p-3 space-y-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-xs font-medium">access_token</span>
|
||||
<Badge variant={atFp.kind === 'jwt' ? 'default' : atFp.kind === 'opaque' ? 'secondary' : 'outline'}>
|
||||
{atFp.kind}
|
||||
</Badge>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
len {atFp.length} · dots {atFp.dots}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[11px] font-mono text-muted-foreground break-all">{atFp.preview}</p>
|
||||
{atPayload && (
|
||||
<pre className="text-[11px] bg-background/80 rounded p-2 overflow-x-auto max-h-28">
|
||||
{JSON.stringify(
|
||||
{
|
||||
iss: atPayload.iss,
|
||||
sub: atPayload.sub,
|
||||
aud: atPayload.aud,
|
||||
exp: atPayload.exp,
|
||||
exp_h: formatExp(atPayload.exp),
|
||||
},
|
||||
null,
|
||||
0,
|
||||
)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/30 p-3 space-y-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-xs font-medium">id_token</span>
|
||||
<Badge variant={idFp.kind === 'jwt' ? 'default' : idFp.kind === 'opaque' ? 'secondary' : 'outline'}>
|
||||
{idFp.kind}
|
||||
</Badge>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
len {idFp.length} · dots {idFp.dots}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[11px] font-mono text-muted-foreground break-all">{idFp.preview}</p>
|
||||
{idPayload && (
|
||||
<pre className="text-[11px] bg-background/80 rounded p-2 overflow-x-auto max-h-28">
|
||||
{JSON.stringify(
|
||||
{
|
||||
iss: idPayload.iss,
|
||||
sub: idPayload.sub,
|
||||
aud: idPayload.aud,
|
||||
exp: idPayload.exp,
|
||||
exp_h: formatExp(idPayload.exp),
|
||||
email: idPayload.email,
|
||||
},
|
||||
null,
|
||||
0,
|
||||
)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
<Row label="Bearer for API">
|
||||
{jwtPick ? (
|
||||
<span>
|
||||
<Badge variant="outline">{jwtPick.source}</Badge>{' '}
|
||||
<span className="text-muted-foreground">(JWKS verifies this JWT)</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-amber-700 dark:text-amber-400">no JWT — cannot call verify endpoint</span>
|
||||
)}
|
||||
</Row>
|
||||
{pickedPayload && (
|
||||
<pre className="text-[11px] bg-muted rounded-md p-2 overflow-x-auto max-h-32">
|
||||
{JSON.stringify(pickedPayload, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
<section className="space-y-2">
|
||||
<h3 className="text-sm font-semibold">Profile (userinfo / id_token claims)</h3>
|
||||
<pre className="text-[11px] bg-muted rounded-md p-3 overflow-x-auto max-h-40">
|
||||
{auth.user?.profile
|
||||
? JSON.stringify(auth.user.profile, null, 2)
|
||||
: '—'}
|
||||
</pre>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
<section className="space-y-2">
|
||||
<h3 className="text-sm font-semibold">Last HTTP</h3>
|
||||
<Row label="request">{lastHttp?.url ?? '—'}</Row>
|
||||
<Row label="status">
|
||||
{lastHttp ? (
|
||||
<>
|
||||
{lastHttp.status} {lastHttp.ok ? 'OK' : '(not ok)'}
|
||||
</>
|
||||
) : (
|
||||
'—'
|
||||
)}
|
||||
</Row>
|
||||
</section>
|
||||
|
||||
{fetchError && (
|
||||
<pre className="text-xs bg-destructive/10 text-destructive rounded-md p-3 overflow-x-auto">
|
||||
{fetchError}
|
||||
</pre>
|
||||
)}
|
||||
|
||||
{last && (
|
||||
<section className="space-y-2">
|
||||
<h3 className="text-sm font-semibold">API response (/api/admin/authz/debug)</h3>
|
||||
<pre className="text-xs bg-muted rounded-md p-3 overflow-x-auto max-h-[360px]">
|
||||
{JSON.stringify(last, null, 2)}
|
||||
</pre>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{fetchErrorDbNext && (
|
||||
<pre className="text-xs bg-destructive/10 text-destructive rounded-md p-3 overflow-x-auto">
|
||||
{fetchErrorDbNext}
|
||||
</pre>
|
||||
)}
|
||||
|
||||
{lastDbNext && (
|
||||
<section className="space-y-2">
|
||||
<h3 className="text-sm font-semibold">
|
||||
API response (/api/admin/authz/db-next) — <code className="text-xs">DATABASE_URL_NEXT</code> via{' '}
|
||||
<code className="text-xs">db-users-next.ts</code>
|
||||
</h3>
|
||||
<Row label="request">{lastDbNextHttp?.url ?? '—'}</Row>
|
||||
<Row label="status">
|
||||
{lastDbNextHttp ? `${lastDbNextHttp.status} ${lastDbNextHttp.ok ? 'OK' : ''}` : '—'}
|
||||
</Row>
|
||||
<pre className="text-xs bg-muted rounded-md p-3 overflow-x-auto max-h-[360px]">
|
||||
{JSON.stringify(lastDbNext, null, 2)}
|
||||
</pre>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{fetchErrorExpUsers && (
|
||||
<pre className="text-xs bg-destructive/10 text-destructive rounded-md p-3 overflow-x-auto">
|
||||
{fetchErrorExpUsers}
|
||||
</pre>
|
||||
)}
|
||||
|
||||
{lastExpUsers != null && (
|
||||
<section className="space-y-2">
|
||||
<h3 className="text-sm font-semibold">
|
||||
API response (/api/admin/authz/experiment/users) — new-auth experiment only
|
||||
</h3>
|
||||
<Row label="request">{lastExpUsersHttp?.url ?? '—'}</Row>
|
||||
<Row label="status">
|
||||
{lastExpUsersHttp ? `${lastExpUsersHttp.status} ${lastExpUsersHttp.ok ? 'OK' : ''}` : '—'}
|
||||
</Row>
|
||||
<pre className="text-xs bg-muted rounded-md p-3 overflow-x-auto max-h-[360px]">
|
||||
{JSON.stringify(lastExpUsers, null, 2)}
|
||||
</pre>
|
||||
</section>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user