241 lines
9.3 KiB
TypeScript
241 lines
9.3 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { getServerSession } from "next-auth/next";
|
|
import { prisma } from "@/lib/prisma";
|
|
import { sendToNode } from "@/lib/websocket";
|
|
import { authOptions } from "@/lib/auth-config";
|
|
|
|
async function requireAuth() {
|
|
const session = await getServerSession(authOptions);
|
|
if (!session?.user) return null;
|
|
return session.user as { id: string; email: string; role: string; establishmentId?: string };
|
|
}
|
|
|
|
function userCanAccessNode(user: { role: string; establishmentId?: string }, node: any) {
|
|
if (user.role === "superadmin") return true;
|
|
const establishmentId = node?.student?.class?.establishmentId;
|
|
return establishmentId && establishmentId === user.establishmentId;
|
|
}
|
|
|
|
function userCanAccessInstance(user: { role: string; establishmentId?: string }, instance: any) {
|
|
if (user.role === "superadmin") return true;
|
|
const establishmentId = instance?.node?.student?.class?.establishmentId;
|
|
return establishmentId && establishmentId === user.establishmentId;
|
|
}
|
|
|
|
export async function GET(req: NextRequest) {
|
|
const user = await requireAuth();
|
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
|
|
const { searchParams } = new URL(req.url);
|
|
const nodeId = searchParams.get("nodeId");
|
|
const establishmentIdParam = searchParams.get("establishmentId");
|
|
|
|
let where: any = {};
|
|
if (nodeId) where.nodeId = nodeId;
|
|
|
|
if (user.role !== "superadmin") {
|
|
const classes = await prisma.class.findMany({
|
|
where: { establishmentId: user.establishmentId },
|
|
select: { id: true },
|
|
});
|
|
const students = await prisma.student.findMany({
|
|
where: { classId: { in: classes.map((c) => c.id) } },
|
|
select: { id: true },
|
|
});
|
|
const nodes = await prisma.node.findMany({
|
|
where: { studentId: { in: students.map((s) => s.id) } },
|
|
select: { id: true },
|
|
});
|
|
where.nodeId = { in: nodes.map((n) => n.id) };
|
|
} else if (establishmentIdParam) {
|
|
const classes = await prisma.class.findMany({ where: { establishmentId: establishmentIdParam }, select: { id: true } });
|
|
const students = await prisma.student.findMany({ where: { classId: { in: classes.map((c) => c.id) } }, select: { id: true } });
|
|
const nodes = await prisma.node.findMany({ where: { studentId: { in: students.map((s) => s.id) } }, select: { id: true } });
|
|
where.nodeId = { in: nodes.map((n) => n.id) };
|
|
}
|
|
|
|
const instances = await prisma.instance.findMany({
|
|
where,
|
|
include: {
|
|
node: {
|
|
include: {
|
|
student: {
|
|
include: {
|
|
class: {
|
|
include: {
|
|
establishment: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
template: true,
|
|
},
|
|
orderBy: { createdAt: "desc" },
|
|
});
|
|
|
|
const enriched = instances.map((inst) => {
|
|
const domain = inst.node.student?.class.establishment?.domain;
|
|
const publicUrl = domain ? `https://${inst.id}.${domain}` : null;
|
|
const localUrl = inst.node.tailscaleIp ? `http://${inst.node.tailscaleIp}:${inst.port}` : null;
|
|
return {
|
|
...inst,
|
|
publicUrl,
|
|
localUrl,
|
|
};
|
|
});
|
|
|
|
return NextResponse.json(enriched);
|
|
}
|
|
|
|
export async function POST(req: NextRequest) {
|
|
const user = await requireAuth();
|
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
|
|
const body = await req.json();
|
|
const { nodeId, templateId, port } = body;
|
|
if (!nodeId || !templateId) {
|
|
return NextResponse.json({ error: "Missing nodeId or templateId" }, { status: 400 });
|
|
}
|
|
|
|
const template = await prisma.template.findUnique({ where: { id: templateId } });
|
|
if (!template) return NextResponse.json({ error: "Template not found" }, { status: 404 });
|
|
|
|
const node = await prisma.node.findUnique({
|
|
where: { id: nodeId },
|
|
include: { student: { include: { class: { include: { establishment: true } } } } },
|
|
});
|
|
if (!node) return NextResponse.json({ error: "Node not found" }, { status: 404 });
|
|
if (!userCanAccessNode(user, node)) {
|
|
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
}
|
|
|
|
const instance = await prisma.instance.create({
|
|
data: { nodeId, templateId, port: port || 8080, status: "stopped" },
|
|
});
|
|
|
|
const domain = node.student?.class.establishment?.domain;
|
|
const publicDomain = domain ? `${instance.id}.${domain}` : "localhost";
|
|
const publicUrl = domain ? `https://${publicDomain}` : null;
|
|
const sent = sendToNode(nodeId, {
|
|
action: "start",
|
|
instanceId: instance.id,
|
|
type: template.type,
|
|
port: instance.port,
|
|
composeConfig: template.composeConfig
|
|
.replace(/{PORT}/g, String(instance.port))
|
|
.replace(/{INSTANCE_ID}/g, instance.id)
|
|
.replace(/{PUBLIC_URL}/g, publicUrl || `http://localhost:${instance.port}`)
|
|
.replace(/{PUBLIC_DOMAIN}/g, "localhost"),
|
|
initScript: template.initScript
|
|
? template.initScript
|
|
.replace(/{PORT}/g, String(instance.port))
|
|
.replace(/{INSTANCE_ID}/g, instance.id)
|
|
.replace(/{PUBLIC_URL}/g, publicUrl || `http://localhost:${instance.port}`)
|
|
.replace(/{PUBLIC_DOMAIN}/g, "localhost")
|
|
: undefined,
|
|
});
|
|
|
|
if (!sent) {
|
|
await prisma.instance.update({ where: { id: instance.id }, data: { status: "error" } });
|
|
}
|
|
|
|
return NextResponse.json(instance, { status: 201 });
|
|
}
|
|
|
|
export async function PATCH(req: NextRequest) {
|
|
const user = await requireAuth();
|
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
|
|
const body = await req.json();
|
|
const { id, action } = body;
|
|
if (!id || !action) {
|
|
return NextResponse.json({ error: "Missing id or action" }, { status: 400 });
|
|
}
|
|
|
|
const instance = await prisma.instance.findUnique({
|
|
where: { id },
|
|
include: { template: true, node: { include: { student: { include: { class: { include: { establishment: true } } } } } } },
|
|
});
|
|
if (!instance) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
if (!userCanAccessInstance(user, instance)) {
|
|
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
}
|
|
|
|
const domain = instance.node.student?.class.establishment?.domain;
|
|
const publicDomain = domain ? `${instance.id}.${domain}` : "localhost";
|
|
const publicUrl = domain ? `https://${publicDomain}` : null;
|
|
|
|
if (action === "stop") {
|
|
const sent = sendToNode(instance.nodeId, { action: "stop", instanceId: instance.id });
|
|
if (!sent) await prisma.instance.update({ where: { id }, data: { status: "error" } });
|
|
} else if (action === "start") {
|
|
const sent = sendToNode(instance.nodeId, {
|
|
action: "start",
|
|
instanceId: instance.id,
|
|
type: instance.template.type,
|
|
port: instance.port,
|
|
composeConfig: instance.template.composeConfig
|
|
.replace(/{PORT}/g, String(instance.port))
|
|
.replace(/{INSTANCE_ID}/g, instance.id)
|
|
.replace(/{PUBLIC_URL}/g, publicUrl || `http://localhost:${instance.port}`)
|
|
.replace(/{PUBLIC_DOMAIN}/g, "localhost"),
|
|
initScript: instance.template.initScript
|
|
? instance.template.initScript
|
|
.replace(/{PORT}/g, String(instance.port))
|
|
.replace(/{INSTANCE_ID}/g, instance.id)
|
|
.replace(/{PUBLIC_URL}/g, publicUrl || `http://localhost:${instance.port}`)
|
|
.replace(/{PUBLIC_DOMAIN}/g, "localhost")
|
|
: undefined,
|
|
});
|
|
if (!sent) await prisma.instance.update({ where: { id }, data: { status: "error" } });
|
|
} else if (action === "reset") {
|
|
const sent = sendToNode(instance.nodeId, {
|
|
action: "reset",
|
|
instanceId: instance.id,
|
|
type: instance.template.type,
|
|
port: instance.port,
|
|
composeConfig: instance.template.composeConfig
|
|
.replace(/{PORT}/g, String(instance.port))
|
|
.replace(/{INSTANCE_ID}/g, instance.id)
|
|
.replace(/{PUBLIC_URL}/g, publicUrl || `http://localhost:${instance.port}`)
|
|
.replace(/{PUBLIC_DOMAIN}/g, "localhost"),
|
|
initScript: instance.template.initScript
|
|
? instance.template.initScript
|
|
.replace(/{PORT}/g, String(instance.port))
|
|
.replace(/{INSTANCE_ID}/g, instance.id)
|
|
.replace(/{PUBLIC_URL}/g, publicUrl || `http://localhost:${instance.port}`)
|
|
.replace(/{PUBLIC_DOMAIN}/g, "localhost")
|
|
: undefined,
|
|
});
|
|
if (!sent) await prisma.instance.update({ where: { id }, data: { status: "error" } });
|
|
} else {
|
|
return NextResponse.json({ error: "Invalid action" }, { status: 400 });
|
|
}
|
|
|
|
return NextResponse.json({ ok: true });
|
|
}
|
|
|
|
export async function DELETE(req: NextRequest) {
|
|
const user = await requireAuth();
|
|
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
|
|
const { searchParams } = new URL(req.url);
|
|
const id = searchParams.get("id");
|
|
if (!id) return NextResponse.json({ error: "Missing id" }, { status: 400 });
|
|
|
|
const instance = await prisma.instance.findUnique({
|
|
where: { id },
|
|
include: { node: { include: { student: { include: { class: { include: { establishment: true } } } } } } },
|
|
});
|
|
if (!instance) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
if (!userCanAccessInstance(user, instance)) {
|
|
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
}
|
|
|
|
if (instance) sendToNode(instance.nodeId, { action: "delete", instanceId: instance.id });
|
|
await prisma.instance.delete({ where: { id } });
|
|
return NextResponse.json({ ok: true });
|
|
}
|