/** * POST /api/admin/migrate * * One-shot migration endpoint. Requires the ADMIN_MIGRATE_SECRET env var * to be set and passed as x-admin-secret header (or ?secret= query param). * * Idempotent — safe to call multiple times (all statements use IF NOT EXISTS). * * curl -X POST https://vibnai.com/api/admin/migrate \ * -H "x-admin-secret: " */ import { NextRequest, NextResponse } from "next/server"; import { query } from "@/lib/db-postgres"; import { readFileSync } from "fs"; import { join } from "path"; export async function POST(req: NextRequest) { const secret = process.env.ADMIN_MIGRATE_SECRET ?? ""; if (!secret) { return NextResponse.json( { error: "ADMIN_MIGRATE_SECRET env var not set — migration endpoint disabled" }, { status: 403 } ); } const incoming = req.headers.get("x-admin-secret") ?? new URL(req.url).searchParams.get("secret") ?? ""; if (incoming !== secret) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } const results: Array<{ statement: string; ok: boolean; error?: string }> = []; // Inline the DDL so this works even if the SQL file isn't on the runtime fs const statements = [ `CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`, `CREATE TABLE IF NOT EXISTS fs_users ( id TEXT PRIMARY KEY, user_id TEXT, data JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() )`, `CREATE INDEX IF NOT EXISTS fs_users_email_idx ON fs_users ((data->>'email'))`, `CREATE INDEX IF NOT EXISTS fs_users_user_id_idx ON fs_users (user_id)`, `CREATE TABLE IF NOT EXISTS fs_projects ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, workspace TEXT NOT NULL, slug TEXT NOT NULL UNIQUE, data JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() )`, `CREATE INDEX IF NOT EXISTS fs_projects_user_idx ON fs_projects (user_id)`, `CREATE INDEX IF NOT EXISTS fs_projects_workspace_idx ON fs_projects (workspace)`, `CREATE TABLE IF NOT EXISTS fs_sessions ( id TEXT PRIMARY KEY, user_id TEXT, data JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() )`, `CREATE INDEX IF NOT EXISTS fs_sessions_user_idx ON fs_sessions (user_id)`, `CREATE INDEX IF NOT EXISTS fs_sessions_project_idx ON fs_sessions ((data->>'projectId'))`, // agent_sessions uses TEXT for project_id to match fs_projects.id `CREATE TABLE IF NOT EXISTS agent_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), project_id TEXT NOT NULL, app_name TEXT NOT NULL, app_path TEXT NOT NULL, task TEXT NOT NULL, plan JSONB, status TEXT NOT NULL DEFAULT 'pending', output JSONB NOT NULL DEFAULT '[]', changed_files JSONB NOT NULL DEFAULT '[]', error TEXT, started_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() )`, `CREATE INDEX IF NOT EXISTS agent_sessions_project_idx ON agent_sessions (project_id, created_at DESC)`, `CREATE INDEX IF NOT EXISTS agent_sessions_status_idx ON agent_sessions (status)`, `CREATE TABLE IF NOT EXISTS agent_session_events ( id BIGSERIAL PRIMARY KEY, session_id UUID NOT NULL REFERENCES agent_sessions(id) ON DELETE CASCADE, project_id TEXT NOT NULL, seq INT NOT NULL, ts TIMESTAMPTZ NOT NULL, type TEXT NOT NULL, payload JSONB NOT NULL DEFAULT '{}'::jsonb, client_event_id UUID UNIQUE, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE(session_id, seq) )`, `CREATE INDEX IF NOT EXISTS agent_session_events_session_seq_idx ON agent_session_events (session_id, seq)`, // NextAuth / Prisma tables `CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, name TEXT, email TEXT UNIQUE, email_verified TIMESTAMPTZ, image TEXT )`, `CREATE TABLE IF NOT EXISTS accounts ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, type TEXT NOT NULL, provider TEXT NOT NULL, provider_account_id TEXT NOT NULL, refresh_token TEXT, access_token TEXT, expires_at INTEGER, token_type TEXT, scope TEXT, id_token TEXT, session_state TEXT, UNIQUE (provider, provider_account_id) )`, `CREATE TABLE IF NOT EXISTS sessions ( id TEXT PRIMARY KEY, session_token TEXT UNIQUE NOT NULL, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, expires TIMESTAMPTZ NOT NULL )`, `CREATE TABLE IF NOT EXISTS verification_tokens ( identifier TEXT NOT NULL, token TEXT UNIQUE NOT NULL, expires TIMESTAMPTZ NOT NULL, UNIQUE (identifier, token) )`, ]; for (const stmt of statements) { const label = stmt.trim().split("\n")[0].trim().slice(0, 80); try { await query(stmt, []); results.push({ statement: label, ok: true }); } catch (err) { results.push({ statement: label, ok: false, error: err instanceof Error ? err.message : String(err), }); } } const failed = results.filter(r => !r.ok); return NextResponse.json( { ok: failed.length === 0, results }, { status: failed.length === 0 ? 200 : 207 } ); }