/** * GET /api/workspaces/[slug]/apps/[uuid]/envs — list env vars * PATCH /api/workspaces/[slug]/apps/[uuid]/envs — upsert one env var * body: { key, value, is_preview?, is_build_time?, is_literal?, is_multiline? } * DELETE /api/workspaces/[slug]/apps/[uuid]/envs?key=FOO — delete one env var * * Tenant boundary: the app must belong to the workspace's Coolify project. */ import { NextResponse } from 'next/server'; import { requireWorkspacePrincipal } from '@/lib/auth/workspace-auth'; import { deleteApplicationEnv, getApplicationInProject, listApplicationEnvs, TenantError, upsertApplicationEnv, } from '@/lib/coolify'; async function verify(request: Request, slug: string, uuid: string) { const principal = await requireWorkspacePrincipal(request, { targetSlug: slug }); if (principal instanceof NextResponse) return { error: principal }; const ws = principal.workspace; if (!ws.coolify_project_uuid) { return { error: NextResponse.json( { error: 'Workspace has no Coolify project yet' }, { status: 503 } ), }; } try { await getApplicationInProject(uuid, ws.coolify_project_uuid); } catch (err) { if (err instanceof TenantError) { return { error: NextResponse.json({ error: err.message }, { status: 403 }) }; } return { error: NextResponse.json( { error: 'Coolify request failed', details: String(err) }, { status: 502 } ), }; } return { principal }; } export async function GET( request: Request, { params }: { params: Promise<{ slug: string; uuid: string }> } ) { const { slug, uuid } = await params; const check = await verify(request, slug, uuid); if ('error' in check) return check.error; try { const envs = await listApplicationEnvs(uuid); // Redact values by default for API-key callers — they can re-fetch // with ?reveal=true when they need the actual values (e.g. to copy // a DATABASE_URL). Session callers always get full values. const url = new URL(request.url); const reveal = check.principal.source === 'session' || url.searchParams.get('reveal') === 'true'; return NextResponse.json({ envs: envs.map(e => ({ key: e.key, value: reveal ? e.value : maskValue(e.value), isPreview: e.is_preview ?? false, isBuildTime: e.is_build_time ?? false, isLiteral: e.is_literal ?? false, isMultiline: e.is_multiline ?? false, })), }); } catch (err) { return NextResponse.json( { error: 'Coolify request failed', details: String(err) }, { status: 502 } ); } } export async function PATCH( request: Request, { params }: { params: Promise<{ slug: string; uuid: string }> } ) { const { slug, uuid } = await params; const check = await verify(request, slug, uuid); if ('error' in check) return check.error; let body: { key?: string; value?: string; is_preview?: boolean; is_build_time?: boolean; is_literal?: boolean; is_multiline?: boolean; }; try { body = await request.json(); } catch { return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); } if (!body.key || typeof body.value !== 'string') { return NextResponse.json({ error: 'Fields "key" and "value" are required' }, { status: 400 }); } try { const env = await upsertApplicationEnv(uuid, { key: body.key, value: body.value, is_preview: body.is_preview ?? false, is_build_time: body.is_build_time ?? false, is_literal: body.is_literal ?? false, is_multiline: body.is_multiline ?? false, }); return NextResponse.json({ ok: true, key: env.key }); } catch (err) { return NextResponse.json( { error: 'Coolify request failed', details: String(err) }, { status: 502 } ); } } export async function DELETE( request: Request, { params }: { params: Promise<{ slug: string; uuid: string }> } ) { const { slug, uuid } = await params; const check = await verify(request, slug, uuid); if ('error' in check) return check.error; const key = new URL(request.url).searchParams.get('key'); if (!key) { return NextResponse.json({ error: 'Query param "key" is required' }, { status: 400 }); } try { await deleteApplicationEnv(uuid, key); return NextResponse.json({ ok: true, key }); } catch (err) { return NextResponse.json( { error: 'Coolify request failed', details: String(err) }, { status: 502 } ); } } function maskValue(v: string): string { if (!v) return ''; if (v.length <= 4) return '•'.repeat(v.length); return `${v.slice(0, 2)}${'•'.repeat(Math.min(v.length - 4, 10))}${v.slice(-2)}`; }