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.
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 2m17s

This commit is contained in:
Julien Froidefond
2026-02-20 13:13:41 +01:00
parent f5cbc578b7
commit 9d8d1b257d
12 changed files with 52 additions and 14 deletions

View File

@@ -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

View File

@@ -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:

View File

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

View File

@@ -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"

View File

@@ -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
}

View File

@@ -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."

View File

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

View File

@@ -23,6 +23,7 @@ export async function GET(req: NextRequest) {
OR: [
{ evaluatorId: userId },
{ sharedWith: { some: { userId } } },
{ isPublic: true },
],
}),
},

View File

@@ -77,7 +77,7 @@ export default function LoginPage() {
<p className="mt-4 font-mono text-xs text-zinc-500 dark:text-zinc-400">
Pas de compte ?{" "}
<Link href="/auth/signup" className="text-cyan-600 dark:text-cyan-400 hover:underline">
S'inscrire
S&apos;inscrire
</Link>
</p>
</div>

View File

@@ -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")

View File

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

View File

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