/** * GET /api/workspaces/[slug]/apps/[uuid]/domains — list current domains * PATCH /api/workspaces/[slug]/apps/[uuid]/domains — replace domain set * * Body: { domains: string[] } — each must end with .{workspace}.vibnai.com. * We enforce workspace-subdomain policy here to prevent AI-driven * hijacking of other workspaces' subdomains. */ import { NextResponse } from 'next/server'; import { requireWorkspacePrincipal } from '@/lib/auth/workspace-auth'; import { getApplicationInProject, setApplicationDomains, TenantError, } from '@/lib/coolify'; import { isDomainUnderWorkspace, parseDomainsString, workspaceAppFqdn, slugify, } from '@/lib/naming'; export async function GET( request: Request, { params }: { params: Promise<{ slug: string; uuid: string }> } ) { const { slug, uuid } = await params; const principal = await requireWorkspacePrincipal(request, { targetSlug: slug }); if (principal instanceof NextResponse) return principal; const ws = principal.workspace; if (!ws.coolify_project_uuid) { return NextResponse.json({ error: 'Workspace has no Coolify project yet' }, { status: 503 }); } try { const app = await getApplicationInProject(uuid, ws.coolify_project_uuid); return NextResponse.json({ uuid: app.uuid, name: app.name, domains: parseDomainsString(app.domains ?? app.fqdn ?? ''), workspaceDomainSuffix: `${ws.slug}.vibnai.com`, }); } catch (err) { if (err instanceof TenantError) { return NextResponse.json({ error: err.message }, { status: 403 }); } return NextResponse.json({ error: 'App not found' }, { status: 404 }); } } export async function PATCH( request: Request, { params }: { params: Promise<{ slug: string; uuid: string }> } ) { const { slug, uuid } = await params; const principal = await requireWorkspacePrincipal(request, { targetSlug: slug }); if (principal instanceof NextResponse) return principal; const ws = principal.workspace; if (!ws.coolify_project_uuid) { return NextResponse.json({ error: 'Workspace has no Coolify project yet' }, { status: 503 }); } let app; try { app = await getApplicationInProject(uuid, ws.coolify_project_uuid); } catch (err) { if (err instanceof TenantError) { return NextResponse.json({ error: err.message }, { status: 403 }); } return NextResponse.json({ error: 'App not found' }, { status: 404 }); } let body: { domains?: string[] } = {}; try { body = await request.json(); } catch { return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); } const raw = Array.isArray(body.domains) ? body.domains : []; if (raw.length === 0) { return NextResponse.json({ error: '`domains` must be a non-empty array' }, { status: 400 }); } // Normalize + policy-check. const normalized: string[] = []; for (const d of raw) { if (typeof d !== 'string' || !d.trim()) continue; const clean = d.replace(/^https?:\/\//, '').replace(/\/+$/, '').toLowerCase(); if (!isDomainUnderWorkspace(clean, ws.slug)) { return NextResponse.json( { error: `Domain ${clean} is not allowed; must end with .${ws.slug}.vibnai.com`, hint: `Use ${workspaceAppFqdn(ws.slug, slugify(app.name))}`, }, { status: 403 } ); } normalized.push(clean); } try { await setApplicationDomains(uuid, normalized, { forceOverride: true }); return NextResponse.json({ ok: true, uuid, domains: normalized }); } catch (err) { return NextResponse.json( { error: 'Coolify domain update failed', details: err instanceof Error ? err.message : String(err) }, { status: 502 } ); } }