From 349c8d0e2a4a4bb16e1fce6b3510717b82bb84be Mon Sep 17 00:00:00 2001 From: root Date: Sat, 6 Jun 2026 21:14:24 +0000 Subject: [PATCH] feat: auto-detect podman/docker in agent, add studentId to activation response, fix download URLs --- Caddyfile | 1 + agent/activation.go | 5 +- agent/docker.go | 13 +++- agent/websocket.go | 3 +- server/app/dashboard/classes/[id]/page.tsx | 5 +- server/app/dashboard/classes/new/page.tsx | 6 +- server/app/dashboard/download/page.tsx | 6 +- server/app/dashboard/students/[id]/page.tsx | 5 +- .../superadmin/establishments/[id]/actions.ts | 40 +++++++++++ .../superadmin/establishments/[id]/page.tsx | 66 ++++++++++++++++++- server/lib/websocket.ts | 2 +- 11 files changed, 132 insertions(+), 20 deletions(-) diff --git a/Caddyfile b/Caddyfile index f3b90a4..7ce18bb 100644 --- a/Caddyfile +++ b/Caddyfile @@ -3,6 +3,7 @@ } :80 { + reverse_proxy /api/websocket* server:3001 reverse_proxy /gitea* gitea:3000 reverse_proxy server:3000 } diff --git a/agent/activation.go b/agent/activation.go index e651199..064a356 100644 --- a/agent/activation.go +++ b/agent/activation.go @@ -7,9 +7,10 @@ import ( ) type Activation struct { - Activated bool `json:"activated"` + Activated bool `json:"activated"` + StudentId string `json:"studentId,omitempty"` StudentName string `json:"studentName,omitempty"` - Code string `json:"code,omitempty"` + Code string `json:"code,omitempty"` } func activationFile(dataDir string) string { diff --git a/agent/docker.go b/agent/docker.go index ea9742c..603e88f 100644 --- a/agent/docker.go +++ b/agent/docker.go @@ -10,6 +10,13 @@ func instanceDir(dataDir, instanceID string) string { return filepath.Join(dataDir, "instances", instanceID) } +func getContainerEngine() string { + if _, err := exec.LookPath("podman"); err == nil { + return "podman" + } + return "docker" +} + func writeCompose(dataDir, instanceID, compose string) error { dir := instanceDir(dataDir, instanceID) if err := os.MkdirAll(dir, 0755); err != nil { @@ -21,7 +28,7 @@ func writeCompose(dataDir, instanceID, compose string) error { func dockerComposeUp(dataDir, instanceID string) error { dir := instanceDir(dataDir, instanceID) - cmd := exec.Command("docker", "compose", "-f", filepath.Join(dir, "docker-compose.yml"), "up", "-d") + cmd := exec.Command(getContainerEngine(), "compose", "-f", filepath.Join(dir, "docker-compose.yml"), "up", "-d") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() @@ -29,7 +36,7 @@ func dockerComposeUp(dataDir, instanceID string) error { func dockerComposeDown(dataDir, instanceID string) error { dir := instanceDir(dataDir, instanceID) - cmd := exec.Command("docker", "compose", "-f", filepath.Join(dir, "docker-compose.yml"), "down") + cmd := exec.Command(getContainerEngine(), "compose", "-f", filepath.Join(dir, "docker-compose.yml"), "down") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() @@ -37,7 +44,7 @@ func dockerComposeDown(dataDir, instanceID string) error { func dockerComposeRm(dataDir, instanceID string) error { dir := instanceDir(dataDir, instanceID) - cmd := exec.Command("docker", "compose", "-f", filepath.Join(dir, "docker-compose.yml"), "down", "-v") + cmd := exec.Command(getContainerEngine(), "compose", "-f", filepath.Join(dir, "docker-compose.yml"), "down", "-v") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { diff --git a/agent/websocket.go b/agent/websocket.go index ddc795f..ede8640 100644 --- a/agent/websocket.go +++ b/agent/websocket.go @@ -15,6 +15,7 @@ type WSMessage struct { Type string `json:"type,omitempty"` Port int `json:"port,omitempty"` ComposeConfig string `json:"composeConfig,omitempty"` + StudentId string `json:"studentId,omitempty"` StudentName string `json:"studentName,omitempty"` Error string `json:"error,omitempty"` } @@ -84,7 +85,7 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string) case "activate": // handled by UI, but server can also push activation response if msg.StudentName != "" { - act := &Activation{Activated: true, StudentName: msg.StudentName, Code: msg.Code} + act := &Activation{Activated: true, StudentId: msg.StudentId, StudentName: msg.StudentName, Code: msg.Code} saveActivation(dataDir, act) log.Printf("Activated as %s", msg.StudentName) } diff --git a/server/app/dashboard/classes/[id]/page.tsx b/server/app/dashboard/classes/[id]/page.tsx index 79d81fd..42ee07e 100644 --- a/server/app/dashboard/classes/[id]/page.tsx +++ b/server/app/dashboard/classes/[id]/page.tsx @@ -9,14 +9,15 @@ import { DeleteClassDialog } from "./DeleteClassDialog"; export const dynamic = "force-dynamic"; -export default async function ClassDetailPage({ params }: { params: { id: string } }) { +export default async function ClassDetailPage({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; const session = await getServerSession(authOptions); if (!session?.user) redirect("/login"); if (!session.user.establishmentId && session.user.role !== "superadmin") redirect("/dashboard"); const establishmentId = session.user.establishmentId; const cls = await prisma.class.findFirst({ - where: { id: params.id, ...(establishmentId ? { establishmentId } : {}) }, + where: { id, ...(establishmentId ? { establishmentId } : {}) }, include: { students: true }, }); diff --git a/server/app/dashboard/classes/new/page.tsx b/server/app/dashboard/classes/new/page.tsx index fd22a9e..bd2d622 100644 --- a/server/app/dashboard/classes/new/page.tsx +++ b/server/app/dashboard/classes/new/page.tsx @@ -18,7 +18,7 @@ async function createClass(formData: FormData) { "use server"; const session = await getServerSession(authOptions); if (!session?.user) redirect("/login"); - if (!session.user.establishmentId && session.user.role !== "superadmin") redirect("/dashboard"); + if (!session.user.establishmentId) redirect("/dashboard"); const parsed = schema.safeParse(Object.fromEntries(formData)); if (!parsed.success) return; @@ -27,7 +27,7 @@ async function createClass(formData: FormData) { data: { name: parsed.data.name, level: parsed.data.level, - establishmentId: session.user.establishmentId!, + establishment: { connect: { id: session.user.establishmentId } }, }, }); @@ -37,7 +37,7 @@ async function createClass(formData: FormData) { export default async function NewClassPage() { const session = await getServerSession(authOptions); if (!session?.user) redirect("/login"); - if (!session.user.establishmentId && session.user.role !== "superadmin") redirect("/dashboard"); + if (!session.user.establishmentId) redirect("/dashboard"); return (
diff --git a/server/app/dashboard/download/page.tsx b/server/app/dashboard/download/page.tsx index 92b54b3..ac9486c 100644 --- a/server/app/dashboard/download/page.tsx +++ b/server/app/dashboard/download/page.tsx @@ -14,7 +14,7 @@ export default function DownloadPage() {

Agent EduBox pour Windows (64 bits)

- Télécharger (.exe) + Télécharger (.exe)
@@ -23,7 +23,7 @@ export default function DownloadPage() {

Agent EduBox pour Linux (64 bits)

- Télécharger + Télécharger
@@ -32,7 +32,7 @@ export default function DownloadPage() {

Agent EduBox pour macOS (Intel & Apple Silicon)

- Télécharger + Télécharger
diff --git a/server/app/dashboard/students/[id]/page.tsx b/server/app/dashboard/students/[id]/page.tsx index 7bc2c5a..ef13cd4 100644 --- a/server/app/dashboard/students/[id]/page.tsx +++ b/server/app/dashboard/students/[id]/page.tsx @@ -11,7 +11,8 @@ import { generateActivationCodeAction } from "./actions"; export const dynamic = "force-dynamic"; -export default async function StudentDetailPage({ params }: { params: { id: string } }) { +export default async function StudentDetailPage({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; const session = await getServerSession(authOptions); if (!session?.user) redirect("/login"); if (!session.user.establishmentId && session.user.role !== "superadmin") redirect("/dashboard"); @@ -19,7 +20,7 @@ export default async function StudentDetailPage({ params }: { params: { id: stri const establishmentId = session.user.establishmentId; const student = await prisma.student.findFirst({ where: { - id: params.id, + id, class: establishmentId ? { establishmentId } : undefined, }, include: { class: true }, diff --git a/server/app/superadmin/establishments/[id]/actions.ts b/server/app/superadmin/establishments/[id]/actions.ts index 7642660..1f4b1a9 100644 --- a/server/app/superadmin/establishments/[id]/actions.ts +++ b/server/app/superadmin/establishments/[id]/actions.ts @@ -4,6 +4,7 @@ import { prisma } from "@/lib/prisma"; import { redirect } from "next/navigation"; import { revalidatePath } from "next/cache"; import { z } from "zod"; +import { hashPassword } from "@/lib/auth"; const updateSchema = z.object({ plan: z.enum(["trial", "starter", "standard", "premium"]), @@ -36,3 +37,42 @@ export async function deleteEstablishment(establishmentId: string) { }); redirect("/superadmin/establishments"); } + +const createAdminSchema = z.object({ + email: z.string().email("Email invalide"), + password: z.string().min(8, "Le mot de passe doit contenir au moins 8 caractères"), +}); + +export async function createAdmin(establishmentId: string, formData: FormData) { + const email = formData.get("email") as string; + const password = formData.get("password") as string; + + const parsed = createAdminSchema.safeParse({ email, password }); + if (!parsed.success) { + throw new Error(parsed.error.issues.map((e) => e.message).join(", ")); + } + + const existing = await prisma.user.findUnique({ where: { email: parsed.data.email } }); + if (existing) throw new Error("Cet email est déjà utilisé"); + + const hashed = await hashPassword(parsed.data.password); + + await prisma.user.create({ + data: { + email: parsed.data.email, + password: hashed, + role: "admin", + establishmentId, + }, + }); + + revalidatePath(`/superadmin/establishments/${establishmentId}`); +} + +export async function deleteAdmin(establishmentId: string, userId: string) { + await prisma.user.deleteMany({ + where: { id: userId, establishmentId, role: "admin" }, + }); + + revalidatePath(`/superadmin/establishments/${establishmentId}`); +} diff --git a/server/app/superadmin/establishments/[id]/page.tsx b/server/app/superadmin/establishments/[id]/page.tsx index 8243fba..53d9736 100644 --- a/server/app/superadmin/establishments/[id]/page.tsx +++ b/server/app/superadmin/establishments/[id]/page.tsx @@ -6,13 +6,16 @@ import { notFound } from "next/navigation"; import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Select } from "@/components/ui/select"; +import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; -import { updateSubscription, deleteEstablishment } from "./actions"; +import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "@/components/ui/table"; +import { updateSubscription, deleteEstablishment, createAdmin, deleteAdmin } from "./actions"; import { DeleteDialog } from "./delete-dialog"; -export default async function EstablishmentDetailPage({ params }: { params: { id: string } }) { +export default async function EstablishmentDetailPage({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; const establishment = await prisma.establishment.findUnique({ - where: { id: params.id }, + where: { id }, include: { subscription: true, _count: { select: { users: true, classes: true } }, @@ -21,7 +24,13 @@ export default async function EstablishmentDetailPage({ params }: { params: { id if (!establishment) notFound(); + const admins = await prisma.user.findMany({ + where: { establishmentId: id, role: "admin" }, + orderBy: { createdAt: "desc" }, + }); + const boundDelete = deleteEstablishment.bind(null, establishment.id); + const boundCreateAdmin = createAdmin.bind(null, establishment.id); return (
@@ -107,6 +116,57 @@ export default async function EstablishmentDetailPage({ params }: { params: { id
+ + + + Administrateurs + + +
+
+ + +
+
+ + +
+ +
+ + + + + Email + Rôle + Créé le + Actions + + + + {admins.map((user) => ( + + {user.email} + {user.role} + {new Date(user.createdAt).toLocaleDateString("fr-FR")} + +
+ + +
+
+ ))} + {admins.length === 0 && ( + + + Aucun administrateur + + + )} +
+
+
+
); } diff --git a/server/lib/websocket.ts b/server/lib/websocket.ts index 5f877b5..639d21c 100644 --- a/server/lib/websocket.ts +++ b/server/lib/websocket.ts @@ -49,7 +49,7 @@ export function initWebSocketServer(wss: WebSocketServer) { update: { studentId: student.id, status: "online", lastSeen: new Date() }, create: { id: nodeId, studentId: student.id, status: "online", lastSeen: new Date() }, }); - ws.send(JSON.stringify({ action: "activated", studentName: `${student.firstName} ${student.lastName}` })); + ws.send(JSON.stringify({ action: "activated", studentId: student.id, studentName: `${student.firstName} ${student.lastName}` })); return; }