Skip to content
File

Blob: src/client/pages/account/userinfo.tsx

typescript91 lines
1import { BackLink } from "@/client/components/ui/back-link";
2import { Card } from "@/client/components/ui/card";
3import { PageHeader } from "@/client/components/ui/page-header";
4import { Skeleton } from "@/client/components/ui/skeleton";
5import { useRequireSession } from "@/client/hooks/use-auth-guard";
6import { type SessionUser } from "@/client/lib/auth-client";
7import { fallbackHandle } from "@/client/lib/handle";
8 
9const ROW_CLASS =
10 "flex flex-col gap-2 py-4 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between sm:gap-6";
11const LABEL_CLASS = "font-mono text-xs font-medium uppercase tracking-wider text-zinc-500";
12const VALUE_CLASS = "flex min-w-0 flex-wrap items-center gap-2 sm:justify-end";
13const CODE_CLASS = "break-all rounded bg-zinc-800/60 px-2 py-1 font-mono text-xs text-zinc-100";
14 
15const AccountUserInfoSkeleton = () => (
16 <div className="mx-auto max-w-3xl" aria-busy="true">
17 <Skeleton className="mb-6 h-4 w-24" />
18 <div className="mb-8">
19 <Skeleton className="mb-3 h-8 w-36" />
20 <Skeleton className="h-4 w-80" />
21 </div>
22 <div className="rounded-2xl border border-zinc-800/60 bg-zinc-900/40 p-5 sm:p-6">
23 <div className="flex flex-col divide-y divide-zinc-800/40">
24 {Array.from({ length: 4 }, (_, i) => (
25 <div key={i} className="flex items-center justify-between gap-4 py-4 first:pt-0 last:pb-0">
26 <Skeleton className="h-4 w-36" />
27 <Skeleton className="h-7 w-48" />
28 </div>
29 ))}
30 </div>
31 </div>
32 </div>
33);
34 
35export const AccountUserInfoPage = () => {
36 const session = useRequireSession("/account/userinfo");
37 
38 if (session.isPending || !session.data) {
39 return <AccountUserInfoSkeleton />;
40 }
41 
42 const sessionUser = session.data.user as SessionUser;
43 // OIDC `sub` is the Better Auth user id — see src/worker/auth/index.ts
44 // customAccessTokenClaims/customUserInfoClaims. Surfacing it here so users
45 // can paste it into RP support tickets without inspecting a token.
46 const sub = sessionUser.id;
47 const verified = sessionUser.emailVerified === true;
48 const preferredUsername = sessionUser.preferredUsername ?? fallbackHandle(sessionUser);
49 const trimmedName = sessionUser.name?.trim() ?? "";
50 
51 return (
52 <div className="mx-auto max-w-3xl">
53 <BackLink to="/account">Account</BackLink>
54 <PageHeader title="User info" description="The OIDC claims downstream apps see when you sign in with tessera." />
55 <Card>
56 <dl className="flex flex-col divide-y divide-zinc-800/40">
57 <div className={ROW_CLASS}>
58 <dt className={LABEL_CLASS}>sub / tessera_sub</dt>
59 <dd className={VALUE_CLASS}>
60 <code className={CODE_CLASS}>{sub}</code>
61 </dd>
62 </div>
63 <div className={ROW_CLASS}>
64 <dt className={LABEL_CLASS}>email</dt>
65 <dd className={VALUE_CLASS}>
66 <code className={CODE_CLASS}>{sessionUser.email}</code>
67 <span className={`text-xs ${verified ? "text-zinc-400" : "text-red-400"}`}>
68 {verified ? "verified" : "unverified"}
69 </span>
70 </dd>
71 </div>
72 <div className={ROW_CLASS}>
73 <dt className={LABEL_CLASS}>preferred_username</dt>
74 <dd className={VALUE_CLASS}>
75 <code className={CODE_CLASS}>{preferredUsername}</code>
76 </dd>
77 </div>
78 {trimmedName ? (
79 <div className={ROW_CLASS}>
80 <dt className={LABEL_CLASS}>name</dt>
81 <dd className={VALUE_CLASS}>
82 <span className="text-sm text-zinc-100">{trimmedName}</span>
83 </dd>
84 </div>
85 ) : null}
86 </dl>
87 </Card>
88 </div>
89 );
90};