import { NextResponse } from 'next/server'; 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); if (!session?.user?.email) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const email = session.user.email; // 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]); const firebaseUserId = users[0]?.id || session.user.id || randomUUID(); const userData = users[0]?.data || {}; const workspace = userData.workspace || email.split('@')[0].toLowerCase().replace(/[^a-z0-9]+/g, '-') + '-account'; const body = await request.json(); const { projectName, projectType, slug, vision, product, workspacePath, chatgptUrl, githubRepo, githubRepoId, githubRepoUrl, githubDefaultBranch, } = body; // Check slug uniqueness const existing = await query(`SELECT id FROM fs_projects WHERE slug = $1 LIMIT 1`, [slug]); if (existing.length > 0) { return NextResponse.json({ error: 'Project slug already exists' }, { status: 400 }); } 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, slug, userId: firebaseUserId, workspace, projectType, productName: product?.name || projectName, productVision: vision || '', isForClient: product?.isForClient || false, hasLogo: product?.hasLogo || false, hasDomain: product?.hasDomain || false, hasWebsite: product?.hasWebsite || false, hasGithub: !!githubRepo, hasChatGPT: !!chatgptUrl, workspacePath: workspacePath || null, workspaceName: workspacePath ? workspacePath.split('/').pop() : null, githubRepo: githubRepo || null, githubRepoId: githubRepoId || null, githubRepoUrl: githubRepoUrl || null, githubDefaultBranch: githubDefaultBranch || null, chatgptUrl: chatgptUrl || null, extensionLinked: false, status: 'active', currentPhase: 'collector', 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, }; await query(` INSERT INTO fs_projects (id, data, user_id, workspace, slug) VALUES ($1, $2::jsonb, $3, $4, $5) `, [projectId, JSON.stringify(projectData), firebaseUserId, workspace, slug]); // Associate any unlinked sessions for this workspace path if (workspacePath) { await query(` UPDATE fs_sessions SET data = jsonb_set( jsonb_set(data, '{projectId}', $1::jsonb), '{needsProjectAssociation}', 'false' ) WHERE user_id = $2 AND data->>'workspacePath' = $3 AND (data->>'needsProjectAssociation')::boolean = true `, [JSON.stringify(projectId), firebaseUserId, workspacePath]); } 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( { error: 'Failed to create project', details: error instanceof Error ? error.message : String(error) }, { status: 500 } ); } }