cba5d9c3ac
Sticky sidebar with back-to-app link. Overview: user counts, recipe photos, AI calls/month. User management with role/tier editing. Audit log (all admin actions). Storage page with photo breakdown by tier. AI config showing DB vs .env key source. Site settings page to override .env values at runtime (encrypted in DB).
62 lines
2.4 KiB
TypeScript
62 lines
2.4 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 } 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/recipes", label: "Recipes", icon: BookOpen },
|
|
{ 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>
|
|
);
|
|
}
|