Repository Public MIT
1import { ActionForm, Field } from "@/components/action-form";2import { dateTime } from "@/lib/format";3import { requireSession } from "@/lib/session";4import { changePasswordAction } from "../actions";56export const metadata = { title: "Account" };78export default async function Page({ searchParams }: { searchParams: Promise<{ first?: string }> }) {9 const { ctx, principal } = await requireSession({ allowPasswordChange: true });10 const first = principal.user.must_change_password || (await searchParams).first === "1";11 const sessions = await ctx.sql<{ id: string; created_at: Date; last_seen_at: Date; ip: string | null; user_agent: string | null }[]>`12 SELECT id, created_at, last_seen_at, ip, user_agent FROM sessions WHERE user_id = ${principal.user.id} AND revoked_at IS NULL AND expires_at > now() ORDER BY created_at DESC LIMIT 20`;13 return (14 <div className="max-w-xl">15 <h1 className="text-2xl font-semibold tracking-tight">Account</h1>16 {first && principal.user.must_change_password && (17 <p role="alert" className="mt-4 rounded-lg border border-warn/50 bg-warn/5 p-4 text-sm">You are using an initial password issued by the operator. Choose your own before doing anything else.</p>18 )}19 <section className="mt-6">20 <h2 className="mb-3 text-base font-semibold">Change password</h2>21 <ActionForm action={changePasswordAction} submit="Change password" className="card space-y-4 p-6">22 <Field label="Current password" name="current" type="password" required autoComplete="current-password" />23 <Field label="New password" name="next" type="password" required minLength={12} maxLength={200} autoComplete="new-password" hint="At least 12 characters. A password manager is strongly recommended." />24 <Field label="Repeat new password" name="confirm" type="password" required minLength={12} autoComplete="new-password" />25 </ActionForm>26 <p className="mt-3 text-xs text-faint">Changing the password signs out every other session. Passkeys and two-factor authentication are planned; until then, use a unique generated password.</p>27 </section>28 <section className="mt-8">29 <h2 className="mb-3 text-base font-semibold">Active sessions</h2>30 <ul className="divide-y divide-line rounded-lg border border-line text-sm">31 {sessions.map((s) => (32 <li key={s.id} className="px-3 py-2">33 <span className="font-mono text-[12px]">{dateTime(s.created_at)}</span> <span className="text-muted">· {s.ip ?? "unknown address"}{s.id === principal.sessionId ? " · this session" : ""}</span>34 <span className="block truncate text-xs text-faint">{s.user_agent}</span>35 </li>36 ))}37 </ul>38 </section>39 </div>40 );41}42