- Add an Admin nav item (key 'admin', href /settings/users) with visibleTo=['admin'] so signed-in users with role='user' don't see it. - nav-config exposes navItemsForRole(role) helper that filters NAV_ITEMS by visibleTo. - Root layout fetches getCurrentUser() and forwards role into AppShell. AppShell narrows the role gate to the rendered nav (sidebar + drawer); /login still short-circuits to the bare header. Unknown role falls back to 'user' visibility (defense-in-depth). - Settings page renders an admin-only card linking to Users so admins have a discoverable in-app entry point too. Tests: - nav-config: navItemsForRole admin/user matrix + admin entry shape. - app-shell: admin link visible for admin, hidden for user, hidden for null/unauthenticated, /login bare header strips nav entirely. - actions/auth: cookie payload encodes role=user, unknown role rejected, AUTH_SECRET-unset path, whitespace-only username rejected, rate-limit key contains client IP, unknown-user path still hits DB+bcrypt. 440 tests now (was 423). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
94 lines
3.3 KiB
TypeScript
94 lines
3.3 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="Display name" value={op.displayName} />
|
|
<Separator />
|
|
<Row label="Username" value={op.username} mono />
|
|
<Separator />
|
|
<Row label="Default timezone" value={op.defaultTimezone} mono />
|
|
<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>
|
|
);
|
|
}
|