page.tsx192 lines · main
1import Link from 'next/link';
2import { notFound } from 'next/navigation';
3
4import { apiFetch } from '@/lib/api';
5import { fetchAuthDashboard } from '../lib/auth-api';
6import { loadAuthV2Workspace } from '../lib/load-workspace';
7import { AuthSetupChecklist } from './setup-checklist';
8
9export const dynamic = 'force-dynamic';
10
11/**
12 * One project's Auth overview — golden-path setup when incomplete,
13 * then counts + shortcuts.
14 */
15export default async function AuthProjectOverviewPage({
16 params,
17}: {
18 params: Promise<{ projectId: string }>;
19}) {
20 const { projectId } = await params;
21 const projects = await loadAuthV2Workspace();
22 const project = projects.find(
23 (p) => p.id === projectId || p.id.toLowerCase() === projectId.toLowerCase(),
24 );
25 if (!project) notFound();
26
27 const id = project.id;
28
29 const [dash, configRes, tenantsRes] = await Promise.all([
30 fetchAuthDashboard(id),
31 apiFetch(`/v1/auth-core/projects/${id}/config`).catch(() => null),
32 apiFetch('/v1/auth-core/tenants').catch(() => null),
33 ]);
34
35 let tenantId = project.tenantId ?? null;
36 let methodsOn: string[] = [];
37 let oauthConfigured: string[] = [];
38 let smsReady = false;
39
40 if (configRes?.ok) {
41 const body = (await configRes.json()) as {
42 tenantId?: string;
43 methods?: Record<string, boolean>;
44 providers?: Array<{ name: string; configured: boolean }>;
45 delivery?: { sms?: { configured?: boolean } };
46 };
47 tenantId = body.tenantId ?? tenantId;
48 smsReady = Boolean(body.delivery?.sms?.configured);
49 if (body.methods) {
50 const labels: Record<string, string> = {
51 emailPassword: 'email + password',
52 passwordlessEmail: 'passwordless-email',
53 magicLink: 'magic-link',
54 passwordlessSms: 'passwordless-sms',
55 passkeys: 'passkeys',
56 mfa: 'mfa',
57 };
58 methodsOn = Object.entries(body.methods)
59 .filter(([, on]) => on)
60 .map(([k]) => labels[k] ?? k);
61 }
62 oauthConfigured =
63 body.providers?.filter((p) => p.configured).map((p) => p.name) ?? [];
64 }
65
66 let tenantRowOn = false;
67 if (tenantsRes?.ok) {
68 try {
69 const body = (await tenantsRes.json()) as {
70 tenants?: Array<{ projectId?: string; tenantId?: string }>;
71 tenantIds?: string[];
72 };
73 const mappedTenant = tenantId;
74 tenantRowOn = Boolean(
75 body.tenants?.some(
76 (t) =>
77 t.projectId === id ||
78 (t.projectId && t.projectId.toLowerCase() === id.toLowerCase()) ||
79 (mappedTenant && t.tenantId === mappedTenant),
80 ) ||
81 (mappedTenant && body.tenantIds?.includes(mappedTenant)),
82 );
83 } catch {
84 tenantRowOn = false;
85 }
86 }
87
88 // tenantRowOn used for light diagnostics in “this project” card below.
89 void tenantRowOn;
90
91 const counts = dash.ok
92 ? dash.data.counts
93 : { users: 0, sessions: 0, thirdPartyLinks: 0, passwordlessCodesActive: 0 };
94
95 return (
96 <section className="space-y-6">
97 {/* Golden path — shown until setup is green (client also collapses). */}
98 <AuthSetupChecklist projectId={id} />
99
100 <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
101 {[
102 { label: 'users', value: counts.users },
103 { label: 'sessions', value: counts.sessions },
104 { label: 'social links', value: counts.thirdPartyLinks },
105 {
106 label: 'active codes',
107 value: counts.passwordlessCodesActive,
108 },
109 ].map((c) => (
110 <div
111 key={c.label}
112 className="rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface)] p-4"
113 >
114 <p className="font-mono text-[10px] uppercase tracking-widest text-[var(--color-text-muted)]">
115 {c.label}
116 </p>
117 <p className="mt-1 font-mono text-2xl text-[var(--color-text)]">
118 {dash.ok ? c.value : '—'}
119 </p>
120 </div>
121 ))}
122 </div>
123
124 <div className="rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface)] p-6">
125 <h2 className="font-mono text-sm text-[var(--color-text)]">
126 this project
127 </h2>
128 <p className="mt-1 font-mono text-[11px] text-[var(--color-text-muted)]">
129 tenant {tenantId ?? '—'}
130 </p>
131 <p className="mt-3 font-mono text-xs text-[var(--color-text-muted)]">
132 methods on:{' '}
133 {methodsOn.length ? methodsOn.join(', ') : 'none yet'}
134 {oauthConfigured.length
135 ? ` · OAuth: ${oauthConfigured.join(', ')}`
136 : ''}
137 {` · SMS: ${smsReady ? 'ready' : 'not set'}`}
138 </p>
139 <Link
140 href={`/dashboard/auth/${id}/providers`}
141 className="mt-4 inline-block rounded-md px-3 py-2 font-mono text-xs font-medium text-black"
142 style={{ background: '#FFFD74' }}
143 >
144 manage sign-in methods →
145 </Link>
146 <p className="mt-2 font-mono text-[10px] text-[var(--color-text-muted)]">
147 open Providers for methods, Twilio SMS, and OAuth client id + secret.
148 </p>
149 <Link
150 href={`/dashboard/auth/${id}/providers?method=passwordlessSms`}
151 className="mt-2 inline-block font-mono text-[11px] underline"
152 style={{ color: 'var(--auth-accent, #FFFD74)' }}
153 >
154 {smsReady ? 'update SMS / Twilio →' : 'set up SMS / Twilio →'}
155 </Link>
156 </div>
157
158 <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
159 {[
160 {
161 href: 'providers',
162 label: 'providers',
163 help: 'sign-in methods + OAuth secrets',
164 },
165 {
166 href: 'branding',
167 label: 'branding',
168 help: 'logo, color, email from-name',
169 },
170 { href: 'users', label: 'users', help: 'app end-users' },
171 { href: 'sessions', label: 'sessions', help: 'who is signed in' },
172 { href: 'keys', label: 'keys', help: 'SDK keys for this app' },
173 { href: 'security', label: 'security', help: 'roles' },
174 { href: 'enterprise', label: 'enterprise', help: 'SAML / OIDC SSO' },
175 ].map((l) => (
176 <Link
177 key={l.href}
178 href={`/dashboard/auth/${id}/${l.href}`}
179 className="rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface)] p-4 transition hover:border-[var(--color-border)]"
180 >
181 <p className="font-mono text-sm text-[var(--color-text)]">
182 {l.label}
183 </p>
184 <p className="mt-1 font-mono text-[11px] text-[var(--color-text-muted)]">
185 {l.help}
186 </p>
187 </Link>
188 ))}
189 </div>
190 </section>
191 );
192}