From 2d8d59322d37612ca09d98e9b097490de5f1a729 Mon Sep 17 00:00:00 2001 From: Froidefond Julien Date: Wed, 25 Feb 2026 13:43:57 +0100 Subject: [PATCH] =?UTF-8?q?refactor:=20d=C3=A9duplication=20=E2=80=94=20he?= =?UTF-8?q?lpers=20actions,=20parseurs=20partag=C3=A9s,=20types=20auth?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/actions/admin.ts | 12 ++++++------ src/actions/evaluations.ts | 31 +++++++++++++++---------------- src/actions/share.ts | 25 ++++++++----------------- src/components/DimensionCard.tsx | 20 +------------------- src/lib/action-helpers.ts | 15 +++++++++++++++ src/lib/export-utils.ts | 4 ++-- src/types/next-auth.d.ts | 2 +- 7 files changed, 48 insertions(+), 61 deletions(-) create mode 100644 src/lib/action-helpers.ts diff --git a/src/actions/admin.ts b/src/actions/admin.ts index 32a7d49..4ae96e7 100644 --- a/src/actions/admin.ts +++ b/src/actions/admin.ts @@ -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 = { success: true; data?: T } | { success: false; error: string }; +export type { ActionResult }; export async function setUserRole(userId: string, role: "admin" | "evaluator"): Promise { - 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 { - 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" }; diff --git a/src/actions/evaluations.ts b/src/actions/evaluations.ts index e67e974..9249838 100644 --- a/src/actions/evaluations.ts +++ b/src/actions/evaluations.ts @@ -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 = { success: true; data?: T } | { success: false; error: string }; +export type { ActionResult }; export async function fetchEvaluation(id: string): Promise>>> { - 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 { - 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> { - 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 { - 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 { - 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 } }); diff --git a/src/actions/share.ts b/src/actions/share.ts index 84fadcd..8e3a12a 100644 --- a/src/actions/share.ts +++ b/src/actions/share.ts @@ -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 = { success: true; data?: T } | { success: false; error: string }; +export type { ActionResult }; export async function addShare(evaluationId: string, userId: string): Promise { - 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 { - 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 { diff --git a/src/components/DimensionCard.tsx b/src/components/DimensionCard.tsx index 56fd092..f75bd68 100644 --- a/src/components/DimensionCard.tsx +++ b/src/components/DimensionCard.tsx @@ -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"; diff --git a/src/lib/action-helpers.ts b/src/lib/action-helpers.ts new file mode 100644 index 0000000..374bebc --- /dev/null +++ b/src/lib/action-helpers.ts @@ -0,0 +1,15 @@ +import { auth } from "@/auth"; +import { canAccessEvaluation } from "@/lib/evaluation-access"; + +export type ActionResult = { 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; +} diff --git a/src/lib/export-utils.ts b/src/lib/export-utils.ts index ed135ef..dada8f2 100644 --- a/src/lib/export-utils.ts +++ b/src/lib/export-utils.ts @@ -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++) { diff --git a/src/types/next-auth.d.ts b/src/types/next-auth.d.ts index eaacf76..2bbcbea 100644 --- a/src/types/next-auth.d.ts +++ b/src/types/next-auth.d.ts @@ -10,7 +10,7 @@ declare module "next-auth" { id: string; email?: string | null; name?: string | null; - role?: string; + role: string; }; } }