Rip out Theia, ship P5.1 attach E2E + Justine UI work-in-progress
Theia rip-out: - Delete app/api/theia-auth/route.ts (Traefik ForwardAuth shim) - Delete app/api/projects/[projectId]/workspace/route.ts and app/api/projects/prewarm/route.ts (Cloud Run Theia provisioning) - Delete lib/cloud-run-workspace.ts and lib/coolify-workspace.ts - Strip provisionTheiaWorkspace + theiaWorkspaceUrl/theiaAppUuid/ theiaError from app/api/projects/create/route.ts response - Remove Theia callbackUrl branch in app/auth/page.tsx - Drop "Open in Theia" button + xterm/Theia PTY copy in build/page.tsx - Drop theiaWorkspaceUrl from deployment/page.tsx Project type - Strip Theia IDE line + theia-code-os from advisor + agent-chat context strings - Scrub Theia mention from lib/auth/workspace-auth.ts comment P5.1 (custom apex domains + DNS): - lib/coolify.ts + lib/opensrs.ts: nameserver normalization, OpenSRS XML auth, Cloud DNS plumbing - scripts/smoke-attach-e2e.ts: full prod GCP + sandbox OpenSRS + prod Coolify smoke covering register/zone/A/NS/PATCH/cleanup In-progress (Justine onboarding/build, MVP setup, agent telemetry): - New (justine)/stories, project (home) layouts, mvp-setup, run, tasks routes + supporting components - Project shell + sidebar + nav refactor for the Stackless palette - Agent session API hardening (sessions, events, stream, approve, retry, stop) + atlas-chat, advisor, design-surfaces refresh - New scripts/sync-db-url-from-coolify.mjs + scripts/prisma-db-push.mjs + docker-compose.local-db.yml for local Prisma workflows - lib/dev-bypass.ts, lib/chat-context-refs.ts, lib/prd-sections.ts - Misc: stories CSS, debug/prisma route, modal-theme, BuildLivePlanPanel Made-with: Cursor
This commit is contained in:
40
app/api/debug/prisma/route.ts
Normal file
40
app/api/debug/prisma/route.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
/**
|
||||
* Dev-only: verifies Prisma can connect (NextAuth adapter needs this after Google redirects back).
|
||||
* Open GET /api/debug/prisma while running next dev.
|
||||
*/
|
||||
export async function GET() {
|
||||
if (process.env.NODE_ENV !== "development") {
|
||||
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const hasUrl = Boolean(process.env.DATABASE_URL?.trim() || process.env.POSTGRES_URL?.trim());
|
||||
const prisma = new PrismaClient();
|
||||
try {
|
||||
await prisma.$queryRaw`SELECT 1`;
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
databaseUrlConfigured: hasUrl,
|
||||
hint: "Prisma connects; if auth still fails, check Google client id/secret and terminal [next-auth] logs.",
|
||||
});
|
||||
} catch (e: unknown) {
|
||||
const message = e instanceof Error ? e.message : "Unknown error";
|
||||
const publicHost = /Can't reach database server at `([\d.]+):(\d+)`/.exec(message);
|
||||
const hint = publicHost
|
||||
? `No TCP route to Postgres at ${publicHost[1]}:${publicHost[2]} from this machine. In Coolify: confirm the DB service publishes that host port and Postgres listens on 0.0.0.0. On the cloud firewall (e.g. GCP), allow inbound TCP ${publicHost[2]} from your IP (or use VPN). Test: nc -zv ${publicHost[1]} ${publicHost[2]} or psql. Then npm run db:push from vibn-frontend.`
|
||||
: "If the URL uses a Coolify internal hostname, it only works inside Docker. Otherwise check DATABASE_URL, firewall, and run npm run db:push.";
|
||||
return NextResponse.json(
|
||||
{
|
||||
ok: false,
|
||||
databaseUrlConfigured: hasUrl,
|
||||
message,
|
||||
hint,
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
} finally {
|
||||
await prisma.$disconnect().catch(() => {});
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth/authOptions';
|
||||
import { authSession } from "@/lib/auth/session-server";
|
||||
import { query } from '@/lib/db-postgres';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
@@ -43,7 +42,7 @@ export async function POST(request: Request) {
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
@@ -73,7 +72,7 @@ export async function GET(request: Request) {
|
||||
|
||||
export async function DELETE(request: Request) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
@@ -10,8 +10,7 @@
|
||||
* and injects it as knowledge_context into the orchestrator's system prompt.
|
||||
*/
|
||||
import { NextRequest } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth/authOptions';
|
||||
import { authSession } from "@/lib/auth/session-server";
|
||||
import { query } from '@/lib/db-postgres';
|
||||
|
||||
const AGENT_RUNNER_URL = process.env.AGENT_RUNNER_URL ?? 'https://agents.vibnai.com';
|
||||
@@ -49,7 +48,6 @@ async function buildKnowledgeContext(projectId: string, email: string): Promise<
|
||||
const architecture = d.architecture as Record<string, unknown> | null ?? null;
|
||||
const apps = (d.apps as Array<{ name: string; domain?: string; coolifyServiceUuid?: string }>) ?? [];
|
||||
const coolifyProjectUuid = (d.coolifyProjectUuid as string) ?? '';
|
||||
const theiaUrl = (d.theiaWorkspaceUrl as string) ?? '';
|
||||
|
||||
const lines: string[] = [];
|
||||
|
||||
@@ -65,14 +63,13 @@ Operating principles:
|
||||
- Be brief. No preamble, no "Great question!".
|
||||
- You decide the technical approach — never ask the founder to choose.
|
||||
- Be honest when you're uncertain or when data isn't available.
|
||||
- Do NOT spawn agents on the protected platform repos (vibn-frontend, theia-code-os, vibn-agent-runner, vibn-api, master-ai).`);
|
||||
- Do NOT spawn agents on the protected platform repos (vibn-frontend, vibn-agent-runner, vibn-api, master-ai).`);
|
||||
|
||||
// Project identity
|
||||
lines.push(`\n## Project: ${name}`);
|
||||
if (vision) lines.push(`Vision: ${vision}`);
|
||||
if (giteaRepo) lines.push(`Gitea repo: ${giteaRepo} — use read_repo_file and list_repos to explore it`);
|
||||
if (coolifyProjectUuid) lines.push(`Coolify project UUID: ${coolifyProjectUuid} — use coolify_list_applications to find its apps`);
|
||||
if (theiaUrl) lines.push(`Theia IDE: ${theiaUrl}`);
|
||||
|
||||
// Architecture document
|
||||
if (architecture) {
|
||||
@@ -129,7 +126,7 @@ export async function POST(
|
||||
) {
|
||||
const { projectId } = await params;
|
||||
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth/next";
|
||||
import { authOptions } from "@/lib/auth/authOptions";
|
||||
import { authSession } from "@/lib/auth/session-server";
|
||||
import { query } from "@/lib/db-postgres";
|
||||
|
||||
const AGENT_RUNNER_URL = process.env.AGENT_RUNNER_URL ?? "http://localhost:3333";
|
||||
@@ -87,7 +86,7 @@ export async function POST(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ projectId: string }> }
|
||||
) {
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
@@ -114,7 +113,6 @@ export async function POST(
|
||||
p.giteaRepo ? `Gitea repo: ${p.giteaRepo}` : null,
|
||||
p.coolifyAppUuid ? `Coolify app UUID: ${p.coolifyAppUuid}` : null,
|
||||
p.deploymentUrl ? `Live URL: ${p.deploymentUrl}` : null,
|
||||
p.theiaWorkspaceUrl ? `IDE: ${p.theiaWorkspaceUrl}` : null,
|
||||
].filter(Boolean);
|
||||
projectContext = lines.join("\n");
|
||||
}
|
||||
@@ -190,7 +188,7 @@ export async function DELETE(
|
||||
_req: NextRequest,
|
||||
{ params }: { params: Promise<{ projectId: string }> }
|
||||
) {
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
* Body: { commitMessage: string }
|
||||
*/
|
||||
import { NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/lib/auth/authOptions";
|
||||
import { authSession } from "@/lib/auth/session-server";
|
||||
import { query } from "@/lib/db-postgres";
|
||||
|
||||
const AGENT_RUNNER_URL = process.env.AGENT_RUNNER_URL ?? "http://localhost:3333";
|
||||
@@ -29,7 +28,7 @@ export async function POST(
|
||||
) {
|
||||
try {
|
||||
const { projectId, sessionId } = await params;
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
* Batch append from vibn-agent-runner (x-agent-runner-secret).
|
||||
*/
|
||||
import { NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/lib/auth/authOptions";
|
||||
import { authSession } from "@/lib/auth/session-server";
|
||||
import { query, getPool } from "@/lib/db-postgres";
|
||||
|
||||
export interface AgentSessionEventRow {
|
||||
@@ -23,7 +22,7 @@ export async function GET(
|
||||
) {
|
||||
try {
|
||||
const { projectId, sessionId } = await params;
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
* GET /api/projects/.../agent/sessions/.../events/stream?afterSeq=0
|
||||
* Server-Sent Events: tail agent_session_events while the session is active.
|
||||
*/
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/lib/auth/authOptions";
|
||||
import { authSession } from "@/lib/auth/session-server";
|
||||
import { query, queryOne } from "@/lib/db-postgres";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -17,7 +16,7 @@ export async function GET(
|
||||
req: Request,
|
||||
{ params }: { params: Promise<{ projectId: string; sessionId: string }> }
|
||||
) {
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
* understands what was already tried
|
||||
*/
|
||||
import { NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/lib/auth/authOptions";
|
||||
import { authSession } from "@/lib/auth/session-server";
|
||||
import { query } from "@/lib/db-postgres";
|
||||
|
||||
const AGENT_RUNNER_URL = process.env.AGENT_RUNNER_URL ?? "http://localhost:3333";
|
||||
@@ -21,7 +20,7 @@ export async function POST(
|
||||
) {
|
||||
try {
|
||||
const { projectId, sessionId } = await params;
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
* (handled in /stop/route.ts)
|
||||
*/
|
||||
import { NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/lib/auth/authOptions";
|
||||
import { authSession } from "@/lib/auth/session-server";
|
||||
import { query } from "@/lib/db-postgres";
|
||||
|
||||
export async function GET(
|
||||
@@ -17,7 +16,7 @@ export async function GET(
|
||||
) {
|
||||
try {
|
||||
const { projectId, sessionId } = await params;
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/lib/auth/authOptions";
|
||||
import { authSession } from "@/lib/auth/session-server";
|
||||
import { query } from "@/lib/db-postgres";
|
||||
|
||||
const AGENT_RUNNER_URL = process.env.AGENT_RUNNER_URL ?? "http://localhost:3333";
|
||||
@@ -11,7 +10,7 @@ export async function POST(
|
||||
) {
|
||||
try {
|
||||
const { projectId, sessionId } = await params;
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
* List all sessions for a project, newest first.
|
||||
*/
|
||||
import { NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/lib/auth/authOptions";
|
||||
import { authSession } from "@/lib/auth/session-server";
|
||||
import { query } from "@/lib/db-postgres";
|
||||
|
||||
const AGENT_RUNNER_URL = process.env.AGENT_RUNNER_URL ?? "http://localhost:3333";
|
||||
@@ -33,7 +32,7 @@ export async function POST(
|
||||
) {
|
||||
try {
|
||||
const { projectId } = await params;
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
@@ -131,7 +130,7 @@ export async function GET(
|
||||
) {
|
||||
try {
|
||||
const { projectId } = await params;
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth/authOptions';
|
||||
import { authSession } from "@/lib/auth/session-server";
|
||||
import { query } from '@/lib/db-postgres';
|
||||
|
||||
export async function GET(
|
||||
@@ -9,7 +8,7 @@ export async function GET(
|
||||
) {
|
||||
try {
|
||||
const { projectId } = await params;
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth/authOptions';
|
||||
import { authSession } from "@/lib/auth/session-server";
|
||||
import { query } from '@/lib/db-postgres';
|
||||
|
||||
export const maxDuration = 60;
|
||||
@@ -37,7 +36,7 @@ export async function POST(
|
||||
) {
|
||||
try {
|
||||
const { projectId } = await params;
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth/authOptions';
|
||||
import { authSession } from "@/lib/auth/session-server";
|
||||
import { query } from '@/lib/db-postgres';
|
||||
import { execSync } from 'child_process';
|
||||
import { existsSync, readdirSync, readFileSync, statSync, rmSync } from 'fs';
|
||||
@@ -79,7 +78,7 @@ export async function POST(
|
||||
) {
|
||||
try {
|
||||
const { projectId } = await params;
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth/authOptions';
|
||||
import { authSession } from "@/lib/auth/session-server";
|
||||
import { query } from '@/lib/db-postgres';
|
||||
|
||||
const AGENT_RUNNER_URL = process.env.AGENT_RUNNER_URL ?? 'http://localhost:3333';
|
||||
@@ -10,7 +9,7 @@ export async function GET(
|
||||
_req: Request,
|
||||
{ params }: { params: Promise<{ projectId: string }> }
|
||||
) {
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
@@ -68,7 +67,7 @@ export async function POST(
|
||||
_req: Request,
|
||||
{ params }: { params: Promise<{ projectId: string }> }
|
||||
) {
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth/authOptions';
|
||||
import { authSession } from "@/lib/auth/session-server";
|
||||
import { query } from '@/lib/db-postgres';
|
||||
|
||||
const GITEA_API_URL = process.env.GITEA_API_URL ?? 'https://git.vibnai.com';
|
||||
@@ -25,7 +24,7 @@ export async function GET(
|
||||
) {
|
||||
const { projectId } = await params;
|
||||
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
@@ -125,7 +124,7 @@ export async function PATCH(
|
||||
) {
|
||||
const { projectId } = await params;
|
||||
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth/next";
|
||||
import { authOptions } from "@/lib/auth/authOptions";
|
||||
import { authSession } from "@/lib/auth/session-server";
|
||||
import { query } from "@/lib/db-postgres";
|
||||
|
||||
const AGENT_RUNNER_URL = process.env.AGENT_RUNNER_URL ?? "http://localhost:3333";
|
||||
@@ -13,7 +12,7 @@ export async function GET(
|
||||
_req: NextRequest,
|
||||
{ params }: { params: Promise<{ projectId: string }> }
|
||||
) {
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
@@ -43,7 +42,7 @@ export async function POST(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ projectId: string }> }
|
||||
) {
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
@@ -184,7 +183,7 @@ export async function PATCH(
|
||||
_req: NextRequest,
|
||||
{ params }: { params: Promise<{ projectId: string }> }
|
||||
) {
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
@@ -1,18 +1,47 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth/next";
|
||||
import { authOptions } from "@/lib/auth/authOptions";
|
||||
import { authSession } from "@/lib/auth/session-server";
|
||||
import { query } from "@/lib/db-postgres";
|
||||
import {
|
||||
augmentAtlasMessage,
|
||||
parseContextRefs,
|
||||
} from "@/lib/chat-context-refs";
|
||||
|
||||
const AGENT_RUNNER_URL = process.env.AGENT_RUNNER_URL ?? "http://localhost:3333";
|
||||
|
||||
const ALLOWED_SCOPES = new Set(["overview", "build"]);
|
||||
|
||||
function normalizeScope(raw: string | null | undefined): "overview" | "build" {
|
||||
const s = (raw ?? "overview").trim();
|
||||
return ALLOWED_SCOPES.has(s) ? (s as "overview" | "build") : "overview";
|
||||
}
|
||||
|
||||
function runnerSessionId(projectId: string, scope: "overview" | "build"): string {
|
||||
return scope === "overview" ? `atlas_${projectId}` : `atlas_${projectId}__build`;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DB helpers — atlas_conversations table
|
||||
// DB — atlas_chat_threads (project_id + scope); legacy atlas_conversations → overview
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let tableReady = false;
|
||||
let threadsTableReady = false;
|
||||
let legacyTableChecked = false;
|
||||
|
||||
async function ensureTable() {
|
||||
if (tableReady) return;
|
||||
async function ensureThreadsTable() {
|
||||
if (threadsTableReady) return;
|
||||
await query(`
|
||||
CREATE TABLE IF NOT EXISTS atlas_chat_threads (
|
||||
project_id TEXT NOT NULL,
|
||||
scope TEXT NOT NULL,
|
||||
messages JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (project_id, scope)
|
||||
)
|
||||
`);
|
||||
threadsTableReady = true;
|
||||
}
|
||||
|
||||
async function ensureLegacyConversationsTable() {
|
||||
if (legacyTableChecked) return;
|
||||
await query(`
|
||||
CREATE TABLE IF NOT EXISTS atlas_conversations (
|
||||
project_id TEXT PRIMARY KEY,
|
||||
@@ -20,31 +49,47 @@ async function ensureTable() {
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)
|
||||
`);
|
||||
tableReady = true;
|
||||
legacyTableChecked = true;
|
||||
}
|
||||
|
||||
async function loadAtlasHistory(projectId: string): Promise<any[]> {
|
||||
async function loadAtlasHistory(projectId: string, scope: "overview" | "build"): Promise<any[]> {
|
||||
try {
|
||||
await ensureTable();
|
||||
await ensureThreadsTable();
|
||||
const rows = await query<{ messages: any[] }>(
|
||||
`SELECT messages FROM atlas_conversations WHERE project_id = $1`,
|
||||
[projectId]
|
||||
`SELECT messages FROM atlas_chat_threads WHERE project_id = $1 AND scope = $2`,
|
||||
[projectId, scope]
|
||||
);
|
||||
return rows[0]?.messages ?? [];
|
||||
if (rows.length > 0) {
|
||||
const fromThreads = rows[0]?.messages;
|
||||
return Array.isArray(fromThreads) ? fromThreads : [];
|
||||
}
|
||||
if (scope === "overview") {
|
||||
await ensureLegacyConversationsTable();
|
||||
const leg = await query<{ messages: any[] }>(
|
||||
`SELECT messages FROM atlas_conversations WHERE project_id = $1`,
|
||||
[projectId]
|
||||
);
|
||||
const legacyMsgs = leg[0]?.messages ?? [];
|
||||
if (Array.isArray(legacyMsgs) && legacyMsgs.length > 0) {
|
||||
await saveAtlasHistory(projectId, scope, legacyMsgs);
|
||||
return legacyMsgs;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function saveAtlasHistory(projectId: string, messages: any[]): Promise<void> {
|
||||
async function saveAtlasHistory(projectId: string, scope: "overview" | "build", messages: any[]): Promise<void> {
|
||||
try {
|
||||
await ensureTable();
|
||||
await ensureThreadsTable();
|
||||
await query(
|
||||
`INSERT INTO atlas_conversations (project_id, messages, updated_at)
|
||||
VALUES ($1, $2::jsonb, NOW())
|
||||
ON CONFLICT (project_id) DO UPDATE
|
||||
SET messages = $2::jsonb, updated_at = NOW()`,
|
||||
[projectId, JSON.stringify(messages)]
|
||||
`INSERT INTO atlas_chat_threads (project_id, scope, messages, updated_at)
|
||||
VALUES ($1, $2, $3::jsonb, NOW())
|
||||
ON CONFLICT (project_id, scope) DO UPDATE
|
||||
SET messages = $3::jsonb, updated_at = NOW()`,
|
||||
[projectId, scope, JSON.stringify(messages)]
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("[atlas-chat] Failed to save history:", e);
|
||||
@@ -66,21 +111,36 @@ async function savePrd(projectId: string, prdContent: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/** Replace the latest user message content so DB/UI never show the internal ref prefix. */
|
||||
function scrubLastUserMessageContent(history: unknown[], cleanText: string): unknown[] {
|
||||
if (!Array.isArray(history) || history.length === 0) return history;
|
||||
const h = history.map(m => (m && typeof m === "object" ? { ...(m as object) } : m));
|
||||
for (let i = h.length - 1; i >= 0; i--) {
|
||||
const m = h[i] as { role?: string; content?: string };
|
||||
if (m?.role === "user" && typeof m.content === "string") {
|
||||
h[i] = { ...m, content: cleanText };
|
||||
break;
|
||||
}
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GET — load stored conversation messages for display
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function GET(
|
||||
_req: NextRequest,
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ projectId: string }> }
|
||||
) {
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { projectId } = await params;
|
||||
const history = await loadAtlasHistory(projectId);
|
||||
const scope = normalizeScope(req.nextUrl.searchParams.get("scope"));
|
||||
const history = await loadAtlasHistory(projectId, scope);
|
||||
|
||||
// Filter to only user/assistant messages (no system prompts) for display
|
||||
const messages = history
|
||||
@@ -98,43 +158,50 @@ export async function POST(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ projectId: string }> }
|
||||
) {
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { projectId } = await params;
|
||||
const { message } = await req.json();
|
||||
const body = await req.json();
|
||||
const message = body?.message as string | undefined;
|
||||
const contextRefs = parseContextRefs(body?.contextRefs);
|
||||
if (!message?.trim()) {
|
||||
return NextResponse.json({ error: "message is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const sessionId = `atlas_${projectId}`;
|
||||
const scope = normalizeScope(body?.scope as string | undefined);
|
||||
const sessionId = runnerSessionId(projectId, scope);
|
||||
const cleanUserText = message.trim();
|
||||
|
||||
// Load conversation history from DB to persist across agent runner restarts.
|
||||
// Strip tool_call / tool_response messages — replaying them across sessions
|
||||
// causes Gemini to reject the request with a turn-ordering error.
|
||||
const rawHistory = await loadAtlasHistory(projectId);
|
||||
const rawHistory = await loadAtlasHistory(projectId, scope);
|
||||
const history = rawHistory.filter((m: any) =>
|
||||
(m.role === "user" || m.role === "assistant") && m.content
|
||||
);
|
||||
|
||||
// __init__ is a special internal trigger used only when there is no existing history.
|
||||
// If history already exists, ignore the init request (conversation already started).
|
||||
const isInit = message.trim() === "__atlas_init__";
|
||||
const isInit = cleanUserText === "__atlas_init__";
|
||||
if (isInit && history.length > 0) {
|
||||
return NextResponse.json({ reply: null, alreadyStarted: true });
|
||||
}
|
||||
|
||||
const runnerMessage = isInit
|
||||
? scope === "build"
|
||||
? "Begin as Vibn in build mode. The user is working in their monorepo. Ask what they want to ship or fix next, and offer concrete implementation guidance. Do not acknowledge this as an internal trigger."
|
||||
: "Begin the conversation. Introduce yourself as Vibn and ask what the user is building. Do not acknowledge this as an internal trigger."
|
||||
: augmentAtlasMessage(cleanUserText, contextRefs);
|
||||
|
||||
try {
|
||||
const res = await fetch(`${AGENT_RUNNER_URL}/atlas/chat`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
// For init, send the greeting prompt but don't store it as a user message
|
||||
message: isInit
|
||||
? "Begin the conversation. Introduce yourself as Vibn and ask what the user is building. Do not acknowledge this as an internal trigger."
|
||||
: message,
|
||||
message: runnerMessage,
|
||||
session_id: sessionId,
|
||||
history,
|
||||
is_init: isInit,
|
||||
@@ -153,11 +220,16 @@ export async function POST(
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
// Persist updated history
|
||||
await saveAtlasHistory(projectId, data.history ?? []);
|
||||
let historyOut = data.history ?? [];
|
||||
// Store the user's line without the internal reference block (UI shows clean text).
|
||||
if (!isInit && cleanUserText !== "__atlas_init__") {
|
||||
historyOut = scrubLastUserMessageContent(historyOut, cleanUserText);
|
||||
}
|
||||
|
||||
// If Atlas finalized the PRD, save it to the project
|
||||
if (data.prdContent) {
|
||||
await saveAtlasHistory(projectId, scope, historyOut);
|
||||
|
||||
// If Atlas finalized the PRD, save it to the project (discovery / overview)
|
||||
if (data.prdContent && scope === "overview") {
|
||||
await savePrd(projectId, data.prdContent);
|
||||
}
|
||||
|
||||
@@ -181,24 +253,35 @@ export async function POST(
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function DELETE(
|
||||
_req: NextRequest,
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ projectId: string }> }
|
||||
) {
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { projectId } = await params;
|
||||
const sessionId = `atlas_${projectId}`;
|
||||
const scope = normalizeScope(req.nextUrl.searchParams.get("scope"));
|
||||
const sessionId = runnerSessionId(projectId, scope);
|
||||
|
||||
try {
|
||||
await fetch(`${AGENT_RUNNER_URL}/atlas/sessions/${sessionId}`, { method: "DELETE" });
|
||||
await fetch(`${AGENT_RUNNER_URL}/atlas/sessions/${encodeURIComponent(sessionId)}`, { method: "DELETE" });
|
||||
} catch { /* runner may be down */ }
|
||||
|
||||
try {
|
||||
await query(`DELETE FROM atlas_conversations WHERE project_id = $1`, [projectId]);
|
||||
await ensureThreadsTable();
|
||||
await query(
|
||||
`DELETE FROM atlas_chat_threads WHERE project_id = $1 AND scope = $2`,
|
||||
[projectId, scope]
|
||||
);
|
||||
} catch { /* table may not exist yet */ }
|
||||
|
||||
if (scope === "overview") {
|
||||
try {
|
||||
await query(`DELETE FROM atlas_conversations WHERE project_id = $1`, [projectId]);
|
||||
} catch { /* legacy */ }
|
||||
}
|
||||
|
||||
return NextResponse.json({ cleared: true });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth/authOptions';
|
||||
import { authSession } from "@/lib/auth/session-server";
|
||||
import { query } from '@/lib/db-postgres';
|
||||
|
||||
/**
|
||||
@@ -12,7 +11,7 @@ export async function GET(
|
||||
) {
|
||||
try {
|
||||
const { projectId } = await params;
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const rows = await query<{ data: Record<string, unknown> }>(
|
||||
@@ -49,7 +48,7 @@ export async function PATCH(
|
||||
) {
|
||||
try {
|
||||
const { projectId } = await params;
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
// Step 1: read current data — explicit ::text casts on every param
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
* Response for file: { type: "file", content: string, encoding: "utf8" | "base64" }
|
||||
*/
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth/authOptions';
|
||||
import { authSession } from "@/lib/auth/session-server";
|
||||
import { query } from '@/lib/db-postgres';
|
||||
|
||||
const GITEA_API_URL = process.env.GITEA_API_URL ?? 'https://git.vibnai.com';
|
||||
@@ -39,7 +38,7 @@ export async function GET(
|
||||
) {
|
||||
try {
|
||||
const { projectId } = await params;
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth/authOptions';
|
||||
import { authSession } from "@/lib/auth/session-server";
|
||||
import { query } from '@/lib/db-postgres';
|
||||
|
||||
export const maxDuration = 120;
|
||||
@@ -28,7 +27,7 @@ export async function POST(
|
||||
) {
|
||||
try {
|
||||
const { projectId } = await params;
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth/authOptions';
|
||||
import { authSession } from "@/lib/auth/session-server";
|
||||
import { query } from '@/lib/db-postgres';
|
||||
import { createKnowledgeItem } from '@/lib/server/knowledge';
|
||||
import type { KnowledgeSourceMeta } from '@/lib/types/knowledge';
|
||||
@@ -34,7 +33,7 @@ export async function POST(
|
||||
return NextResponse.json({ error: 'transcript is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth/authOptions';
|
||||
import { authSession } from "@/lib/auth/session-server";
|
||||
import { query } from '@/lib/db-postgres';
|
||||
|
||||
export async function GET(
|
||||
@@ -10,7 +9,7 @@ export async function GET(
|
||||
try {
|
||||
const { projectId } = await params;
|
||||
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth/next";
|
||||
import { authOptions } from "@/lib/auth/authOptions";
|
||||
import { authSession } from "@/lib/auth/session-server";
|
||||
import { query } from "@/lib/db-postgres";
|
||||
|
||||
async function assertOwnership(projectId: string, email: string): Promise<boolean> {
|
||||
@@ -18,7 +17,7 @@ export async function GET(
|
||||
_req: NextRequest,
|
||||
{ params }: { params: Promise<{ projectId: string }> }
|
||||
) {
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
|
||||
const { projectId } = await params;
|
||||
@@ -41,7 +40,7 @@ export async function POST(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ projectId: string }> }
|
||||
) {
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
|
||||
const { projectId } = await params;
|
||||
@@ -83,7 +82,7 @@ export async function DELETE(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ projectId: string }> }
|
||||
) {
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
|
||||
const { projectId } = await params;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth/authOptions';
|
||||
import { authSession } from "@/lib/auth/session-server";
|
||||
import { query } from '@/lib/db-postgres';
|
||||
import { listApplications, CoolifyApplication } from '@/lib/coolify';
|
||||
|
||||
@@ -20,7 +19,7 @@ export async function GET(
|
||||
) {
|
||||
const { projectId } = await params;
|
||||
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth/authOptions';
|
||||
import { authSession } from "@/lib/auth/session-server";
|
||||
import { query } from '@/lib/db-postgres';
|
||||
|
||||
export async function GET(
|
||||
@@ -10,7 +9,7 @@ export async function GET(
|
||||
try {
|
||||
const { projectId } = await params;
|
||||
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
@@ -45,7 +44,7 @@ export async function PATCH(
|
||||
const { projectId } = await params;
|
||||
const body = await request.json();
|
||||
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth/next";
|
||||
import { authOptions } from "@/lib/auth/authOptions";
|
||||
import { authSession } from "@/lib/auth/session-server";
|
||||
import { query } from "@/lib/db-postgres";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -11,7 +10,7 @@ export async function POST(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ projectId: string }> }
|
||||
) {
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
@@ -85,7 +84,7 @@ export async function GET(
|
||||
_req: NextRequest,
|
||||
{ params }: { params: Promise<{ projectId: string }> }
|
||||
) {
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth/authOptions';
|
||||
import { authSession } from "@/lib/auth/session-server";
|
||||
import { query } from '@/lib/db-postgres';
|
||||
|
||||
export async function POST(
|
||||
@@ -9,7 +8,7 @@ export async function POST(
|
||||
) {
|
||||
try {
|
||||
const { projectId } = await params;
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth/authOptions';
|
||||
import { query } from '@/lib/db-postgres';
|
||||
import { provisionTheiaWorkspace } from '@/lib/cloud-run-workspace';
|
||||
|
||||
export async function POST(
|
||||
_request: Request,
|
||||
{ params }: { params: Promise<{ projectId: string }> },
|
||||
) {
|
||||
try {
|
||||
const { projectId } = await params;
|
||||
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Verify ownership
|
||||
const rows = await query<{ id: string; data: any }>(`
|
||||
SELECT p.id, p.data
|
||||
FROM fs_projects p
|
||||
JOIN fs_users u ON u.id = p.user_id
|
||||
WHERE p.id = $1 AND u.data->>'email' = $2
|
||||
LIMIT 1
|
||||
`, [projectId, session.user.email]);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return NextResponse.json({ error: 'Project not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const project = rows[0].data;
|
||||
|
||||
if (project.theiaWorkspaceUrl) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
workspaceUrl: project.theiaWorkspaceUrl,
|
||||
message: 'Workspace already provisioned',
|
||||
});
|
||||
}
|
||||
|
||||
const slug = project.slug;
|
||||
if (!slug) {
|
||||
return NextResponse.json({ error: 'Project has no slug — cannot provision workspace' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Provision Cloud Run workspace
|
||||
const workspace = await provisionTheiaWorkspace(slug, projectId, project.giteaRepo ?? null);
|
||||
|
||||
// Save URL back to project record
|
||||
await query(`
|
||||
UPDATE fs_projects
|
||||
SET data = data || jsonb_build_object(
|
||||
'theiaWorkspaceUrl', $1::text,
|
||||
'theiaAppUuid', $2::text
|
||||
)
|
||||
WHERE id = $3
|
||||
`, [workspace.serviceUrl, workspace.serviceName, projectId]);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
workspaceUrl: workspace.serviceUrl,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[POST /api/projects/:id/workspace] Error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to provision workspace', details: error instanceof Error ? error.message : String(error) },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import { randomUUID } from 'crypto';
|
||||
import { createRepo, createWebhook, getRepo, listWebhooks, GITEA_ADMIN_USER_EXPORT } from '@/lib/gitea';
|
||||
import { pushTurborepoScaffold } from '@/lib/scaffold';
|
||||
import { createMonorepoAppService } from '@/lib/coolify';
|
||||
import { provisionTheiaWorkspace } from '@/lib/cloud-run-workspace';
|
||||
import { getOrCreateProvisionedWorkspace } from '@/lib/workspaces';
|
||||
import type { ProjectPhaseData, ProjectPhaseScores } from '@/lib/types/project-artifacts';
|
||||
|
||||
@@ -208,24 +207,7 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 3. Provision dedicated Theia workspace
|
||||
// ──────────────────────────────────────────────
|
||||
let theiaWorkspaceUrl: string | null = null;
|
||||
let theiaAppUuid: string | null = null;
|
||||
let theiaError: string | null = null;
|
||||
|
||||
try {
|
||||
const workspace = await provisionTheiaWorkspace(slug, projectId, giteaRepo);
|
||||
theiaWorkspaceUrl = workspace.serviceUrl;
|
||||
theiaAppUuid = workspace.serviceName;
|
||||
console.log(`[API] Theia workspace provisioned: ${theiaWorkspaceUrl}`);
|
||||
} catch (err) {
|
||||
theiaError = err instanceof Error ? err.message : String(err);
|
||||
console.error('[API] Theia workspace provisioning failed (non-fatal):', theiaError);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 4. Save project record
|
||||
// 3. Save project record
|
||||
// ──────────────────────────────────────────────
|
||||
const projectData = {
|
||||
id: projectId,
|
||||
@@ -262,10 +244,6 @@ export async function POST(request: Request) {
|
||||
giteaSshUrl,
|
||||
giteaWebhookId,
|
||||
giteaError,
|
||||
// Theia workspace
|
||||
theiaWorkspaceUrl,
|
||||
theiaAppUuid,
|
||||
theiaError,
|
||||
// Context snapshot (kept fresh by webhooks)
|
||||
contextSnapshot: null,
|
||||
// Coolify project — one per VIBN project, scopes all app services + DBs
|
||||
@@ -344,8 +322,6 @@ export async function POST(request: Request) {
|
||||
? { repo: giteaRepo, repoUrl: giteaRepoUrl, cloneUrl: giteaCloneUrl, sshUrl: giteaSshUrl }
|
||||
: null,
|
||||
giteaError: giteaError ?? undefined,
|
||||
theiaWorkspaceUrl,
|
||||
theiaError: theiaError ?? undefined,
|
||||
isImport: !!githubRepoUrl,
|
||||
analysisJobId: analysisJobId ?? undefined,
|
||||
});
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth/authOptions';
|
||||
import { authSession } from "@/lib/auth/session-server";
|
||||
import { query } from '@/lib/db-postgres';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
@@ -8,14 +8,13 @@
|
||||
*/
|
||||
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth/authOptions';
|
||||
import { authSession } from "@/lib/auth/session-server";
|
||||
import { query } from '@/lib/db-postgres';
|
||||
import { deployApplication } from '@/lib/coolify';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth/authOptions';
|
||||
import { prewarmWorkspace } from '@/lib/cloud-run-workspace';
|
||||
|
||||
/**
|
||||
* POST /api/projects/prewarm
|
||||
* Body: { urls: string[] }
|
||||
*
|
||||
* Fires warm-up requests to Cloud Run workspace URLs so containers
|
||||
* are running by the time the user clicks "Open IDE". Server-side
|
||||
* to avoid CORS issues with run.app domains.
|
||||
*/
|
||||
export async function POST(req: NextRequest) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { urls } = await req.json() as { urls: string[] };
|
||||
if (!Array.isArray(urls) || urls.length === 0) {
|
||||
return NextResponse.json({ warmed: 0 });
|
||||
}
|
||||
|
||||
// Fire all prewarm pings in parallel — intentionally not awaited
|
||||
Promise.allSettled(urls.map(url => prewarmWorkspace(url))).catch(() => {});
|
||||
|
||||
return NextResponse.json({ warmed: urls.length });
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth/authOptions';
|
||||
import { authSession } from "@/lib/auth/session-server";
|
||||
import { query } from '@/lib/db-postgres';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth/authOptions';
|
||||
import { authSession } from "@/lib/auth/session-server";
|
||||
import { query } from '@/lib/db-postgres';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json([], { status: 200 });
|
||||
}
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
/**
|
||||
* GET /api/theia-auth
|
||||
*
|
||||
* Traefik ForwardAuth endpoint for Theia IDE domains.
|
||||
*
|
||||
* Handles two cases:
|
||||
* 1. theia.vibnai.com — shared IDE: any authenticated user may access
|
||||
* 2. {slug}.ide.vibnai.com — per-project IDE: only the project owner may access
|
||||
*
|
||||
* Traefik calls this URL for every request to those Theia domains, forwarding
|
||||
* the user's Cookie header via authRequestHeaders. We validate the NextAuth
|
||||
* database session directly in Postgres (avoids Prisma / authOptions build-time
|
||||
* issues under --network host).
|
||||
*
|
||||
* Returns:
|
||||
* 200 — valid session (and owner check passed), Traefik lets the request through
|
||||
* 302 — no/expired session, redirect browser to Vibn login
|
||||
* 403 — authenticated but not the project owner
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { query } from '@/lib/db-postgres';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
const APP_URL = process.env.NEXTAUTH_URL ?? 'https://vibnai.com';
|
||||
const THEIA_URL = 'https://theia.vibnai.com';
|
||||
const IDE_SUFFIX = '.ide.vibnai.com';
|
||||
|
||||
const SESSION_COOKIE_NAMES = [
|
||||
'__Secure-next-auth.session-token',
|
||||
'next-auth.session-token',
|
||||
];
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
// ── 1. Extract session token ──────────────────────────────────────────────
|
||||
let sessionToken: string | null = null;
|
||||
for (const name of SESSION_COOKIE_NAMES) {
|
||||
const val = request.cookies.get(name)?.value;
|
||||
if (val) { sessionToken = val; break; }
|
||||
}
|
||||
|
||||
if (!sessionToken) return redirectToLogin(request);
|
||||
|
||||
// ── 2. Validate session in Postgres ──────────────────────────────────────
|
||||
let userEmail: string | null = null;
|
||||
let userName: string | null = null;
|
||||
let userId: string | null = null;
|
||||
|
||||
try {
|
||||
const rows = await query<{ email: string; name: string; user_id: string }>(
|
||||
`SELECT u.email, u.name, s.user_id
|
||||
FROM sessions s
|
||||
JOIN users u ON u.id = s.user_id
|
||||
WHERE s.session_token = $1
|
||||
AND s.expires > NOW()
|
||||
LIMIT 1`,
|
||||
[sessionToken],
|
||||
);
|
||||
if (rows.length > 0) {
|
||||
userEmail = rows[0].email;
|
||||
userName = rows[0].name;
|
||||
userId = rows[0].user_id;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[theia-auth] DB error:', err);
|
||||
return redirectToLogin(request);
|
||||
}
|
||||
|
||||
if (!userEmail || !userId) return redirectToLogin(request);
|
||||
|
||||
// ── 3. Per-project ownership check for *.ide.vibnai.com ──────────────────
|
||||
const forwardedHost = request.headers.get('x-forwarded-host') ?? '';
|
||||
|
||||
if (forwardedHost.endsWith(IDE_SUFFIX)) {
|
||||
const slug = forwardedHost.slice(0, -IDE_SUFFIX.length);
|
||||
|
||||
try {
|
||||
const rows = await query<{ user_id: string }>(
|
||||
`SELECT user_id FROM fs_projects WHERE slug = $1 LIMIT 1`,
|
||||
[slug],
|
||||
);
|
||||
|
||||
if (rows.length === 0) {
|
||||
// Unknown project slug — deny
|
||||
return new NextResponse('Workspace not found', { status: 403 });
|
||||
}
|
||||
|
||||
const ownerUserId = rows[0].user_id;
|
||||
if (ownerUserId !== userId) {
|
||||
// Authenticated but not the owner
|
||||
return new NextResponse('Access denied — this workspace belongs to another user', { status: 403 });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[theia-auth] project ownership check error:', err);
|
||||
return redirectToLogin(request);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 4. Allow — pass user identity headers to Theia ───────────────────────
|
||||
return new NextResponse(null, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'X-Auth-Email': userEmail,
|
||||
'X-Auth-Name': userName ?? '',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function redirectToLogin(request: NextRequest): NextResponse {
|
||||
// Use THEIA_URL as the callbackUrl so the user lands back on Theia after login.
|
||||
// (X-Forwarded-Host points to vibnai.com via Traefik, not the original Theia domain.)
|
||||
const loginUrl = `${APP_URL}/auth?callbackUrl=${encodeURIComponent(THEIA_URL)}`;
|
||||
return NextResponse.redirect(loginUrl, { status: 302 });
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth/authOptions';
|
||||
import { authSession } from "@/lib/auth/session-server";
|
||||
import { query } from '@/lib/db-postgres';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
const session = await authSession();
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: 'No authorization token provided' }, { status: 401 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user