yiekheng c493101b60 feat(web): password policy, sign-out, dashboard isolation, activity tweaks
Multi-fix batch from a rapid feedback round:

- Password policy mirrors Facebook's documented rule (≥6 chars + mix of
  letters with numbers/symbols). Centralised in
  apps/web/src/lib/password-policy.ts; createUserAction,
  resetUserPasswordAction, the AddUser form, and the row Reset-password
  flow all use it. CLI scripts/set-password.ts inlines the same check
  so the bootstrap path stays consistent.
- App shell adds a Sign-out button in both the desktop sidebar footer
  and the mobile drawer footer, with the signed-in username next to it.
  Layout passes username down alongside role. Theme toggle was removed
  from the shell per request — operators don't need it in the chrome.
- Dashboard stats: getDashboardStats was running findMany on reminders
  with NO operator filter, so a brand-new user saw global counts from
  every tenant. Switched to an INNER JOIN on whatsapp_accounts so the
  card on / only counts this user's reminders. (Counts had been showing
  '1 / 1 / 3 / 5' to a fresh user — the cross-tenant leak the user
  flagged.)
- /activity drops the All tab and the Clear-history button. Default
  filter is now Success when no ?filter= is set; Partial keeps fanning
  into Paused + Failed; Skipped still merges into Archived.
- /settings drops the Display name row entirely and only shows the Role
  row to admins. Layout receives username so the shell can also surface
  it next to the Sign-out button.
- Tests: password-policy.test.ts (11 cases), updated users.test.ts to
  use policy-compliant passwords + cover letters-only / digits-only
  rejection, sidebar-footer assertion swapped from theme-toggle to the
  new Sign-out + username markup. 453 tests green; typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:46:29 +08:00

96 lines
3.2 KiB
TypeScript

import Link from "next/link";
import { ShieldCheckIcon, ChevronRightIcon } from "lucide-react";
import { getSeededOperator } from "@/lib/operator";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { ThemeToggle } from "@/components/theme-toggle";
import { NotificationsToggle } from "@/components/notifications-toggle";
import { PageShell } from "@/components/page-shell";
export default async function SettingsPage() {
const op = await getSeededOperator();
const isAdmin = op.role === "admin";
return (
<PageShell title="Settings" narrow>
<Card>
<CardHeader>
<CardTitle>Operator</CardTitle>
</CardHeader>
<CardContent className="space-y-3 text-sm">
<Row label="Username" value={op.username} mono />
<Separator />
<Row label="Default timezone" value={op.defaultTimezone} mono />
{isAdmin && (
<>
<Separator />
<Row label="Role" value={op.role} mono />
</>
)}
</CardContent>
</Card>
{isAdmin && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<ShieldCheckIcon className="size-4" />
Admin
</CardTitle>
<CardDescription>
Manage which usernames can sign in and what role each
one has. Visible to admins only.
</CardDescription>
</CardHeader>
<CardContent className="p-0">
<Link
// eslint-disable-next-line @typescript-eslint/no-explicit-any
href={"/settings/users" as any}
className="flex items-center justify-between gap-3 px-6 py-3 text-sm font-medium hover:bg-muted focus-visible:bg-muted rounded-b-xl"
>
<span>Users</span>
<ChevronRightIcon className="size-4 text-muted-foreground" />
</Link>
</CardContent>
</Card>
)}
<Card>
<CardHeader>
<CardTitle>Notifications</CardTitle>
<CardDescription>
Browser notifications when a reminder fires successfully or a
test message is sent. Uses the in-tab Notification API works
while the app is open. Background push is on the roadmap.
</CardDescription>
</CardHeader>
<CardContent>
<NotificationsToggle />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Appearance</CardTitle>
</CardHeader>
<CardContent className="flex items-center justify-between">
<div className="text-sm text-muted-foreground">Theme</div>
<ThemeToggle />
</CardContent>
</Card>
<p className="text-center text-xs text-muted-foreground">
cm WhatsApp Bot · self-hosted
</p>
</PageShell>
);
}
function Row({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
return (
<div className="flex items-center justify-between gap-3">
<dt className="text-muted-foreground">{label}</dt>
<dd className={mono ? "font-mono text-xs" : ""}>{value}</dd>
</div>
);
}