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) );