SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
6.8 KB · 138 lines tsx
Raw Blame History
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