/** * GET /api/chat/threads — list user's threads * GET /api/chat/threads?projectId=… — list user's threads for one project * POST /api/chat/threads — create a new thread (optionally scoped to a project) * * Threads can be either: * - workspace-level (project_id NULL) — created from /projects, etc. * - project-scoped (project_id set) — created from a project page so * the AI can pin the right project context in its system prompt. * * The schema is migrated idempotently the first time the route is hit * after deploy (no manual migration needed). */ import { NextResponse } from 'next/server'; import { authSession } from '@/lib/auth/session-server'; import { query } from '@/lib/db-postgres'; let chatTablesReady = false; async function ensureChatTables() { if (chatTablesReady) return; await query(` CREATE TABLE IF NOT EXISTS fs_chat_threads ( id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text, user_id TEXT NOT NULL, workspace TEXT NOT NULL DEFAULT '', 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_chat_threads_user_ws_idx ON fs_chat_threads (user_id, workspace, updated_at DESC); CREATE TABLE IF NOT EXISTS fs_chat_messages ( id BIGSERIAL PRIMARY KEY, thread_id TEXT NOT NULL REFERENCES fs_chat_threads(id) ON DELETE CASCADE, user_id TEXT NOT NULL, data JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS fs_chat_messages_thread_idx ON fs_chat_messages (thread_id, created_at ASC); `, []); // Idempotent migration: add project_id + composite index for fast // per-project listing. No-op on subsequent boots. await query(` ALTER TABLE fs_chat_threads ADD COLUMN IF NOT EXISTS project_id TEXT; CREATE INDEX IF NOT EXISTS fs_chat_threads_user_proj_idx ON fs_chat_threads (user_id, project_id, updated_at DESC); `, []); chatTablesReady = true; } export async function GET(request: Request) { await ensureChatTables(); const session = await authSession(); if (!session?.user?.email) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); const { searchParams } = new URL(request.url); const workspace = searchParams.get('workspace') || ''; const projectId = searchParams.get('projectId') || null; // When projectId is supplied, narrow to that project. When omitted, // return only WORKSPACE-level threads (project_id IS NULL) so the // workspace chat UI doesn't get spammed with every project's history. const sql = projectId ? `SELECT id, project_id, data, created_at, updated_at FROM fs_chat_threads WHERE user_id = $1 AND workspace = $2 AND project_id = $3 ORDER BY updated_at DESC LIMIT 50` : `SELECT id, project_id, data, created_at, updated_at FROM fs_chat_threads WHERE user_id = $1 AND workspace = $2 AND project_id IS NULL ORDER BY updated_at DESC LIMIT 50`; const args = projectId ? [session.user.email, workspace, projectId] : [session.user.email, workspace]; const rows = await query(sql, args); return NextResponse.json({ threads: rows.map((r: any) => ({ id: r.id, projectId: r.project_id ?? null, title: r.data?.title || 'New conversation', updatedAt: r.updated_at, createdAt: r.created_at, })), }); } export async function POST(request: Request) { await ensureChatTables(); const session = await authSession(); if (!session?.user?.email) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); const { workspace, title, projectId } = await request.json().catch(() => ({})); if (!workspace) return NextResponse.json({ error: 'workspace required' }, { status: 400 }); // Verify the project belongs to the requesting user before tagging // a thread to it. Silently drop the projectId if the check fails so // a misbehaving client can't tag threads onto someone else's project. let safeProjectId: string | null = null; if (projectId) { const owned = await query<{ id: string }>( `SELECT p.id 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 (owned.length > 0) safeProjectId = projectId; } const rows = await query( `INSERT INTO fs_chat_threads (user_id, workspace, project_id, data) VALUES ($1, $2, $3, $4) RETURNING id, project_id, data, created_at, updated_at`, [ session.user.email, workspace, safeProjectId, JSON.stringify({ title: title || 'New conversation', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }), ], ); const r = rows[0]; return NextResponse.json({ thread: { id: r.id, projectId: r.project_id ?? null, title: r.data?.title || 'New conversation', createdAt: r.created_at, updatedAt: r.updated_at, }, }); }