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
+75
View File
@@ -0,0 +1,75 @@
import type { Metadata } from "next";
import { db } from "@epicure/db";
import { users } from "@epicure/db";
import { desc } from "@epicure/db";
import { Badge } from "@/components/ui/badge";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import Link from "next/link";
export const metadata: Metadata = { title: "User Management" };
const ROLE_COLORS = {
user: "secondary",
moderator: "outline",
admin: "destructive",
} as const;
const TIER_COLORS = {
free: "secondary",
pro: "default",
} as const;
export default async function AdminUsersPage() {
const allUsers = await db
.select()
.from(users)
.orderBy(desc(users.createdAt))
.limit(100);
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold tracking-tight">Users</h1>
<div className="rounded-md border">
<table className="w-full text-sm">
<thead className="border-b bg-muted/50">
<tr>
<th className="px-4 py-3 text-left font-medium">User</th>
<th className="px-4 py-3 text-left font-medium">Role</th>
<th className="px-4 py-3 text-left font-medium">Tier</th>
<th className="px-4 py-3 text-left font-medium">Joined</th>
</tr>
</thead>
<tbody>
{allUsers.map((user) => (
<tr key={user.id} className="border-b last:border-0 hover:bg-muted/30">
<td className="px-4 py-3">
<Link href={`/admin/users/${user.id}`} className="flex items-center gap-3 group">
<Avatar className="h-8 w-8">
<AvatarImage src={user.avatarUrl ?? ""} />
<AvatarFallback className="text-xs">
{user.name.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div>
<div className="font-medium group-hover:underline underline-offset-4">{user.name}</div>
<div className="text-muted-foreground text-xs">{user.email}</div>
</div>
</Link>
</td>
<td className="px-4 py-3">
<Badge variant={ROLE_COLORS[user.role]}>{user.role}</Badge>
</td>
<td className="px-4 py-3">
<Badge variant={TIER_COLORS[user.tier]}>{user.tier}</Badge>
</td>
<td className="px-4 py-3 text-muted-foreground">
{user.createdAt.toLocaleDateString()}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}