TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import type { Metadata } from 'next';2import { and, desc, eq, gt, isNull } from '@/lib/db';3import { db, loginEvents, sessions, trustedDevices } from '@/lib/db';4import { currentSessionId, requireUser } from '@/lib/auth/session';5import { deviceLabel } from '@/lib/auth/request';6import { revokeDeviceAction, revokeSessionAction, signOutEverywhereAction, toggleAlwaysAskCodeAction } from '@/lib/auth/account-actions';7import { PageHeader, btnDanger, btnSecondary } from '@/components/account/page-header';8import { Card, CardHeader, Badge, Table, th, td } from '@/components/ui/primitives';9import { fmtRelative } from '@/lib/format';10import { ChangePasswordForm, MfaSection } from './forms';1112export const metadata: Metadata = { title: 'Security', robots: { index: false } };1314export default async function SecurityPage() {15 const u = await requireUser('/account/security');16 const current = await currentSessionId();17 const [sess, devices, logins] = await Promise.all([18 db().select().from(sessions).where(and(eq(sessions.userId, u.id), isNull(sessions.revokedAt), gt(sessions.expiresAt, new Date()))).orderBy(desc(sessions.lastSeenAt)),19 db().select().from(trustedDevices).where(and(eq(trustedDevices.userId, u.id), isNull(trustedDevices.revokedAt), gt(trustedDevices.expiresAt, new Date()))).orderBy(desc(trustedDevices.lastUsedAt)),20 db().select().from(loginEvents).where(eq(loginEvents.userId, u.id)).orderBy(desc(loginEvents.createdAt)).limit(25),21 ]);22 return (23 <>24 <PageHeader title="Security" description="Password, two-step verification, trusted devices and sign-in history." />25 <div className="grid gap-4 lg:grid-cols-2">26 <Card>27 <CardHeader title="Two-step verification" subtitle={u.mfaEnabled ? 'Authenticator app enabled' : 'E-mail codes protect new devices; add an authenticator for stronger protection.'} action={<Badge tone={u.mfaEnabled ? 'gain' : 'alert'}>{u.mfaEnabled ? 'Authenticator on' : 'E-mail codes'}</Badge>} />28 <div className="p-4">29 <MfaSection enabled={u.mfaEnabled} />30 <form action={toggleAlwaysAskCodeAction} className="mt-5 border-t border-border pt-4">31 <label className="flex items-start gap-2.5 text-sm">32 <input type="checkbox" name="alwaysAskCode" defaultChecked={u.alwaysAskCode} className="mt-0.5 h-4 w-4" onChange={undefined} />33 <span>34 <span className="block">Ask for an e-mailed code on every new device</span>35 <span className="block text-xs text-muted">Applies when the authenticator is off. Trusted devices skip it for 30 days.</span>36 </span>37 </label>38 <button type="submit" className={`${btnSecondary} mt-3 h-8 text-xs`}>39 Save40 </button>41 </form>42 </div>43 </Card>44 <Card>45 <CardHeader title="Password" subtitle={u.passwordChangedAt ? `Last changed ${fmtRelative(u.passwordChangedAt)}` : 'Never changed'} />46 <div className="p-4">47 <ChangePasswordForm />48 </div>49 </Card>50 <Card>51 <CardHeader title="Active sessions" subtitle={`${sess.length} signed-in session${sess.length === 1 ? '' : 's'}`} action={52 <form action={signOutEverywhereAction}>53 <button className={btnDanger}>Sign out everywhere else</button>54 </form>55 } />56 <ul className="divide-y divide-border">57 {sess.map((s) => (58 <li key={s.id} className="flex items-center justify-between gap-3 px-4 py-2.5 text-sm">59 <div className="min-w-0">60 <p className="truncate">61 {deviceLabel(s.userAgent)} {s.id === current ? <Badge tone="index" className="ml-1">This device</Badge> : null}62 </p>63 <p className="text-xs text-subtle">64 {s.ip ?? 'unknown IP'} · active {fmtRelative(s.lastSeenAt ?? s.createdAt)} · expires {fmtRelative(s.expiresAt)}65 </p>66 </div>67 {s.id !== current ? (68 <form action={revokeSessionAction}>69 <input type="hidden" name="sessionId" value={s.id} />70 <button className={btnDanger}>Revoke</button>71 </form>72 ) : null}73 </li>74 ))}75 </ul>76 </Card>77 <Card>78 <CardHeader title="Trusted devices" subtitle="Devices that skip the second step for 30 days." />79 {devices.length === 0 ? (80 <p className="px-4 py-6 text-center text-xs text-muted">No trusted devices.</p>81 ) : (82 <ul className="divide-y divide-border">83 {devices.map((d) => (84 <li key={d.id} className="flex items-center justify-between gap-3 px-4 py-2.5 text-sm">85 <div className="min-w-0">86 <p className="truncate">{d.label ?? deviceLabel(d.userAgent)}</p>87 <p className="text-xs text-subtle">88 {d.ip ?? 'unknown IP'} · last used {fmtRelative(d.lastUsedAt ?? d.createdAt)} · until {d.expiresAt.toISOString().slice(0, 10)}89 </p>90 </div>91 <form action={revokeDeviceAction}>92 <input type="hidden" name="deviceId" value={d.id} />93 <button className={btnDanger}>Forget</button>94 </form>95 </li>96 ))}97 </ul>98 )}99 </Card>100 </div>101 <Card className="mt-4">102 <CardHeader title="Sign-in history" subtitle="Last 25 events." />103 <Table>104 <thead>105 <tr>106 <th className={th}>When</th>107 <th className={th}>Outcome</th>108 <th className={th}>Method</th>109 <th className={th}>Device</th>110 <th className={th}>IP</th>111 </tr>112 </thead>113 <tbody>114 {logins.map((l) => (115 <tr key={l.id}>116 <td className={td}>{fmtRelative(l.createdAt)}</td>117 <td className={td}>118 <Badge tone={l.outcome === 'success' ? 'gain' : l.outcome === 'mfa_required' ? 'neutral' : 'loss'}>{l.outcome.replace(/_/g, ' ')}</Badge>119 </td>120 <td className={td}>{l.method ?? '—'}</td>121 <td className={td}>{deviceLabel(l.userAgent)}</td>122 <td className={`${td} mono-num text-xs`}>{l.ip ?? '—'}</td>123 </tr>124 ))}125 {logins.length === 0 ? (126 <tr>127 <td className={td} colSpan={5}>128 <span className="text-muted">No sign-in events yet.</span>129 </td>130 </tr>131 ) : null}132 </tbody>133 </Table>134 </Card>135 </>136 );137}138