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:
@@ -0,0 +1,419 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
type User = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
banned: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
_count: { games: number };
|
||||||
|
};
|
||||||
|
|
||||||
|
type Game = {
|
||||||
|
id: string;
|
||||||
|
mode: string;
|
||||||
|
startArticle: string;
|
||||||
|
targetArticle: string;
|
||||||
|
clicks: number;
|
||||||
|
timeSeconds: number;
|
||||||
|
won: boolean;
|
||||||
|
playedAt: string;
|
||||||
|
user: { name: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
type ModeStat = {
|
||||||
|
mode: string;
|
||||||
|
_count: { id: number };
|
||||||
|
_avg: { clicks: number | null; timeSeconds: number | null };
|
||||||
|
};
|
||||||
|
|
||||||
|
type ActivityDay = { date: string; count: number };
|
||||||
|
|
||||||
|
type AdminStats = {
|
||||||
|
totalUsers: number;
|
||||||
|
totalGames: number;
|
||||||
|
todayGames: number;
|
||||||
|
recentUsers: User[];
|
||||||
|
recentGames: Game[];
|
||||||
|
modeStats: ModeStat[];
|
||||||
|
activity: ActivityDay[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type DailyPuzzle = {
|
||||||
|
id: string;
|
||||||
|
date: string;
|
||||||
|
startArticle: string;
|
||||||
|
targetArticle: string;
|
||||||
|
_count: { results: number };
|
||||||
|
};
|
||||||
|
|
||||||
|
function fmt(s: number) {
|
||||||
|
const m = Math.floor(s / 60);
|
||||||
|
const sec = Math.round(s % 60);
|
||||||
|
return m > 0 ? `${m}m${sec.toString().padStart(2, "0")}s` : `${sec}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MiniBarChart({ data }: { data: ActivityDay[] }) {
|
||||||
|
const max = Math.max(...data.map((d) => d.count), 1);
|
||||||
|
return (
|
||||||
|
<div className="flex items-end gap-1 h-16">
|
||||||
|
{data.map((d) => {
|
||||||
|
const pct = (d.count / max) * 100;
|
||||||
|
const day = new Date(d.date + "T12:00:00").toLocaleDateString("fr-FR", { day: "numeric", month: "short" });
|
||||||
|
return (
|
||||||
|
<div key={d.date} className="flex-1 flex flex-col items-center gap-1 group relative">
|
||||||
|
<div
|
||||||
|
className="w-full bg-[#7c3aed]/60 rounded-sm group-hover:bg-[#7c3aed] transition-colors"
|
||||||
|
style={{ height: `${Math.max(pct, 4)}%` }}
|
||||||
|
/>
|
||||||
|
<div className="absolute bottom-full mb-1 left-1/2 -translate-x-1/2 bg-[#2e2e2e] text-[#f0f0f0] text-[10px] px-1.5 py-0.5 rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-10">
|
||||||
|
{day} — {d.count}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AdminPage() {
|
||||||
|
const [data, setData] = useState<AdminStats | null>(null);
|
||||||
|
const [dailyPuzzles, setDailyPuzzles] = useState<DailyPuzzle[] | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [tab, setTab] = useState<"overview" | "users" | "games" | "daily">("overview");
|
||||||
|
|
||||||
|
// Daily puzzle form
|
||||||
|
const [puzzleForm, setPuzzleForm] = useState({ date: "", startArticle: "", targetArticle: "" });
|
||||||
|
const [editingPuzzle, setEditingPuzzle] = useState<DailyPuzzle | null>(null);
|
||||||
|
const [puzzleSaving, setPuzzleSaving] = useState(false);
|
||||||
|
|
||||||
|
// Confirm delete
|
||||||
|
const [confirmDelete, setConfirmDelete] = useState<{ type: "user" | "puzzle"; id: string; label: string } | null>(null);
|
||||||
|
|
||||||
|
function refreshStats() {
|
||||||
|
return fetch("/api/admin/stats")
|
||||||
|
.then((r) => { if (!r.ok) throw new Error(); return r.json(); })
|
||||||
|
.then(setData);
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshDaily() {
|
||||||
|
return fetch("/api/admin/daily")
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then(setDailyPuzzles);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Promise.all([refreshStats(), refreshDaily()])
|
||||||
|
.catch(() => setData(null))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function handleBan(user: User) {
|
||||||
|
const res = await fetch(`/api/admin/users/${user.id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ banned: !user.banned }),
|
||||||
|
});
|
||||||
|
if (res.ok) await refreshStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeleteUser(id: string) {
|
||||||
|
await fetch(`/api/admin/users/${id}`, { method: "DELETE" });
|
||||||
|
await refreshStats();
|
||||||
|
setConfirmDelete(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeletePuzzle(id: string) {
|
||||||
|
await fetch(`/api/admin/daily/${id}`, { method: "DELETE" });
|
||||||
|
await refreshDaily();
|
||||||
|
setConfirmDelete(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSavePuzzle() {
|
||||||
|
setPuzzleSaving(true);
|
||||||
|
try {
|
||||||
|
if (editingPuzzle) {
|
||||||
|
await fetch(`/api/admin/daily/${editingPuzzle.id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ startArticle: puzzleForm.startArticle, targetArticle: puzzleForm.targetArticle }),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await fetch("/api/admin/daily", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(puzzleForm),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await refreshDaily();
|
||||||
|
setPuzzleForm({ date: "", startArticle: "", targetArticle: "" });
|
||||||
|
setEditingPuzzle(null);
|
||||||
|
} finally {
|
||||||
|
setPuzzleSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startEditPuzzle(p: DailyPuzzle) {
|
||||||
|
setEditingPuzzle(p);
|
||||||
|
setPuzzleForm({ date: p.date, startArticle: p.startArticle, targetArticle: p.targetArticle });
|
||||||
|
}
|
||||||
|
|
||||||
|
const card = "bg-[#1a1a1a] border border-[#2e2e2e] rounded-xl p-4";
|
||||||
|
const tabBtn = (active: boolean) =>
|
||||||
|
`px-3 py-2 rounded-lg text-xs sm:text-sm font-semibold transition-colors cursor-pointer ${
|
||||||
|
active ? "bg-[#7c3aed] text-white" : "bg-[#1a1a1a] border border-[#2e2e2e] text-[#888] hover:text-[#f0f0f0]"
|
||||||
|
}`;
|
||||||
|
const inputCls = "w-full min-h-10 px-3 bg-[#0f0f0f] border border-[#2e2e2e] rounded-xl text-sm text-[#f0f0f0] outline-none focus:border-[#7c3aed] transition-colors";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-dvh w-full bg-[#0f0f0f] text-[#f0f0f0] px-4 py-8 flex flex-col gap-6 max-w-5xl mx-auto">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-black">Admin</h1>
|
||||||
|
<p className="text-xs text-[#555] mt-0.5">WikiRush — panneau d'administration</p>
|
||||||
|
</div>
|
||||||
|
<Link href="/" className="min-h-9 px-3 rounded-lg text-sm font-semibold bg-[#242424] border border-[#2e2e2e] text-[#f0f0f0] hover:bg-[#1a1a1a] transition-colors">
|
||||||
|
← Accueil
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading && <div className="text-[#888] text-sm animate-pulse">Chargement...</div>}
|
||||||
|
{!loading && !data && <div className="text-red-400 text-sm">Accès refusé ou erreur serveur.</div>}
|
||||||
|
|
||||||
|
{/* Confirm dialog */}
|
||||||
|
{confirmDelete && (
|
||||||
|
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 px-4">
|
||||||
|
<div className="bg-[#1a1a1a] border border-[#2e2e2e] rounded-2xl p-6 max-w-sm w-full flex flex-col gap-4">
|
||||||
|
<p className="font-semibold text-sm">Confirmer la suppression</p>
|
||||||
|
<p className="text-[#888] text-sm">Supprimer <span className="text-[#f0f0f0] font-semibold">{confirmDelete.label}</span> ? Cette action est irréversible.</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button className="flex-1 min-h-10 rounded-xl text-sm font-semibold bg-[#242424] border border-[#2e2e2e] text-[#f0f0f0] cursor-pointer hover:bg-[#2e2e2e]" onClick={() => setConfirmDelete(null)}>Annuler</button>
|
||||||
|
<button
|
||||||
|
className="flex-1 min-h-10 rounded-xl text-sm font-semibold bg-red-700 text-white cursor-pointer hover:bg-red-600"
|
||||||
|
onClick={() => confirmDelete.type === "user" ? handleDeleteUser(confirmDelete.id) : handleDeletePuzzle(confirmDelete.id)}
|
||||||
|
>
|
||||||
|
Supprimer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{data && (
|
||||||
|
<>
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className="flex gap-2 flex-wrap">
|
||||||
|
<button className={tabBtn(tab === "overview")} onClick={() => setTab("overview")}>Vue d'ensemble</button>
|
||||||
|
<button className={tabBtn(tab === "users")} onClick={() => setTab("users")}>
|
||||||
|
Utilisateurs <span className="ml-1 text-[#555]">({data.totalUsers})</span>
|
||||||
|
</button>
|
||||||
|
<button className={tabBtn(tab === "games")} onClick={() => setTab("games")}>Parties récentes</button>
|
||||||
|
<button className={tabBtn(tab === "daily")} onClick={() => setTab("daily")}>Défis du jour</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Overview */}
|
||||||
|
{tab === "overview" && (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||||||
|
{[
|
||||||
|
{ label: "Utilisateurs", value: data.totalUsers.toLocaleString("fr-FR") },
|
||||||
|
{ label: "Parties totales", value: data.totalGames.toLocaleString("fr-FR") },
|
||||||
|
{ label: "Parties aujourd'hui", value: data.todayGames.toLocaleString("fr-FR") },
|
||||||
|
].map(({ label, value }) => (
|
||||||
|
<div key={label} className={card}>
|
||||||
|
<p className="text-[10px] text-[#555] uppercase tracking-widest mb-1">{label}</p>
|
||||||
|
<p className="text-3xl font-black text-[#7c3aed]">{value}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Graphique activité */}
|
||||||
|
<div className={card}>
|
||||||
|
<h2 className="text-xs font-bold text-[#888] uppercase tracking-wider mb-4">Activité — 14 derniers jours</h2>
|
||||||
|
<MiniBarChart data={data.activity} />
|
||||||
|
<div className="flex justify-between mt-1">
|
||||||
|
<span className="text-[10px] text-[#555]">{new Date(data.activity[0]?.date + "T12:00:00").toLocaleDateString("fr-FR", { day: "numeric", month: "short" })}</span>
|
||||||
|
<span className="text-[10px] text-[#555]">aujourd'hui</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modes */}
|
||||||
|
<div className={card}>
|
||||||
|
<h2 className="text-xs font-bold text-[#888] uppercase tracking-wider mb-3">Parties par mode</h2>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{data.modeStats.sort((a, b) => b._count.id - a._count.id).map((m) => (
|
||||||
|
<div key={m.mode} className="flex items-center justify-between text-sm py-1.5 border-b border-[#2e2e2e] last:border-0">
|
||||||
|
<span className="font-semibold capitalize">{m.mode}</span>
|
||||||
|
<div className="flex items-center gap-4 text-[#888]">
|
||||||
|
<span>{m._count.id.toLocaleString("fr-FR")} parties</span>
|
||||||
|
{m._avg.clicks !== null && <span>~{Math.round(m._avg.clicks)} clics</span>}
|
||||||
|
{m._avg.timeSeconds !== null && <span>~{fmt(m._avg.timeSeconds)}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Users */}
|
||||||
|
{tab === "users" && (
|
||||||
|
<div className={card}>
|
||||||
|
<h2 className="text-xs font-bold text-[#888] uppercase tracking-wider mb-3">Utilisateurs</h2>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{data.recentUsers.map((u) => (
|
||||||
|
<div key={u.id} className={`flex items-center justify-between py-2.5 border-b border-[#2e2e2e] last:border-0 gap-3 ${u.banned ? "opacity-50" : ""}`}>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="font-semibold text-sm truncate flex items-center gap-2">
|
||||||
|
{u.name}
|
||||||
|
{u.banned && <span className="text-[10px] font-bold px-1.5 py-0.5 rounded-full bg-red-900/40 text-red-400 uppercase">Banni</span>}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-[#555] truncate">{u.email}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right shrink-0 hidden sm:block">
|
||||||
|
<p className="text-xs text-[#888]">{u._count.games} parties</p>
|
||||||
|
<p className="text-[10px] text-[#555]">{new Date(u.createdAt).toLocaleDateString("fr-FR")}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5 shrink-0">
|
||||||
|
<button
|
||||||
|
onClick={() => handleBan(u)}
|
||||||
|
className={`min-h-8 px-2.5 rounded-lg text-xs font-semibold cursor-pointer transition-colors ${u.banned ? "bg-green-900/30 text-green-400 hover:bg-green-900/50" : "bg-orange-900/30 text-orange-400 hover:bg-orange-900/50"}`}
|
||||||
|
>
|
||||||
|
{u.banned ? "Débannir" : "Bannir"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setConfirmDelete({ type: "user", id: u.id, label: u.name })}
|
||||||
|
className="min-h-8 px-2.5 rounded-lg text-xs font-semibold bg-red-900/30 text-red-400 hover:bg-red-900/50 cursor-pointer transition-colors"
|
||||||
|
>
|
||||||
|
Supprimer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Games */}
|
||||||
|
{tab === "games" && (
|
||||||
|
<div className={card}>
|
||||||
|
<h2 className="text-xs font-bold text-[#888] uppercase tracking-wider mb-3">50 dernières parties</h2>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{data.recentGames.map((g) => (
|
||||||
|
<div key={g.id} className="flex items-center justify-between py-2 border-b border-[#2e2e2e] last:border-0 gap-3">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm truncate">
|
||||||
|
<span className="font-semibold">{g.user.name}</span>
|
||||||
|
<span className="text-[#555] mx-1">—</span>
|
||||||
|
<span className="text-[#888] text-xs">{g.startArticle} → {g.targetArticle}</span>
|
||||||
|
</p>
|
||||||
|
<p className="text-[10px] text-[#555]">{new Date(g.playedAt).toLocaleString("fr-FR")}</p>
|
||||||
|
</div>
|
||||||
|
<div className="shrink-0 flex items-center gap-2">
|
||||||
|
<span className={`text-[10px] font-bold px-2 py-0.5 rounded-full uppercase ${g.won ? "bg-green-900/40 text-green-400" : "bg-red-900/40 text-red-400"}`}>
|
||||||
|
{g.won ? "Gagné" : "Perdu"}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-[#888] capitalize hidden sm:inline">{g.mode}</span>
|
||||||
|
<span className="text-xs text-[#555]">{g.clicks} clics</span>
|
||||||
|
<span className="text-xs text-[#555] hidden sm:inline">{fmt(g.timeSeconds)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Daily puzzles */}
|
||||||
|
{tab === "daily" && (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{/* Formulaire */}
|
||||||
|
<div className={card}>
|
||||||
|
<h2 className="text-xs font-bold text-[#888] uppercase tracking-wider mb-3">
|
||||||
|
{editingPuzzle ? `Modifier le puzzle du ${editingPuzzle.date}` : "Nouveau puzzle"}
|
||||||
|
</h2>
|
||||||
|
<div className="flex flex-col gap-2.5">
|
||||||
|
{!editingPuzzle && (
|
||||||
|
<input
|
||||||
|
className={inputCls}
|
||||||
|
type="date"
|
||||||
|
value={puzzleForm.date}
|
||||||
|
onChange={(e) => setPuzzleForm((f) => ({ ...f, date: e.target.value }))}
|
||||||
|
placeholder="Date (YYYY-MM-DD)"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
className={inputCls}
|
||||||
|
type="text"
|
||||||
|
value={puzzleForm.startArticle}
|
||||||
|
onChange={(e) => setPuzzleForm((f) => ({ ...f, startArticle: e.target.value }))}
|
||||||
|
placeholder="Article de départ (ex: Tour Eiffel)"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className={inputCls}
|
||||||
|
type="text"
|
||||||
|
value={puzzleForm.targetArticle}
|
||||||
|
onChange={(e) => setPuzzleForm((f) => ({ ...f, targetArticle: e.target.value }))}
|
||||||
|
placeholder="Article cible (ex: Napoléon Bonaparte)"
|
||||||
|
/>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={handleSavePuzzle}
|
||||||
|
disabled={puzzleSaving || (!editingPuzzle && !puzzleForm.date) || !puzzleForm.startArticle || !puzzleForm.targetArticle}
|
||||||
|
className="flex-1 min-h-10 rounded-xl text-sm font-semibold bg-[#7c3aed] text-white hover:bg-[#6d28d9] disabled:opacity-40 cursor-pointer transition-colors"
|
||||||
|
>
|
||||||
|
{puzzleSaving ? "Enregistrement..." : editingPuzzle ? "Modifier" : "Créer"}
|
||||||
|
</button>
|
||||||
|
{editingPuzzle && (
|
||||||
|
<button
|
||||||
|
onClick={() => { setEditingPuzzle(null); setPuzzleForm({ date: "", startArticle: "", targetArticle: "" }); }}
|
||||||
|
className="min-h-10 px-4 rounded-xl text-sm font-semibold bg-[#242424] border border-[#2e2e2e] text-[#888] cursor-pointer hover:text-[#f0f0f0]"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Liste */}
|
||||||
|
{dailyPuzzles && (
|
||||||
|
<div className={card}>
|
||||||
|
<h2 className="text-xs font-bold text-[#888] uppercase tracking-wider mb-3">Puzzles existants</h2>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{dailyPuzzles.map((p) => (
|
||||||
|
<div key={p.id} className="flex items-center justify-between py-2.5 border-b border-[#2e2e2e] last:border-0 gap-3">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-semibold">{p.date}</p>
|
||||||
|
<p className="text-xs text-[#888] truncate">{p.startArticle} → {p.targetArticle}</p>
|
||||||
|
<p className="text-[10px] text-[#555]">{p._count.results} résultat{p._count.results > 1 ? "s" : ""}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5 shrink-0">
|
||||||
|
<button
|
||||||
|
onClick={() => startEditPuzzle(p)}
|
||||||
|
className="min-h-8 px-2.5 rounded-lg text-xs font-semibold bg-[#242424] border border-[#2e2e2e] text-[#888] hover:text-[#f0f0f0] cursor-pointer"
|
||||||
|
>
|
||||||
|
Modifier
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setConfirmDelete({ type: "puzzle", id: p.id, label: `puzzle du ${p.date}` })}
|
||||||
|
className="min-h-8 px-2.5 rounded-lg text-xs font-semibold bg-red-900/30 text-red-400 hover:bg-red-900/50 cursor-pointer"
|
||||||
|
>
|
||||||
|
Supprimer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { prisma } from "../../../../../lib/prisma";
|
||||||
|
|
||||||
|
type Params = { params: Promise<{ id: string }> };
|
||||||
|
|
||||||
|
// PATCH /api/admin/daily/[id] — modifier un puzzle
|
||||||
|
export async function PATCH(req: Request, { params }: Params) {
|
||||||
|
const { id } = await params;
|
||||||
|
const { startArticle, targetArticle } = await req.json() as {
|
||||||
|
startArticle: string;
|
||||||
|
targetArticle: string;
|
||||||
|
};
|
||||||
|
const puzzle = await prisma.dailyPuzzle.update({
|
||||||
|
where: { id },
|
||||||
|
data: { startArticle, targetArticle },
|
||||||
|
});
|
||||||
|
return NextResponse.json(puzzle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE /api/admin/daily/[id] — supprimer un puzzle
|
||||||
|
export async function DELETE(_req: Request, { params }: Params) {
|
||||||
|
const { id } = await params;
|
||||||
|
await prisma.dailyPuzzle.delete({ where: { id } });
|
||||||
|
return NextResponse.json({ ok: true });
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { prisma } from "../../../../lib/prisma";
|
||||||
|
|
||||||
|
// GET /api/admin/daily — liste tous les puzzles
|
||||||
|
export async function GET() {
|
||||||
|
const puzzles = await prisma.dailyPuzzle.findMany({
|
||||||
|
orderBy: { date: "desc" },
|
||||||
|
take: 30,
|
||||||
|
include: { _count: { select: { results: true } } },
|
||||||
|
});
|
||||||
|
return NextResponse.json(puzzles);
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/admin/daily — créer un puzzle pour une date
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
const { date, startArticle, targetArticle } = await req.json() as {
|
||||||
|
date: string;
|
||||||
|
startArticle: string;
|
||||||
|
targetArticle: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const puzzle = await prisma.dailyPuzzle.upsert({
|
||||||
|
where: { date },
|
||||||
|
create: { date, startArticle, targetArticle },
|
||||||
|
update: { startArticle, targetArticle },
|
||||||
|
});
|
||||||
|
return NextResponse.json(puzzle);
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { prisma } from "../../../../lib/prisma";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const now = new Date();
|
||||||
|
const todayStart = new Date(now);
|
||||||
|
todayStart.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
// Activité sur les 14 derniers jours
|
||||||
|
const since14 = new Date(todayStart);
|
||||||
|
since14.setDate(since14.getDate() - 13);
|
||||||
|
|
||||||
|
const [totalUsers, totalGames, todayGames, recentUsers, recentGames, modeStats, activityRaw] =
|
||||||
|
await Promise.all([
|
||||||
|
prisma.user.count(),
|
||||||
|
prisma.game.count(),
|
||||||
|
prisma.game.count({ where: { playedAt: { gte: todayStart } } }),
|
||||||
|
prisma.user.findMany({
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
take: 50,
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
banned: true,
|
||||||
|
createdAt: true,
|
||||||
|
_count: { select: { games: true } },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.game.findMany({
|
||||||
|
orderBy: { playedAt: "desc" },
|
||||||
|
take: 50,
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
mode: true,
|
||||||
|
startArticle: true,
|
||||||
|
targetArticle: true,
|
||||||
|
clicks: true,
|
||||||
|
timeSeconds: true,
|
||||||
|
won: true,
|
||||||
|
playedAt: true,
|
||||||
|
user: { select: { name: true } },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.game.groupBy({
|
||||||
|
by: ["mode"],
|
||||||
|
_count: { id: true },
|
||||||
|
_avg: { clicks: true, timeSeconds: true },
|
||||||
|
}),
|
||||||
|
prisma.game.findMany({
|
||||||
|
where: { playedAt: { gte: since14 } },
|
||||||
|
select: { playedAt: true },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Agréger l'activité par jour
|
||||||
|
const activityMap = new Map<string, number>();
|
||||||
|
for (let i = 0; i < 14; i++) {
|
||||||
|
const d = new Date(since14);
|
||||||
|
d.setDate(d.getDate() + i);
|
||||||
|
activityMap.set(d.toISOString().slice(0, 10), 0);
|
||||||
|
}
|
||||||
|
for (const g of activityRaw) {
|
||||||
|
const key = g.playedAt.toISOString().slice(0, 10);
|
||||||
|
activityMap.set(key, (activityMap.get(key) ?? 0) + 1);
|
||||||
|
}
|
||||||
|
const activity = Array.from(activityMap.entries())
|
||||||
|
.sort(([a], [b]) => a.localeCompare(b))
|
||||||
|
.map(([date, count]) => ({ date, count }));
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
totalUsers,
|
||||||
|
totalGames,
|
||||||
|
todayGames,
|
||||||
|
recentUsers,
|
||||||
|
recentGames,
|
||||||
|
modeStats,
|
||||||
|
activity,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { prisma } from "../../../../../lib/prisma";
|
||||||
|
|
||||||
|
type Params = { params: Promise<{ id: string }> };
|
||||||
|
|
||||||
|
// DELETE /api/admin/users/[id] — supprimer un utilisateur
|
||||||
|
export async function DELETE(_req: Request, { params }: Params) {
|
||||||
|
const { id } = await params;
|
||||||
|
await prisma.user.delete({ where: { id } });
|
||||||
|
return NextResponse.json({ ok: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// PATCH /api/admin/users/[id] — ban/unban
|
||||||
|
export async function PATCH(req: Request, { params }: Params) {
|
||||||
|
const { id } = await params;
|
||||||
|
const { banned } = await req.json() as { banned: boolean };
|
||||||
|
const user = await prisma.user.update({ where: { id }, data: { banned } });
|
||||||
|
return NextResponse.json({ id: user.id, banned: user.banned });
|
||||||
|
}
|
||||||
@@ -30,10 +30,6 @@ type HomeScreenProps = {
|
|||||||
setPlayerName: (v: string) => void;
|
setPlayerName: (v: string) => void;
|
||||||
joinCode: string;
|
joinCode: string;
|
||||||
setJoinCode: (v: string) => void;
|
setJoinCode: (v: string) => void;
|
||||||
maxPlayers: number;
|
|
||||||
setMaxPlayers: (v: number) => void;
|
|
||||||
totalRounds: number;
|
|
||||||
setTotalRounds: (v: number) => void;
|
|
||||||
error: string | null;
|
error: string | null;
|
||||||
setError: (v: string | null) => void;
|
setError: (v: string | null) => void;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
@@ -53,10 +49,6 @@ export function HomeScreen({
|
|||||||
setPlayerName,
|
setPlayerName,
|
||||||
joinCode,
|
joinCode,
|
||||||
setJoinCode,
|
setJoinCode,
|
||||||
maxPlayers,
|
|
||||||
setMaxPlayers,
|
|
||||||
totalRounds,
|
|
||||||
setTotalRounds,
|
|
||||||
error,
|
error,
|
||||||
setError,
|
setError,
|
||||||
loading,
|
loading,
|
||||||
@@ -155,47 +147,13 @@ export function HomeScreen({
|
|||||||
<h3 className="text-[10px] sm:text-xs font-bold text-[#888] uppercase tracking-wider">
|
<h3 className="text-[10px] sm:text-xs font-bold text-[#888] uppercase tracking-wider">
|
||||||
Multijoueur
|
Multijoueur
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex items-center justify-between gap-3">
|
<button
|
||||||
<button
|
className="w-full min-h-11 rounded-xl text-sm font-semibold bg-[#7c3aed] text-white hover:bg-[#6d28d9] disabled:opacity-50 cursor-pointer transition-colors"
|
||||||
className="flex-1 min-h-11 rounded-xl text-sm font-semibold bg-[#7c3aed] text-white hover:bg-[#6d28d9] disabled:opacity-50 cursor-pointer transition-colors"
|
onClick={onCreateRoom}
|
||||||
onClick={onCreateRoom}
|
disabled={loading}
|
||||||
disabled={loading}
|
>
|
||||||
>
|
{loading ? "Création..." : "Créer une partie"}
|
||||||
{loading ? "Création..." : "Créer une partie"}
|
</button>
|
||||||
</button>
|
|
||||||
<div className="flex items-center gap-1.5 shrink-0">
|
|
||||||
<span className="text-xs text-[#888] whitespace-nowrap">
|
|
||||||
Max
|
|
||||||
</span>
|
|
||||||
<select
|
|
||||||
value={maxPlayers}
|
|
||||||
onChange={(e) => setMaxPlayers(Number(e.target.value))}
|
|
||||||
className="min-h-11 px-2 bg-[#1a1a1a] border border-[#2e2e2e] rounded-xl text-sm text-[#f0f0f0] outline-none focus:border-[#7c3aed] cursor-pointer transition-colors"
|
|
||||||
>
|
|
||||||
{[2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16].map((n) => (
|
|
||||||
<option key={n} value={n}>
|
|
||||||
{n} joueurs
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1.5 shrink-0">
|
|
||||||
<span className="text-xs text-[#888] whitespace-nowrap">
|
|
||||||
Manches
|
|
||||||
</span>
|
|
||||||
<select
|
|
||||||
value={totalRounds}
|
|
||||||
onChange={(e) => setTotalRounds(Number(e.target.value))}
|
|
||||||
className="min-h-11 px-2 bg-[#1a1a1a] border border-[#2e2e2e] rounded-xl text-sm text-[#f0f0f0] outline-none focus:border-[#7c3aed] cursor-pointer transition-colors"
|
|
||||||
>
|
|
||||||
{[1, 2, 3, 4, 5, 7, 10].map((n) => (
|
|
||||||
<option key={n} value={n}>
|
|
||||||
{n}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Join row - colonne sur mobile, ligne sur sm+ */}
|
{/* Join row - colonne sur mobile, ligne sur sm+ */}
|
||||||
<div className="flex flex-col sm:flex-row gap-2">
|
<div className="flex flex-col sm:flex-row gap-2">
|
||||||
<input
|
<input
|
||||||
|
|||||||
+132
-20
@@ -13,13 +13,29 @@ type LobbyScreenProps = {
|
|||||||
onLeave: () => void;
|
onLeave: () => void;
|
||||||
onStart: () => void;
|
onStart: () => void;
|
||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
|
maxPlayers: number;
|
||||||
|
setMaxPlayers: (v: number) => void;
|
||||||
|
totalRounds: number;
|
||||||
|
setTotalRounds: (v: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function LobbyScreen({
|
export function LobbyScreen({
|
||||||
room, playerId, error, setError, loading, onLeave, onStart, onReset,
|
room,
|
||||||
|
playerId,
|
||||||
|
error,
|
||||||
|
setError,
|
||||||
|
loading,
|
||||||
|
onLeave,
|
||||||
|
onStart,
|
||||||
|
onReset,
|
||||||
|
maxPlayers,
|
||||||
|
setMaxPlayers,
|
||||||
|
totalRounds,
|
||||||
|
setTotalRounds,
|
||||||
}: LobbyScreenProps) {
|
}: LobbyScreenProps) {
|
||||||
const isHost = room.players.find((p) => p.id === playerId)?.isHost ?? false;
|
const isHost = room.players.find((p) => p.id === playerId)?.isHost ?? false;
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [blurred, setBlurred] = useState(true);
|
||||||
const { allowed: searchAllowed, toggle: toggleSearch } = useSearchAllowed();
|
const { allowed: searchAllowed, toggle: toggleSearch } = useSearchAllowed();
|
||||||
|
|
||||||
function copyCode() {
|
function copyCode() {
|
||||||
@@ -29,33 +45,62 @@ export function LobbyScreen({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const btnGhost = "w-full min-h-11 px-5 rounded-xl text-sm font-semibold bg-[#242424] border border-[#2e2e2e] text-[#f0f0f0] hover:bg-[#1a1a1a] cursor-pointer transition-colors";
|
const btnGhost =
|
||||||
const btnPrimary = "w-full min-h-11 px-5 rounded-xl text-sm font-semibold bg-[#7c3aed] text-white hover:bg-[#6d28d9] disabled:opacity-50 cursor-pointer transition-colors";
|
"w-full min-h-11 px-5 rounded-xl text-sm font-semibold bg-[#242424] border border-[#2e2e2e] text-[#f0f0f0] hover:bg-[#1a1a1a] cursor-pointer transition-colors";
|
||||||
|
const btnPrimary =
|
||||||
|
"w-full min-h-11 px-5 rounded-xl text-sm font-semibold bg-[#7c3aed] text-white hover:bg-[#6d28d9] disabled:opacity-50 cursor-pointer transition-colors";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-dvh w-full bg-[#0f0f0f] text-[#f0f0f0] animate-fade-in flex flex-col items-center px-4 py-5 gap-5 sm:gap-6 max-w-120 mx-auto">
|
<div className="min-h-dvh w-full bg-[#0f0f0f] text-[#f0f0f0] animate-fade-in flex flex-col items-center px-4 py-5 gap-5 sm:gap-6 max-w-120 mx-auto">
|
||||||
<button className="self-start min-h-9 px-3 rounded-lg text-sm font-semibold bg-[#242424] border border-[#2e2e2e] text-[#f0f0f0] hover:bg-[#1a1a1a] cursor-pointer" onClick={onLeave}>
|
<button
|
||||||
|
className="self-start min-h-9 px-3 rounded-lg text-sm font-semibold bg-[#242424] border border-[#2e2e2e] text-[#f0f0f0] hover:bg-[#1a1a1a] cursor-pointer"
|
||||||
|
onClick={onLeave}
|
||||||
|
>
|
||||||
Quitter
|
Quitter
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Code */}
|
{/* Code */}
|
||||||
<div className="w-full text-center bg-[#1a1a1a] border border-[#2e2e2e] rounded-xl p-5 sm:p-6">
|
<div className="w-full text-center bg-[#1a1a1a] border border-[#2e2e2e] rounded-xl p-5 sm:p-6">
|
||||||
<span className="block text-[10px] sm:text-xs text-[#888] uppercase tracking-widest mb-2">Code de la salle</span>
|
<div className="flex items-center justify-between mb-2">
|
||||||
<button className="flex items-center justify-center gap-2 mx-auto px-2 py-1 rounded-lg hover:bg-[#242424] cursor-pointer bg-transparent border-none" onClick={copyCode}>
|
<span className="text-[10px] sm:text-xs text-[#888] uppercase tracking-widest">
|
||||||
<span className="text-5xl sm:text-[clamp(42px,12vw,64px)] font-black font-mono tracking-[0.15em] text-[#7c3aed] leading-none">
|
Code de la salle
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setBlurred((b) => !b)}
|
||||||
|
className="text-[10px] sm:text-xs text-[#555] hover:text-[#888] transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
{blurred ? "Afficher" : "Masquer"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="flex items-center justify-center gap-2 mx-auto px-2 py-1 rounded-lg hover:bg-[#242424] cursor-pointer bg-transparent border-none"
|
||||||
|
onClick={copyCode}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`text-5xl sm:text-[clamp(42px,12vw,64px)] font-black font-mono tracking-[0.15em] text-[#7c3aed] leading-none transition-all duration-200 select-none ${blurred ? "blur-md" : ""}`}
|
||||||
|
>
|
||||||
{room.code}
|
{room.code}
|
||||||
</span>
|
</span>
|
||||||
<span className={`text-lg sm:text-xl leading-none transition-colors ${copied ? "text-green-400" : "text-[#888]"}`}>
|
<span
|
||||||
|
className={`text-lg sm:text-xl leading-none transition-colors ${copied ? "text-green-400" : "text-[#888]"}`}
|
||||||
|
>
|
||||||
{copied ? "✓" : "⧉"}
|
{copied ? "✓" : "⧉"}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<span className="block text-xs text-[#888] mt-2">{copied ? "Copié !" : "Clique pour copier"}</span>
|
<span className="block text-xs text-[#888] mt-2">
|
||||||
|
{copied ? "Copié !" : "Clique pour copier"}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="w-full flex items-center justify-between gap-3 bg-red-950/40 border border-red-600 text-red-300 px-3 py-2.5 rounded-xl text-xs sm:text-sm">
|
<div className="w-full flex items-center justify-between gap-3 bg-red-950/40 border border-red-600 text-red-300 px-3 py-2.5 rounded-xl text-xs sm:text-sm">
|
||||||
{error}
|
{error}
|
||||||
<button onClick={() => setError(null)} className="shrink-0 px-1.5 rounded hover:bg-white/10 cursor-pointer">✕</button>
|
<button
|
||||||
|
onClick={() => setError(null)}
|
||||||
|
className="shrink-0 px-1.5 rounded hover:bg-white/10 cursor-pointer"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -66,39 +111,106 @@ export function LobbyScreen({
|
|||||||
</h3>
|
</h3>
|
||||||
<ul className="flex flex-col gap-2">
|
<ul className="flex flex-col gap-2">
|
||||||
{room.players.map((p) => (
|
{room.players.map((p) => (
|
||||||
<li key={p.id} className={`flex items-center gap-2 bg-[#1a1a1a] rounded-xl px-3.5 py-2.5 min-h-11 border ${p.id === playerId ? "border-[#7c3aed]" : "border-[#2e2e2e]"}`}>
|
<li
|
||||||
<span className="flex-1 font-semibold text-sm truncate">{p.name}</span>
|
key={p.id}
|
||||||
{p.isHost && <span className="text-[10px] font-bold px-2 py-0.5 rounded-full bg-purple-900/40 text-purple-300 uppercase tracking-wide shrink-0">Hôte</span>}
|
className={`flex items-center gap-2 bg-[#1a1a1a] rounded-xl px-3.5 py-2.5 min-h-11 border ${p.id === playerId ? "border-[#7c3aed]" : "border-[#2e2e2e]"}`}
|
||||||
{p.id === playerId && <span className="text-[10px] font-bold px-2 py-0.5 rounded-full bg-blue-900/40 text-blue-300 uppercase tracking-wide shrink-0">Toi</span>}
|
>
|
||||||
<span className="text-sm font-bold text-[#7c3aed] shrink-0">{p.score} pts</span>
|
<span className="flex-1 font-semibold text-sm truncate">
|
||||||
|
{p.name}
|
||||||
|
</span>
|
||||||
|
{p.isHost && (
|
||||||
|
<span className="text-[10px] font-bold px-2 py-0.5 rounded-full bg-purple-900/40 text-purple-300 uppercase tracking-wide shrink-0">
|
||||||
|
Hôte
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{p.id === playerId && (
|
||||||
|
<span className="text-[10px] font-bold px-2 py-0.5 rounded-full bg-blue-900/40 text-blue-300 uppercase tracking-wide shrink-0">
|
||||||
|
Toi
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="text-sm font-bold text-[#7c3aed] shrink-0">
|
||||||
|
{p.score} pts
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isHost && (
|
||||||
|
<div className="w-full bg-[#1a1a1a] border border-[#2e2e2e] rounded-xl p-4 flex flex-col gap-3">
|
||||||
|
<h3 className="text-[10px] sm:text-xs font-bold text-[#888] uppercase tracking-wider">
|
||||||
|
Paramètres
|
||||||
|
</h3>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<div className="flex-1 flex flex-col gap-1.5">
|
||||||
|
<span className="text-xs text-[#888]">Joueurs max</span>
|
||||||
|
<select
|
||||||
|
value={maxPlayers}
|
||||||
|
onChange={(e) => setMaxPlayers(Number(e.target.value))}
|
||||||
|
className="w-full min-h-11 px-2 bg-[#0f0f0f] border border-[#2e2e2e] rounded-xl text-sm text-[#f0f0f0] outline-none focus:border-[#7c3aed] cursor-pointer transition-colors"
|
||||||
|
>
|
||||||
|
{[2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16].map((n) => (
|
||||||
|
<option key={n} value={n}>
|
||||||
|
{n} joueurs
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 flex flex-col gap-1.5">
|
||||||
|
<span className="text-xs text-[#888]">Manches</span>
|
||||||
|
<select
|
||||||
|
value={totalRounds}
|
||||||
|
onChange={(e) => setTotalRounds(Number(e.target.value))}
|
||||||
|
className="w-full min-h-11 px-2 bg-[#0f0f0f] border border-[#2e2e2e] rounded-xl text-sm text-[#f0f0f0] outline-none focus:border-[#7c3aed] cursor-pointer transition-colors"
|
||||||
|
>
|
||||||
|
{[1, 2, 3, 4, 5, 7, 10].map((n) => (
|
||||||
|
<option key={n} value={n}>
|
||||||
|
{n}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={toggleSearch}
|
onClick={toggleSearch}
|
||||||
className={`w-full min-h-11 rounded-xl text-sm font-semibold border transition-colors cursor-pointer flex items-center justify-between px-4 ${searchAllowed ? "bg-[#7c3aed]/10 border-[#7c3aed] text-[#a78bfa]" : "bg-[#1a1a1a] border-[#2e2e2e] text-[#888]"}`}
|
className={`w-full min-h-11 rounded-xl text-sm font-semibold border transition-colors cursor-pointer flex items-center justify-between px-4 ${searchAllowed ? "bg-[#7c3aed]/10 border-[#7c3aed] text-[#a78bfa]" : "bg-[#1a1a1a] border-[#2e2e2e] text-[#888]"}`}
|
||||||
>
|
>
|
||||||
<span>🔍 Recherche Ctrl+F</span>
|
<span>🔍 Recherche Ctrl+F</span>
|
||||||
<span className={`text-xs font-bold px-2 py-0.5 rounded-full ${searchAllowed ? "bg-[#7c3aed]/30 text-[#a78bfa]" : "bg-[#242424] text-[#555]"}`}>
|
<span
|
||||||
|
className={`text-xs font-bold px-2 py-0.5 rounded-full ${searchAllowed ? "bg-[#7c3aed]/30 text-[#a78bfa]" : "bg-[#242424] text-[#555]"}`}
|
||||||
|
>
|
||||||
{searchAllowed ? "Autorisée" : "Bloquée"}
|
{searchAllowed ? "Autorisée" : "Bloquée"}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{room.round > 0 && <p className="text-xs text-[#888] text-center">Manche {room.round} terminée</p>}
|
{room.round > 0 && (
|
||||||
|
<p className="text-xs text-[#888] text-center">
|
||||||
|
Manche {room.round} terminée
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
{isHost ? (
|
{isHost ? (
|
||||||
<div className="w-full flex flex-col gap-2.5">
|
<div className="w-full flex flex-col gap-2.5">
|
||||||
<button className={btnPrimary} onClick={onStart} disabled={loading}>
|
<button className={btnPrimary} onClick={onStart} disabled={loading}>
|
||||||
{loading ? "Préparation..." : room.round === 0 ? "Démarrer la partie" : "Manche suivante"}
|
{loading
|
||||||
|
? "Préparation..."
|
||||||
|
: room.round === 0
|
||||||
|
? "Démarrer la partie"
|
||||||
|
: "Manche suivante"}
|
||||||
</button>
|
</button>
|
||||||
{room.round > 0 && (
|
{room.round > 0 && (
|
||||||
<button className={btnGhost} onClick={onReset}>Nouvelle partie (reset scores)</button>
|
<button className={btnGhost} onClick={onReset}>
|
||||||
|
Nouvelle partie (reset scores)
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm text-[#888] text-center animate-pulse-slow">En attente que l'hôte démarre...</p>
|
<p className="text-sm text-[#888] text-center animate-pulse-slow">
|
||||||
|
En attente que l'hôte démarre...
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -67,8 +67,6 @@ export function ScreenRouter({
|
|||||||
<HomeScreen
|
<HomeScreen
|
||||||
playerName={playerName} setPlayerName={setPlayerName}
|
playerName={playerName} setPlayerName={setPlayerName}
|
||||||
joinCode={joinCode} setJoinCode={setJoinCode}
|
joinCode={joinCode} setJoinCode={setJoinCode}
|
||||||
maxPlayers={maxPlayers} setMaxPlayers={setMaxPlayers}
|
|
||||||
totalRounds={totalRounds} setTotalRounds={setTotalRounds}
|
|
||||||
error={error} setError={setError} loading={loading}
|
error={error} setError={setError} loading={loading}
|
||||||
onCreateRoom={handlers.handleCreateRoom}
|
onCreateRoom={handlers.handleCreateRoom}
|
||||||
onJoinRoom={handlers.handleJoinRoom}
|
onJoinRoom={handlers.handleJoinRoom}
|
||||||
@@ -108,6 +106,8 @@ export function ScreenRouter({
|
|||||||
onLeave={handlers.handleLeave}
|
onLeave={handlers.handleLeave}
|
||||||
onStart={handlers.handleStartGame}
|
onStart={handlers.handleStartGame}
|
||||||
onReset={handlers.handleResetGame}
|
onReset={handlers.handleResetGame}
|
||||||
|
maxPlayers={maxPlayers} setMaxPlayers={setMaxPlayers}
|
||||||
|
totalRounds={totalRounds} setTotalRounds={setTotalRounds}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -60,25 +60,35 @@ export default function MentionsLegalesPage() {
|
|||||||
</h2>
|
</h2>
|
||||||
<div className="bg-[#1a1a1a] border border-[#2e2e2e] rounded-xl p-4 flex flex-col gap-1.5 text-sm">
|
<div className="bg-[#1a1a1a] border border-[#2e2e2e] rounded-xl p-4 flex flex-col gap-1.5 text-sm">
|
||||||
<p>
|
<p>
|
||||||
<span className="text-[#888]">Raison sociale :</span>{" "}
|
<span className="text-[#888]">Raison sociale :</span> SAS
|
||||||
QuantumCraft Studios
|
SAPINET
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span className="text-[#888]">SIRET :</span> 932 107 758 00017
|
<span className="text-[#888]">SIREN :</span> 899 483 457 (RCS
|
||||||
|
de Nanterre)
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span className="text-[#888]">Adresse :</span> 58 Rue de
|
<span className="text-[#888]">Adresse :</span> 65 rue de la
|
||||||
Monceau, 75008 Paris, France
|
Croix, 92000 Nanterre
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span className="text-[#888]">Contact :</span>{" "}
|
||||||
|
<a
|
||||||
|
href="mailto:contact@sapi.net"
|
||||||
|
className="text-[#7c3aed] hover:underline"
|
||||||
|
>
|
||||||
|
contact@sapi.net
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span className="text-[#888]">Site web :</span>{" "}
|
<span className="text-[#888]">Site web :</span>{" "}
|
||||||
<a
|
<a
|
||||||
href="https://www.quantumcraft-studios.com"
|
href="https://sapi.net"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-[#7c3aed] hover:underline"
|
className="text-[#7c3aed] hover:underline"
|
||||||
>
|
>
|
||||||
quantumcraft-studios.com
|
sapi.net
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
|||||||
user.password,
|
user.password,
|
||||||
);
|
);
|
||||||
if (!valid) return null;
|
if (!valid) return null;
|
||||||
|
if (user.banned) return null;
|
||||||
return { id: user.id, name: user.name, email: user.email };
|
return { id: user.id, name: user.name, email: user.email };
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -31,11 +32,17 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
|||||||
},
|
},
|
||||||
callbacks: {
|
callbacks: {
|
||||||
jwt({ token, user }) {
|
jwt({ token, user }) {
|
||||||
if (user) token.id = user.id;
|
if (user) {
|
||||||
|
token.id = user.id;
|
||||||
|
token.email = user.email;
|
||||||
|
}
|
||||||
return token;
|
return token;
|
||||||
},
|
},
|
||||||
session({ session, token }) {
|
session({ session, token }) {
|
||||||
if (session.user) session.user.id = token.id as string;
|
if (session.user) {
|
||||||
|
session.user.id = token.id as string;
|
||||||
|
session.user.email = token.email as string;
|
||||||
|
}
|
||||||
return session;
|
return session;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ export type StringFilter<$PrismaModel = never> = {
|
|||||||
not?: Prisma.NestedStringFilter<$PrismaModel> | string
|
not?: Prisma.NestedStringFilter<$PrismaModel> | string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type BoolFilter<$PrismaModel = never> = {
|
||||||
|
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
|
||||||
|
}
|
||||||
|
|
||||||
export type DateTimeFilter<$PrismaModel = never> = {
|
export type DateTimeFilter<$PrismaModel = never> = {
|
||||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
in?: Date[] | string[]
|
in?: Date[] | string[]
|
||||||
@@ -56,6 +61,14 @@ export type StringWithAggregatesFilter<$PrismaModel = never> = {
|
|||||||
_max?: Prisma.NestedStringFilter<$PrismaModel>
|
_max?: Prisma.NestedStringFilter<$PrismaModel>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type BoolWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
in?: Date[] | string[]
|
in?: Date[] | string[]
|
||||||
@@ -92,11 +105,6 @@ export type FloatFilter<$PrismaModel = never> = {
|
|||||||
not?: Prisma.NestedFloatFilter<$PrismaModel> | number
|
not?: Prisma.NestedFloatFilter<$PrismaModel> | number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BoolFilter<$PrismaModel = never> = {
|
|
||||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
|
||||||
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export type IntWithAggregatesFilter<$PrismaModel = never> = {
|
export type IntWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
in?: number[]
|
in?: number[]
|
||||||
@@ -129,14 +137,6 @@ export type FloatWithAggregatesFilter<$PrismaModel = never> = {
|
|||||||
_max?: Prisma.NestedFloatFilter<$PrismaModel>
|
_max?: Prisma.NestedFloatFilter<$PrismaModel>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BoolWithAggregatesFilter<$PrismaModel = never> = {
|
|
||||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
|
||||||
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
|
|
||||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
|
||||||
_min?: Prisma.NestedBoolFilter<$PrismaModel>
|
|
||||||
_max?: Prisma.NestedBoolFilter<$PrismaModel>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NestedStringFilter<$PrismaModel = never> = {
|
export type NestedStringFilter<$PrismaModel = never> = {
|
||||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
in?: string[]
|
in?: string[]
|
||||||
@@ -151,6 +151,11 @@ export type NestedStringFilter<$PrismaModel = never> = {
|
|||||||
not?: Prisma.NestedStringFilter<$PrismaModel> | string
|
not?: Prisma.NestedStringFilter<$PrismaModel> | string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NestedBoolFilter<$PrismaModel = never> = {
|
||||||
|
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
|
||||||
|
}
|
||||||
|
|
||||||
export type NestedDateTimeFilter<$PrismaModel = never> = {
|
export type NestedDateTimeFilter<$PrismaModel = never> = {
|
||||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
in?: Date[] | string[]
|
in?: Date[] | string[]
|
||||||
@@ -190,6 +195,14 @@ export type NestedIntFilter<$PrismaModel = never> = {
|
|||||||
not?: Prisma.NestedIntFilter<$PrismaModel> | number
|
not?: Prisma.NestedIntFilter<$PrismaModel> | number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NestedBoolWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||||
in?: Date[] | string[]
|
in?: Date[] | string[]
|
||||||
@@ -215,11 +228,6 @@ export type NestedFloatFilter<$PrismaModel = never> = {
|
|||||||
not?: Prisma.NestedFloatFilter<$PrismaModel> | number
|
not?: Prisma.NestedFloatFilter<$PrismaModel> | number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NestedBoolFilter<$PrismaModel = never> = {
|
|
||||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
|
||||||
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NestedIntWithAggregatesFilter<$PrismaModel = never> = {
|
export type NestedIntWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||||
in?: number[]
|
in?: number[]
|
||||||
@@ -252,12 +260,4 @@ export type NestedFloatWithAggregatesFilter<$PrismaModel = never> = {
|
|||||||
_max?: Prisma.NestedFloatFilter<$PrismaModel>
|
_max?: Prisma.NestedFloatFilter<$PrismaModel>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NestedBoolWithAggregatesFilter<$PrismaModel = never> = {
|
|
||||||
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
|
||||||
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
|
|
||||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
|
||||||
_min?: Prisma.NestedBoolFilter<$PrismaModel>
|
|
||||||
_max?: Prisma.NestedBoolFilter<$PrismaModel>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -744,6 +744,7 @@ export const UserScalarFieldEnum = {
|
|||||||
name: 'name',
|
name: 'name',
|
||||||
email: 'email',
|
email: 'email',
|
||||||
password: 'password',
|
password: 'password',
|
||||||
|
banned: 'banned',
|
||||||
createdAt: 'createdAt'
|
createdAt: 'createdAt'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
@@ -811,6 +812,13 @@ export type StringFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel,
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to a field of type 'Boolean'
|
||||||
|
*/
|
||||||
|
export type BooleanFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Boolean'>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reference to a field of type 'DateTime'
|
* Reference to a field of type 'DateTime'
|
||||||
*/
|
*/
|
||||||
@@ -831,13 +839,6 @@ export type IntFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'In
|
|||||||
export type FloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float'>
|
export type FloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float'>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reference to a field of type 'Boolean'
|
|
||||||
*/
|
|
||||||
export type BooleanFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Boolean'>
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Batch Payload for updateMany & deleteMany & createMany
|
* Batch Payload for updateMany & deleteMany & createMany
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ export const UserScalarFieldEnum = {
|
|||||||
name: 'name',
|
name: 'name',
|
||||||
email: 'email',
|
email: 'email',
|
||||||
password: 'password',
|
password: 'password',
|
||||||
|
banned: 'banned',
|
||||||
createdAt: 'createdAt'
|
createdAt: 'createdAt'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
|||||||
@@ -535,10 +535,6 @@ export type FloatFieldUpdateOperationsInput = {
|
|||||||
divide?: number
|
divide?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BoolFieldUpdateOperationsInput = {
|
|
||||||
set?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export type GameCreateWithoutUserInput = {
|
export type GameCreateWithoutUserInput = {
|
||||||
id?: string
|
id?: string
|
||||||
mode: string
|
mode: string
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export type UserMinAggregateOutputType = {
|
|||||||
name: string | null
|
name: string | null
|
||||||
email: string | null
|
email: string | null
|
||||||
password: string | null
|
password: string | null
|
||||||
|
banned: boolean | null
|
||||||
createdAt: Date | null
|
createdAt: Date | null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ export type UserMaxAggregateOutputType = {
|
|||||||
name: string | null
|
name: string | null
|
||||||
email: string | null
|
email: string | null
|
||||||
password: string | null
|
password: string | null
|
||||||
|
banned: boolean | null
|
||||||
createdAt: Date | null
|
createdAt: Date | null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,6 +47,7 @@ export type UserCountAggregateOutputType = {
|
|||||||
name: number
|
name: number
|
||||||
email: number
|
email: number
|
||||||
password: number
|
password: number
|
||||||
|
banned: number
|
||||||
createdAt: number
|
createdAt: number
|
||||||
_all: number
|
_all: number
|
||||||
}
|
}
|
||||||
@@ -55,6 +58,7 @@ export type UserMinAggregateInputType = {
|
|||||||
name?: true
|
name?: true
|
||||||
email?: true
|
email?: true
|
||||||
password?: true
|
password?: true
|
||||||
|
banned?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +67,7 @@ export type UserMaxAggregateInputType = {
|
|||||||
name?: true
|
name?: true
|
||||||
email?: true
|
email?: true
|
||||||
password?: true
|
password?: true
|
||||||
|
banned?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,6 +76,7 @@ export type UserCountAggregateInputType = {
|
|||||||
name?: true
|
name?: true
|
||||||
email?: true
|
email?: true
|
||||||
password?: true
|
password?: true
|
||||||
|
banned?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
_all?: true
|
_all?: true
|
||||||
}
|
}
|
||||||
@@ -152,6 +158,7 @@ export type UserGroupByOutputType = {
|
|||||||
name: string
|
name: string
|
||||||
email: string
|
email: string
|
||||||
password: string
|
password: string
|
||||||
|
banned: boolean
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
_count: UserCountAggregateOutputType | null
|
_count: UserCountAggregateOutputType | null
|
||||||
_min: UserMinAggregateOutputType | null
|
_min: UserMinAggregateOutputType | null
|
||||||
@@ -181,6 +188,7 @@ export type UserWhereInput = {
|
|||||||
name?: Prisma.StringFilter<"User"> | string
|
name?: Prisma.StringFilter<"User"> | string
|
||||||
email?: Prisma.StringFilter<"User"> | string
|
email?: Prisma.StringFilter<"User"> | string
|
||||||
password?: Prisma.StringFilter<"User"> | string
|
password?: Prisma.StringFilter<"User"> | string
|
||||||
|
banned?: Prisma.BoolFilter<"User"> | boolean
|
||||||
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
||||||
games?: Prisma.GameListRelationFilter
|
games?: Prisma.GameListRelationFilter
|
||||||
dailyResults?: Prisma.DailyResultListRelationFilter
|
dailyResults?: Prisma.DailyResultListRelationFilter
|
||||||
@@ -191,6 +199,7 @@ export type UserOrderByWithRelationInput = {
|
|||||||
name?: Prisma.SortOrder
|
name?: Prisma.SortOrder
|
||||||
email?: Prisma.SortOrder
|
email?: Prisma.SortOrder
|
||||||
password?: Prisma.SortOrder
|
password?: Prisma.SortOrder
|
||||||
|
banned?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
games?: Prisma.GameOrderByRelationAggregateInput
|
games?: Prisma.GameOrderByRelationAggregateInput
|
||||||
dailyResults?: Prisma.DailyResultOrderByRelationAggregateInput
|
dailyResults?: Prisma.DailyResultOrderByRelationAggregateInput
|
||||||
@@ -204,6 +213,7 @@ export type UserWhereUniqueInput = Prisma.AtLeast<{
|
|||||||
NOT?: Prisma.UserWhereInput | Prisma.UserWhereInput[]
|
NOT?: Prisma.UserWhereInput | Prisma.UserWhereInput[]
|
||||||
name?: Prisma.StringFilter<"User"> | string
|
name?: Prisma.StringFilter<"User"> | string
|
||||||
password?: Prisma.StringFilter<"User"> | string
|
password?: Prisma.StringFilter<"User"> | string
|
||||||
|
banned?: Prisma.BoolFilter<"User"> | boolean
|
||||||
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
||||||
games?: Prisma.GameListRelationFilter
|
games?: Prisma.GameListRelationFilter
|
||||||
dailyResults?: Prisma.DailyResultListRelationFilter
|
dailyResults?: Prisma.DailyResultListRelationFilter
|
||||||
@@ -214,6 +224,7 @@ export type UserOrderByWithAggregationInput = {
|
|||||||
name?: Prisma.SortOrder
|
name?: Prisma.SortOrder
|
||||||
email?: Prisma.SortOrder
|
email?: Prisma.SortOrder
|
||||||
password?: Prisma.SortOrder
|
password?: Prisma.SortOrder
|
||||||
|
banned?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
_count?: Prisma.UserCountOrderByAggregateInput
|
_count?: Prisma.UserCountOrderByAggregateInput
|
||||||
_max?: Prisma.UserMaxOrderByAggregateInput
|
_max?: Prisma.UserMaxOrderByAggregateInput
|
||||||
@@ -228,6 +239,7 @@ export type UserScalarWhereWithAggregatesInput = {
|
|||||||
name?: Prisma.StringWithAggregatesFilter<"User"> | string
|
name?: Prisma.StringWithAggregatesFilter<"User"> | string
|
||||||
email?: Prisma.StringWithAggregatesFilter<"User"> | string
|
email?: Prisma.StringWithAggregatesFilter<"User"> | string
|
||||||
password?: Prisma.StringWithAggregatesFilter<"User"> | string
|
password?: Prisma.StringWithAggregatesFilter<"User"> | string
|
||||||
|
banned?: Prisma.BoolWithAggregatesFilter<"User"> | boolean
|
||||||
createdAt?: Prisma.DateTimeWithAggregatesFilter<"User"> | Date | string
|
createdAt?: Prisma.DateTimeWithAggregatesFilter<"User"> | Date | string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,6 +248,7 @@ export type UserCreateInput = {
|
|||||||
name: string
|
name: string
|
||||||
email: string
|
email: string
|
||||||
password: string
|
password: string
|
||||||
|
banned?: boolean
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
games?: Prisma.GameCreateNestedManyWithoutUserInput
|
games?: Prisma.GameCreateNestedManyWithoutUserInput
|
||||||
dailyResults?: Prisma.DailyResultCreateNestedManyWithoutUserInput
|
dailyResults?: Prisma.DailyResultCreateNestedManyWithoutUserInput
|
||||||
@@ -246,6 +259,7 @@ export type UserUncheckedCreateInput = {
|
|||||||
name: string
|
name: string
|
||||||
email: string
|
email: string
|
||||||
password: string
|
password: string
|
||||||
|
banned?: boolean
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
games?: Prisma.GameUncheckedCreateNestedManyWithoutUserInput
|
games?: Prisma.GameUncheckedCreateNestedManyWithoutUserInput
|
||||||
dailyResults?: Prisma.DailyResultUncheckedCreateNestedManyWithoutUserInput
|
dailyResults?: Prisma.DailyResultUncheckedCreateNestedManyWithoutUserInput
|
||||||
@@ -256,6 +270,7 @@ export type UserUpdateInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
email?: Prisma.StringFieldUpdateOperationsInput | string
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
password?: Prisma.StringFieldUpdateOperationsInput | string
|
password?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
games?: Prisma.GameUpdateManyWithoutUserNestedInput
|
games?: Prisma.GameUpdateManyWithoutUserNestedInput
|
||||||
dailyResults?: Prisma.DailyResultUpdateManyWithoutUserNestedInput
|
dailyResults?: Prisma.DailyResultUpdateManyWithoutUserNestedInput
|
||||||
@@ -266,6 +281,7 @@ export type UserUncheckedUpdateInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
email?: Prisma.StringFieldUpdateOperationsInput | string
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
password?: Prisma.StringFieldUpdateOperationsInput | string
|
password?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
games?: Prisma.GameUncheckedUpdateManyWithoutUserNestedInput
|
games?: Prisma.GameUncheckedUpdateManyWithoutUserNestedInput
|
||||||
dailyResults?: Prisma.DailyResultUncheckedUpdateManyWithoutUserNestedInput
|
dailyResults?: Prisma.DailyResultUncheckedUpdateManyWithoutUserNestedInput
|
||||||
@@ -276,6 +292,7 @@ export type UserCreateManyInput = {
|
|||||||
name: string
|
name: string
|
||||||
email: string
|
email: string
|
||||||
password: string
|
password: string
|
||||||
|
banned?: boolean
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,6 +301,7 @@ export type UserUpdateManyMutationInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
email?: Prisma.StringFieldUpdateOperationsInput | string
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
password?: Prisma.StringFieldUpdateOperationsInput | string
|
password?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,6 +310,7 @@ export type UserUncheckedUpdateManyInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
email?: Prisma.StringFieldUpdateOperationsInput | string
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
password?: Prisma.StringFieldUpdateOperationsInput | string
|
password?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,6 +319,7 @@ export type UserCountOrderByAggregateInput = {
|
|||||||
name?: Prisma.SortOrder
|
name?: Prisma.SortOrder
|
||||||
email?: Prisma.SortOrder
|
email?: Prisma.SortOrder
|
||||||
password?: Prisma.SortOrder
|
password?: Prisma.SortOrder
|
||||||
|
banned?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,6 +328,7 @@ export type UserMaxOrderByAggregateInput = {
|
|||||||
name?: Prisma.SortOrder
|
name?: Prisma.SortOrder
|
||||||
email?: Prisma.SortOrder
|
email?: Prisma.SortOrder
|
||||||
password?: Prisma.SortOrder
|
password?: Prisma.SortOrder
|
||||||
|
banned?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,6 +337,7 @@ export type UserMinOrderByAggregateInput = {
|
|||||||
name?: Prisma.SortOrder
|
name?: Prisma.SortOrder
|
||||||
email?: Prisma.SortOrder
|
email?: Prisma.SortOrder
|
||||||
password?: Prisma.SortOrder
|
password?: Prisma.SortOrder
|
||||||
|
banned?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,6 +350,10 @@ export type StringFieldUpdateOperationsInput = {
|
|||||||
set?: string
|
set?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type BoolFieldUpdateOperationsInput = {
|
||||||
|
set?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export type DateTimeFieldUpdateOperationsInput = {
|
export type DateTimeFieldUpdateOperationsInput = {
|
||||||
set?: Date | string
|
set?: Date | string
|
||||||
}
|
}
|
||||||
@@ -365,6 +391,7 @@ export type UserCreateWithoutGamesInput = {
|
|||||||
name: string
|
name: string
|
||||||
email: string
|
email: string
|
||||||
password: string
|
password: string
|
||||||
|
banned?: boolean
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
dailyResults?: Prisma.DailyResultCreateNestedManyWithoutUserInput
|
dailyResults?: Prisma.DailyResultCreateNestedManyWithoutUserInput
|
||||||
}
|
}
|
||||||
@@ -374,6 +401,7 @@ export type UserUncheckedCreateWithoutGamesInput = {
|
|||||||
name: string
|
name: string
|
||||||
email: string
|
email: string
|
||||||
password: string
|
password: string
|
||||||
|
banned?: boolean
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
dailyResults?: Prisma.DailyResultUncheckedCreateNestedManyWithoutUserInput
|
dailyResults?: Prisma.DailyResultUncheckedCreateNestedManyWithoutUserInput
|
||||||
}
|
}
|
||||||
@@ -399,6 +427,7 @@ export type UserUpdateWithoutGamesInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
email?: Prisma.StringFieldUpdateOperationsInput | string
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
password?: Prisma.StringFieldUpdateOperationsInput | string
|
password?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
dailyResults?: Prisma.DailyResultUpdateManyWithoutUserNestedInput
|
dailyResults?: Prisma.DailyResultUpdateManyWithoutUserNestedInput
|
||||||
}
|
}
|
||||||
@@ -408,6 +437,7 @@ export type UserUncheckedUpdateWithoutGamesInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
email?: Prisma.StringFieldUpdateOperationsInput | string
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
password?: Prisma.StringFieldUpdateOperationsInput | string
|
password?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
dailyResults?: Prisma.DailyResultUncheckedUpdateManyWithoutUserNestedInput
|
dailyResults?: Prisma.DailyResultUncheckedUpdateManyWithoutUserNestedInput
|
||||||
}
|
}
|
||||||
@@ -417,6 +447,7 @@ export type UserCreateWithoutDailyResultsInput = {
|
|||||||
name: string
|
name: string
|
||||||
email: string
|
email: string
|
||||||
password: string
|
password: string
|
||||||
|
banned?: boolean
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
games?: Prisma.GameCreateNestedManyWithoutUserInput
|
games?: Prisma.GameCreateNestedManyWithoutUserInput
|
||||||
}
|
}
|
||||||
@@ -426,6 +457,7 @@ export type UserUncheckedCreateWithoutDailyResultsInput = {
|
|||||||
name: string
|
name: string
|
||||||
email: string
|
email: string
|
||||||
password: string
|
password: string
|
||||||
|
banned?: boolean
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
games?: Prisma.GameUncheckedCreateNestedManyWithoutUserInput
|
games?: Prisma.GameUncheckedCreateNestedManyWithoutUserInput
|
||||||
}
|
}
|
||||||
@@ -451,6 +483,7 @@ export type UserUpdateWithoutDailyResultsInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
email?: Prisma.StringFieldUpdateOperationsInput | string
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
password?: Prisma.StringFieldUpdateOperationsInput | string
|
password?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
games?: Prisma.GameUpdateManyWithoutUserNestedInput
|
games?: Prisma.GameUpdateManyWithoutUserNestedInput
|
||||||
}
|
}
|
||||||
@@ -460,6 +493,7 @@ export type UserUncheckedUpdateWithoutDailyResultsInput = {
|
|||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
email?: Prisma.StringFieldUpdateOperationsInput | string
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
password?: Prisma.StringFieldUpdateOperationsInput | string
|
password?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
games?: Prisma.GameUncheckedUpdateManyWithoutUserNestedInput
|
games?: Prisma.GameUncheckedUpdateManyWithoutUserNestedInput
|
||||||
}
|
}
|
||||||
@@ -509,6 +543,7 @@ export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = r
|
|||||||
name?: boolean
|
name?: boolean
|
||||||
email?: boolean
|
email?: boolean
|
||||||
password?: boolean
|
password?: boolean
|
||||||
|
banned?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
games?: boolean | Prisma.User$gamesArgs<ExtArgs>
|
games?: boolean | Prisma.User$gamesArgs<ExtArgs>
|
||||||
dailyResults?: boolean | Prisma.User$dailyResultsArgs<ExtArgs>
|
dailyResults?: boolean | Prisma.User$dailyResultsArgs<ExtArgs>
|
||||||
@@ -520,6 +555,7 @@ export type UserSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensio
|
|||||||
name?: boolean
|
name?: boolean
|
||||||
email?: boolean
|
email?: boolean
|
||||||
password?: boolean
|
password?: boolean
|
||||||
|
banned?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
}, ExtArgs["result"]["user"]>
|
}, ExtArgs["result"]["user"]>
|
||||||
|
|
||||||
@@ -528,6 +564,7 @@ export type UserSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensio
|
|||||||
name?: boolean
|
name?: boolean
|
||||||
email?: boolean
|
email?: boolean
|
||||||
password?: boolean
|
password?: boolean
|
||||||
|
banned?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
}, ExtArgs["result"]["user"]>
|
}, ExtArgs["result"]["user"]>
|
||||||
|
|
||||||
@@ -536,10 +573,11 @@ export type UserSelectScalar = {
|
|||||||
name?: boolean
|
name?: boolean
|
||||||
email?: boolean
|
email?: boolean
|
||||||
password?: boolean
|
password?: boolean
|
||||||
|
banned?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "email" | "password" | "createdAt", ExtArgs["result"]["user"]>
|
export type UserOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "email" | "password" | "banned" | "createdAt", ExtArgs["result"]["user"]>
|
||||||
export type UserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type UserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
games?: boolean | Prisma.User$gamesArgs<ExtArgs>
|
games?: boolean | Prisma.User$gamesArgs<ExtArgs>
|
||||||
dailyResults?: boolean | Prisma.User$dailyResultsArgs<ExtArgs>
|
dailyResults?: boolean | Prisma.User$dailyResultsArgs<ExtArgs>
|
||||||
@@ -559,6 +597,7 @@ export type $UserPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
|||||||
name: string
|
name: string
|
||||||
email: string
|
email: string
|
||||||
password: string
|
password: string
|
||||||
|
banned: boolean
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
}, ExtArgs["result"]["user"]>
|
}, ExtArgs["result"]["user"]>
|
||||||
composites: {}
|
composites: {}
|
||||||
@@ -989,6 +1028,7 @@ export interface UserFieldRefs {
|
|||||||
readonly name: Prisma.FieldRef<"User", 'String'>
|
readonly name: Prisma.FieldRef<"User", 'String'>
|
||||||
readonly email: Prisma.FieldRef<"User", 'String'>
|
readonly email: Prisma.FieldRef<"User", 'String'>
|
||||||
readonly password: Prisma.FieldRef<"User", 'String'>
|
readonly password: Prisma.FieldRef<"User", 'String'>
|
||||||
|
readonly banned: Prisma.FieldRef<"User", 'Boolean'>
|
||||||
readonly createdAt: Prisma.FieldRef<"User", 'DateTime'>
|
readonly createdAt: Prisma.FieldRef<"User", 'DateTime'>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,10 @@
|
|||||||
"postinstall": "npx node-gyp rebuild --directory node_modules/.pnpm/better-sqlite3@12.8.0/node_modules/better-sqlite3 2>/dev/null || true"
|
"postinstall": "npx node-gyp rebuild --directory node_modules/.pnpm/better-sqlite3@12.8.0/node_modules/better-sqlite3 2>/dev/null || true"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@panva/hkdf": "^1.2.1",
|
||||||
"@prisma/client": "^7.7.0",
|
"@prisma/client": "^7.7.0",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
|
"jose": "^6.2.2",
|
||||||
"next": "16.2.3",
|
"next": "16.2.3",
|
||||||
"next-auth": "5.0.0-beta.30",
|
"next-auth": "5.0.0-beta.30",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
|
|||||||
Generated
+6
@@ -8,12 +8,18 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@panva/hkdf':
|
||||||
|
specifier: ^1.2.1
|
||||||
|
version: 1.2.1
|
||||||
'@prisma/client':
|
'@prisma/client':
|
||||||
specifier: ^7.7.0
|
specifier: ^7.7.0
|
||||||
version: 7.7.0(prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(better-sqlite3@12.8.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3)
|
version: 7.7.0(prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(better-sqlite3@12.8.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3)
|
||||||
bcryptjs:
|
bcryptjs:
|
||||||
specifier: ^3.0.3
|
specifier: ^3.0.3
|
||||||
version: 3.0.3
|
version: 3.0.3
|
||||||
|
jose:
|
||||||
|
specifier: ^6.2.2
|
||||||
|
version: 6.2.2
|
||||||
next:
|
next:
|
||||||
specifier: 16.2.3
|
specifier: 16.2.3
|
||||||
version: 16.2.3(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 16.2.3(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
-- RedefineTables
|
||||||
|
PRAGMA defer_foreign_keys=ON;
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_User" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"password" TEXT NOT NULL,
|
||||||
|
"banned" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
INSERT INTO "new_User" ("createdAt", "email", "id", "name", "password") SELECT "createdAt", "email", "id", "name", "password" FROM "User";
|
||||||
|
DROP TABLE "User";
|
||||||
|
ALTER TABLE "new_User" RENAME TO "User";
|
||||||
|
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
|
PRAGMA defer_foreign_keys=OFF;
|
||||||
@@ -12,6 +12,7 @@ model User {
|
|||||||
name String
|
name String
|
||||||
email String @unique
|
email String @unique
|
||||||
password String
|
password String
|
||||||
|
banned Boolean @default(false)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
games Game[]
|
games Game[]
|
||||||
dailyResults DailyResult[]
|
dailyResults DailyResult[]
|
||||||
|
|||||||
@@ -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*"],
|
||||||
|
};
|
||||||
BIN
Binary file not shown.
Reference in New Issue
Block a user