feat(ai-access): per-workspace Gitea bot + tenant-safe Coolify proxy + MCP
Ship Phases 1–3 of the multi-tenant AI access plan so an AI agent can
act on a Vibn workspace with one bearer token and zero admin reach.
Phase 1 — Gitea bot per workspace
- Add gitea_bot_username / gitea_bot_user_id / gitea_bot_token_encrypted
columns to vibn_workspaces (migrate route).
- New lib/auth/secret-box.ts (AES-256-GCM, VIBN_SECRETS_KEY) for PAT at rest.
- Extend lib/gitea.ts with createUser, createAccessTokenFor (Sudo PAT),
createOrgTeam, addOrgTeamMember, ensureOrgTeamMembership.
- ensureWorkspaceProvisioned now mints a vibn-bot-<slug> user, adds it to
a Writers team (write perms only) on the workspace's org, and stores
its PAT encrypted.
- GET /api/workspaces/[slug]/gitea-credentials returns a workspace-scoped
bot PAT + clone URL template; session or vibn_sk_ bearer auth.
Phase 2 — Tenant-safe Coolify proxy + real MCP
- lib/coolify.ts: projectUuidOf, listApplicationsInProject,
getApplicationInProject, TenantError, env CRUD, deployments list.
- Workspace-scoped REST endpoints (all filtered by coolify_project_uuid):
GET/POST /api/workspaces/[slug]/apps/[uuid](/deploy|/envs|/deployments),
GET /api/workspaces/[slug]/deployments/[deploymentUuid]/logs.
- Full rewrite of /api/mcp off legacy Firebase onto Postgres vibn_sk_
keys, exposing workspace.describe, gitea.credentials, projects.*,
apps.* (list/get/deploy/deployments, envs.list/upsert/delete).
Phase 3 — Settings UI AI bundle
- GET /api/workspaces/[slug]/bootstrap.sh: curl|sh installer that writes
.cursor/rules, .cursor/mcp.json and appends VIBN_* to .env.local.
Embeds the caller's vibn_sk_ token when invoked with bearer auth.
- WorkspaceKeysPanel: single AiAccessBundleCard with system-prompt block,
one-line bootstrap, Reveal-bot-PAT button, collapsible manual-setup
fallback. Minted-key modal also shows the bootstrap one-liner.
Ops prerequisites:
- Set VIBN_SECRETS_KEY (>=16 chars) on the frontend.
- Run /api/admin/migrate to add the three bot columns.
- GITEA_API_TOKEN must be a site-admin token (needed for admin/users
+ Sudo PAT mint); otherwise provision_status lands on 'partial'.
Made-with: Cursor
This commit is contained in:
@@ -1,294 +1,70 @@
|
||||
/**
|
||||
* Vibn MCP HTTP API
|
||||
*
|
||||
* Exposes MCP capabilities over HTTP for web-based AI assistants
|
||||
* Vibn MCP HTTP bridge.
|
||||
*
|
||||
* Authenticates via a workspace-scoped `vibn_sk_...` token (session
|
||||
* cookies also work for browser debugging). Every tool call is
|
||||
* executed inside the bound workspace's tenant boundary — Coolify
|
||||
* requests verify the app's project uuid, and git credentials are
|
||||
* pinned to the workspace's Gitea org/bot.
|
||||
*
|
||||
* Exposed tools are a stable subset of the Vibn REST API so agents
|
||||
* have one well-typed entry point regardless of deployment host.
|
||||
*
|
||||
* Protocol notes:
|
||||
* - This is a thin, JSON-over-HTTP MCP shim. The `mcp.json` in a
|
||||
* user's Cursor config points at this URL and stores the bearer
|
||||
* token. We keep the shape compatible with MCP clients that
|
||||
* speak `{ action, params }` calls.
|
||||
*/
|
||||
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getAdminAuth, getAdminDb } from '@/lib/firebase/admin';
|
||||
import { requireWorkspacePrincipal } from '@/lib/auth/workspace-auth';
|
||||
import { getWorkspaceBotCredentials, ensureWorkspaceProvisioned } from '@/lib/workspaces';
|
||||
import {
|
||||
deployApplication,
|
||||
getApplicationInProject,
|
||||
listApplicationDeployments,
|
||||
listApplicationEnvs,
|
||||
listApplicationsInProject,
|
||||
projectUuidOf,
|
||||
TenantError,
|
||||
upsertApplicationEnv,
|
||||
deleteApplicationEnv,
|
||||
} from '@/lib/coolify';
|
||||
import { query } from '@/lib/db-postgres';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
// Authenticate user
|
||||
const authHeader = request.headers.get('Authorization');
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
const GITEA_API_URL = process.env.GITEA_API_URL ?? 'https://git.vibnai.com';
|
||||
|
||||
const token = authHeader.split('Bearer ')[1];
|
||||
const adminAuth = getAdminAuth();
|
||||
const adminDb = getAdminDb();
|
||||
|
||||
let userId: string;
|
||||
|
||||
// Try MCP API key first (for ChatGPT integration)
|
||||
if (token.startsWith('vibn_mcp_')) {
|
||||
const mcpKeysSnapshot = await adminDb
|
||||
.collection('mcpKeys')
|
||||
.where('key', '==', token)
|
||||
.limit(1)
|
||||
.get();
|
||||
// ──────────────────────────────────────────────────
|
||||
// Capability descriptor
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
if (mcpKeysSnapshot.empty) {
|
||||
return NextResponse.json({ error: 'Invalid MCP API key' }, { status: 401 });
|
||||
}
|
||||
|
||||
const keyDoc = mcpKeysSnapshot.docs[0];
|
||||
userId = keyDoc.data().userId;
|
||||
|
||||
// Update last used timestamp
|
||||
await keyDoc.ref.update({
|
||||
lastUsed: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
// Try Firebase ID token (for direct user access)
|
||||
try {
|
||||
const decodedToken = await adminAuth.verifyIdToken(token);
|
||||
userId = decodedToken.uid;
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { action, params } = body;
|
||||
|
||||
// Handle different MCP actions
|
||||
switch (action) {
|
||||
case 'list_resources': {
|
||||
return NextResponse.json({
|
||||
resources: [
|
||||
{
|
||||
uri: `vibn://projects/${userId}`,
|
||||
name: 'My Projects',
|
||||
description: 'All your Vibn projects',
|
||||
mimeType: 'application/json',
|
||||
},
|
||||
{
|
||||
uri: `vibn://sessions/${userId}`,
|
||||
name: 'My Sessions',
|
||||
description: 'All your coding sessions',
|
||||
mimeType: 'application/json',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
case 'read_resource': {
|
||||
const { uri } = params;
|
||||
|
||||
if (uri === `vibn://projects/${userId}`) {
|
||||
const projectsSnapshot = await adminDb
|
||||
.collection('projects')
|
||||
.where('userId', '==', userId)
|
||||
.orderBy('createdAt', 'desc')
|
||||
.limit(50)
|
||||
.get();
|
||||
|
||||
const projects = projectsSnapshot.docs.map(doc => ({
|
||||
id: doc.id,
|
||||
...doc.data(),
|
||||
}));
|
||||
|
||||
return NextResponse.json({
|
||||
contents: [
|
||||
{
|
||||
uri,
|
||||
mimeType: 'application/json',
|
||||
text: JSON.stringify(projects, null, 2),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (uri.startsWith('vibn://projects/') && uri.split('/').length === 4) {
|
||||
const projectId = uri.split('/')[3];
|
||||
const projectDoc = await adminDb.collection('projects').doc(projectId).get();
|
||||
|
||||
if (!projectDoc.exists || projectDoc.data()?.userId !== userId) {
|
||||
return NextResponse.json({ error: 'Project not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
contents: [
|
||||
{
|
||||
uri,
|
||||
mimeType: 'application/json',
|
||||
text: JSON.stringify({ id: projectDoc.id, ...projectDoc.data() }, null, 2),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (uri === `vibn://sessions/${userId}`) {
|
||||
const sessionsSnapshot = await adminDb
|
||||
.collection('sessions')
|
||||
.where('userId', '==', userId)
|
||||
.orderBy('createdAt', 'desc')
|
||||
.limit(50)
|
||||
.get();
|
||||
|
||||
const sessions = sessionsSnapshot.docs.map(doc => ({
|
||||
id: doc.id,
|
||||
...doc.data(),
|
||||
}));
|
||||
|
||||
return NextResponse.json({
|
||||
contents: [
|
||||
{
|
||||
uri,
|
||||
mimeType: 'application/json',
|
||||
text: JSON.stringify(sessions, null, 2),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Unknown resource' }, { status: 404 });
|
||||
}
|
||||
|
||||
case 'call_tool': {
|
||||
const { name, arguments: args } = params;
|
||||
|
||||
if (name === 'get_project_summary') {
|
||||
const { projectId } = args;
|
||||
|
||||
const projectDoc = await adminDb.collection('projects').doc(projectId).get();
|
||||
if (!projectDoc.exists || projectDoc.data()?.userId !== userId) {
|
||||
return NextResponse.json({ error: 'Project not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const project = { id: projectDoc.id, ...projectDoc.data() };
|
||||
|
||||
const sessionsSnapshot = await adminDb
|
||||
.collection('sessions')
|
||||
.where('projectId', '==', projectId)
|
||||
.where('userId', '==', userId)
|
||||
.get();
|
||||
|
||||
const sessions = sessionsSnapshot.docs.map(doc => doc.data());
|
||||
const totalCost = sessions.reduce((sum, s: any) => sum + (s.cost || 0), 0);
|
||||
const totalTokens = sessions.reduce((sum, s: any) => sum + (s.tokensUsed || 0), 0);
|
||||
const totalDuration = sessions.reduce((sum, s: any) => sum + (s.duration || 0), 0);
|
||||
|
||||
const summary = {
|
||||
project,
|
||||
stats: {
|
||||
totalSessions: sessions.length,
|
||||
totalCost,
|
||||
totalTokens,
|
||||
totalDuration,
|
||||
},
|
||||
recentSessions: sessions.slice(0, 5),
|
||||
};
|
||||
|
||||
return NextResponse.json({
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(summary, null, 2),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (name === 'search_sessions') {
|
||||
const { projectId, workspacePath } = args;
|
||||
|
||||
let query = adminDb.collection('sessions').where('userId', '==', userId);
|
||||
|
||||
if (projectId) {
|
||||
query = query.where('projectId', '==', projectId) as any;
|
||||
}
|
||||
if (workspacePath) {
|
||||
query = query.where('workspacePath', '==', workspacePath) as any;
|
||||
}
|
||||
|
||||
const snapshot = await (query as any).orderBy('createdAt', 'desc').limit(50).get();
|
||||
const sessions = snapshot.docs.map((doc: any) => ({
|
||||
id: doc.id,
|
||||
...doc.data(),
|
||||
}));
|
||||
|
||||
return NextResponse.json({
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(sessions, null, 2),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (name === 'get_conversation_context') {
|
||||
const { projectId, limit = 50 } = args;
|
||||
|
||||
const projectDoc = await adminDb.collection('projects').doc(projectId).get();
|
||||
if (!projectDoc.exists || projectDoc.data()?.userId !== userId) {
|
||||
return NextResponse.json({ error: 'Project not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const conversationsSnapshot = await adminDb
|
||||
.collection('projects')
|
||||
.doc(projectId)
|
||||
.collection('aiConversations')
|
||||
.orderBy('createdAt', 'asc')
|
||||
.limit(limit)
|
||||
.get();
|
||||
|
||||
const conversations = conversationsSnapshot.docs.map(doc => ({
|
||||
id: doc.id,
|
||||
...doc.data(),
|
||||
}));
|
||||
|
||||
return NextResponse.json({
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(conversations, null, 2),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Unknown tool' }, { status: 404 });
|
||||
}
|
||||
|
||||
default:
|
||||
return NextResponse.json({ error: 'Unknown action' }, { status: 400 });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('MCP API error:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to process MCP request',
|
||||
details: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// GET endpoint to list capabilities
|
||||
export async function GET(request: Request) {
|
||||
export async function GET() {
|
||||
return NextResponse.json({
|
||||
name: 'vibn-mcp-server',
|
||||
version: '1.0.0',
|
||||
name: 'vibn-mcp',
|
||||
version: '2.0.0',
|
||||
authentication: {
|
||||
scheme: 'Bearer',
|
||||
tokenPrefix: 'vibn_sk_',
|
||||
description:
|
||||
'Workspace-scoped token minted at /settings. Every tool call is ' +
|
||||
'automatically restricted to the workspace the token belongs to.',
|
||||
},
|
||||
capabilities: {
|
||||
resources: {
|
||||
supported: true,
|
||||
endpoints: [
|
||||
'vibn://projects/{userId}',
|
||||
'vibn://projects/{userId}/{projectId}',
|
||||
'vibn://sessions/{userId}',
|
||||
],
|
||||
},
|
||||
tools: {
|
||||
supported: true,
|
||||
available: [
|
||||
'get_project_summary',
|
||||
'search_sessions',
|
||||
'get_conversation_context',
|
||||
'workspace.describe',
|
||||
'gitea.credentials',
|
||||
'projects.list',
|
||||
'projects.get',
|
||||
'apps.list',
|
||||
'apps.get',
|
||||
'apps.deploy',
|
||||
'apps.deployments',
|
||||
'apps.envs.list',
|
||||
'apps.envs.upsert',
|
||||
'apps.envs.delete',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -296,3 +72,282 @@ export async function GET(request: Request) {
|
||||
});
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// Tool dispatcher
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const principal = await requireWorkspacePrincipal(request);
|
||||
if (principal instanceof NextResponse) return principal;
|
||||
|
||||
let body: { action?: string; tool?: string; params?: Record<string, unknown> };
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Accept either `{ action, params }` or `{ tool, params }` shapes.
|
||||
const action = (body.tool ?? body.action ?? '') as string;
|
||||
const params = (body.params ?? {}) as Record<string, any>;
|
||||
|
||||
try {
|
||||
switch (action) {
|
||||
case 'workspace.describe':
|
||||
return NextResponse.json({ result: describeWorkspace(principal) });
|
||||
|
||||
case 'gitea.credentials':
|
||||
return await toolGiteaCredentials(principal);
|
||||
|
||||
case 'projects.list':
|
||||
return await toolProjectsList(principal);
|
||||
|
||||
case 'projects.get':
|
||||
return await toolProjectsGet(principal, params);
|
||||
|
||||
case 'apps.list':
|
||||
return await toolAppsList(principal);
|
||||
|
||||
case 'apps.get':
|
||||
return await toolAppsGet(principal, params);
|
||||
|
||||
case 'apps.deploy':
|
||||
return await toolAppsDeploy(principal, params);
|
||||
|
||||
case 'apps.deployments':
|
||||
return await toolAppsDeployments(principal, params);
|
||||
|
||||
case 'apps.envs.list':
|
||||
return await toolAppsEnvsList(principal, params);
|
||||
|
||||
case 'apps.envs.upsert':
|
||||
return await toolAppsEnvsUpsert(principal, params);
|
||||
|
||||
case 'apps.envs.delete':
|
||||
return await toolAppsEnvsDelete(principal, params);
|
||||
|
||||
default:
|
||||
return NextResponse.json(
|
||||
{ error: `Unknown tool "${action}"` },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof TenantError) {
|
||||
return NextResponse.json({ error: err.message }, { status: 403 });
|
||||
}
|
||||
console.error('[mcp] tool failed', action, err);
|
||||
return NextResponse.json(
|
||||
{ error: 'Tool execution failed', details: err instanceof Error ? err.message : String(err) },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// Tool implementations
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
type Principal = Extract<
|
||||
Awaited<ReturnType<typeof requireWorkspacePrincipal>>,
|
||||
{ source: 'session' | 'api_key' }
|
||||
>;
|
||||
|
||||
function describeWorkspace(principal: Principal) {
|
||||
const w = principal.workspace;
|
||||
return {
|
||||
slug: w.slug,
|
||||
name: w.name,
|
||||
coolifyProjectUuid: w.coolify_project_uuid,
|
||||
giteaOrg: w.gitea_org,
|
||||
giteaBotUsername: w.gitea_bot_username,
|
||||
provisionStatus: w.provision_status,
|
||||
provisionError: w.provision_error,
|
||||
principal: { source: principal.source, apiKeyId: principal.apiKeyId ?? null },
|
||||
};
|
||||
}
|
||||
|
||||
async function toolGiteaCredentials(principal: Principal) {
|
||||
let ws = principal.workspace;
|
||||
if (!ws.gitea_bot_token_encrypted || !ws.gitea_org) {
|
||||
ws = await ensureWorkspaceProvisioned(ws);
|
||||
}
|
||||
const creds = getWorkspaceBotCredentials(ws);
|
||||
if (!creds) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Workspace has no Gitea bot yet', provisionStatus: ws.provision_status },
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
const apiBase = GITEA_API_URL.replace(/\/$/, '');
|
||||
const host = new URL(apiBase).host;
|
||||
return NextResponse.json({
|
||||
result: {
|
||||
org: creds.org,
|
||||
username: creds.username,
|
||||
token: creds.token,
|
||||
apiBase,
|
||||
host,
|
||||
cloneUrlTemplate: `https://${creds.username}:${creds.token}@${host}/${creds.org}/{{repo}}.git`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function toolProjectsList(principal: Principal) {
|
||||
const rows = await query<{ id: string; data: any; created_at: Date; updated_at: Date }>(
|
||||
`SELECT id, data, created_at, updated_at
|
||||
FROM fs_projects
|
||||
WHERE vibn_workspace_id = $1
|
||||
OR workspace = $2
|
||||
ORDER BY created_at DESC`,
|
||||
[principal.workspace.id, principal.workspace.slug]
|
||||
);
|
||||
return NextResponse.json({
|
||||
result: rows.map(r => ({
|
||||
id: r.id,
|
||||
name: r.data?.name ?? null,
|
||||
repo: r.data?.repoName ?? null,
|
||||
giteaRepo: r.data?.giteaRepo ?? null,
|
||||
coolifyAppUuid: r.data?.coolifyAppUuid ?? null,
|
||||
createdAt: r.created_at,
|
||||
updatedAt: r.updated_at,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
async function toolProjectsGet(principal: Principal, params: Record<string, any>) {
|
||||
const projectId = String(params.projectId ?? params.id ?? '').trim();
|
||||
if (!projectId) {
|
||||
return NextResponse.json({ error: 'Param "projectId" is required' }, { status: 400 });
|
||||
}
|
||||
const rows = await query<{ id: string; data: any; created_at: Date; updated_at: Date }>(
|
||||
`SELECT id, data, created_at, updated_at
|
||||
FROM fs_projects
|
||||
WHERE id = $1
|
||||
AND (vibn_workspace_id = $2 OR workspace = $3)
|
||||
LIMIT 1`,
|
||||
[projectId, principal.workspace.id, principal.workspace.slug]
|
||||
);
|
||||
if (rows.length === 0) {
|
||||
return NextResponse.json({ error: 'Project not found in this workspace' }, { status: 404 });
|
||||
}
|
||||
const r = rows[0];
|
||||
return NextResponse.json({
|
||||
result: { id: r.id, data: r.data, createdAt: r.created_at, updatedAt: r.updated_at },
|
||||
});
|
||||
}
|
||||
|
||||
function requireCoolifyProject(principal: Principal): string | NextResponse {
|
||||
const projectUuid = principal.workspace.coolify_project_uuid;
|
||||
if (!projectUuid) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Workspace has no Coolify project yet' },
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
return projectUuid;
|
||||
}
|
||||
|
||||
async function toolAppsList(principal: Principal) {
|
||||
const projectUuid = requireCoolifyProject(principal);
|
||||
if (projectUuid instanceof NextResponse) return projectUuid;
|
||||
const apps = await listApplicationsInProject(projectUuid);
|
||||
return NextResponse.json({
|
||||
result: apps.map(a => ({
|
||||
uuid: a.uuid,
|
||||
name: a.name,
|
||||
status: a.status,
|
||||
fqdn: a.fqdn ?? null,
|
||||
gitRepository: a.git_repository ?? null,
|
||||
gitBranch: a.git_branch ?? null,
|
||||
projectUuid: projectUuidOf(a),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
async function toolAppsGet(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);
|
||||
return NextResponse.json({ result: app });
|
||||
}
|
||||
|
||||
async function toolAppsDeploy(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 { deployment_uuid } = await deployApplication(appUuid);
|
||||
return NextResponse.json({ result: { deploymentUuid: deployment_uuid, appUuid } });
|
||||
}
|
||||
|
||||
async function toolAppsDeployments(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 deployments = await listApplicationDeployments(appUuid);
|
||||
return NextResponse.json({ result: deployments });
|
||||
}
|
||||
|
||||
async function toolAppsEnvsList(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 envs = await listApplicationEnvs(appUuid);
|
||||
return NextResponse.json({ result: envs });
|
||||
}
|
||||
|
||||
async function toolAppsEnvsUpsert(principal: Principal, params: Record<string, any>) {
|
||||
const projectUuid = requireCoolifyProject(principal);
|
||||
if (projectUuid instanceof NextResponse) return projectUuid;
|
||||
const appUuid = String(params.uuid ?? params.appUuid ?? '').trim();
|
||||
const key = typeof params.key === 'string' ? params.key : '';
|
||||
const value = typeof params.value === 'string' ? params.value : '';
|
||||
if (!appUuid || !key) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Params "uuid" and "key" are required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
await getApplicationInProject(appUuid, projectUuid);
|
||||
const result = await upsertApplicationEnv(appUuid, {
|
||||
key,
|
||||
value,
|
||||
is_preview: !!params.is_preview,
|
||||
is_build_time: !!params.is_build_time,
|
||||
is_literal: !!params.is_literal,
|
||||
is_multiline: !!params.is_multiline,
|
||||
});
|
||||
return NextResponse.json({ result });
|
||||
}
|
||||
|
||||
async function toolAppsEnvsDelete(principal: Principal, params: Record<string, any>) {
|
||||
const projectUuid = requireCoolifyProject(principal);
|
||||
if (projectUuid instanceof NextResponse) return projectUuid;
|
||||
const appUuid = String(params.uuid ?? params.appUuid ?? '').trim();
|
||||
const key = typeof params.key === 'string' ? params.key : '';
|
||||
if (!appUuid || !key) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Params "uuid" and "key" are required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
await getApplicationInProject(appUuid, projectUuid);
|
||||
await deleteApplicationEnv(appUuid, key);
|
||||
return NextResponse.json({ result: { ok: true, key } });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user