feat: Gitea auto-provisioning and webhook context sync
- Add lib/gitea.ts: Gitea API client (createRepo, createWebhook, deleteRepo, verifyWebhookSignature) - Add lib/coolify.ts: Coolify API client (projects, databases, applications, deployments) - Update api/projects/create: auto-creates a private Gitea repo and registers a webhook on every new project; stores giteaRepo, giteaRepoUrl, giteaCloneUrl, giteaSshUrl, giteaWebhookId in project data; Gitea errors are non-fatal so project creation still succeeds - Add api/webhooks/gitea: handles push, pull_request, issues events; verifies HMAC signature; updates contextSnapshot on project record - Add api/webhooks/coolify: handles deployment status events; updates contextSnapshot.lastDeployment on project record Requires env vars: GITEA_API_URL, GITEA_API_TOKEN, GITEA_ADMIN_USER, GITEA_WEBHOOK_SECRET, COOLIFY_URL, COOLIFY_API_TOKEN Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,8 +3,13 @@ import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth/authOptions';
|
||||
import { query } from '@/lib/db-postgres';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { createRepo, createWebhook, GITEA_ADMIN_USER_EXPORT } from '@/lib/gitea';
|
||||
import type { ProjectPhaseData, ProjectPhaseScores } from '@/lib/types/project-artifacts';
|
||||
|
||||
const GITEA_ADMIN_USER = GITEA_ADMIN_USER_EXPORT;
|
||||
const APP_URL = process.env.NEXTAUTH_URL ?? process.env.NEXT_PUBLIC_APP_URL ?? 'https://vibnai.com';
|
||||
const GITEA_WEBHOOK_SECRET = process.env.GITEA_WEBHOOK_SECRET ?? 'vibn-webhook-secret';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
@@ -14,7 +19,7 @@ export async function POST(request: Request) {
|
||||
|
||||
const email = session.user.email;
|
||||
|
||||
// Resolve Firebase user ID and workspace from fs_users
|
||||
// Resolve user record from fs_users
|
||||
const users = await query<{ id: string; data: any }>(`
|
||||
SELECT id, data FROM fs_users WHERE data->>'email' = $1 LIMIT 1
|
||||
`, [email]);
|
||||
@@ -49,6 +54,44 @@ export async function POST(request: Request) {
|
||||
const projectId = randomUUID();
|
||||
const now = new Date().toISOString();
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 1. Provision Gitea repo
|
||||
// ──────────────────────────────────────────────
|
||||
let giteaRepo: string | null = null;
|
||||
let giteaRepoUrl: string | null = null;
|
||||
let giteaCloneUrl: string | null = null;
|
||||
let giteaSshUrl: string | null = null;
|
||||
let giteaWebhookId: number | null = null;
|
||||
let giteaError: string | null = null;
|
||||
|
||||
try {
|
||||
const repoName = slug; // e.g. "taskmaster"
|
||||
const repo = await createRepo(repoName, {
|
||||
description: `${projectName} — managed by Vibn`,
|
||||
private: true,
|
||||
auto_init: true,
|
||||
});
|
||||
|
||||
giteaRepo = repo.full_name; // e.g. "mark/taskmaster"
|
||||
giteaRepoUrl = repo.html_url; // e.g. "https://git.vibnai.com/mark/taskmaster"
|
||||
giteaCloneUrl = repo.clone_url;
|
||||
giteaSshUrl = repo.ssh_url;
|
||||
|
||||
// 2. Register webhook on the repo pointing back to Vibn
|
||||
const webhookUrl = `${APP_URL}/api/webhooks/gitea?projectId=${projectId}`;
|
||||
const hook = await createWebhook(GITEA_ADMIN_USER, repoName, webhookUrl, GITEA_WEBHOOK_SECRET);
|
||||
giteaWebhookId = hook.id;
|
||||
|
||||
console.log(`[API] Gitea repo created: ${giteaRepo}, webhook: ${giteaWebhookId}`);
|
||||
} catch (err) {
|
||||
// Non-fatal — log and continue. Project is still created.
|
||||
giteaError = err instanceof Error ? err.message : String(err);
|
||||
console.error('[API] Gitea provisioning failed (non-fatal):', giteaError);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 3. Save project record
|
||||
// ──────────────────────────────────────────────
|
||||
const projectData = {
|
||||
id: projectId,
|
||||
name: projectName,
|
||||
@@ -77,6 +120,15 @@ export async function POST(request: Request) {
|
||||
phaseStatus: 'not_started',
|
||||
phaseData: {} as ProjectPhaseData,
|
||||
phaseScores: {} as ProjectPhaseScores,
|
||||
// Gitea provisioning
|
||||
giteaRepo,
|
||||
giteaRepoUrl,
|
||||
giteaCloneUrl,
|
||||
giteaSshUrl,
|
||||
giteaWebhookId,
|
||||
giteaError,
|
||||
// Context snapshot (kept fresh by webhooks)
|
||||
contextSnapshot: null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
@@ -86,7 +138,7 @@ export async function POST(request: Request) {
|
||||
VALUES ($1, $2::jsonb, $3, $4, $5)
|
||||
`, [projectId, JSON.stringify(projectData), firebaseUserId, workspace, slug]);
|
||||
|
||||
// Associate unlinked sessions for this workspace path
|
||||
// Associate any unlinked sessions for this workspace path
|
||||
if (workspacePath) {
|
||||
await query(`
|
||||
UPDATE fs_sessions
|
||||
@@ -100,8 +152,18 @@ export async function POST(request: Request) {
|
||||
`, [JSON.stringify(projectId), firebaseUserId, workspacePath]);
|
||||
}
|
||||
|
||||
console.log('[API] Created project', projectId, slug);
|
||||
return NextResponse.json({ success: true, projectId, slug, workspace });
|
||||
console.log('[API] Created project', projectId, slug, '| gitea:', giteaRepo ?? 'skipped');
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
projectId,
|
||||
slug,
|
||||
workspace,
|
||||
gitea: giteaRepo
|
||||
? { repo: giteaRepo, repoUrl: giteaRepoUrl, cloneUrl: giteaCloneUrl, sshUrl: giteaSshUrl }
|
||||
: null,
|
||||
giteaError: giteaError ?? undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[POST /api/projects/create] Error:', error);
|
||||
return NextResponse.json(
|
||||
|
||||
Reference in New Issue
Block a user