Files
Epicure/apps/web/app/admin/layout.tsx
T
Arnaud c5e1643d39 feat: per-category email prefs, admin ops webhooks, per-user feature toggles (v0.61.0)
- Notification email preferences: every push category (follow, comment,
  reply, reaction, rating, mention, leftoverExpiring, shoppingList) now has
  an independent email toggle, plus a Weekly Digest toggle. Previously email
  sent unconditionally whenever the recipient had one; now gated the same
  way push already was. The weekly-digest cron route excludes opted-out
  users.

- Admin-only site-wide webhooks (Admin → Webhooks): new signups, support
  tickets, and reports filed can now fire an HMAC-signed HTTP webhook
  (Slack/Discord/ops alerting), independent of the existing per-user
  webhooks (which stay scoped to a user's own recipe/meal-plan/shopping-list
  events). Signing/delivery logic factored into lib/webhook-delivery.ts and
  shared by both dispatchers instead of duplicated.

- Settings → Features: users can hide Nutrition, Pantry, Meal Plan,
  Shopping Lists, Collections, or Messages from their own nav. Purely
  cosmetic — hidden pages stay reachable by direct link, nothing is
  access-restricted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 23:07:28 +02:00

78 lines
3.5 KiB
TypeScript

import { redirect } from "next/navigation";
import { headers } from "next/headers";
import { auth } from "@/lib/auth/server";
import { db, users, eq } from "@epicure/db";
import Link from "next/link";
import { Shield, Users, BookOpen, Settings, BarChart3, ClipboardList, HardDrive, Bot, ArrowLeft, Gauge, Mail, Flag, History, LifeBuoy, TrendingUp, Webhook } from "lucide-react";
import { cn } from "@/lib/utils";
const adminNav = [
{ href: "/admin", label: "Overview", icon: BarChart3 },
{ href: "/admin/insights", label: "Insights", icon: TrendingUp },
{ href: "/admin/users", label: "Users", icon: Users },
{ href: "/admin/invites", label: "Invites", icon: Mail },
{ href: "/admin/recipes", label: "Recipes", icon: BookOpen },
{ href: "/admin/reports", label: "Reports", icon: Flag },
{ href: "/admin/support", label: "Support", icon: LifeBuoy },
{ href: "/admin/tiers", label: "Tier Limits", icon: Gauge },
{ href: "/admin/webhooks", label: "Webhooks", icon: Webhook },
{ href: "/admin/audit-logs", label: "Audit Logs", icon: ClipboardList },
{ href: "/admin/storage", label: "Storage", icon: HardDrive },
{ href: "/admin/ai-config", label: "AI Config", icon: Bot },
{ href: "/admin/settings", label: "Settings", icon: Settings },
{ href: "/admin/changelog", label: "Changelog", icon: History },
];
export default async function AdminLayout({ children }: { children: React.ReactNode }) {
const session = await auth.api.getSession({ headers: await headers() });
if (!session) redirect("/login");
const [dbUser] = await db.select({ role: users.role }).from(users).where(eq(users.id, session.user.id));
if (dbUser?.role !== "admin") redirect("/recipes");
return (
<div className="flex flex-col md:flex-row min-h-screen">
<aside className="w-full md:w-56 md:shrink-0 border-b md:border-b-0 md:border-r bg-muted/30 flex flex-col md:sticky md:top-0 md:h-screen">
<div className="flex items-center justify-between gap-2 p-4 border-b font-semibold">
<span className="flex items-center gap-2">
<Shield className="h-4 w-4 text-destructive" />
Admin
</span>
<Link
href="/recipes"
className="md:hidden flex items-center gap-1 text-xs font-normal text-muted-foreground hover:text-foreground"
>
<ArrowLeft className="h-3.5 w-3.5" />
Back to app
</Link>
</div>
<nav className="flex overflow-x-auto gap-1 p-2 md:flex-col md:overflow-visible md:flex-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
{adminNav.map(({ href, label, icon: Icon }) => (
<Link
key={href}
href={href}
className={cn(
"flex items-center gap-2 rounded-md px-3 py-2 text-sm whitespace-nowrap shrink-0 md:shrink transition-colors",
"hover:bg-accent hover:text-accent-foreground"
)}
>
<Icon className="h-4 w-4" />
{label}
</Link>
))}
</nav>
<div className="p-2 border-t hidden md:block">
<Link
href="/recipes"
className="flex items-center gap-2 rounded-md px-3 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
>
<ArrowLeft className="h-4 w-4" />
Back to app
</Link>
</div>
</aside>
<main className="flex-1 p-4 md:p-8 overflow-auto min-w-0">{children}</main>
</div>
);
}