diff --git a/.gitignore b/.gitignore index 574301b..652136d 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ agent/edubox-agent agent/edubox-agent.exe agent/edubox-agent-mac agent/ui/*.go.html +headscale/*.sqlite* +headscale/*.key +headscale/*.state diff --git a/Caddyfile b/Caddyfile index 2509e6d..30dbdb1 100644 --- a/Caddyfile +++ b/Caddyfile @@ -1,5 +1,8 @@ { - auto_https off + email admin@edudeploy.com + on_demand_tls { + ask http://server:3000/api/check-domain + } } :80 { @@ -8,7 +11,30 @@ root /usr/share/caddy/agent } } + route /api/websocket* { + reverse_proxy server:3001 + } + route /api/check-domain* { + reverse_proxy server:3000 + } + route * { + redir https://{host}{uri} permanent + } +} + +headscale.alfrednobel.edudeploy.com { + reverse_proxy headscale:8080 +} + +alfrednobel.edudeploy.com { + reverse_proxy /api/websocket* server:3001 + reverse_proxy server:3000 +} + +:443 { + tls { + on_demand + } reverse_proxy /api/websocket* server:3001 - reverse_proxy /gitea* gitea:3000 reverse_proxy server:3000 } diff --git a/agent/build.sh b/agent/build.sh index e1a8363..c8fe686 100755 --- a/agent/build.sh +++ b/agent/build.sh @@ -1,7 +1,7 @@ #!/bin/bash set -e -VERSION="0.2.1" +VERSION="0.2.3" LDFLAGS="-X main.version=${VERSION}" echo "Building EduBox Agent v${VERSION}..." diff --git a/agent/main.go b/agent/main.go index 6d88a11..8e072fb 100644 --- a/agent/main.go +++ b/agent/main.go @@ -6,16 +6,19 @@ import ( "log" "os" "path/filepath" + "time" ) // version is injected at build time via -ldflags "-X main.version=X.Y.Z" var version = "dev" var ( - serverAddr = flag.String("server", "ws://localhost:3001", "Adresse WebSocket du serveur") - nodeID = flag.String("node-id", defaultNodeID(), "ID du nœud (défaut: hostname)") - dataDir = flag.String("data-dir", "./edubox-data", "Répertoire de données") - uiEnabled = flag.Bool("ui", true, "Activer l'interface locale HTMX") + serverAddr = flag.String("server", "ws://localhost:3001", "Adresse WebSocket du serveur") + nodeID = flag.String("node-id", defaultNodeID(), "ID du nœud (défaut: hostname)") + dataDir = flag.String("data-dir", "./edubox-data", "Répertoire de données") + uiEnabled = flag.Bool("ui", true, "Activer l'interface locale HTMX") + headscaleURL = flag.String("headscale-url", "", "URL du serveur Headscale (ex: http://151.80.60.98:8080)") + headscaleAuthKey = flag.String("headscale-auth-key", "", "Clé d'authentification Headscale") ) func defaultNodeID() string { @@ -51,5 +54,31 @@ func main() { go startUI(*dataDir, *nodeID, *serverAddr) } - startWebSocket(*serverAddr, *nodeID, *dataDir) + go startWebSocket(*serverAddr, *nodeID, *dataDir) + + if *headscaleURL != "" && *headscaleAuthKey != "" { + go startTailscaleAndReport(*dataDir, *nodeID, *headscaleURL, *headscaleAuthKey) + } + + select {} +} + +func startTailscaleAndReport(dataDir, nodeID, headscaleURL, authKey string) { + tsDir := filepath.Join(dataDir, "tailscale") + ip, err := startTailscale(tsDir, nodeID, headscaleURL, authKey) + if err != nil { + log.Printf("Tailscale error: %v", err) + return + } + log.Printf("Tailscale IP obtained: %s", ip) + + for { + if err := sendMessage(WSMessage{Action: "tailscale_ip", NodeID: nodeID, TailscaleIP: ip}); err != nil { + log.Printf("Waiting for WebSocket to send tailscale_ip...") + time.Sleep(1 * time.Second) + continue + } + log.Printf("Sent tailscale_ip to server: %s", ip) + break + } } diff --git a/agent/tailscale.go b/agent/tailscale.go index 253b7a3..f66125e 100644 --- a/agent/tailscale.go +++ b/agent/tailscale.go @@ -1,14 +1,24 @@ package main import ( + "context" "fmt" "log" "net" + "os" + "io" + "time" "tailscale.com/tsnet" ) -func startTailscale(dataDir string, nodeID string) (net.Listener, error) { +var globalTSServer *tsnet.Server + +func startTailscale(dataDir, nodeID, headscaleURL, authKey string) (string, error) { + // Configure tsnet to use our Headscale server + os.Setenv("TS_AUTHKEY", authKey) + os.Setenv("TS_CONTROL_URL", headscaleURL) + s := &tsnet.Server{ Hostname: nodeID, Dir: dataDir, @@ -16,13 +26,80 @@ func startTailscale(dataDir string, nodeID string) (net.Listener, error) { } if err := s.Start(); err != nil { - return nil, fmt.Errorf("tailscale start: %w", err) + return "", fmt.Errorf("tailscale start: %w", err) } - ln, err := s.Listen("tcp", ":0") + globalTSServer = s + + // Wait for Tailscale to come up and retrieve IP + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + lc, err := s.LocalClient() if err != nil { - return nil, fmt.Errorf("tailscale listen: %w", err) + return "", fmt.Errorf("tailscale local client: %w", err) } + var tailscaleIP string + for { + status, err := lc.Status(ctx) + if err != nil { + return "", fmt.Errorf("tailscale status: %w", err) + } + if status.Self != nil && len(status.Self.TailscaleIPs) > 0 { + tailscaleIP = status.Self.TailscaleIPs[0].String() + break + } + select { + case <-ctx.Done(): + return "", fmt.Errorf("tailscale IP timeout") + case <-time.After(1 * time.Second): + } + } + + log.Printf("Tailscale started with IP: %s", tailscaleIP) + return tailscaleIP, nil +} + +func startTailscaleProxy(port int) (net.Listener, error) { + if globalTSServer == nil { + return nil, fmt.Errorf("tailscale server not started") + } + ln, err := globalTSServer.Listen("tcp", fmt.Sprintf(":%d", port)) + if err != nil { + return nil, fmt.Errorf("tailscale listen on port %d: %w", port, err) + } + go func() { + for { + conn, err := ln.Accept() + if err != nil { + log.Printf("tailscale proxy accept error on port %d: %v", port, err) + return + } + go handleProxyConn(conn, port) + } + }() + log.Printf("Tailscale proxy started on port %d", port) return ln, nil } + +func handleProxyConn(src net.Conn, port int) { + defer src.Close() + dst, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", port)) + if err != nil { + log.Printf("tailscale proxy dial localhost:%d error: %v", port, err) + return + } + defer dst.Close() + + done := make(chan struct{}, 2) + go func() { + _, _ = io.Copy(dst, src) + done <- struct{}{} + }() + go func() { + _, _ = io.Copy(src, dst) + done <- struct{}{} + }() + <-done +} diff --git a/agent/websocket.go b/agent/websocket.go index 629b2fd..ec9f529 100644 --- a/agent/websocket.go +++ b/agent/websocket.go @@ -3,6 +3,7 @@ package main import ( "fmt" "log" + "net" "sync" "time" @@ -20,6 +21,7 @@ type WSMessage struct { StudentId string `json:"studentId,omitempty"` StudentName string `json:"studentName,omitempty"` Error string `json:"error,omitempty"` + TailscaleIP string `json:"tailscaleIp,omitempty"` } var ( @@ -27,6 +29,11 @@ var ( mainConnMu sync.Mutex ) +var ( + tsProxies = make(map[int]net.Listener) + tsProxiesMu sync.Mutex +) + func sendMessage(msg WSMessage) error { mainConnMu.Lock() defer mainConnMu.Unlock() @@ -197,12 +204,32 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string) sendMessage(WSMessage{Action: "instance_error", InstanceID: msg.InstanceID, Error: err.Error()}) return } + // Start Tailscale proxy so the server can reach localhost via Tailscale IP + tsProxiesMu.Lock() + if _, exists := tsProxies[msg.Port]; !exists { + if ln, err := startTailscaleProxy(msg.Port); err == nil { + tsProxies[msg.Port] = ln + } else { + log.Printf("startTailscaleProxy error: %v", err) + } + } + tsProxiesMu.Unlock() + status := getInstanceStatus(dataDir, msg.InstanceID) _ = upsertInstance(dataDir, &InstanceInfo{ID: msg.InstanceID, TemplateName: msg.Type, Port: msg.Port, Status: status}) sendMessage(WSMessage{Action: "instance_started", InstanceID: msg.InstanceID, Port: msg.Port}) notifyUI(map[string]interface{}{"action": "instances_updated"}) case "stop": log.Printf("Stop instance %s", msg.InstanceID) + // Stop Tailscale proxy for this instance port + if inst, _ := loadInstances(dataDir); inst[msg.InstanceID] != nil { + tsProxiesMu.Lock() + if ln, exists := tsProxies[inst[msg.InstanceID].Port]; exists { + _ = ln.Close() + delete(tsProxies, inst[msg.InstanceID].Port) + } + tsProxiesMu.Unlock() + } if err := dockerComposeDown(dataDir, msg.InstanceID); err != nil { log.Printf("dockerComposeDown error: %v", err) } @@ -226,6 +253,17 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string) sendMessage(WSMessage{Action: "instance_error", InstanceID: msg.InstanceID, Error: err.Error()}) return } + // Start Tailscale proxy so the server can reach localhost via Tailscale IP + tsProxiesMu.Lock() + if _, exists := tsProxies[msg.Port]; !exists { + if ln, err := startTailscaleProxy(msg.Port); err == nil { + tsProxies[msg.Port] = ln + } else { + log.Printf("startTailscaleProxy error: %v", err) + } + } + tsProxiesMu.Unlock() + status := getInstanceStatus(dataDir, msg.InstanceID) _ = upsertInstance(dataDir, &InstanceInfo{ID: msg.InstanceID, TemplateName: msg.Type, Port: msg.Port, Status: status}) sendMessage(WSMessage{Action: "instance_started", InstanceID: msg.InstanceID, Port: msg.Port}) diff --git a/docker-compose.yml b/docker-compose.yml index a9caa1f..dcc6643 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,6 +31,7 @@ services: SUPERADMIN_PASSWORD: ${SUPERADMIN_PASSWORD} HEADSCALE_URL: ${HEADSCALE_URL} HEADSCALE_AUTH_KEY: ${HEADSCALE_AUTH_KEY} + MAIN_DOMAIN: ${MAIN_DOMAIN} GITEA_URL: ${GITEA_URL} GITEA_TOKEN: ${GITEA_TOKEN} depends_on: @@ -39,6 +40,34 @@ services: networks: - edubox + tailscale: + image: tailscale/tailscale:latest + container_name: edubox-tailscale + restart: unless-stopped + network_mode: service:server + cap_add: + - NET_ADMIN + - NET_RAW + - SYS_MODULE + devices: + - /dev/net/tun:/dev/net/tun + volumes: + - tailscale_data:/var/lib/tailscale + environment: + HEADSCALE_URL: ${HEADSCALE_URL} + HEADSCALE_AUTH_KEY: ${HEADSCALE_AUTH_KEY} + command: > + sh -c "echo 'net.ipv4.ip_forward = 1' >> /etc/sysctl.conf && + echo 'net.ipv6.conf.all.forwarding = 1' >> /etc/sysctl.conf && + sysctl -p && + mkdir -p /var/run/tailscale && + tailscaled --state=/var/lib/tailscale/tailscaled.state --socket=/var/run/tailscale/tailscaled.sock & + sleep 5 && + tailscale up --authkey=$${HEADSCALE_AUTH_KEY} --login-server=$${HEADSCALE_URL} --accept-routes --hostname=edubox-server --reset && + tail -f /dev/null" + depends_on: + - server + caddy: image: caddy:2-alpine container_name: edubox-caddy @@ -60,9 +89,10 @@ services: restart: unless-stopped command: serve ports: - - "41641:41641/udp" + - "8080:8080" + - "3478:3478/udp" volumes: - - headscale_data:/etc/headscale + - ./headscale:/etc/headscale networks: - edubox @@ -88,6 +118,7 @@ volumes: caddy_config: headscale_data: gitea_data: + tailscale_data: networks: edubox: diff --git a/headscale/config.yaml b/headscale/config.yaml new file mode 100644 index 0000000..b41f6b4 --- /dev/null +++ b/headscale/config.yaml @@ -0,0 +1,42 @@ +# Headscale configuration for EduBox +server_url: https://headscale.alfrednobel.edudeploy.com +listen_addr: 0.0.0.0:8080 +metrics_listen_addr: 0.0.0.0:9090 +grpc_listen_addr: 127.0.0.1:50443 + +noise: + private_key_path: /etc/headscale/noise_private.key + +prefixes: + v4: 100.64.0.0/10 + v6: fd7a:115c:a1e0::/48 + allocation: sequential + +dns: + magic_dns: true + base_domain: edubox.local + nameservers: + global: + - 1.1.1.1 + - 8.8.8.8 + override_local_dns: true + +derp: + server: + enabled: true + region_id: 999 + region_code: headscale + region_name: Headscale Embedded DERP + stun_listen_addr: 0.0.0.0:3478 + private_key_path: /etc/headscale/derp_server_private.key + urls: [] + paths: [] + +database: + type: sqlite3 + sqlite: + path: /etc/headscale/db.sqlite + +log: + format: text + level: info diff --git a/server/Dockerfile b/server/Dockerfile index 276d57e..1c7a5d0 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -5,5 +5,6 @@ COPY package.json package-lock.json* ./ COPY prisma ./prisma RUN npm ci COPY . . +RUN npx prisma generate RUN npm run build CMD ["node_modules/.bin/next", "start"] diff --git a/server/app/api/check-domain/route.ts b/server/app/api/check-domain/route.ts new file mode 100644 index 0000000..b0ec1fa --- /dev/null +++ b/server/app/api/check-domain/route.ts @@ -0,0 +1,28 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; + +const MAIN_DOMAIN = process.env.MAIN_DOMAIN || "alfrednobel.edudeploy.com"; + +export async function GET(req: NextRequest) { + const { searchParams } = new URL(req.url); + const domain = searchParams.get("domain"); + + if (!domain) { + return NextResponse.json({ ok: false }, { status: 400 }); + } + + if (domain === MAIN_DOMAIN || domain === `headscale.${MAIN_DOMAIN}`) { + return NextResponse.json({ ok: true }); + } + + if (!domain.endsWith(`.${MAIN_DOMAIN}`)) { + return NextResponse.json({ ok: false }, { status: 400 }); + } + + const subdomain = domain.replace(`.${MAIN_DOMAIN}`, ""); + const instance = await prisma.instance.findUnique({ + where: { id: subdomain }, + }); + + return NextResponse.json({ ok: !!instance }); +} diff --git a/server/app/api/instances/route.ts b/server/app/api/instances/route.ts index 58c7473..55cf808 100644 --- a/server/app/api/instances/route.ts +++ b/server/app/api/instances/route.ts @@ -18,10 +18,37 @@ export async function GET(req: NextRequest) { const instances = await prisma.instance.findMany({ where, - include: { node: { include: { student: { include: { class: true } } } }, template: true }, + include: { + node: { + include: { + student: { + include: { + class: { + include: { + establishment: true, + }, + }, + }, + }, + }, + }, + template: true, + }, orderBy: { createdAt: "desc" }, }); - return NextResponse.json(instances); + + const enriched = instances.map((inst) => { + const domain = inst.node.student?.class.establishment?.domain; + const publicUrl = domain + ? `https://${inst.id}.${domain}` + : null; + return { + ...inst, + publicUrl, + }; + }); + + return NextResponse.json(enriched); } export async function POST(req: NextRequest) { @@ -35,12 +62,24 @@ export async function POST(req: NextRequest) { data: { nodeId, templateId, port: port || 8080, status: "stopped" }, }); + const node = await prisma.node.findUnique({ + where: { id: nodeId }, + include: { student: { include: { class: { include: { establishment: true } } } } }, + }); + + const domain = node?.student?.class.establishment?.domain; + const publicUrl = domain ? `https://${instance.id}.${domain}` : 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), + 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, domain || "localhost"), }); if (!sent) { @@ -53,9 +92,12 @@ export async function POST(req: NextRequest) { export async function PATCH(req: NextRequest) { const body = await req.json(); const { id, action } = body; - const instance = await prisma.instance.findUnique({ where: { id }, include: { template: true } }); + 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 }); + const domain = instance.node.student?.class.establishment?.domain; + const publicUrl = domain ? `https://${instance.id}.${domain}` : null; + if (action === "stop") { sendToNode(instance.nodeId, { action: "stop", instanceId: instance.id }); await prisma.instance.update({ where: { id }, data: { status: "stopped" } }); @@ -65,7 +107,11 @@ export async function PATCH(req: NextRequest) { 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), + 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, domain || "localhost"), }); if (!sent) await prisma.instance.update({ where: { id }, data: { status: "error" } }); } else if (action === "reset") { diff --git a/server/app/api/proxy/[[...path]]/route.ts b/server/app/api/proxy/[[...path]]/route.ts new file mode 100644 index 0000000..0262708 --- /dev/null +++ b/server/app/api/proxy/[[...path]]/route.ts @@ -0,0 +1,63 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; + +const MAIN_DOMAIN = process.env.MAIN_DOMAIN || "alfrednobel.edudeploy.com"; + +async function proxyRequest(req: NextRequest) { + const host = req.headers.get("host") || ""; + const cleanHost = host.split(":")[0]; + + if (!cleanHost.endsWith(`.${MAIN_DOMAIN}`)) { + return NextResponse.json({ error: "Invalid host" }, { status: 404 }); + } + + const subdomain = cleanHost.replace(`.${MAIN_DOMAIN}`, ""); + + const instance = await prisma.instance.findUnique({ + where: { id: subdomain }, + include: { node: true }, + }); + + if (!instance || !instance.node?.tailscaleIp) { + return NextResponse.json( + { error: "Instance not found or not connected" }, + { status: 404 } + ); + } + + const targetUrl = new URL(req.url); + const upstream = `http://${instance.node.tailscaleIp}:${instance.port}${targetUrl.pathname}${targetUrl.search}`; + + const headers = new Headers(req.headers); + headers.delete("host"); + headers.set("host", cleanHost); + + const upstreamRes = await fetch(upstream, { + method: req.method, + headers, + body: + req.method !== "GET" && req.method !== "HEAD" + ? (req.body as BodyInit) + : undefined, + redirect: "manual", + }); + + const responseHeaders = new Headers(upstreamRes.headers); + // Remove content-encoding because Next.js/fetch handles decompression automatically + responseHeaders.delete("content-encoding"); + responseHeaders.delete("content-length"); + + return new Response(upstreamRes.body, { + status: upstreamRes.status, + statusText: upstreamRes.statusText, + headers: responseHeaders, + }); +} + +export const GET = proxyRequest; +export const POST = proxyRequest; +export const PUT = proxyRequest; +export const PATCH = proxyRequest; +export const DELETE = proxyRequest; +export const HEAD = proxyRequest; +export const OPTIONS = proxyRequest; diff --git a/server/app/dashboard/instances/page.tsx b/server/app/dashboard/instances/page.tsx index 6bc4615..ef2a77e 100644 --- a/server/app/dashboard/instances/page.tsx +++ b/server/app/dashboard/instances/page.tsx @@ -21,10 +21,18 @@ export default async function InstancesPage() { where: establishmentId ? { node: { student: { class: { establishmentId } } } } : {}, - include: { node: { include: { student: { include: { class: true } } } }, template: true }, + include: { node: { include: { student: { include: { class: { include: { establishment: true } } } } } }, template: true }, orderBy: { createdAt: "desc" }, }); + const enrichedInstances = instances.map((inst) => { + const domain = inst.node.student?.class.establishment?.domain; + return { + ...inst, + publicUrl: domain ? `https://${inst.id}.${domain}` : null, + }; + }); + return (
@@ -44,11 +52,12 @@ export default async function InstancesPage() { Étudiant Port Statut + URL publique Actions - {instances.map((inst) => ( + {enrichedInstances.map((inst: any) => ( {inst.id.slice(0, 8)}... {inst.template.name} @@ -58,6 +67,9 @@ export default async function InstancesPage() { {inst.status} + + {inst.publicUrl ? {inst.publicUrl} : "-"} + @@ -65,7 +77,7 @@ export default async function InstancesPage() { ))} {instances.length === 0 && ( - Aucune instance + Aucune instance )} diff --git a/server/lib/websocket.ts b/server/lib/websocket.ts index 6e8003b..26a4d50 100644 --- a/server/lib/websocket.ts +++ b/server/lib/websocket.ts @@ -11,6 +11,7 @@ interface NodeMessage { composeConfig?: string; studentName?: string; error?: string; + tailscaleIp?: string; } const nodes = new Map(); @@ -66,6 +67,15 @@ export function initWebSocketServer(wss: WebSocketServer) { return; } + if (msg.action === "tailscale_ip" && nodeId && msg.tailscaleIp) { + await prisma.node.update({ + where: { id: nodeId }, + data: { tailscaleIp: msg.tailscaleIp }, + }); + console.log("[WS] Tailscale IP updated for", nodeId, ":", msg.tailscaleIp); + return; + } + if (msg.action === "instance_started" && msg.instanceId) { await prisma.instance.update({ where: { id: msg.instanceId }, diff --git a/server/middleware.ts b/server/middleware.ts index d89bb0f..2ca753b 100644 --- a/server/middleware.ts +++ b/server/middleware.ts @@ -1,35 +1,28 @@ -import { withAuth } from "next-auth/middleware"; import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; -export default withAuth( - function middleware(req) { - const { pathname } = req.nextUrl; - const role = req.nextauth.token?.role as string; +const MAIN_DOMAIN = process.env.MAIN_DOMAIN || "alfrednobel.edudeploy.com"; - if (pathname.startsWith("/superadmin")) { - if (role !== "superadmin") { - return NextResponse.redirect(new URL("/dashboard", req.url)); - } - } - - if (pathname.startsWith("/dashboard")) { - if (!role || (role !== "admin" && role !== "teacher" && role !== "superadmin")) { - return NextResponse.redirect(new URL("/login", req.url)); - } - } +export function middleware(req: NextRequest) { + const host = req.headers.get("host") || ""; + const cleanHost = host.split(":")[0]; + if (cleanHost === MAIN_DOMAIN || cleanHost === `www.${MAIN_DOMAIN}`) { return NextResponse.next(); - }, - { - callbacks: { - authorized({ req, token }) { - if (req.nextUrl.pathname.startsWith("/login")) return true; - return !!token; - }, - }, } -); + + if (!cleanHost.endsWith(`.${MAIN_DOMAIN}`)) { + return NextResponse.next(); + } + + const pathname = req.nextUrl.pathname; + const search = req.nextUrl.search; + + // Rewrite to the internal proxy API while preserving the original host header + const rewriteUrl = new URL(`/api/proxy${pathname}${search}`, req.url); + return NextResponse.rewrite(rewriteUrl); +} export const config = { - matcher: ["/dashboard/:path*", "/superadmin/:path*", "/api/protected/:path*"], + matcher: ["/((?!api/proxy|_next|static|favicon.ico|.*\\.).*)"], }; diff --git a/server/prisma/migrations/20250612195600_add_establishment_domain/migration.sql b/server/prisma/migrations/20250612195600_add_establishment_domain/migration.sql new file mode 100644 index 0000000..12c747a --- /dev/null +++ b/server/prisma/migrations/20250612195600_add_establishment_domain/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Establishment" ADD COLUMN "domain" TEXT; diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index af7f575..6155d4a 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -11,6 +11,7 @@ model Establishment { id String @id @default(cuid()) name String slug String @unique + domain String? createdAt DateTime @default(now()) subscription Subscription? users User[] diff --git a/server/prisma/seed.ts b/server/prisma/seed.ts index 0e9d21d..eda2b1e 100644 --- a/server/prisma/seed.ts +++ b/server/prisma/seed.ts @@ -102,14 +102,14 @@ async function main() { WORDPRESS_DB_PASSWORD: ${t.dbPassword} WORDPRESS_DB_PREFIX: wp_ WORDPRESS_CONFIG_EXTRA: | - define('WP_HOME', 'http://localhost:{PORT}'); - define('WP_SITEURL', 'http://localhost:{PORT}'); + define('WP_HOME', '{PUBLIC_URL}'); + define('WP_SITEURL', '{PUBLIC_URL}'); PS_DB_HOST: ${dbHost}:${dbPort} PS_DB_NAME: ${t.dbName} PS_DB_USER: ${t.dbUser} PS_DB_PASSWORD: ${t.dbPassword} PS_DB_PREFIX: ps_ - PS_DOMAIN: localhost:{PORT} + PS_DOMAIN: {PUBLIC_DOMAIN} PS_SHOP_NAME: ${t.name} PS_INSTALL_AUTO: "0" INSTANCE_ID: {INSTANCE_ID}