57c29f62b4
- user_blocks table (composite PK), Block/Unblock button on profiles. Blocking severs any existing follow relationship both ways and prevents the blocked party from following, commenting on the blocker's recipes, or the blocker from seeing their comments. - reports table (recipe/comment/user targets, pending/reviewed/ dismissed status). ReportButton on comments, admin review queue at /admin/reports with dismiss/mark-reviewed actions, audit-logged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
65 lines
2.6 KiB
TypeScript
65 lines
2.6 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 } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
const adminNav = [
|
|
{ href: "/admin", label: "Overview", icon: BarChart3 },
|
|
{ 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/tiers", label: "Tier Limits", icon: Gauge },
|
|
{ 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 },
|
|
];
|
|
|
|
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 min-h-screen">
|
|
<aside className="w-56 border-r bg-muted/30 flex flex-col sticky top-0 h-screen">
|
|
<div className="flex items-center gap-2 p-4 border-b font-semibold">
|
|
<Shield className="h-4 w-4 text-destructive" />
|
|
Admin
|
|
</div>
|
|
<nav className="flex flex-col gap-1 p-2 flex-1">
|
|
{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 transition-colors",
|
|
"hover:bg-accent hover:text-accent-foreground"
|
|
)}
|
|
>
|
|
<Icon className="h-4 w-4" />
|
|
{label}
|
|
</Link>
|
|
))}
|
|
</nav>
|
|
<div className="p-2 border-t">
|
|
<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-8 overflow-auto">{children}</main>
|
|
</div>
|
|
);
|
|
}
|