feat(admin): admin panel with overview, users, audit logs, storage, AI config, settings

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).
This commit is contained in:
Arnaud
2026-07-01 08:10:59 +02:00
parent b2d592afe8
commit cba5d9c3ac
14 changed files with 1197 additions and 0 deletions
@@ -0,0 +1,56 @@
import { NextRequest, NextResponse } from "next/server";
import { requireAdmin } from "@/lib/api-auth";
import { db, users, auditLogs, eq } from "@epicure/db";
import { randomUUID } from "crypto";
interface RouteContext {
params: Promise<{ id: string }>;
}
export async function PATCH(req: NextRequest, { params }: RouteContext) {
const { session, response } = await requireAdmin();
if (response) return response;
const { id } = await params;
const body = await req.json() as { role?: string; tier?: string };
const { role, tier } = body;
const validRoles = ["user", "moderator", "admin"] as const;
const validTiers = ["free", "pro"] as const;
if (role !== undefined && !validRoles.includes(role as typeof validRoles[number])) {
return NextResponse.json({ error: "Invalid role" }, { status: 400 });
}
if (tier !== undefined && !validTiers.includes(tier as typeof validTiers[number])) {
return NextResponse.json({ error: "Invalid tier" }, { status: 400 });
}
const updateData: Partial<{ role: "user" | "moderator" | "admin"; tier: "free" | "pro"; updatedAt: Date }> = {
updatedAt: new Date(),
};
if (role) updateData.role = role as "user" | "moderator" | "admin";
if (tier) updateData.tier = tier as "free" | "pro";
const [updated] = await db
.update(users)
.set(updateData)
.where(eq(users.id, id))
.returning({ id: users.id, role: users.role, tier: users.tier });
if (!updated) {
return NextResponse.json({ error: "User not found" }, { status: 404 });
}
// Write audit log
await db.insert(auditLogs).values({
id: randomUUID(),
userId: session!.user.id,
action: "admin.user.update",
targetType: "user",
targetId: id,
metadata: JSON.stringify({ role, tier }),
createdAt: new Date(),
});
return NextResponse.json({ user: updated });
}