chat routes accept workspace API key (thin-client Change 8.1)

This commit is contained in:
2026-06-01 12:50:47 -07:00
parent 6a688c8dd1
commit ef0d84cf5f
3 changed files with 77 additions and 35 deletions

View File

@@ -15,8 +15,8 @@
* data: {"type":"error","error":"..."} * data: {"type":"error","error":"..."}
*/ */
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { authSession } from "@/lib/auth/session-server"; import { requireWorkspacePrincipal } from "@/lib/auth/workspace-auth";
import { query } from "@/lib/db-postgres"; import { query, queryOne } from "@/lib/db-postgres";
import { callVibnChat } from "@/lib/ai/vibn-chat-model"; import { callVibnChat } from "@/lib/ai/vibn-chat-model";
import { VIBN_TOOL_DEFINITIONS, executeMcpTool } from "@/lib/ai/vibn-tools"; import { VIBN_TOOL_DEFINITIONS, executeMcpTool } from "@/lib/ai/vibn-tools";
import { import {
@@ -394,10 +394,17 @@ function lastToolResultsHadFailure(messages: ChatMessage[], lookback = 3) {
export async function POST(request: Request) { export async function POST(request: Request) {
await ensureChatTables(); await ensureChatTables();
const session = await authSession(); const principal = await requireWorkspacePrincipal(request);
if (!session?.user?.email) { if (principal instanceof NextResponse) return principal;
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const userRow = await queryOne<{ data: any }>(
`SELECT data FROM fs_users WHERE id = $1 LIMIT 1`,
[principal.userId]
);
if (!userRow?.data?.email) {
return NextResponse.json({ error: "Unauthorized user" }, { status: 401 });
} }
const sessionEmail = userRow.data.email;
let body: { let body: {
thread_id: string; thread_id: string;
@@ -428,7 +435,7 @@ export async function POST(request: Request) {
); );
} }
const email = session.user.email; const email = sessionEmail;
// Verify thread belongs to user, and capture its project scope (if any). // Verify thread belongs to user, and capture its project scope (if any).
const threads = await query<{ id: string; project_id: string | null }>( const threads = await query<{ id: string; project_id: string | null }>(

View File

@@ -4,18 +4,25 @@
* DELETE /api/chat/threads/[id] — delete a thread * DELETE /api/chat/threads/[id] — delete a thread
*/ */
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { authSession } from '@/lib/auth/session-server'; import { requireWorkspacePrincipal } from '@/lib/auth/workspace-auth';
import { query } from '@/lib/db-postgres'; import { query, queryOne } from '@/lib/db-postgres';
export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) { export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
const session = await authSession(); const principal = await requireWorkspacePrincipal(request);
if (!session?.user?.email) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); if (principal instanceof NextResponse) return principal;
const userRow = await queryOne<{ data: any }>(
`SELECT data FROM fs_users WHERE id = $1 LIMIT 1`,
[principal.userId]
);
if (!userRow?.data?.email) return NextResponse.json({ error: 'Unauthorized user' }, { status: 401 });
const sessionEmail = userRow.data.email;
const { id } = await params; const { id } = await params;
const threads = await query<any>( const threads = await query<any>(
`SELECT id, data, created_at, updated_at FROM fs_chat_threads WHERE id = $1 AND user_id = $2`, `SELECT id, data, created_at, updated_at FROM fs_chat_threads WHERE id = $1 AND user_id = $2`,
[id, session.user.email], [id, sessionEmail],
); );
if (!threads.length) return NextResponse.json({ error: 'Not found' }, { status: 404 }); if (!threads.length) return NextResponse.json({ error: 'Not found' }, { status: 404 });
@@ -32,8 +39,15 @@ export async function GET(request: Request, { params }: { params: Promise<{ id:
} }
export async function PATCH(request: Request, { params }: { params: Promise<{ id: string }> }) { export async function PATCH(request: Request, { params }: { params: Promise<{ id: string }> }) {
const session = await authSession(); const principal = await requireWorkspacePrincipal(request);
if (!session?.user?.email) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); if (principal instanceof NextResponse) return principal;
const userRow = await queryOne<{ data: any }>(
`SELECT data FROM fs_users WHERE id = $1 LIMIT 1`,
[principal.userId]
);
if (!userRow?.data?.email) return NextResponse.json({ error: 'Unauthorized user' }, { status: 401 });
const sessionEmail = userRow.data.email;
const { id } = await params; const { id } = await params;
const { title } = await request.json().catch(() => ({})); const { title } = await request.json().catch(() => ({}));
@@ -41,16 +55,23 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ id
await query( await query(
`UPDATE fs_chat_threads SET data = data || $3, updated_at = NOW() WHERE id = $1 AND user_id = $2`, `UPDATE fs_chat_threads SET data = data || $3, updated_at = NOW() WHERE id = $1 AND user_id = $2`,
[id, session.user.email, JSON.stringify({ title })], [id, sessionEmail, JSON.stringify({ title })],
); );
return NextResponse.json({ ok: true }); return NextResponse.json({ ok: true });
} }
export async function DELETE(request: Request, { params }: { params: Promise<{ id: string }> }) { export async function DELETE(request: Request, { params }: { params: Promise<{ id: string }> }) {
const session = await authSession(); const principal = await requireWorkspacePrincipal(request);
if (!session?.user?.email) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); if (principal instanceof NextResponse) return principal;
const userRow = await queryOne<{ data: any }>(
`SELECT data FROM fs_users WHERE id = $1 LIMIT 1`,
[principal.userId]
);
if (!userRow?.data?.email) return NextResponse.json({ error: 'Unauthorized user' }, { status: 401 });
const sessionEmail = userRow.data.email;
const { id } = await params; const { id } = await params;
await query(`DELETE FROM fs_chat_threads WHERE id = $1 AND user_id = $2`, [id, session.user.email]); await query(`DELETE FROM fs_chat_threads WHERE id = $1 AND user_id = $2`, [id, sessionEmail]);
return NextResponse.json({ ok: true }); return NextResponse.json({ ok: true });
} }

View File

@@ -12,8 +12,8 @@
* after deploy (no manual migration needed). * after deploy (no manual migration needed).
*/ */
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { authSession } from '@/lib/auth/session-server'; import { requireWorkspacePrincipal } from '@/lib/auth/workspace-auth';
import { query } from '@/lib/db-postgres'; import { query, queryOne } from '@/lib/db-postgres';
let chatTablesReady = false; let chatTablesReady = false;
async function ensureChatTables() { async function ensureChatTables() {
@@ -54,8 +54,15 @@ async function ensureChatTables() {
export async function GET(request: Request) { export async function GET(request: Request) {
await ensureChatTables(); await ensureChatTables();
const session = await authSession(); const principal = await requireWorkspacePrincipal(request);
if (!session?.user?.email) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); if (principal instanceof NextResponse) return principal;
const userRow = await queryOne<{ data: any }>(
`SELECT data FROM fs_users WHERE id = $1 LIMIT 1`,
[principal.userId]
);
if (!userRow?.data?.email) return NextResponse.json({ error: 'Unauthorized user' }, { status: 401 });
const sessionEmail = userRow.data.email;
const { searchParams } = new URL(request.url); const { searchParams } = new URL(request.url);
const workspace = searchParams.get('workspace') || ''; const workspace = searchParams.get('workspace') || '';
@@ -90,8 +97,8 @@ export async function GET(request: Request) {
WHERE t.user_id = $1 AND t.workspace = $2 AND t.project_id IS NULL WHERE t.user_id = $1 AND t.workspace = $2 AND t.project_id IS NULL
ORDER BY t.updated_at DESC LIMIT 50`; ORDER BY t.updated_at DESC LIMIT 50`;
const args = projectId const args = projectId
? [session.user.email, workspace, projectId] ? [sessionEmail, workspace, projectId]
: [session.user.email, workspace]; : [sessionEmail, workspace];
const rows = await query<any>(sql, args); const rows = await query<any>(sql, args);
@@ -110,8 +117,15 @@ export async function GET(request: Request) {
export async function POST(request: Request) { export async function POST(request: Request) {
await ensureChatTables(); await ensureChatTables();
const session = await authSession(); const principal = await requireWorkspacePrincipal(request);
if (!session?.user?.email) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); if (principal instanceof NextResponse) return principal;
const userRow = await queryOne<{ data: any }>(
`SELECT data FROM fs_users WHERE id = $1 LIMIT 1`,
[principal.userId]
);
if (!userRow?.data?.email) return NextResponse.json({ error: 'Unauthorized user' }, { status: 401 });
const sessionEmail = userRow.data.email;
const { workspace, title, projectId } = await request.json().catch(() => ({})); const { workspace, title, projectId } = await request.json().catch(() => ({}));
if (!workspace) return NextResponse.json({ error: 'workspace required' }, { status: 400 }); if (!workspace) return NextResponse.json({ error: 'workspace required' }, { status: 400 });
@@ -125,17 +139,17 @@ export async function POST(request: Request) {
`SELECT p.id FROM fs_projects p `SELECT p.id FROM fs_projects p
JOIN fs_users u ON u.id = p.user_id JOIN fs_users u ON u.id = p.user_id
WHERE p.id = $1 AND u.data->>'email' = $2 LIMIT 1`, WHERE p.id = $1 AND u.data->>'email' = $2 LIMIT 1`,
[projectId, session.user.email], [projectId, sessionEmail],
); );
if (owned.length > 0) safeProjectId = projectId; if (owned.length > 0) safeProjectId = projectId;
} }
const rows = await query<any>( const rows = await query<any>(
`INSERT INTO fs_chat_threads (user_id, workspace, project_id, data) `INSERT INTO fs_chat_threads (user_id, workspace, project_id, data)
VALUES ($1, $2, $3, $4) VALUES ($1, $2, $3, $4)
RETURNING id, project_id, data, created_at, updated_at`, RETURNING id, project_id, data, created_at, updated_at`,
[ [
session.user.email, sessionEmail,
workspace, workspace,
safeProjectId, safeProjectId,
JSON.stringify({ title: title || 'New conversation', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }), JSON.stringify({ title: title || 'New conversation', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }),