feat: auto-detect podman/docker in agent, add studentId to activation response, fix download URLs

This commit is contained in:
root
2026-06-06 21:14:24 +00:00
parent a1883080d3
commit 349c8d0e2a
11 changed files with 132 additions and 20 deletions
@@ -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}`);
}
@@ -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 (
<div className="space-y-6">
@@ -107,6 +116,57 @@ export default async function EstablishmentDetailPage({ params }: { params: { id
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle>Administrateurs</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<form action={boundCreateAdmin} className="grid gap-4 md:grid-cols-3 items-end">
<div className="space-y-2">
<label htmlFor="email" className="text-sm font-medium">Email</label>
<Input id="email" name="email" type="email" required />
</div>
<div className="space-y-2">
<label htmlFor="password" className="text-sm font-medium">Mot de passe</label>
<Input id="password" name="password" type="password" required minLength={8} />
</div>
<Button type="submit">Créer un admin</Button>
</form>
<Table>
<TableHeader>
<TableRow>
<TableHead>Email</TableHead>
<TableHead>Rôle</TableHead>
<TableHead>Créé le</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{admins.map((user) => (
<TableRow key={user.id}>
<TableCell className="font-medium">{user.email}</TableCell>
<TableCell><Badge variant="secondary">{user.role}</Badge></TableCell>
<TableCell>{new Date(user.createdAt).toLocaleDateString("fr-FR")}</TableCell>
<TableCell className="text-right">
<form action={deleteAdmin.bind(null, establishment.id, user.id)}>
<Button type="submit" variant="destructive" size="sm">Supprimer</Button>
</form>
</TableCell>
</TableRow>
))}
{admins.length === 0 && (
<TableRow>
<TableCell colSpan={4} className="text-center text-muted-foreground">
Aucun administrateur
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
);
}