From 9d8d1b257d821fc538a81ff033f925494a378a25 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Fri, 20 Feb 2026 13:13:41 +0100 Subject: [PATCH] Refactor Docker Compose configuration to use dynamic volume paths, update deployment workflow to create necessary directories, and enhance Prisma schema with public visibility for evaluations. Improve access control in API routes and adjust evaluation creation logic to include public visibility. Fix minor issues in login and evaluation pages for better user experience. --- .gitea/workflows/deploy.yml | 2 ++ docker-compose.yml | 5 +--- .../migration.sql | 26 +++++++++++++++++++ prisma/migrations/migration_lock.toml | 3 +++ prisma/schema.prisma | 1 + prisma/seed.ts | 3 ++- src/app/api/evaluations/[id]/route.ts | 13 +++++++--- src/app/api/evaluations/route.ts | 1 + src/app/auth/login/page.tsx | 2 +- src/app/evaluations/new/page.tsx | 2 +- src/components/DimensionCard.tsx | 4 +-- src/components/ShareModal.tsx | 4 +-- 12 files changed, 52 insertions(+), 14 deletions(-) create mode 100644 prisma/migrations/20260220120133_add_is_public/migration.sql create mode 100644 prisma/migrations/migration_lock.toml diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 2daa3e0..dfadc99 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -17,5 +17,7 @@ jobs: DOCKER_BUILDKIT: 1 COMPOSE_DOCKER_CLI_BUILD: 1 AUTH_SECRET: ${{ secrets.AUTH_SECRET }} + DB_VOLUME_PATH: ${{ variables.DB_VOLUME_PATH }} run: | + mkdir -p "${DB_VOLUME_PATH:-./data/db}" docker compose up -d --build diff --git a/docker-compose.yml b/docker-compose.yml index 95d424e..ca894f6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,8 +6,5 @@ services: environment: - DATABASE_URL=file:/data/db/dev.db volumes: - - db-data:/data/db + - ${DB_VOLUME_PATH:-./data/db}:/data/db restart: unless-stopped - -volumes: - db-data: diff --git a/prisma/migrations/20260220120133_add_is_public/migration.sql b/prisma/migrations/20260220120133_add_is_public/migration.sql new file mode 100644 index 0000000..d0015bb --- /dev/null +++ b/prisma/migrations/20260220120133_add_is_public/migration.sql @@ -0,0 +1,26 @@ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Evaluation" ( + "id" TEXT NOT NULL PRIMARY KEY, + "candidateName" TEXT NOT NULL, + "candidateRole" TEXT NOT NULL, + "candidateTeam" TEXT, + "evaluatorName" TEXT NOT NULL, + "evaluatorId" TEXT, + "evaluationDate" DATETIME NOT NULL, + "templateId" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'draft', + "findings" TEXT, + "recommendations" TEXT, + "isPublic" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "Evaluation_evaluatorId_fkey" FOREIGN KEY ("evaluatorId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "Evaluation_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_Evaluation" ("candidateName", "candidateRole", "candidateTeam", "createdAt", "evaluationDate", "evaluatorId", "evaluatorName", "findings", "id", "recommendations", "status", "templateId", "updatedAt") SELECT "candidateName", "candidateRole", "candidateTeam", "createdAt", "evaluationDate", "evaluatorId", "evaluatorName", "findings", "id", "recommendations", "status", "templateId", "updatedAt" FROM "Evaluation"; +DROP TABLE "Evaluation"; +ALTER TABLE "new_Evaluation" RENAME TO "Evaluation"; +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..e5e5c47 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "sqlite" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2c31c99..837c1fe 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -63,6 +63,7 @@ model Evaluation { dimensionScores DimensionScore[] auditLogs AuditLog[] sharedWith EvaluationShare[] + isPublic Boolean @default(false) // visible par tous (ex. démo) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } diff --git a/prisma/seed.ts b/prisma/seed.ts index 4486a8c..2ad27fd 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,4 +1,5 @@ import { PrismaClient } from "@prisma/client"; +import bcrypt from "bcryptjs"; const prisma = new PrismaClient(); @@ -434,7 +435,6 @@ async function main() { }); if (!template) throw new Error("Template not found"); - const bcrypt = require("bcryptjs"); const adminHash = bcrypt.hashSync("admin123", 10); const admin = await prisma.user.upsert({ where: { email: "admin@cars-front.local" }, @@ -492,6 +492,7 @@ async function main() { evaluationDate: new Date(2025, 1, 15 + i), templateId: template.id, status: i === 0 ? "submitted" : "draft", + isPublic: true, // démo visible par tous findings: i === 0 ? "Bonne maîtrise des outils et des prompts. Conception et exploration à renforcer. Alignement NFR correct." diff --git a/src/app/api/evaluations/[id]/route.ts b/src/app/api/evaluations/[id]/route.ts index 0788f89..32535db 100644 --- a/src/app/api/evaluations/[id]/route.ts +++ b/src/app/api/evaluations/[id]/route.ts @@ -3,15 +3,21 @@ import { Prisma } from "@prisma/client"; import { auth } from "@/auth"; import { prisma } from "@/lib/db"; -async function canAccessEvaluation(evaluationId: string, userId: string, isAdmin: boolean) { +async function canAccessEvaluation( + evaluationId: string, + userId: string, + isAdmin: boolean, + readOnly = false +) { if (isAdmin) return true; const eval_ = await prisma.evaluation.findUnique({ where: { id: evaluationId }, - select: { evaluatorId: true, sharedWith: { select: { userId: true } } }, + select: { evaluatorId: true, isPublic: true, sharedWith: { select: { userId: true } } }, }); if (!eval_) return false; if (eval_.evaluatorId === userId) return true; if (eval_.sharedWith.some((s) => s.userId === userId)) return true; + if (readOnly && eval_.isPublic) return true; return false; } @@ -45,7 +51,8 @@ export async function GET( const hasAccess = await canAccessEvaluation( id, session.user.id, - session.user.role === "admin" + session.user.role === "admin", + true // read-only: public evals accessibles en lecture ); if (!hasAccess) { return NextResponse.json({ error: "Accès refusé" }, { status: 403 }); diff --git a/src/app/api/evaluations/route.ts b/src/app/api/evaluations/route.ts index aee6f47..57bb5a9 100644 --- a/src/app/api/evaluations/route.ts +++ b/src/app/api/evaluations/route.ts @@ -23,6 +23,7 @@ export async function GET(req: NextRequest) { OR: [ { evaluatorId: userId }, { sharedWith: { some: { userId } } }, + { isPublic: true }, ], }), }, diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index a9340b6..4ecbfd0 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -77,7 +77,7 @@ export default function LoginPage() {

Pas de compte ?{" "} - S'inscrire + S'inscrire

diff --git a/src/app/evaluations/new/page.tsx b/src/app/evaluations/new/page.tsx index e1d2683..f471186 100644 --- a/src/app/evaluations/new/page.tsx +++ b/src/app/evaluations/new/page.tsx @@ -25,7 +25,7 @@ export default function NewEvaluationPage() { const display = session.user.name || session.user.email || ""; setForm((f) => ({ ...f, evaluatorName: display })); } - }, [session?.user?.name, session?.user?.email]); + }, [session?.user]); useEffect(() => { fetch("/api/templates") diff --git a/src/components/DimensionCard.tsx b/src/components/DimensionCard.tsx index c8c7b0b..fd5c52f 100644 --- a/src/components/DimensionCard.tsx +++ b/src/components/DimensionCard.tsx @@ -86,13 +86,13 @@ export function DimensionCard({ dimension, score, index, evaluationId, onScoreCh useEffect(() => { if (evaluationId && typeof window !== "undefined") { const stored = getStoredExpanded(evaluationId, dimension.id); - if (stored !== null) setExpanded(stored); + if (stored !== null) queueMicrotask(() => setExpanded(stored)); } }, [evaluationId, dimension.id]); useEffect(() => { if (collapseAllTrigger != null && collapseAllTrigger > 0) { - setExpanded(false); + queueMicrotask(() => setExpanded(false)); if (evaluationId) setStoredExpanded(evaluationId, dimension.id, false); } }, [collapseAllTrigger, evaluationId, dimension.id]); diff --git a/src/components/ShareModal.tsx b/src/components/ShareModal.tsx index 1e56821..d3b0664 100644 --- a/src/components/ShareModal.tsx +++ b/src/components/ShareModal.tsx @@ -32,11 +32,11 @@ export function ShareModal({ sharedWith, onUpdate, }: ShareModalProps) { - if (!isOpen) return null; - const [shareUserId, setShareUserId] = useState(""); const [loading, setLoading] = useState(false); + if (!isOpen) return null; + const availableUsers = users.filter( (u) => u.id !== evaluatorId && !sharedWith.some((s) => s.user.id === u.id) );