migrate: replace Firebase with PostgreSQL across core routes
- chat-context.ts: session history now from fs_sessions - /api/sessions: reads from fs_sessions (NextAuth session auth) - /api/github/connect: NextAuth session + stores in fs_users.data - /api/user/api-key: NextAuth session + stores in fs_users.data - /api/projects/[id]/vision: PATCH to fs_projects JSONB - /api/projects/[id]/knowledge/items: reads from fs_knowledge_items - /api/projects/[id]/knowledge/import-ai-chat: uses pg createKnowledgeItem - lib/server/knowledge.ts: fully rewritten to use PostgreSQL - entrypoint.sh: add fs_knowledge_items and chat_conversations tables Made-with: Cursor
This commit is contained in:
@@ -1,151 +1,96 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { getAdminAuth, getAdminDb } from '@/lib/firebase/admin';
|
import { getServerSession } from 'next-auth';
|
||||||
import { FieldValue } from 'firebase-admin/firestore';
|
import { authOptions } from '@/lib/auth/authOptions';
|
||||||
|
import { query } from '@/lib/db-postgres';
|
||||||
|
|
||||||
/**
|
|
||||||
* Store GitHub connection for authenticated user
|
|
||||||
* Encrypts and stores the access token securely
|
|
||||||
*/
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const authHeader = request.headers.get('Authorization');
|
const session = await getServerSession(authOptions);
|
||||||
if (!authHeader?.startsWith('Bearer ')) {
|
if (!session?.user?.email) {
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const idToken = authHeader.split('Bearer ')[1];
|
|
||||||
const adminAuth = getAdminAuth();
|
|
||||||
const adminDb = getAdminDb();
|
|
||||||
|
|
||||||
let userId: string;
|
|
||||||
try {
|
|
||||||
const decodedToken = await adminAuth.verifyIdToken(idToken);
|
|
||||||
userId = decodedToken.uid;
|
|
||||||
} catch (error) {
|
|
||||||
return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { accessToken, githubUser } = await request.json();
|
const { accessToken, githubUser } = await request.json();
|
||||||
|
|
||||||
if (!accessToken || !githubUser) {
|
if (!accessToken || !githubUser) {
|
||||||
return NextResponse.json(
|
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
||||||
{ error: 'Missing required fields' },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Encrypt the access token before storing
|
await query(
|
||||||
// For now, we'll store it directly (should use crypto.subtle or a library)
|
`UPDATE fs_users
|
||||||
const encryptedToken = accessToken; // PLACEHOLDER
|
SET data = data || $1::jsonb, updated_at = NOW()
|
||||||
|
WHERE data->>'email' = $2`,
|
||||||
|
[
|
||||||
|
JSON.stringify({
|
||||||
|
githubConnected: true,
|
||||||
|
githubUserId: githubUser.id,
|
||||||
|
githubUsername: githubUser.login,
|
||||||
|
githubName: githubUser.name,
|
||||||
|
githubEmail: githubUser.email,
|
||||||
|
githubAvatarUrl: githubUser.avatar_url,
|
||||||
|
githubAccessToken: accessToken,
|
||||||
|
githubConnectedAt: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
session.user.email,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
// Store GitHub connection
|
return NextResponse.json({ success: true, githubUsername: githubUser.login });
|
||||||
const connectionRef = adminDb.collection('githubConnections').doc(userId);
|
|
||||||
await connectionRef.set({
|
|
||||||
userId,
|
|
||||||
githubUserId: githubUser.id,
|
|
||||||
githubUsername: githubUser.login,
|
|
||||||
githubName: githubUser.name,
|
|
||||||
githubEmail: githubUser.email,
|
|
||||||
githubAvatarUrl: githubUser.avatar_url,
|
|
||||||
accessToken: encryptedToken,
|
|
||||||
connectedAt: FieldValue.serverTimestamp(),
|
|
||||||
lastSyncedAt: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
githubUsername: githubUser.login,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[GitHub Connect] Error:', error);
|
console.error('[GitHub Connect] Error:', error);
|
||||||
return NextResponse.json(
|
return NextResponse.json({ error: 'Failed to store GitHub connection' }, { status: 500 });
|
||||||
{ error: 'Failed to store GitHub connection' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get GitHub connection status for authenticated user
|
|
||||||
*/
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
try {
|
try {
|
||||||
const authHeader = request.headers.get('Authorization');
|
const session = await getServerSession(authOptions);
|
||||||
if (!authHeader?.startsWith('Bearer ')) {
|
if (!session?.user?.email) {
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const idToken = authHeader.split('Bearer ')[1];
|
const rows = await query<{ data: any }>(
|
||||||
const adminAuth = getAdminAuth();
|
`SELECT data FROM fs_users WHERE data->>'email' = $1 LIMIT 1`,
|
||||||
const adminDb = getAdminDb();
|
[session.user.email]
|
||||||
|
);
|
||||||
|
|
||||||
let userId: string;
|
if (rows.length === 0 || !rows[0].data?.githubConnected) {
|
||||||
try {
|
|
||||||
const decodedToken = await adminAuth.verifyIdToken(idToken);
|
|
||||||
userId = decodedToken.uid;
|
|
||||||
} catch (error) {
|
|
||||||
return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const connectionDoc = await adminDb
|
|
||||||
.collection('githubConnections')
|
|
||||||
.doc(userId)
|
|
||||||
.get();
|
|
||||||
|
|
||||||
if (!connectionDoc.exists) {
|
|
||||||
return NextResponse.json({ connected: false });
|
return NextResponse.json({ connected: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = connectionDoc.data()!;
|
const d = rows[0].data;
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
connected: true,
|
connected: true,
|
||||||
githubUsername: data.githubUsername,
|
githubUsername: d.githubUsername,
|
||||||
githubName: data.githubName,
|
githubName: d.githubName,
|
||||||
githubAvatarUrl: data.githubAvatarUrl,
|
githubAvatarUrl: d.githubAvatarUrl,
|
||||||
connectedAt: data.connectedAt,
|
connectedAt: d.githubConnectedAt,
|
||||||
lastSyncedAt: data.lastSyncedAt,
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[GitHub Connect] Error:', error);
|
console.error('[GitHub Connect] Error:', error);
|
||||||
return NextResponse.json(
|
return NextResponse.json({ error: 'Failed to fetch GitHub connection' }, { status: 500 });
|
||||||
{ error: 'Failed to fetch GitHub connection' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Disconnect GitHub account
|
|
||||||
*/
|
|
||||||
export async function DELETE(request: Request) {
|
export async function DELETE(request: Request) {
|
||||||
try {
|
try {
|
||||||
const authHeader = request.headers.get('Authorization');
|
const session = await getServerSession(authOptions);
|
||||||
if (!authHeader?.startsWith('Bearer ')) {
|
if (!session?.user?.email) {
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const idToken = authHeader.split('Bearer ')[1];
|
await query(
|
||||||
const adminAuth = getAdminAuth();
|
`UPDATE fs_users
|
||||||
const adminDb = getAdminDb();
|
SET data = data - 'githubConnected' - 'githubUserId' - 'githubUsername'
|
||||||
|
- 'githubName' - 'githubEmail' - 'githubAvatarUrl'
|
||||||
let userId: string;
|
- 'githubAccessToken' - 'githubConnectedAt',
|
||||||
try {
|
updated_at = NOW()
|
||||||
const decodedToken = await adminAuth.verifyIdToken(idToken);
|
WHERE data->>'email' = $1`,
|
||||||
userId = decodedToken.uid;
|
[session.user.email]
|
||||||
} catch (error) {
|
);
|
||||||
return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
await adminDb.collection('githubConnections').doc(userId).delete();
|
|
||||||
|
|
||||||
return NextResponse.json({ success: true });
|
return NextResponse.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[GitHub Disconnect] Error:', error);
|
console.error('[GitHub Disconnect] Error:', error);
|
||||||
return NextResponse.json(
|
return NextResponse.json({ error: 'Failed to disconnect GitHub' }, { status: 500 });
|
||||||
{ error: 'Failed to disconnect GitHub' },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { getAdminDb } from '@/lib/firebase/admin';
|
import { getServerSession } from 'next-auth';
|
||||||
|
import { authOptions } from '@/lib/auth/authOptions';
|
||||||
|
import { query } from '@/lib/db-postgres';
|
||||||
import { createKnowledgeItem } from '@/lib/server/knowledge';
|
import { createKnowledgeItem } from '@/lib/server/knowledge';
|
||||||
import type { KnowledgeSourceMeta } from '@/lib/types/knowledge';
|
import type { KnowledgeSourceMeta } from '@/lib/types/knowledge';
|
||||||
|
|
||||||
@@ -32,9 +34,12 @@ export async function POST(
|
|||||||
return NextResponse.json({ error: 'transcript is required' }, { status: 400 });
|
return NextResponse.json({ error: 'transcript is required' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const adminDb = getAdminDb();
|
const session = await getServerSession(authOptions);
|
||||||
const projectSnap = await adminDb.collection('projects').doc(projectId).get();
|
if (!session?.user?.email) {
|
||||||
if (!projectSnap.exists) {
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
const projectRows = await query(`SELECT id FROM fs_projects WHERE id = $1 LIMIT 1`, [projectId]);
|
||||||
|
if (projectRows.length === 0) {
|
||||||
return NextResponse.json({ error: 'Project not found' }, { status: 404 });
|
return NextResponse.json({ error: 'Project not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { getAdminAuth, getAdminDb } from '@/lib/firebase/admin';
|
import { getServerSession } from 'next-auth';
|
||||||
|
import { authOptions } from '@/lib/auth/authOptions';
|
||||||
|
import { query } from '@/lib/db-postgres';
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: Request,
|
request: Request,
|
||||||
@@ -8,74 +10,29 @@ export async function GET(
|
|||||||
try {
|
try {
|
||||||
const { projectId } = await params;
|
const { projectId } = await params;
|
||||||
|
|
||||||
// Authentication (skip in development if no auth header)
|
const session = await getServerSession(authOptions);
|
||||||
const authHeader = request.headers.get('Authorization');
|
if (!session?.user?.email) {
|
||||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
|
||||||
if (!isDevelopment || authHeader?.startsWith('Bearer ')) {
|
|
||||||
if (!authHeader?.startsWith('Bearer ')) {
|
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = authHeader.substring(7);
|
|
||||||
const auth = getAdminAuth();
|
|
||||||
const decoded = await auth.verifyIdToken(token);
|
|
||||||
|
|
||||||
if (!decoded?.uid) {
|
|
||||||
return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch knowledge items from Firestore
|
const rows = await query<{ id: string; data: any; created_at: string; updated_at: string }>(
|
||||||
console.log('[API /knowledge/items] Fetching items for project:', projectId);
|
`SELECT id, data, created_at, updated_at FROM fs_knowledge_items WHERE project_id = $1 ORDER BY created_at DESC LIMIT 100`,
|
||||||
|
[projectId]
|
||||||
let items = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const adminDb = getAdminDb();
|
|
||||||
const knowledgeSnapshot = await adminDb
|
|
||||||
.collection('knowledge')
|
|
||||||
.where('projectId', '==', projectId)
|
|
||||||
.orderBy('createdAt', 'desc')
|
|
||||||
.limit(100)
|
|
||||||
.get();
|
|
||||||
|
|
||||||
console.log('[API /knowledge/items] Found', knowledgeSnapshot.size, 'items');
|
|
||||||
|
|
||||||
items = knowledgeSnapshot.docs.map(doc => {
|
|
||||||
const data = doc.data();
|
|
||||||
return {
|
|
||||||
id: doc.id,
|
|
||||||
title: data.title || data.content?.substring(0, 50) || 'Untitled',
|
|
||||||
sourceType: data.sourceType,
|
|
||||||
content: data.content,
|
|
||||||
sourceMeta: data.sourceMeta,
|
|
||||||
createdAt: data.createdAt?.toDate?.()?.toISOString() || data.createdAt,
|
|
||||||
updatedAt: data.updatedAt?.toDate?.()?.toISOString() || data.updatedAt,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} catch (firestoreError) {
|
|
||||||
console.error('[API /knowledge/items] Firestore query failed:', firestoreError);
|
|
||||||
console.error('[API /knowledge/items] This is likely due to missing Firebase Admin credentials or Firestore not being set up');
|
|
||||||
// Return empty array instead of failing - the UI will show "No chats yet" and "No images yet"
|
|
||||||
items = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
items,
|
|
||||||
count: items.length,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[API /knowledge/items] Error fetching knowledge items:', error);
|
|
||||||
console.error('[API /knowledge/items] Error stack:', error instanceof Error ? error.stack : 'No stack trace');
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: 'Failed to fetch knowledge items',
|
|
||||||
details: error instanceof Error ? error.message : String(error)
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const items = rows.map((row) => ({
|
||||||
|
id: row.id,
|
||||||
|
title: row.data?.title || row.data?.content?.substring(0, 50) || 'Untitled',
|
||||||
|
sourceType: row.data?.sourceType,
|
||||||
|
content: row.data?.content,
|
||||||
|
sourceMeta: row.data?.sourceMeta,
|
||||||
|
createdAt: row.created_at,
|
||||||
|
updatedAt: row.updated_at,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true, items, count: items.length });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[API /knowledge/items] Error:', error);
|
||||||
|
return NextResponse.json({ success: true, items: [], count: 0 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { getAdminDb } from '@/lib/firebase/admin';
|
import { getServerSession } from 'next-auth';
|
||||||
|
import { authOptions } from '@/lib/auth/authOptions';
|
||||||
|
import { query } from '@/lib/db-postgres';
|
||||||
|
|
||||||
/**
|
|
||||||
* Save vision answers to Firestore
|
|
||||||
*/
|
|
||||||
export async function POST(
|
export async function POST(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: Promise<{ projectId: string }> }
|
{ params }: { params: Promise<{ projectId: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const { projectId } = await params;
|
const { projectId } = await params;
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
if (!session?.user?.email) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { visionAnswers } = body;
|
const { visionAnswers } = body;
|
||||||
|
|
||||||
@@ -20,29 +24,41 @@ export async function POST(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const adminDb = getAdminDb();
|
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]
|
||||||
|
);
|
||||||
|
|
||||||
// Save vision answers and mark ready for MVP
|
if (rows.length === 0) {
|
||||||
await adminDb.collection('projects').doc(projectId).set(
|
return NextResponse.json({ error: 'Project not found' }, { status: 404 });
|
||||||
{
|
}
|
||||||
visionAnswers: {
|
|
||||||
q1: visionAnswers.q1,
|
const current = rows[0].data ?? {};
|
||||||
q2: visionAnswers.q2,
|
const updated = {
|
||||||
q3: visionAnswers.q3,
|
...current,
|
||||||
allAnswered: true,
|
visionAnswers: {
|
||||||
updatedAt: visionAnswers.updatedAt || new Date().toISOString(),
|
q1: visionAnswers.q1,
|
||||||
},
|
q2: visionAnswers.q2,
|
||||||
readyForMVP: true,
|
q3: visionAnswers.q3,
|
||||||
currentPhase: 'mvp',
|
allAnswered: true,
|
||||||
phaseStatus: 'ready',
|
updatedAt: visionAnswers.updatedAt || new Date().toISOString(),
|
||||||
},
|
},
|
||||||
{ merge: true }
|
readyForMVP: true,
|
||||||
|
currentPhase: 'mvp',
|
||||||
|
phaseStatus: 'ready',
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await query(
|
||||||
|
`UPDATE fs_projects SET data = $1::jsonb WHERE id = $2`,
|
||||||
|
[JSON.stringify(updated), projectId]
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`[Vision API] Saved vision answers for project ${projectId}`);
|
console.log(`[Vision API] Saved vision answers for project ${projectId}`);
|
||||||
|
|
||||||
// Trigger MVP generation (async - don't wait)
|
// Trigger MVP generation (async - don't wait)
|
||||||
console.log(`[Vision API] Triggering MVP generation for project ${projectId}...`);
|
|
||||||
fetch(new URL(`/api/projects/${projectId}/mvp-checklist`, request.url).toString(), {
|
fetch(new URL(`/api/projects/${projectId}/mvp-checklist`, request.url).toString(), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -65,4 +81,3 @@ export async function POST(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,59 +1,72 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { getAdminDb } from '@/lib/firebase/admin';
|
import { getServerSession } from 'next-auth';
|
||||||
|
import { authOptions } from '@/lib/auth/authOptions';
|
||||||
|
import { query } from '@/lib/db-postgres';
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
try {
|
try {
|
||||||
const { searchParams } = new URL(request.url);
|
const session = await getServerSession(authOptions);
|
||||||
const projectId = searchParams.get('projectId');
|
if (!session?.user?.email) {
|
||||||
const limit = parseInt(searchParams.get('limit') || '10');
|
return NextResponse.json([], { status: 200 });
|
||||||
|
|
||||||
console.log(`[API] Fetching sessions for project ${projectId}, limit ${limit}`);
|
|
||||||
|
|
||||||
const adminDb = getAdminDb();
|
|
||||||
let sessionsQuery = adminDb.collection('sessions');
|
|
||||||
|
|
||||||
// Filter by projectId if provided
|
|
||||||
if (projectId) {
|
|
||||||
sessionsQuery = sessionsQuery.where('projectId', '==', projectId) as any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionsSnapshot = await sessionsQuery
|
const { searchParams } = new URL(request.url);
|
||||||
.orderBy('createdAt', 'desc')
|
const projectId = searchParams.get('projectId');
|
||||||
.limit(limit)
|
const limit = parseInt(searchParams.get('limit') || '50');
|
||||||
.get();
|
|
||||||
|
|
||||||
const sessions = sessionsSnapshot.docs.map(doc => {
|
let rows: any[];
|
||||||
const data = doc.data();
|
if (projectId) {
|
||||||
|
rows = await query<any>(
|
||||||
|
`SELECT s.id, s.data, s.created_at
|
||||||
|
FROM fs_sessions s
|
||||||
|
JOIN fs_users u ON u.id = s.user_id
|
||||||
|
WHERE u.data->>'email' = $1 AND s.data->>'projectId' = $2
|
||||||
|
ORDER BY s.created_at DESC LIMIT $3`,
|
||||||
|
[session.user.email, projectId, limit]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
rows = await query<any>(
|
||||||
|
`SELECT s.id, s.data, s.created_at
|
||||||
|
FROM fs_sessions s
|
||||||
|
JOIN fs_users u ON u.id = s.user_id
|
||||||
|
WHERE u.data->>'email' = $1
|
||||||
|
ORDER BY s.created_at DESC LIMIT $2`,
|
||||||
|
[session.user.email, limit]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessions = rows.map((row: any) => {
|
||||||
|
const d = row.data ?? {};
|
||||||
return {
|
return {
|
||||||
id: doc.id,
|
id: row.id,
|
||||||
session_id: doc.id,
|
session_id: row.id,
|
||||||
projectId: data.projectId,
|
projectId: d.projectId,
|
||||||
userId: data.userId,
|
userId: d.userId,
|
||||||
workspacePath: data.workspacePath,
|
workspacePath: d.workspacePath,
|
||||||
workspaceName: data.workspaceName,
|
workspaceName: d.workspaceName,
|
||||||
startTime: data.startTime,
|
startTime: d.startTime,
|
||||||
endTime: data.endTime,
|
endTime: d.endTime,
|
||||||
duration: data.duration,
|
duration: d.duration,
|
||||||
duration_minutes: data.duration ? Math.round(data.duration / 60) : 0,
|
duration_minutes: d.duration ? Math.round(d.duration / 60) : 0,
|
||||||
tokensUsed: data.tokensUsed || 0,
|
tokensUsed: d.tokensUsed || 0,
|
||||||
total_tokens: data.tokensUsed || 0,
|
total_tokens: d.tokensUsed || 0,
|
||||||
cost: data.cost || 0,
|
cost: d.cost || 0,
|
||||||
estimated_cost_usd: data.cost || 0,
|
estimated_cost_usd: d.cost || 0,
|
||||||
model: data.model || 'unknown',
|
model: d.model || 'unknown',
|
||||||
primary_ai_model: data.model || 'unknown',
|
primary_ai_model: d.model || 'unknown',
|
||||||
filesModified: data.filesModified || [],
|
filesModified: d.filesModified || [],
|
||||||
summary: data.conversationSummary || null,
|
summary: d.conversationSummary || null,
|
||||||
message_count: data.messageCount || 0,
|
message_count: d.messageCount || 0,
|
||||||
ide_name: 'Cursor',
|
ide_name: 'Cursor',
|
||||||
github_branch: data.githubBranch || null,
|
github_branch: d.githubBranch || null,
|
||||||
conversation: data.conversation || [],
|
conversation: d.conversation || [],
|
||||||
file_changes: data.fileChanges || [],
|
file_changes: d.fileChanges || [],
|
||||||
createdAt: data.createdAt,
|
createdAt: row.created_at,
|
||||||
last_updated: data.updatedAt || data.createdAt,
|
last_updated: d.updatedAt || row.created_at,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`[API] Found ${sessions.length} sessions from Firebase`);
|
console.log(`[API] Found ${sessions.length} sessions from PostgreSQL`);
|
||||||
return NextResponse.json(sessions);
|
return NextResponse.json(sessions);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[API] Error fetching sessions:', error);
|
console.error('[API] Error fetching sessions:', error);
|
||||||
|
|||||||
@@ -1,76 +1,41 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { adminAuth, adminDb } from '@/lib/firebase/admin';
|
import { getServerSession } from 'next-auth';
|
||||||
|
import { authOptions } from '@/lib/auth/authOptions';
|
||||||
|
import { query } from '@/lib/db-postgres';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { FieldValue } from 'firebase-admin/firestore';
|
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
try {
|
try {
|
||||||
console.log('[API] Getting API key...');
|
const session = await getServerSession(authOptions);
|
||||||
|
if (!session?.user?.email) {
|
||||||
// Get the authorization header
|
return NextResponse.json({ error: 'No authorization token provided' }, { status: 401 });
|
||||||
const authHeader = request.headers.get('authorization');
|
|
||||||
if (!authHeader?.startsWith('Bearer ')) {
|
|
||||||
console.error('[API] No authorization header');
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'No authorization token provided' },
|
|
||||||
{ status: 401 }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = authHeader.substring(7);
|
const rows = await query<{ data: any }>(
|
||||||
console.log('[API] Token received, verifying...');
|
`SELECT data FROM fs_users WHERE data->>'email' = $1 LIMIT 1`,
|
||||||
|
[session.user.email]
|
||||||
|
);
|
||||||
|
|
||||||
// Verify the Firebase ID token
|
if (rows.length > 0 && rows[0].data?.apiKey) {
|
||||||
const decodedToken = await adminAuth.verifyIdToken(token);
|
return NextResponse.json({ apiKey: rows[0].data.apiKey });
|
||||||
const userId = decodedToken.uid;
|
|
||||||
console.log('[API] Token verified, userId:', userId);
|
|
||||||
|
|
||||||
// Check if user already has an API key
|
|
||||||
console.log('[API] Checking for existing API key...');
|
|
||||||
const userDoc = await adminDb.collection('users').doc(userId).get();
|
|
||||||
|
|
||||||
if (userDoc.exists && userDoc.data()?.apiKey) {
|
|
||||||
console.log('[API] Found existing API key');
|
|
||||||
return NextResponse.json({
|
|
||||||
apiKey: userDoc.data()!.apiKey,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate new API key
|
// Generate new API key and store it
|
||||||
console.log('[API] Generating new API key...');
|
|
||||||
const apiKey = `vibn_${uuidv4().replace(/-/g, '')}`;
|
const apiKey = `vibn_${uuidv4().replace(/-/g, '')}`;
|
||||||
|
|
||||||
// Store API key document
|
await query(
|
||||||
console.log('[API] Storing API key in Firestore...');
|
`UPDATE fs_users
|
||||||
await adminDb.collection('apiKeys').doc(apiKey).set({
|
SET data = data || $1::jsonb, updated_at = NOW()
|
||||||
key: apiKey,
|
WHERE data->>'email' = $2`,
|
||||||
userId,
|
[JSON.stringify({ apiKey, apiKeyCreatedAt: new Date().toISOString() }), session.user.email]
|
||||||
createdAt: FieldValue.serverTimestamp(),
|
);
|
||||||
isActive: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update user document with API key reference (or create if doesn't exist)
|
return NextResponse.json({ apiKey, isNew: true });
|
||||||
console.log('[API] Updating user document...');
|
|
||||||
await adminDb.collection('users').doc(userId).set({
|
|
||||||
apiKey,
|
|
||||||
updatedAt: FieldValue.serverTimestamp(),
|
|
||||||
}, { merge: true });
|
|
||||||
|
|
||||||
console.log('[API] API key created successfully:', apiKey);
|
|
||||||
return NextResponse.json({
|
|
||||||
apiKey,
|
|
||||||
isNew: true,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[API] Error getting/creating API key:', error);
|
console.error('[API] Error getting/creating API key:', error);
|
||||||
console.error('[API] Error stack:', error instanceof Error ? error.stack : 'No stack trace');
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{ error: 'Failed to get API key', details: error instanceof Error ? error.message : String(error) },
|
||||||
error: 'Failed to get API key',
|
|
||||||
details: error instanceof Error ? error.message : String(error),
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,19 @@ pool.query(\`
|
|||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_fs_sessions_user_id ON fs_sessions (user_id);
|
CREATE INDEX IF NOT EXISTS idx_fs_sessions_user_id ON fs_sessions (user_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_fs_sessions_project_id ON fs_sessions ((data->>'projectId'));
|
CREATE INDEX IF NOT EXISTS idx_fs_sessions_project_id ON fs_sessions ((data->>'projectId'));
|
||||||
|
CREATE TABLE IF NOT EXISTS fs_knowledge_items (
|
||||||
|
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
||||||
|
project_id TEXT NOT NULL,
|
||||||
|
data JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_fs_knowledge_project_id ON fs_knowledge_items (project_id);
|
||||||
|
CREATE TABLE IF NOT EXISTS chat_conversations (
|
||||||
|
project_id TEXT PRIMARY KEY,
|
||||||
|
messages JSONB NOT NULL DEFAULT '[]',
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
\`).then(() => { console.log('App tables ready'); pool.end(); }).catch(e => { console.error('Table init error:', e.message); pool.end(); });
|
\`).then(() => { console.log('App tables ready'); pool.end(); }).catch(e => { console.error('Table init error:', e.message); pool.end(); });
|
||||||
"
|
"
|
||||||
|
|
||||||
|
|||||||
@@ -205,17 +205,15 @@ export async function buildProjectContextForChat(
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Query sessions linked to this project
|
// Query sessions linked to this project from PostgreSQL
|
||||||
const sessionsSnapshot = await adminDb
|
const sessionRows = await query<{ id: string; data: any }>(
|
||||||
.collection('sessions')
|
`SELECT id, data FROM fs_sessions WHERE data->>'projectId' = $1 ORDER BY created_at ASC`,
|
||||||
.where('projectId', '==', projectId)
|
[projectId]
|
||||||
.orderBy('startTime', 'asc')
|
);
|
||||||
.get();
|
|
||||||
|
|
||||||
if (!sessionsSnapshot.empty) {
|
if (sessionRows.length > 0) {
|
||||||
sessionHistory.totalSessions = sessionsSnapshot.size;
|
sessionHistory.totalSessions = sessionRows.length;
|
||||||
|
|
||||||
// Extract all messages from all sessions in chronological order
|
|
||||||
const allMessages: Array<{
|
const allMessages: Array<{
|
||||||
role: string;
|
role: string;
|
||||||
content: string;
|
content: string;
|
||||||
@@ -223,32 +221,27 @@ export async function buildProjectContextForChat(
|
|||||||
sessionId: string;
|
sessionId: string;
|
||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
for (const sessionDoc of sessionsSnapshot.docs) {
|
for (const row of sessionRows) {
|
||||||
const sessionData = sessionDoc.data();
|
const conversation = row.data?.conversation || [];
|
||||||
const conversation = sessionData.conversation || [];
|
|
||||||
|
|
||||||
// Add messages from this session
|
|
||||||
for (const msg of conversation) {
|
for (const msg of conversation) {
|
||||||
if (msg.content && msg.content.trim()) {
|
if (msg.content && msg.content.trim()) {
|
||||||
allMessages.push({
|
allMessages.push({
|
||||||
role: msg.role || 'unknown',
|
role: msg.role || 'unknown',
|
||||||
content: msg.content,
|
content: msg.content,
|
||||||
timestamp: msg.timestamp instanceof Date
|
timestamp: typeof msg.timestamp === 'string'
|
||||||
? msg.timestamp.toISOString()
|
? msg.timestamp
|
||||||
: (typeof msg.timestamp === 'string' ? msg.timestamp : new Date().toISOString()),
|
: new Date().toISOString(),
|
||||||
sessionId: sessionDoc.id,
|
sessionId: row.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort all messages by timestamp (chronological order)
|
|
||||||
allMessages.sort((a, b) =>
|
allMessages.sort((a, b) =>
|
||||||
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
||||||
);
|
);
|
||||||
|
|
||||||
sessionHistory.messages = allMessages;
|
sessionHistory.messages = allMessages;
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`[Chat Context] Loaded ${sessionHistory.totalSessions} sessions with ${allMessages.length} total messages for project ${projectId}`
|
`[Chat Context] Loaded ${sessionHistory.totalSessions} sessions with ${allMessages.length} total messages for project ${projectId}`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import { getAdminDb } from '@/lib/firebase/admin';
|
import { query } from '@/lib/db-postgres';
|
||||||
import { FieldValue } from 'firebase-admin/firestore';
|
import { randomUUID } from 'crypto';
|
||||||
import type {
|
import type {
|
||||||
KnowledgeItem,
|
KnowledgeItem,
|
||||||
KnowledgeSourceMeta,
|
KnowledgeSourceMeta,
|
||||||
KnowledgeSourceType,
|
KnowledgeSourceType,
|
||||||
} from '@/lib/types/knowledge';
|
} from '@/lib/types/knowledge';
|
||||||
|
|
||||||
const COLLECTION = 'knowledge_items';
|
|
||||||
|
|
||||||
interface CreateKnowledgeItemInput {
|
interface CreateKnowledgeItemInput {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
sourceType: KnowledgeSourceType;
|
sourceType: KnowledgeSourceType;
|
||||||
@@ -16,59 +14,62 @@ interface CreateKnowledgeItemInput {
|
|||||||
sourceMeta?: KnowledgeSourceMeta;
|
sourceMeta?: KnowledgeSourceMeta;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rowToItem(row: { id: string; project_id: string; data: any; created_at: string; updated_at: string }): KnowledgeItem {
|
||||||
|
const d = row.data ?? {};
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
projectId: row.project_id,
|
||||||
|
sourceType: d.sourceType,
|
||||||
|
title: d.title ?? null,
|
||||||
|
content: d.content,
|
||||||
|
sourceMeta: d.sourceMeta ?? null,
|
||||||
|
createdAt: row.created_at,
|
||||||
|
updatedAt: row.updated_at,
|
||||||
|
} as KnowledgeItem;
|
||||||
|
}
|
||||||
|
|
||||||
export async function createKnowledgeItem(
|
export async function createKnowledgeItem(
|
||||||
input: CreateKnowledgeItemInput,
|
input: CreateKnowledgeItemInput,
|
||||||
): Promise<KnowledgeItem> {
|
): Promise<KnowledgeItem> {
|
||||||
const adminDb = getAdminDb();
|
const id = randomUUID();
|
||||||
const docRef = adminDb.collection(COLLECTION).doc();
|
const data = {
|
||||||
|
|
||||||
const payload = {
|
|
||||||
id: docRef.id,
|
|
||||||
projectId: input.projectId,
|
|
||||||
sourceType: input.sourceType,
|
sourceType: input.sourceType,
|
||||||
title: input.title ?? null,
|
title: input.title ?? null,
|
||||||
content: input.content,
|
content: input.content,
|
||||||
sourceMeta: input.sourceMeta ?? null,
|
sourceMeta: input.sourceMeta ?? null,
|
||||||
createdAt: FieldValue.serverTimestamp(),
|
|
||||||
updatedAt: FieldValue.serverTimestamp(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await docRef.set(payload);
|
await query(
|
||||||
const snapshot = await docRef.get();
|
`INSERT INTO fs_knowledge_items (id, project_id, data) VALUES ($1, $2, $3::jsonb)`,
|
||||||
return snapshot.data() as KnowledgeItem;
|
[id, input.projectId, JSON.stringify(data)]
|
||||||
|
);
|
||||||
|
|
||||||
|
const rows = await query<any>(
|
||||||
|
`SELECT id, project_id, data, created_at, updated_at FROM fs_knowledge_items WHERE id = $1`,
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
|
||||||
|
return rowToItem(rows[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getKnowledgeItem(
|
export async function getKnowledgeItem(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
knowledgeItemId: string,
|
knowledgeItemId: string,
|
||||||
): Promise<KnowledgeItem | null> {
|
): Promise<KnowledgeItem | null> {
|
||||||
const adminDb = getAdminDb();
|
const rows = await query<any>(
|
||||||
const docRef = adminDb.collection(COLLECTION).doc(knowledgeItemId);
|
`SELECT id, project_id, data, created_at, updated_at FROM fs_knowledge_items WHERE id = $1 AND project_id = $2`,
|
||||||
const snapshot = await docRef.get();
|
[knowledgeItemId, projectId]
|
||||||
|
);
|
||||||
if (!snapshot.exists) {
|
if (rows.length === 0) return null;
|
||||||
return null;
|
return rowToItem(rows[0]);
|
||||||
}
|
|
||||||
|
|
||||||
const data = snapshot.data() as KnowledgeItem;
|
|
||||||
if (data.projectId !== projectId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listKnowledgeItems(
|
export async function listKnowledgeItems(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
): Promise<KnowledgeItem[]> {
|
): Promise<KnowledgeItem[]> {
|
||||||
const adminDb = getAdminDb();
|
const rows = await query<any>(
|
||||||
const querySnapshot = await adminDb
|
`SELECT id, project_id, data, created_at, updated_at FROM fs_knowledge_items WHERE project_id = $1 ORDER BY created_at DESC`,
|
||||||
.collection(COLLECTION)
|
[projectId]
|
||||||
.where('projectId', '==', projectId)
|
);
|
||||||
.orderBy('createdAt', 'desc')
|
return rows.map(rowToItem);
|
||||||
.get();
|
|
||||||
|
|
||||||
return querySnapshot.docs.map((doc) => doc.data() as KnowledgeItem);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user