Compare commits
2 Commits
ebd8573299
...
e4a4e5a869
| Author | SHA1 | Date | |
|---|---|---|---|
| e4a4e5a869 | |||
| 2d8d59322d |
@@ -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" };
|
||||
|
||||
@@ -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 } });
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import { auth } from "@/auth";
|
||||
import { Header } from "@/components/Header";
|
||||
import { ThemeProvider } from "@/components/ThemeProvider";
|
||||
import { SessionProvider } from "@/components/SessionProvider";
|
||||
@@ -26,17 +27,18 @@ export const metadata: Metadata = {
|
||||
manifest: "/manifest.json",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const session = await auth();
|
||||
return (
|
||||
<html lang="fr" suppressHydrationWarning>
|
||||
<body className={`${geistSans.variable} ${geistMono.variable} min-h-screen bg-zinc-100 text-zinc-900 dark:bg-zinc-950 dark:text-zinc-50 antialiased`}>
|
||||
<SessionProvider>
|
||||
<ThemeProvider>
|
||||
<Header />
|
||||
<Header session={session} />
|
||||
<main className="mx-auto max-w-7xl px-4 py-6">{children}</main>
|
||||
</ThemeProvider>
|
||||
</SessionProvider>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import type { Session } from "next-auth";
|
||||
import { ThemeToggle } from "./ThemeToggle";
|
||||
import { SignOutButton } from "./SignOutButton";
|
||||
|
||||
export function Header() {
|
||||
const { data: session, status } = useSession();
|
||||
|
||||
export function Header({ session }: { session: Session | null }) {
|
||||
return (
|
||||
<header className="sticky top-0 z-50 border-b border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900 shadow-sm dark:shadow-none backdrop-blur-sm">
|
||||
<div className="mx-auto flex h-12 max-w-6xl items-center justify-between px-4">
|
||||
@@ -17,7 +14,7 @@ export function Header() {
|
||||
iag-eval
|
||||
</Link>
|
||||
<nav className="flex items-center gap-6 font-mono text-xs">
|
||||
{status === "authenticated" ? (
|
||||
{session ? (
|
||||
<>
|
||||
<Link
|
||||
href="/dashboard"
|
||||
@@ -31,7 +28,7 @@ export function Header() {
|
||||
>
|
||||
/new
|
||||
</Link>
|
||||
{session?.user?.role === "admin" && (
|
||||
{session.user.role === "admin" && (
|
||||
<Link
|
||||
href="/admin"
|
||||
className="text-zinc-500 hover:text-cyan-600 dark:text-zinc-400 dark:hover:text-cyan-400 transition-colors"
|
||||
@@ -46,18 +43,9 @@ export function Header() {
|
||||
/paramètres
|
||||
</Link>
|
||||
<span className="text-zinc-400 dark:text-zinc-500">
|
||||
{session?.user?.email}
|
||||
{session.user.email}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
await signOut({ redirect: false });
|
||||
window.location.href = "/auth/login";
|
||||
}}
|
||||
className="text-zinc-500 hover:text-red-500 dark:text-zinc-400 dark:hover:text-red-400 transition-colors"
|
||||
>
|
||||
déconnexion
|
||||
</button>
|
||||
<SignOutButton />
|
||||
</>
|
||||
) : (
|
||||
<Link
|
||||
|
||||
18
src/components/SignOutButton.tsx
Normal file
18
src/components/SignOutButton.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { signOut } from "next-auth/react";
|
||||
|
||||
export function SignOutButton() {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
await signOut({ redirect: false });
|
||||
window.location.href = "/auth/login";
|
||||
}}
|
||||
className="text-zinc-500 hover:text-red-500 dark:text-zinc-400 dark:hover:text-red-400 transition-colors"
|
||||
>
|
||||
déconnexion
|
||||
</button>
|
||||
);
|
||||
}
|
||||
15
src/lib/action-helpers.ts
Normal file
15
src/lib/action-helpers.ts
Normal 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;
|
||||
}
|
||||
@@ -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++) {
|
||||
|
||||
2
src/types/next-auth.d.ts
vendored
2
src/types/next-auth.d.ts
vendored
@@ -10,7 +10,7 @@ declare module "next-auth" {
|
||||
id: string;
|
||||
email?: string | null;
|
||||
name?: string | null;
|
||||
role?: string;
|
||||
role: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user