Phase 4: AI-driven app/database/auth lifecycle
Workspace-owned deploy infra so AI agents can create and destroy
Coolify resources without ever touching the root admin token.
vibn_workspaces
+ coolify_server_uuid, coolify_destination_uuid
+ coolify_environment_name (default "production")
+ coolify_private_key_uuid, gitea_bot_ssh_key_id
ensureWorkspaceProvisioned
+ generates an ed25519 keypair per workspace
+ pushes pubkey to the Gitea bot user (read/write scoped by team)
+ registers privkey in Coolify as a reusable deploy key
New endpoints under /api/workspaces/[slug]/
apps/ POST (private-deploy-key from Gitea repo)
apps/[uuid] PATCH, DELETE?confirm=<name>
apps/[uuid]/domains GET, PATCH (policy: *.{ws}.vibnai.com only)
databases/ GET, POST (8 types incl. postgres, clickhouse, dragonfly)
databases/[uuid] GET, PATCH, DELETE?confirm=<name>
auth/ GET, POST (Pocketbase, Authentik, Keycloak, Pocket-ID, Logto, Supertokens)
auth/[uuid] DELETE?confirm=<name>
MCP (/api/mcp) gains 15 new tools that mirror the REST surface and
enforce the same workspace tenancy + delete-confirm guard.
Safety: destructive ops require ?confirm=<exact-resource-name>; volumes
are kept by default (pass delete_volumes=true to drop).
Made-with: Cursor
This commit is contained in:
@@ -30,8 +30,31 @@ import {
|
||||
TenantError,
|
||||
upsertApplicationEnv,
|
||||
deleteApplicationEnv,
|
||||
// Phase 4 ── create/update/delete + domains + databases + services
|
||||
createPrivateDeployKeyApp,
|
||||
updateApplication,
|
||||
deleteApplication,
|
||||
setApplicationDomains,
|
||||
listDatabasesInProject,
|
||||
createDatabase,
|
||||
getDatabaseInProject,
|
||||
updateDatabase,
|
||||
deleteDatabase,
|
||||
listServicesInProject,
|
||||
createService,
|
||||
getServiceInProject,
|
||||
deleteService,
|
||||
type CoolifyDatabaseType,
|
||||
} from '@/lib/coolify';
|
||||
import { query } from '@/lib/db-postgres';
|
||||
import { getRepo } from '@/lib/gitea';
|
||||
import {
|
||||
giteaSshUrl,
|
||||
isDomainUnderWorkspace,
|
||||
slugify,
|
||||
toDomainsString,
|
||||
workspaceAppFqdn,
|
||||
} from '@/lib/naming';
|
||||
|
||||
const GITEA_API_URL = process.env.GITEA_API_URL ?? 'https://git.vibnai.com';
|
||||
|
||||
@@ -42,7 +65,7 @@ const GITEA_API_URL = process.env.GITEA_API_URL ?? 'https://git.vibnai.com';
|
||||
export async function GET() {
|
||||
return NextResponse.json({
|
||||
name: 'vibn-mcp',
|
||||
version: '2.0.0',
|
||||
version: '2.1.0',
|
||||
authentication: {
|
||||
scheme: 'Bearer',
|
||||
tokenPrefix: 'vibn_sk_',
|
||||
@@ -60,11 +83,24 @@ export async function GET() {
|
||||
'projects.get',
|
||||
'apps.list',
|
||||
'apps.get',
|
||||
'apps.create',
|
||||
'apps.update',
|
||||
'apps.delete',
|
||||
'apps.deploy',
|
||||
'apps.deployments',
|
||||
'apps.domains.list',
|
||||
'apps.domains.set',
|
||||
'apps.envs.list',
|
||||
'apps.envs.upsert',
|
||||
'apps.envs.delete',
|
||||
'databases.list',
|
||||
'databases.create',
|
||||
'databases.get',
|
||||
'databases.update',
|
||||
'databases.delete',
|
||||
'auth.list',
|
||||
'auth.create',
|
||||
'auth.delete',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -126,6 +162,35 @@ export async function POST(request: Request) {
|
||||
case 'apps.envs.delete':
|
||||
return await toolAppsEnvsDelete(principal, params);
|
||||
|
||||
case 'apps.create':
|
||||
return await toolAppsCreate(principal, params);
|
||||
case 'apps.update':
|
||||
return await toolAppsUpdate(principal, params);
|
||||
case 'apps.delete':
|
||||
return await toolAppsDelete(principal, params);
|
||||
case 'apps.domains.list':
|
||||
return await toolAppsDomainsList(principal, params);
|
||||
case 'apps.domains.set':
|
||||
return await toolAppsDomainsSet(principal, params);
|
||||
|
||||
case 'databases.list':
|
||||
return await toolDatabasesList(principal);
|
||||
case 'databases.create':
|
||||
return await toolDatabasesCreate(principal, params);
|
||||
case 'databases.get':
|
||||
return await toolDatabasesGet(principal, params);
|
||||
case 'databases.update':
|
||||
return await toolDatabasesUpdate(principal, params);
|
||||
case 'databases.delete':
|
||||
return await toolDatabasesDelete(principal, params);
|
||||
|
||||
case 'auth.list':
|
||||
return await toolAuthList(principal);
|
||||
case 'auth.create':
|
||||
return await toolAuthCreate(principal, params);
|
||||
case 'auth.delete':
|
||||
return await toolAuthDelete(principal, params);
|
||||
|
||||
default:
|
||||
return NextResponse.json(
|
||||
{ error: `Unknown tool "${action}"` },
|
||||
@@ -351,3 +416,406 @@ async function toolAppsEnvsDelete(principal: Principal, params: Record<string, a
|
||||
await deleteApplicationEnv(appUuid, key);
|
||||
return NextResponse.json({ result: { ok: true, key } });
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// Phase 4: apps create/update/delete + domains
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
async function toolAppsCreate(principal: Principal, params: Record<string, any>) {
|
||||
const ws = principal.workspace;
|
||||
if (!ws.coolify_project_uuid || !ws.coolify_private_key_uuid || !ws.gitea_org) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Workspace not fully provisioned (need Coolify project + deploy key + Gitea org)' },
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
|
||||
const repoIn = String(params.repo ?? '').trim();
|
||||
if (!repoIn) return NextResponse.json({ error: 'Param "repo" is required' }, { status: 400 });
|
||||
|
||||
const parts = repoIn.replace(/\.git$/, '').split('/');
|
||||
const repoOrg = parts.length === 2 ? parts[0] : ws.gitea_org;
|
||||
const repoName = parts.length === 2 ? parts[1] : parts[0];
|
||||
if (repoOrg !== ws.gitea_org) {
|
||||
return NextResponse.json(
|
||||
{ error: `Repo owner ${repoOrg} is not this workspace's org ${ws.gitea_org}` },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
const repo = await getRepo(repoOrg, repoName);
|
||||
if (!repo) {
|
||||
return NextResponse.json({ error: `Repo ${repoOrg}/${repoName} not found in Gitea` }, { status: 404 });
|
||||
}
|
||||
|
||||
const appName = slugify(String(params.name ?? repoName));
|
||||
const fqdn = String(params.domain ?? '').trim()
|
||||
? String(params.domain).replace(/^https?:\/\//, '')
|
||||
: workspaceAppFqdn(ws.slug, appName);
|
||||
if (!isDomainUnderWorkspace(fqdn, ws.slug)) {
|
||||
return NextResponse.json(
|
||||
{ error: `Domain ${fqdn} must end with .${ws.slug}.vibnai.com` },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const created = await createPrivateDeployKeyApp({
|
||||
projectUuid: ws.coolify_project_uuid,
|
||||
serverUuid: ws.coolify_server_uuid ?? undefined,
|
||||
environmentName: ws.coolify_environment_name,
|
||||
destinationUuid: ws.coolify_destination_uuid ?? undefined,
|
||||
privateKeyUuid: ws.coolify_private_key_uuid,
|
||||
gitRepository: giteaSshUrl(repoOrg, repoName),
|
||||
gitBranch: String(params.branch ?? repo.default_branch ?? 'main'),
|
||||
portsExposes: String(params.ports ?? '3000'),
|
||||
buildPack: (params.buildPack as any) ?? 'nixpacks',
|
||||
name: appName,
|
||||
domains: toDomainsString([fqdn]),
|
||||
isAutoDeployEnabled: true,
|
||||
isForceHttpsEnabled: true,
|
||||
instantDeploy: false,
|
||||
});
|
||||
|
||||
// Attach envs
|
||||
if (params.envs && typeof params.envs === 'object') {
|
||||
for (const [k, v] of Object.entries(params.envs as Record<string, unknown>)) {
|
||||
if (!/^[A-Z_][A-Z0-9_]*$/i.test(k)) continue;
|
||||
try {
|
||||
await upsertApplicationEnv(created.uuid, { key: k, value: String(v) });
|
||||
} catch (e) {
|
||||
console.warn('[mcp apps.create] upsert env failed', k, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let deploymentUuid: string | null = null;
|
||||
if (params.instantDeploy !== false) {
|
||||
try {
|
||||
const dep = await deployApplication(created.uuid);
|
||||
deploymentUuid = dep.deployment_uuid ?? null;
|
||||
} catch (e) {
|
||||
console.warn('[mcp apps.create] first deploy failed', e);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
result: {
|
||||
uuid: created.uuid,
|
||||
name: appName,
|
||||
domain: fqdn,
|
||||
url: `https://${fqdn}`,
|
||||
deploymentUuid,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function toolAppsUpdate(principal: Principal, params: Record<string, any>) {
|
||||
const projectUuid = requireCoolifyProject(principal);
|
||||
if (projectUuid instanceof NextResponse) return projectUuid;
|
||||
const appUuid = String(params.uuid ?? params.appUuid ?? '').trim();
|
||||
if (!appUuid) return NextResponse.json({ error: 'Param "uuid" is required' }, { status: 400 });
|
||||
|
||||
await getApplicationInProject(appUuid, projectUuid);
|
||||
|
||||
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<string, unknown> = {};
|
||||
for (const [k, v] of Object.entries(params)) {
|
||||
if (allowed.has(k) && v !== undefined) patch[k] = v;
|
||||
}
|
||||
if (Object.keys(patch).length === 0) {
|
||||
return NextResponse.json({ error: 'No updatable fields in params' }, { status: 400 });
|
||||
}
|
||||
await updateApplication(appUuid, patch);
|
||||
return NextResponse.json({ result: { ok: true, uuid: appUuid } });
|
||||
}
|
||||
|
||||
async function toolAppsDelete(principal: Principal, params: Record<string, any>) {
|
||||
const projectUuid = requireCoolifyProject(principal);
|
||||
if (projectUuid instanceof NextResponse) return projectUuid;
|
||||
const appUuid = String(params.uuid ?? params.appUuid ?? '').trim();
|
||||
if (!appUuid) return NextResponse.json({ error: 'Param "uuid" is required' }, { status: 400 });
|
||||
|
||||
const app = await getApplicationInProject(appUuid, projectUuid);
|
||||
const confirm = String(params.confirm ?? '');
|
||||
if (confirm !== app.name) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Confirmation required', hint: `Pass confirm=${app.name} to delete` },
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
const deleteVolumes = params.deleteVolumes === true;
|
||||
await deleteApplication(appUuid, {
|
||||
deleteConfigurations: true,
|
||||
deleteVolumes,
|
||||
deleteConnectedNetworks: true,
|
||||
dockerCleanup: true,
|
||||
});
|
||||
return NextResponse.json({
|
||||
result: { ok: true, deleted: { uuid: appUuid, name: app.name, volumesKept: !deleteVolumes } },
|
||||
});
|
||||
}
|
||||
|
||||
async function toolAppsDomainsList(principal: Principal, params: Record<string, any>) {
|
||||
const projectUuid = requireCoolifyProject(principal);
|
||||
if (projectUuid instanceof NextResponse) return projectUuid;
|
||||
const appUuid = String(params.uuid ?? params.appUuid ?? '').trim();
|
||||
if (!appUuid) return NextResponse.json({ error: 'Param "uuid" is required' }, { status: 400 });
|
||||
const app = await getApplicationInProject(appUuid, projectUuid);
|
||||
const raw = (app.domains ?? app.fqdn ?? '') as string;
|
||||
const list = raw
|
||||
.split(/[,\s]+/)
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean)
|
||||
.map(s => s.replace(/^https?:\/\//, '').replace(/\/+$/, ''));
|
||||
return NextResponse.json({ result: { uuid: appUuid, domains: list } });
|
||||
}
|
||||
|
||||
async function toolAppsDomainsSet(principal: Principal, params: Record<string, any>) {
|
||||
const ws = principal.workspace;
|
||||
const projectUuid = requireCoolifyProject(principal);
|
||||
if (projectUuid instanceof NextResponse) return projectUuid;
|
||||
const appUuid = String(params.uuid ?? params.appUuid ?? '').trim();
|
||||
const domainsIn = Array.isArray(params.domains) ? params.domains : [];
|
||||
if (!appUuid || domainsIn.length === 0) {
|
||||
return NextResponse.json({ error: 'Params "uuid" and "domains[]" are required' }, { status: 400 });
|
||||
}
|
||||
await getApplicationInProject(appUuid, projectUuid);
|
||||
const normalized: string[] = [];
|
||||
for (const d of domainsIn) {
|
||||
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} must end with .${ws.slug}.vibnai.com` },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
normalized.push(clean);
|
||||
}
|
||||
await setApplicationDomains(appUuid, normalized, { forceOverride: true });
|
||||
return NextResponse.json({ result: { uuid: appUuid, domains: normalized } });
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// Phase 4: databases
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
const DB_TYPES: readonly CoolifyDatabaseType[] = [
|
||||
'postgresql', 'mysql', 'mariadb', 'mongodb',
|
||||
'redis', 'keydb', 'dragonfly', 'clickhouse',
|
||||
];
|
||||
|
||||
async function toolDatabasesList(principal: Principal) {
|
||||
const projectUuid = requireCoolifyProject(principal);
|
||||
if (projectUuid instanceof NextResponse) return projectUuid;
|
||||
const dbs = await listDatabasesInProject(projectUuid);
|
||||
return NextResponse.json({
|
||||
result: dbs.map(d => ({
|
||||
uuid: d.uuid,
|
||||
name: d.name,
|
||||
type: d.type ?? null,
|
||||
status: d.status,
|
||||
isPublic: d.is_public ?? false,
|
||||
publicPort: d.public_port ?? null,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
async function toolDatabasesCreate(principal: Principal, params: Record<string, any>) {
|
||||
const ws = principal.workspace;
|
||||
const projectUuid = requireCoolifyProject(principal);
|
||||
if (projectUuid instanceof NextResponse) return projectUuid;
|
||||
const type = String(params.type ?? '').toLowerCase() as CoolifyDatabaseType;
|
||||
if (!DB_TYPES.includes(type)) {
|
||||
return NextResponse.json(
|
||||
{ error: `Param "type" must be one of: ${DB_TYPES.join(', ')}` },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
const name = slugify(String(params.name ?? `${type}-${Date.now().toString(36)}`));
|
||||
const { uuid } = await createDatabase({
|
||||
type,
|
||||
name,
|
||||
description: params.description ? String(params.description) : undefined,
|
||||
projectUuid,
|
||||
serverUuid: ws.coolify_server_uuid ?? undefined,
|
||||
environmentName: ws.coolify_environment_name,
|
||||
destinationUuid: ws.coolify_destination_uuid ?? undefined,
|
||||
isPublic: params.isPublic === true,
|
||||
publicPort: typeof params.publicPort === 'number' ? params.publicPort : undefined,
|
||||
image: params.image ? String(params.image) : undefined,
|
||||
credentials: params.credentials && typeof params.credentials === 'object' ? params.credentials : {},
|
||||
limits: params.limits && typeof params.limits === 'object' ? params.limits : undefined,
|
||||
instantDeploy: params.instantDeploy !== false,
|
||||
});
|
||||
const db = await getDatabaseInProject(uuid, projectUuid);
|
||||
return NextResponse.json({
|
||||
result: {
|
||||
uuid: db.uuid,
|
||||
name: db.name,
|
||||
type: db.type ?? type,
|
||||
status: db.status,
|
||||
internalUrl: db.internal_db_url ?? null,
|
||||
externalUrl: db.external_db_url ?? null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function toolDatabasesGet(principal: Principal, params: Record<string, any>) {
|
||||
const projectUuid = requireCoolifyProject(principal);
|
||||
if (projectUuid instanceof NextResponse) return projectUuid;
|
||||
const uuid = String(params.uuid ?? '').trim();
|
||||
if (!uuid) return NextResponse.json({ error: 'Param "uuid" is required' }, { status: 400 });
|
||||
const db = await getDatabaseInProject(uuid, projectUuid);
|
||||
return NextResponse.json({
|
||||
result: {
|
||||
uuid: db.uuid,
|
||||
name: db.name,
|
||||
type: db.type ?? null,
|
||||
status: db.status,
|
||||
isPublic: db.is_public ?? false,
|
||||
publicPort: db.public_port ?? null,
|
||||
internalUrl: db.internal_db_url ?? null,
|
||||
externalUrl: db.external_db_url ?? null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function toolDatabasesUpdate(principal: Principal, params: Record<string, any>) {
|
||||
const projectUuid = requireCoolifyProject(principal);
|
||||
if (projectUuid instanceof NextResponse) return projectUuid;
|
||||
const uuid = String(params.uuid ?? '').trim();
|
||||
if (!uuid) return NextResponse.json({ error: 'Param "uuid" is required' }, { status: 400 });
|
||||
await getDatabaseInProject(uuid, projectUuid);
|
||||
const allowed = new Set(['name', 'description', 'is_public', 'public_port', 'image', 'limits_memory', 'limits_cpus']);
|
||||
const patch: Record<string, unknown> = {};
|
||||
for (const [k, v] of Object.entries(params)) {
|
||||
if (allowed.has(k) && v !== undefined) patch[k] = v;
|
||||
}
|
||||
if (Object.keys(patch).length === 0) {
|
||||
return NextResponse.json({ error: 'No updatable fields in params' }, { status: 400 });
|
||||
}
|
||||
await updateDatabase(uuid, patch);
|
||||
return NextResponse.json({ result: { ok: true, uuid } });
|
||||
}
|
||||
|
||||
async function toolDatabasesDelete(principal: Principal, params: Record<string, any>) {
|
||||
const projectUuid = requireCoolifyProject(principal);
|
||||
if (projectUuid instanceof NextResponse) return projectUuid;
|
||||
const uuid = String(params.uuid ?? '').trim();
|
||||
if (!uuid) return NextResponse.json({ error: 'Param "uuid" is required' }, { status: 400 });
|
||||
const db = await getDatabaseInProject(uuid, projectUuid);
|
||||
const confirm = String(params.confirm ?? '');
|
||||
if (confirm !== db.name) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Confirmation required', hint: `Pass confirm=${db.name} to delete` },
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
const deleteVolumes = params.deleteVolumes === true;
|
||||
await deleteDatabase(uuid, {
|
||||
deleteConfigurations: true,
|
||||
deleteVolumes,
|
||||
deleteConnectedNetworks: true,
|
||||
dockerCleanup: true,
|
||||
});
|
||||
return NextResponse.json({
|
||||
result: { ok: true, deleted: { uuid, name: db.name, volumesKept: !deleteVolumes } },
|
||||
});
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// Phase 4: auth providers (Coolify services, curated allowlist)
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
const AUTH_PROVIDERS_MCP: Record<string, string> = {
|
||||
pocketbase: 'pocketbase',
|
||||
authentik: 'authentik',
|
||||
keycloak: 'keycloak',
|
||||
'keycloak-with-postgres': 'keycloak-with-postgres',
|
||||
'pocket-id': 'pocket-id',
|
||||
'pocket-id-with-postgresql': 'pocket-id-with-postgresql',
|
||||
logto: 'logto',
|
||||
'supertokens-with-postgresql': 'supertokens-with-postgresql',
|
||||
};
|
||||
|
||||
async function toolAuthList(principal: Principal) {
|
||||
const projectUuid = requireCoolifyProject(principal);
|
||||
if (projectUuid instanceof NextResponse) return projectUuid;
|
||||
const all = await listServicesInProject(projectUuid);
|
||||
const slugs = new Set(Object.values(AUTH_PROVIDERS_MCP));
|
||||
return NextResponse.json({
|
||||
result: {
|
||||
providers: all
|
||||
.filter(s => {
|
||||
for (const slug of slugs) {
|
||||
if (s.name === slug || s.name.startsWith(`${slug}-`)) return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.map(s => ({ uuid: s.uuid, name: s.name, status: s.status ?? null })),
|
||||
allowedProviders: Object.keys(AUTH_PROVIDERS_MCP),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function toolAuthCreate(principal: Principal, params: Record<string, any>) {
|
||||
const ws = principal.workspace;
|
||||
const projectUuid = requireCoolifyProject(principal);
|
||||
if (projectUuid instanceof NextResponse) return projectUuid;
|
||||
const key = String(params.provider ?? '').toLowerCase().trim();
|
||||
const coolifyType = AUTH_PROVIDERS_MCP[key];
|
||||
if (!coolifyType) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: `Unsupported provider "${key}"`,
|
||||
allowed: Object.keys(AUTH_PROVIDERS_MCP),
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
const name = slugify(String(params.name ?? key));
|
||||
const { uuid } = await createService({
|
||||
projectUuid,
|
||||
type: coolifyType,
|
||||
name,
|
||||
description: params.description ? String(params.description) : undefined,
|
||||
serverUuid: ws.coolify_server_uuid ?? undefined,
|
||||
environmentName: ws.coolify_environment_name,
|
||||
destinationUuid: ws.coolify_destination_uuid ?? undefined,
|
||||
instantDeploy: params.instantDeploy !== false,
|
||||
});
|
||||
const svc = await getServiceInProject(uuid, projectUuid);
|
||||
return NextResponse.json({
|
||||
result: { uuid: svc.uuid, name: svc.name, provider: key, status: svc.status ?? null },
|
||||
});
|
||||
}
|
||||
|
||||
async function toolAuthDelete(principal: Principal, params: Record<string, any>) {
|
||||
const projectUuid = requireCoolifyProject(principal);
|
||||
if (projectUuid instanceof NextResponse) return projectUuid;
|
||||
const uuid = String(params.uuid ?? '').trim();
|
||||
if (!uuid) return NextResponse.json({ error: 'Param "uuid" is required' }, { status: 400 });
|
||||
const svc = await getServiceInProject(uuid, projectUuid);
|
||||
const confirm = String(params.confirm ?? '');
|
||||
if (confirm !== svc.name) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Confirmation required', hint: `Pass confirm=${svc.name} to delete` },
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
const deleteVolumes = params.deleteVolumes === true;
|
||||
await deleteService(uuid, {
|
||||
deleteConfigurations: true,
|
||||
deleteVolumes,
|
||||
deleteConnectedNetworks: true,
|
||||
dockerCleanup: true,
|
||||
});
|
||||
return NextResponse.json({
|
||||
result: { ok: true, deleted: { uuid, name: svc.name, volumesKept: !deleteVolumes } },
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user