feat(admin): add admin panel with user management, daily puzzle CRUD, and activity graph

- Add admin page at /admin (restricted to ADMIN_EMAIL via proxy)
- Implement user ban/unban and deletion
- Add daily puzzle creation, modification, deletion
- Include 14-day activity chart with hover details
- Add User.banned field to schema and migrate database
- Block banned users from logging in
- Add ADMIN_EMAIL to .env.local configuration
- Update Prisma client after schema changes
This commit is contained in:
jessy-david-dev
2026-04-11 15:10:08 +02:00
parent 5d8f7eae45
commit 8ff64b3470
22 changed files with 899 additions and 122 deletions
+55
View File
@@ -0,0 +1,55 @@
import { NextRequest, NextResponse } from "next/server";
import { jwtDecrypt } from "jose";
import { hkdf } from "@panva/hkdf";
const ADMIN_EMAIL = process.env.ADMIN_EMAIL;
async function getDerivedEncryptionKey(secret: string, salt: string) {
return hkdf(
"sha256",
secret,
salt,
`Auth.js Generated Encryption Key (${salt})`,
64,
);
}
async function getEmailFromRequest(req: NextRequest): Promise<string | null> {
const secret = process.env.AUTH_SECRET;
if (!secret) return null;
const cookieName =
process.env.NODE_ENV === "production"
? "__Secure-authjs.session-token"
: "authjs.session-token";
const token = req.cookies.get(cookieName)?.value;
if (!token) return null;
try {
const encryptionKey = await getDerivedEncryptionKey(secret, cookieName);
const { payload } = await jwtDecrypt(token, encryptionKey, {
clockTolerance: 15,
keyManagementAlgorithms: ["dir"],
contentEncryptionAlgorithms: ["A256CBC-HS512", "A256GCM"],
});
return (payload.email as string) ?? null;
} catch {
return null;
}
}
export async function proxy(req: NextRequest) {
const email = await getEmailFromRequest(req);
if (!ADMIN_EMAIL || email !== ADMIN_EMAIL) {
if (req.nextUrl.pathname.startsWith("/api/")) {
return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
}
return NextResponse.redirect(new URL("/", req.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/admin/:path*", "/api/admin/:path*"],
};