SPB Git
3.4 KB · 21 lines tsx
1import { generateSecret, generateURI } from "otplib";2import QRCode from "qrcode";3import { sql } from "@/db";4import { requireUserPage } from "@/lib/auth/guard";5import { csrfToken } from "@/lib/auth/session";6import { accountAction } from "@/lib/admin/actions";7import { Csrf, Field, Notice, PageTitle } from "@/components/admin-ui";8export default async function Account({ searchParams }: { searchParams: Promise<Record<string, string | undefined>> }) {9  const sp = await searchParams; const u = await requireUserPage(); const csrf = csrfToken(u);10  const sessions = await sql`SELECT id, ip, user_agent, created_at, last_seen_at FROM sessions WHERE user_id = ${u.id} AND revoked_at IS NULL AND expires_at > now() ORDER BY last_seen_at DESC`;11  const secret = u.totpEnabled ? null : generateSecret(); const qr = secret ? await QRCode.toDataURL(generateURI({ issuer: "wp.spboucher.ai", label: u.email, secret })) : null;12  return (<div><PageTitle title="Account" /><Notice sp={sp} />{sp.must_change && <p className="card p-3 mb-4 text-warn border-warn">You must change the initial password before using the administration.</p>}13    <div className="grid lg:grid-cols-2 gap-6">14      <form action={accountAction} className="card p-4 space-y-3"><h2 className="text-lg">Change password</h2><Csrf token={csrf} /><input type="hidden" name="op" value="password" /><Field label="Current password" name="current" type="password" required /><Field label="New password (≥ 14 chars, upper, lower, digit)" name="new" type="password" required /><Field label="Confirm" name="confirm" type="password" required /><button className="btn btn-primary">Update password</button></form>15      <section className="card p-4 space-y-3"><h2 className="text-lg">Two-factor authentication (TOTP)</h2>16        {u.totpEnabled ? <form action={accountAction} className="space-y-3"><p className="text-ok text-sm">Enabled.</p><Csrf token={csrf} /><input type="hidden" name="op" value="totp_disable" /><Field label="Current password to disable" name="current" type="password" required /><button className="btn btn-sm text-bad">Disable</button></form>17          : <form action={accountAction} className="space-y-3"><p className="meta">Scan with an authenticator app, then enter the 6-digit code.</p>{qr && <img src={qr} alt="TOTP QR code" width={180} height={180} className="border border-line" />}<p className="font-mono text-xs break-all">{secret}</p><Csrf token={csrf} /><input type="hidden" name="op" value="totp_enable" /><input type="hidden" name="secret" value={secret ?? ""} /><Field label="Code" name="code" required /><button className="btn btn-primary btn-sm">Enable</button></form>}</section>18      <section className="card p-4 lg:col-span-2"><h2 className="text-lg mb-2">Active sessions</h2><table className="data"><thead><tr><th>IP</th><th>Client</th><th>Created</th><th>Last seen</th></tr></thead><tbody>{sessions.map((s) => <tr key={s.id}><td>{s.ip}{s.id === u.sessionId ? " (this)" : ""}</td><td className="text-xs text-fg-2">{s.user_agent}</td><td className="text-xs">{new Date(s.created_at).toLocaleString("en-CA")}</td><td className="text-xs">{new Date(s.last_seen_at).toLocaleString("en-CA")}</td></tr>)}</tbody></table><form action={accountAction} className="mt-3"><Csrf token={csrf} /><input type="hidden" name="op" value="revoke_others" /><button className="btn btn-sm">Sign out other sessions</button></form></section>19    </div></div>);20}21
Files · main
Menu

Theme