refactor: déduplication — helpers actions, parseurs partagés, types auth

- Crée src/lib/action-helpers.ts avec ActionResult, requireAuth(),
  requireEvaluationAccess() — type et pattern dupliqués 3× supprimés
- evaluations.ts, share.ts, admin.ts importent depuis action-helpers;
  admin.ts: "Forbidden" → "Accès refusé" pour cohérence
- parseQuestions/parseRubric exportées depuis export-utils et supprimées
  de DimensionCard (copie exacte retirée)
- next-auth.d.ts: Session.user.role passe de optional à required string

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-25 13:43:57 +01:00
parent ebd8573299
commit 2d8d59322d
7 changed files with 48 additions and 61 deletions

View File

@@ -1,14 +1,14 @@
"use server";
import { auth } from "@/auth";
import { prisma } from "@/lib/db";
import { requireAuth, type ActionResult } from "@/lib/action-helpers";
import { revalidatePath } from "next/cache";
export type ActionResult<T = void> = { success: true; data?: T } | { success: false; error: string };
export type { ActionResult };
export async function setUserRole(userId: string, role: "admin" | "evaluator"): Promise<ActionResult> {
const session = await auth();
if (session?.user?.role !== "admin") return { success: false, error: "Forbidden" };
const session = await requireAuth();
if (!session || session.user.role !== "admin") return { success: false, error: "Accès refusé" };
if (!role || !["admin", "evaluator"].includes(role)) {
return { success: false, error: "Rôle invalide (admin | evaluator)" };
@@ -25,8 +25,8 @@ export async function setUserRole(userId: string, role: "admin" | "evaluator"):
}
export async function deleteUser(userId: string): Promise<ActionResult> {
const session = await auth();
if (session?.user?.role !== "admin") return { success: false, error: "Forbidden" };
const session = await requireAuth();
if (!session || session.user.role !== "admin") return { success: false, error: "Accès refusé" };
if (userId === session.user.id) {
return { success: false, error: "Impossible de supprimer votre propre compte" };

View File

@@ -1,16 +1,15 @@
"use server";
import { auth } from "@/auth";
import { prisma } from "@/lib/db";
import { canAccessEvaluation } from "@/lib/evaluation-access";
import { getEvaluation } from "@/lib/server-data";
import { requireAuth, requireEvaluationAccess, type ActionResult } from "@/lib/action-helpers";
import { revalidatePath } from "next/cache";
export type ActionResult<T = void> = { success: true; data?: T } | { success: false; error: string };
export type { ActionResult };
export async function fetchEvaluation(id: string): Promise<ActionResult<Awaited<ReturnType<typeof getEvaluation>>>> {
const session = await auth();
if (!session?.user) return { success: false, error: "Non authentifié" };
const session = await requireAuth();
if (!session) return { success: false, error: "Non authentifié" };
const evaluation = await getEvaluation(id);
if (!evaluation) return { success: false, error: "Évaluation introuvable" };
@@ -19,10 +18,10 @@ export async function fetchEvaluation(id: string): Promise<ActionResult<Awaited<
}
export async function deleteEvaluation(id: string): Promise<ActionResult> {
const session = await auth();
if (!session?.user) return { success: false, error: "Non authentifié" };
const session = await requireAuth();
if (!session) return { success: false, error: "Non authentifié" };
const hasAccess = await canAccessEvaluation(id, session.user.id, session.user.role === "admin");
const hasAccess = await requireEvaluationAccess(id, session.user.id, session.user.role === "admin");
if (!hasAccess) return { success: false, error: "Accès refusé" };
try {
@@ -42,8 +41,8 @@ export async function createEvaluation(data: {
evaluationDate: string;
templateId: string;
}): Promise<ActionResult<{ id: string }>> {
const session = await auth();
if (!session?.user) return { success: false, error: "Non authentifié" };
const session = await requireAuth();
if (!session) return { success: false, error: "Non authentifié" };
const { candidateName, candidateRole, candidateTeam, evaluationDate, templateId } = data;
if (!candidateName || !candidateRole || !evaluationDate || !templateId) {
@@ -113,10 +112,10 @@ export async function updateDimensionScore(
dimensionId: string,
data: { score?: number | null; justification?: string | null; examplesObserved?: string | null; confidence?: string | null; candidateNotes?: string | null }
): Promise<ActionResult> {
const session = await auth();
if (!session?.user) return { success: false, error: "Non authentifié" };
const session = await requireAuth();
if (!session) return { success: false, error: "Non authentifié" };
const hasAccess = await canAccessEvaluation(evaluationId, session.user.id, session.user.role === "admin");
const hasAccess = await requireEvaluationAccess(evaluationId, session.user.id, session.user.role === "admin");
if (!hasAccess) return { success: false, error: "Accès refusé" };
try {
@@ -133,10 +132,10 @@ export async function updateDimensionScore(
}
export async function updateEvaluation(id: string, data: UpdateEvaluationInput): Promise<ActionResult> {
const session = await auth();
if (!session?.user) return { success: false, error: "Non authentifié" };
const session = await requireAuth();
if (!session) return { success: false, error: "Non authentifié" };
const hasAccess = await canAccessEvaluation(id, session.user.id, session.user.role === "admin");
const hasAccess = await requireEvaluationAccess(id, session.user.id, session.user.role === "admin");
if (!hasAccess) return { success: false, error: "Accès refusé" };
const existing = await prisma.evaluation.findUnique({ where: { id } });

View File

@@ -1,21 +1,16 @@
"use server";
import { auth } from "@/auth";
import { prisma } from "@/lib/db";
import { canAccessEvaluation } from "@/lib/evaluation-access";
import { requireAuth, requireEvaluationAccess, type ActionResult } from "@/lib/action-helpers";
import { revalidatePath } from "next/cache";
export type ActionResult<T = void> = { success: true; data?: T } | { success: false; error: string };
export type { ActionResult };
export async function addShare(evaluationId: string, userId: string): Promise<ActionResult> {
const session = await auth();
if (!session?.user) return { success: false, error: "Non authentifié" };
const session = await requireAuth();
if (!session) return { success: false, error: "Non authentifié" };
const hasAccess = await canAccessEvaluation(
evaluationId,
session.user.id,
session.user.role === "admin"
);
const hasAccess = await requireEvaluationAccess(evaluationId, session.user.id, session.user.role === "admin");
if (!hasAccess) return { success: false, error: "Accès refusé" };
if (userId === session.user.id) return { success: false, error: "Vous avez déjà accès" };
@@ -43,14 +38,10 @@ export async function addShare(evaluationId: string, userId: string): Promise<Ac
}
export async function removeShare(evaluationId: string, userId: string): Promise<ActionResult> {
const session = await auth();
if (!session?.user) return { success: false, error: "Non authentifié" };
const session = await requireAuth();
if (!session) return { success: false, error: "Non authentifié" };
const hasAccess = await canAccessEvaluation(
evaluationId,
session.user.id,
session.user.role === "admin"
);
const hasAccess = await requireEvaluationAccess(evaluationId, session.user.id, session.user.role === "admin");
if (!hasAccess) return { success: false, error: "Accès refusé" };
try {

View File

@@ -2,6 +2,7 @@
import { useState, useEffect } from "react";
import { updateDimensionScore } from "@/actions/evaluations";
import { parseQuestions, parseRubric } from "@/lib/export-utils";
const STORAGE_KEY_PREFIX = "eval-dim-expanded";
@@ -56,25 +57,6 @@ interface DimensionCardProps {
collapseAllTrigger?: number;
}
function parseRubric(rubric: string): string[] {
if (rubric === "1-5" || !rubric) return ["1", "2", "3", "4", "5"];
const labels: string[] = [];
for (let i = 1; i <= 5; i++) {
const m = rubric.match(new RegExp(`${i}:([^;]+)`));
labels.push(m ? m[1].trim() : String(i));
}
return labels;
}
function parseQuestions(s: string | null | undefined): string[] {
if (!s) return [];
try {
const arr = JSON.parse(s) as unknown;
return Array.isArray(arr) ? arr.filter((x): x is string => typeof x === "string") : [];
} catch {
return [];
}
}
const inputClass =
"w-full rounded border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-700/80 px-2.5 py-1.5 text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 dark:placeholder-zinc-500 focus:border-cyan-500 focus:ring-1 focus:ring-cyan-500/30";

15
src/lib/action-helpers.ts Normal file
View File

@@ -0,0 +1,15 @@
import { auth } from "@/auth";
import { canAccessEvaluation } from "@/lib/evaluation-access";
export type ActionResult<T = void> = { success: true; data?: T } | { success: false; error: string };
export async function requireAuth() {
const session = await auth();
if (!session?.user) return null;
return session;
}
export async function requireEvaluationAccess(evaluationId: string, userId: string, isAdmin: boolean) {
const hasAccess = await canAccessEvaluation(evaluationId, userId, isAdmin);
return hasAccess;
}

View File

@@ -6,7 +6,7 @@ export interface EvaluationWithScores extends Evaluation {
}
/** Parse suggestedQuestions JSON array */
function parseQuestions(s: string | null | undefined): string[] {
export function parseQuestions(s: string | null | undefined): string[] {
if (!s) return [];
try {
const arr = JSON.parse(s) as unknown;
@@ -17,7 +17,7 @@ function parseQuestions(s: string | null | undefined): string[] {
}
/** Parse rubric "1:X;2:Y;..." into labels */
function parseRubric(rubric: string): string[] {
export function parseRubric(rubric: string): string[] {
if (rubric === "1-5" || !rubric) return ["1", "2", "3", "4", "5"];
const labels: string[] = [];
for (let i = 1; i <= 5; i++) {

View File

@@ -10,7 +10,7 @@ declare module "next-auth" {
id: string;
email?: string | null;
name?: string | null;
role?: string;
role: string;
};
}
}