/** * GET /api/workspaces/[slug]/apps/[uuid] — app details * PATCH /api/workspaces/[slug]/apps/[uuid] — update fields (name/branch/build config) * DELETE /api/workspaces/[slug]/apps/[uuid]?confirm= * — destroy app. Volumes kept by default. * * All verify the app's project uuid matches the workspace's before * acting. DELETE additionally requires `?confirm=` * to prevent AI-driven accidents. */ import { NextResponse } from 'next/server'; import { requireWorkspacePrincipal } from '@/lib/auth/workspace-auth'; import { getApplicationInProject, projectUuidOf, TenantError, updateApplication, deleteApplication, } from '@/lib/coolify'; 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, status: app.status, fqdn: app.fqdn ?? null, domains: app.domains ?? null, gitRepository: app.git_repository ?? null, gitBranch: app.git_branch ?? null, projectUuid: projectUuidOf(app), }); } catch (err) { if (err instanceof TenantError) { return NextResponse.json({ error: err.message }, { status: 403 }); } return NextResponse.json( { error: 'Coolify request failed', details: err instanceof Error ? err.message : String(err) }, { status: 502 } ); } } 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 }); } // Verify tenancy first (400-style fail fast on cross-tenant access). try { 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: Record = {}; try { body = await request.json(); } catch { return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); } // Whitelist which Coolify fields we expose to AI-level callers. // Domains are managed via the dedicated /domains subroute. const allowed = new Set([ 'name', 'description', 'git_branch', 'build_pack', 'ports_exposes', 'install_command', 'build_command', 'start_command', 'base_directory', 'dockerfile_location', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'static_image', ]); const patch: Record = {}; for (const [k, v] of Object.entries(body)) { if (allowed.has(k) && v !== undefined) patch[k] = v; } if (Object.keys(patch).length === 0) { return NextResponse.json({ error: 'No updatable fields in body' }, { status: 400 }); } try { await updateApplication(uuid, patch); return NextResponse.json({ ok: true, uuid }); } catch (err) { return NextResponse.json( { error: 'Coolify update failed', details: err instanceof Error ? err.message : String(err) }, { status: 502 } ); } } export async function DELETE( 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 }); } // Resolve the app and verify tenancy. 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 }); } // Require `?confirm=` to prevent accidental destroys. const url = new URL(request.url); const confirm = url.searchParams.get('confirm'); if (confirm !== app.name) { return NextResponse.json( { error: 'Confirmation required', hint: `Pass ?confirm=${app.name} to delete this app`, }, { status: 409 } ); } // Default: preserve volumes (user data). Caller can opt in. const deleteVolumes = url.searchParams.get('delete_volumes') === 'true'; const deleteConfigurations = url.searchParams.get('delete_configurations') !== 'false'; const deleteConnectedNetworks = url.searchParams.get('delete_connected_networks') !== 'false'; const dockerCleanup = url.searchParams.get('docker_cleanup') !== 'false'; try { await deleteApplication(uuid, { deleteConfigurations, deleteVolumes, deleteConnectedNetworks, dockerCleanup, }); return NextResponse.json({ ok: true, deleted: { uuid, name: app.name, volumesKept: !deleteVolumes } }); } catch (err) { return NextResponse.json( { error: 'Coolify delete failed', details: err instanceof Error ? err.message : String(err) }, { status: 502 } ); } }