Lot 5 : changement de magasin instantané et contrôle des droits

Grille
- Corrige le lot 4 : au changement de magasin, les chiffres et le tri
  affichés suivent tout de suite (nouvelle liste), la sélection est gardée.
- Lignes toujours chargées « Nos 2 magasins » ; changement de magasin sans
  aller-retour serveur (URL mise à jour par l'historique du navigateur).
- Complément de l'API FF par magasin servi à part (/api/grid/rows/store-patch),
  appliqué à son arrivée, calcul identique à l'ancien (lib/store-patch.ts).
- La recherche ne reconstruit plus toutes les lignes à chaque frappe ; la
  sélection n'est remise à zéro qu'au changement de fournisseur.

Sécurité
- lib/authz.ts : session relue en base (cache 1 min), rôle de la base
  prioritaire ; variantes pour actions, routes et pages.
- Paramètres réservés aux administrateurs ; session vérifiée dans les actions
  Grille, Commandes, Historique et les routes ; diagnostics réservés admin.
- Mots de passe et clés d'IA plus envoyés au navigateur ; champ vide = mot de
  passe conservé ; plus de mot de passe dans le localStorage ; l'enregistrement
  de la base n'efface plus les clés d'IA.
- admin/admin créé seulement si aucun utilisateur ; fichier de secours utilisé
  uniquement si la base est injoignable ; data/ ignoré par git.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AcA129WzGTH6zxPHFBGWmn
This commit is contained in:
Claude committed 2026-10-04 06:39:49 +00:00
1 parent b7ee413d67
commit 26b9364232
44 files changed
+1155 -531

No files matched your search

+6 -14
View File
@@ -11,18 +11,10 @@
import { db } from "@/db";
import { apiKeys } from "@/db/schema";
import { desc, eq } from "drizzle-orm";
import { auth } from "@/lib/auth";
import { requireAdmin } from "@/lib/authz";
import { generateApiKey } from "@/lib/api-auth";
import { revalidatePath } from "next/cache";
async function ensureAdmin() {
const session = await auth();
if ((session?.user as { role?: string } | undefined)?.role !== "admin") {
throw new Error("Accès refusé : Droits administrateur requis.");
}
return session;
}
export interface ApiKeyRow {
id: number;
name: string;
@@ -36,7 +28,7 @@ export interface ApiKeyRow {
/** Liste les clés. Ne renvoie jamais de secret — seulement le préfixe lisible. */
export async function getApiKeys(): Promise<ApiKeyRow[]> {
await ensureAdmin();
await requireAdmin();
const rows = await db
.select({
id: apiKeys.id,
@@ -67,7 +59,7 @@ export async function createApiKey(
name: string,
role: "admin" | "user" = "user",
): Promise<{ success: true; key: string; keyPrefix: string } | { success: false; error: string }> {
const session = await ensureAdmin();
const moi = await requireAdmin();
const trimmed = name.trim();
if (!trimmed) return { success: false, error: "Le nom de la clé est obligatoire." };
@@ -80,7 +72,7 @@ export async function createApiKey(
keyPrefix,
keyHash,
role,
createdBy: (session?.user as { name?: string | null } | undefined)?.name ?? null,
createdBy: moi.username || null,
});
revalidatePath("/settings");
return { success: true, key, keyPrefix };
@@ -92,7 +84,7 @@ export async function createApiKey(
/** Révoque une clé : elle est refusée dès l'appel suivant, mais reste listée. */
export async function revokeApiKey(id: number): Promise<{ success: boolean; error?: string }> {
await ensureAdmin();
await requireAdmin();
try {
await db.update(apiKeys).set({ revokedAt: new Date() }).where(eq(apiKeys.id, id));
revalidatePath("/settings");
@@ -105,7 +97,7 @@ export async function revokeApiKey(id: number): Promise<{ success: boolean; erro
/** Supprime définitivement une clé révoquée (nettoyage de la liste). */
export async function deleteApiKey(id: number): Promise<{ success: boolean; error?: string }> {
await ensureAdmin();
await requireAdmin();
try {
await db.delete(apiKeys).where(eq(apiKeys.id, id));
revalidatePath("/settings");
+8 -20
View File
@@ -4,24 +4,14 @@ import { db } from "@/db";
import { users } from "@/db/schema";
import { eq } from "drizzle-orm";
import { hashPassword } from "@/features/auth/logic/auth-logic";
import { auth } from "@/lib/auth";
import { requireAdmin } from "@/lib/authz";
import { revalidatePath } from "next/cache";
/**
* Vérifie si l'utilisateur actuel est un administrateur.
*/
async function ensureAdmin() {
const session = await auth();
if ((session?.user as any)?.role !== "admin") {
throw new Error("Accès refusé : Droits administrateur requis.");
}
}
/**
* Récupère tous les utilisateurs (Admin seulement).
*/
export async function getUsers() {
await ensureAdmin();
await requireAdmin();
return db.select({
id: users.id,
username: users.username,
@@ -34,7 +24,7 @@ export async function getUsers() {
* Crée un nouvel utilisateur.
*/
export async function createUser(username: string, password: string, role: "admin" | "user" = "user") {
await ensureAdmin();
await requireAdmin();
try {
await db.insert(users).values({
@@ -54,10 +44,8 @@ export async function createUser(username: string, password: string, role: "admi
* Supprime un utilisateur.
*/
export async function deleteUser(id: number) {
await ensureAdmin();
const session = await auth();
if (Number((session?.user as any)?.id) === id) {
const moi = await requireAdmin();
if (Number(moi.id) === id) {
return { success: false, error: "Vous ne pouvez pas supprimer votre propre compte." };
}
@@ -65,7 +53,7 @@ export async function deleteUser(id: number) {
await db.delete(users).where(eq(users.id, id));
revalidatePath("/settings");
return { success: true };
} catch (err) {
} catch {
return { success: false, error: "Erreur lors de la suppression." };
}
}
@@ -74,13 +62,13 @@ export async function deleteUser(id: number) {
* Met à jour le mot de passe d'un utilisateur.
*/
export async function updatePassword(id: number, newPassword: string) {
await ensureAdmin();
await requireAdmin();
try {
await db.update(users)
.set({ passwordHash: hashPassword(newPassword) })
.where(eq(users.id, id));
return { success: true };
} catch (err) {
} catch {
return { success: false, error: "Erreur lors de la mise à jour du mot de passe." };
}
}