Update Dockerfile and package.json to use Prisma migrations, add bcryptjs and next-auth dependencies, and enhance README instructions for database setup. Refactor Prisma schema to include password hashing for users and implement evaluation sharing functionality. Improve admin page with user management features and integrate session handling for authentication. Enhance evaluation detail page with sharing options and update API routes for access control based on user roles.
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3m4s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3m4s
This commit is contained in:
@@ -14,9 +14,9 @@ interface CandidateFormProps {
|
||||
}
|
||||
|
||||
const inputClass =
|
||||
"w-full rounded border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-700/80 px-3 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/50 transition-colors";
|
||||
"w-full h-10 rounded-lg border border-zinc-200 dark:border-zinc-600 bg-white dark:bg-zinc-700/60 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 dark:placeholder-zinc-500 focus:border-cyan-500 focus:ring-2 focus:ring-cyan-500/20 transition-all box-border";
|
||||
|
||||
const labelClass = "mb-0.5 block text-xs font-medium text-zinc-600 dark:text-zinc-400";
|
||||
const labelClass = "mb-1 block text-xs font-medium text-zinc-500 dark:text-zinc-400";
|
||||
|
||||
export function CandidateForm({
|
||||
candidateName,
|
||||
@@ -31,8 +31,8 @@ export function CandidateForm({
|
||||
templateDisabled,
|
||||
}: CandidateFormProps) {
|
||||
return (
|
||||
<div className="flex flex-wrap items-end gap-x-6 gap-y-3">
|
||||
<div className="min-w-[140px]">
|
||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="sm:col-span-2 lg:col-span-1">
|
||||
<label className={labelClass}>Candidat</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -43,7 +43,7 @@ export function CandidateForm({
|
||||
placeholder="Alice Chen"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-[140px]">
|
||||
<div>
|
||||
<label className={labelClass}>Rôle</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -54,7 +54,7 @@ export function CandidateForm({
|
||||
placeholder="ML Engineer"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-[140px]">
|
||||
<div>
|
||||
<label className={labelClass}>Équipe</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -65,7 +65,8 @@ export function CandidateForm({
|
||||
placeholder="Cars Front"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-[120px]">
|
||||
<div className="border-t border-zinc-200 dark:border-zinc-600 pt-4 sm:col-span-2 lg:col-span-3" />
|
||||
<div>
|
||||
<label className={labelClass}>Évaluateur</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -76,7 +77,7 @@ export function CandidateForm({
|
||||
placeholder="Jean D."
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-[120px]">
|
||||
<div>
|
||||
<label className={labelClass}>Date</label>
|
||||
<input
|
||||
type="date"
|
||||
@@ -86,7 +87,7 @@ export function CandidateForm({
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-[160px]">
|
||||
<div>
|
||||
<label className={labelClass}>Modèle</label>
|
||||
<select
|
||||
value={templateId}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import { ThemeToggle } from "./ThemeToggle";
|
||||
|
||||
export function Header() {
|
||||
const { data: session, status } = useSession();
|
||||
|
||||
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">
|
||||
@@ -11,15 +14,29 @@ export function Header() {
|
||||
iag-eval
|
||||
</Link>
|
||||
<nav className="flex items-center gap-6 font-mono text-xs">
|
||||
<Link href="/" className="text-zinc-500 hover:text-cyan-600 dark:text-zinc-400 dark:hover:text-cyan-400 transition-colors">
|
||||
/dashboard
|
||||
</Link>
|
||||
<Link href="/evaluations/new" className="text-zinc-500 hover:text-cyan-600 dark:text-zinc-400 dark:hover:text-cyan-400 transition-colors">
|
||||
/new
|
||||
</Link>
|
||||
<Link href="/admin" className="text-zinc-500 hover:text-cyan-600 dark:text-zinc-400 dark:hover:text-cyan-400 transition-colors">
|
||||
/admin
|
||||
</Link>
|
||||
{status === "authenticated" && (
|
||||
<>
|
||||
<Link href="/" className="text-zinc-500 hover:text-cyan-600 dark:text-zinc-400 dark:hover:text-cyan-400 transition-colors">
|
||||
/dashboard
|
||||
</Link>
|
||||
<Link href="/evaluations/new" className="text-zinc-500 hover:text-cyan-600 dark:text-zinc-400 dark:hover:text-cyan-400 transition-colors">
|
||||
/new
|
||||
</Link>
|
||||
{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">
|
||||
/admin
|
||||
</Link>
|
||||
)}
|
||||
<span className="text-zinc-400 dark:text-zinc-500">{session?.user?.email}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => signOut({ callbackUrl: "/auth/login" })}
|
||||
className="text-zinc-500 hover:text-red-500 dark:text-zinc-400 dark:hover:text-red-400 transition-colors"
|
||||
>
|
||||
déconnexion
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<ThemeToggle />
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
7
src/components/SessionProvider.tsx
Normal file
7
src/components/SessionProvider.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { SessionProvider as NextAuthSessionProvider } from "next-auth/react";
|
||||
|
||||
export function SessionProvider({ children }: { children: React.ReactNode }) {
|
||||
return <NextAuthSessionProvider>{children}</NextAuthSessionProvider>;
|
||||
}
|
||||
135
src/components/ShareModal.tsx
Normal file
135
src/components/ShareModal.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string | null;
|
||||
}
|
||||
|
||||
interface SharedUser {
|
||||
id: string;
|
||||
user: { id: string; email: string; name: string | null };
|
||||
}
|
||||
|
||||
interface ShareModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
evaluationId: string;
|
||||
evaluatorId?: string | null;
|
||||
users: User[];
|
||||
sharedWith: SharedUser[];
|
||||
onUpdate: () => void;
|
||||
}
|
||||
|
||||
export function ShareModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
evaluationId,
|
||||
evaluatorId,
|
||||
users,
|
||||
sharedWith,
|
||||
onUpdate,
|
||||
}: ShareModalProps) {
|
||||
if (!isOpen) return null;
|
||||
|
||||
const [shareUserId, setShareUserId] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const availableUsers = users.filter(
|
||||
(u) => u.id !== evaluatorId && !sharedWith.some((s) => s.user.id === u.id)
|
||||
);
|
||||
|
||||
async function handleAdd() {
|
||||
if (!shareUserId) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(`/api/evaluations/${evaluationId}/share`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ userId: shareUserId }),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (res.ok) {
|
||||
setShareUserId("");
|
||||
onUpdate();
|
||||
} else {
|
||||
alert(data.error ?? "Erreur");
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRemove(userId: string) {
|
||||
const res = await fetch(`/api/evaluations/${evaluationId}/share/${userId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (res.ok) onUpdate();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed inset-0 z-40 bg-black/60" onClick={onClose} aria-hidden="true" />
|
||||
<div
|
||||
className="fixed left-1/2 top-1/2 z-50 w-[calc(100%-2rem)] max-w-sm -translate-x-1/2 -translate-y-1/2 rounded-lg border border-zinc-200 dark:border-zinc-600 bg-white dark:bg-zinc-800 p-4 shadow-xl"
|
||||
role="dialog"
|
||||
aria-label="Partager"
|
||||
>
|
||||
<h3 className="mb-4 font-mono text-sm font-medium text-zinc-800 dark:text-zinc-200">
|
||||
Partager
|
||||
</h3>
|
||||
<p className="mb-3 font-mono text-xs text-zinc-600 dark:text-zinc-400">
|
||||
Ajouter un utilisateur pour lui donner accès.
|
||||
</p>
|
||||
<div className="mb-3 flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||
<select
|
||||
value={shareUserId}
|
||||
onChange={(e) => setShareUserId(e.target.value)}
|
||||
className="w-full min-w-0 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 sm:flex-1"
|
||||
>
|
||||
<option value="">— choisir —</option>
|
||||
{availableUsers.map((u) => (
|
||||
<option key={u.id} value={u.id}>
|
||||
{u.name || u.email} ({u.email})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!shareUserId || loading}
|
||||
onClick={handleAdd}
|
||||
className="w-full shrink-0 rounded border border-cyan-500/50 bg-cyan-500/10 px-3 py-1.5 font-mono text-xs text-cyan-600 dark:text-cyan-400 hover:bg-cyan-500/20 disabled:opacity-50 sm:w-auto"
|
||||
>
|
||||
{loading ? "..." : "ajouter"}
|
||||
</button>
|
||||
</div>
|
||||
{sharedWith.length > 0 && (
|
||||
<ul className="mb-4 space-y-1 font-mono text-xs text-zinc-600 dark:text-zinc-400">
|
||||
{sharedWith.map((s) => (
|
||||
<li key={s.id} className="flex items-center justify-between gap-2">
|
||||
<span>{s.user.name || s.user.email}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemove(s.user.id)}
|
||||
className="text-red-500 hover:text-red-400"
|
||||
title="Retirer"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="w-full rounded border border-zinc-300 dark:border-zinc-700 py-2 font-mono text-xs text-zinc-600 dark:text-zinc-500 hover:bg-zinc-100 dark:hover:bg-zinc-800"
|
||||
>
|
||||
fermer
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user