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