page.tsx163 lines · main
1import Link from 'next/link';
2
3import { CogIcon } from '@/components/ui/cog';
4import { MailIcon } from '@/components/ui/mail';
5import { ShieldCheckIcon } from '@/components/ui/shield-check';
6import { UsersIcon } from '@/components/ui/users';
7
8import { apiOrigin } from '@/lib/env';
9import { getSessionUser } from '@/lib/session';
10
11import { Section } from '../_components/section';
12import { StepUpCard } from './step-up-card';
13
14export const metadata = { title: 'settings · admin' };
15export const dynamic = 'force-dynamic';
16
17/**
18 * Operator settings — deliberately minimal and honest. Shows who is signed
19 * in (straight from /v1/me), the live step-up window (from
20 * /v1/admin/launch-status), and plainly explains where password + account
21 * changes live today. No preference toggles exist yet because no api
22 * backs them — nothing here is decorative.
23 */
24export default async function AdminSettingsPage() {
25 // The (admin) layout already gates on getSessionUser().isAdmin, so this
26 // re-fetch always resolves for a rendered page.
27 const user = await getSessionUser();
28
29 return (
30 <div className="flex flex-col gap-10">
31 <header className="flex flex-col gap-2">
32 <div className="flex items-center gap-2">
33 <span className="text-[var(--color-primary)]">
34 <CogIcon size={20} />
35 </span>
36 <h1 className="font-mono text-xl tracking-tight">settings</h1>
37 </div>
38 <p className="max-w-prose font-mono text-sm text-[var(--color-text-muted)]">
39 who&apos;s at the controls and how the cockpit&apos;s security works. only real,
40 working things live here — preference toggles arrive when their apis do.
41 </p>
42 </header>
43
44 {/* ── signed-in operator ─────────────────────────────────────────── */}
45 <Section title="signed-in operator" icon={<UsersIcon size={16} />}>
46 {user === null ? (
47 <div className="rounded-xl border border-dashed border-[var(--color-border-subtle)] bg-[var(--color-surface)] p-6 font-mono text-xs text-[var(--color-text-subtle)]">
48 couldn&apos;t load your session — reload the page.
49 </div>
50 ) : (
51 <div className="flex flex-col gap-6 rounded-xl border border-[var(--color-border-subtle)] bg-[var(--color-surface)] p-6 sm:p-8">
52 <div className="flex flex-wrap items-center gap-3">
53 <span className="font-mono text-lg tracking-tight text-[var(--color-text)]">
54 {user.email}
55 </span>
56 {user.isAdmin ? (
57 <span className="inline-flex items-center gap-1.5 rounded-md bg-[var(--color-primary-subtle)] px-2 py-0.5 font-mono text-[10px] uppercase tracking-wider text-[var(--color-primary)]">
58 platform admin
59 </span>
60 ) : null}
61 {user.emailVerified ? (
62 <span className="inline-flex rounded-md bg-[var(--color-surface-raised)] px-2 py-0.5 font-mono text-[10px] uppercase tracking-wider text-[var(--color-text-muted)]">
63 verified
64 </span>
65 ) : (
66 <span className="inline-flex rounded-md bg-[var(--color-surface-raised)] px-2 py-0.5 font-mono text-[10px] uppercase tracking-wider text-[var(--color-warning)]">
67 unverified
68 </span>
69 )}
70 </div>
71
72 <dl className="grid grid-cols-1 gap-x-8 gap-y-4 font-mono text-xs sm:grid-cols-2 xl:grid-cols-3">
73 <div className="flex flex-col gap-1">
74 <dt className="text-[10px] uppercase tracking-wider text-[var(--color-text-subtle)]">
75 name
76 </dt>
77 <dd className="text-[var(--color-text)]">
78 {user.legalName ?? user.name ?? '—'}
79 </dd>
80 </div>
81 <div className="flex flex-col gap-1">
82 <dt className="text-[10px] uppercase tracking-wider text-[var(--color-text-subtle)]">
83 account created
84 </dt>
85 <dd className="text-[var(--color-text)]">{formatDate(user.createdAt)}</dd>
86 </div>
87 <div className="flex flex-col gap-1">
88 <dt className="text-[10px] uppercase tracking-wider text-[var(--color-text-subtle)]">
89 last sign-in
90 </dt>
91 <dd className="text-[var(--color-text)]">
92 {user.lastSignIn ? (
93 <>
94 {new Date(user.lastSignIn.at).toLocaleString()}
95 {formatNearBy(user.lastSignIn.nearBy) ? (
96 <span className="text-[var(--color-text-subtle)]">
97 {' '}
98 · {formatNearBy(user.lastSignIn.nearBy)}
99 </span>
100 ) : null}
101 </>
102 ) : (
103 'never'
104 )}
105 </dd>
106 </div>
107 </dl>
108 </div>
109 )}
110 </Section>
111
112 {/* ── step-up auth window ────────────────────────────────────────── */}
113 <Section title="step-up auth" icon={<ShieldCheckIcon size={16} />}>
114 <StepUpCard apiOrigin={apiOrigin} />
115 </Section>
116
117 {/* ── passwords & account changes ────────────────────────────────── */}
118 <Section title="passwords & account changes" icon={<MailIcon size={16} />}>
119 <div className="flex flex-col gap-4 rounded-xl border border-[var(--color-border-subtle)] bg-[var(--color-surface)] p-6 sm:p-8 font-mono text-xs leading-relaxed text-[var(--color-text-muted)]">
120 <p>
121 you sign in to this cockpit with email + password at{' '}
122 <code className="rounded bg-[var(--color-surface-raised)] px-1.5 py-0.5 text-[var(--color-text)]">
123 /admin/login
124 </code>
125 . there is no change-password form in the app yet — the auth service exposes the
126 endpoint, but no page uses it. that form is on the roadmap; until it ships, a
127 password change means an operator with database access resetting the credential.
128 </p>
129 <p>
130 everything else about your account — changing your sign-in email, profile and billing
131 details, account deletion — lives in the user dashboard&apos;s settings page, which
132 works for this admin account too.
133 </p>
134 <p>
135 <Link
136 href="/dashboard/settings"
137 className="text-[var(--color-text-link)] transition hover:text-[var(--color-primary)]"
138 >
139 open account settings →
140 </Link>
141 </p>
142 </div>
143 </Section>
144 </div>
145 );
146}
147
148/* ─── small helpers ──────────────────────────────────────────────────────── */
149
150function formatDate(iso: string): string {
151 const d = new Date(iso);
152 if (Number.isNaN(d.getTime())) return iso;
153 return d.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
154}
155
156function formatNearBy(
157 nearBy: { city: string | null; region: string | null; country: string | null } | null,
158): string | null {
159 if (!nearBy) return null;
160 const place = nearBy.city ?? nearBy.region;
161 if (place && nearBy.country) return `${place}, ${nearBy.country}`;
162 return place ?? nearBy.country ?? null;
163}