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 { getAdminAuth, getAdminDb } from '@/lib/firebase/admin';
|
||||
import { FieldValue } from 'firebase-admin/firestore';
|
||||
import { getServerSession } from 'next-auth';
|
||||
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) {
|
||||
try {
|
||||
const authHeader = request.headers.get('Authorization');
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user?.email) {
|
||||
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();
|
||||
|
||||
if (!accessToken || !githubUser) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing required fields' },
|
||||
{ status: 400 }
|
||||
);
|
||||
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
||||
}
|
||||
|
||||
// TODO: Encrypt the access token before storing
|
||||
// For now, we'll store it directly (should use crypto.subtle or a library)
|
||||
const encryptedToken = accessToken; // PLACEHOLDER
|
||||
|
||||
// Store GitHub connection
|
||||
const connectionRef = adminDb.collection('githubConnections').doc(userId);
|
||||
await connectionRef.set({
|
||||
userId,
|
||||
await query(
|
||||
`UPDATE fs_users
|
||||
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,
|
||||
accessToken: encryptedToken,
|
||||
connectedAt: FieldValue.serverTimestamp(),
|
||||
lastSyncedAt: null,
|
||||
});
|
||||
githubAccessToken: accessToken,
|
||||
githubConnectedAt: new Date().toISOString(),
|
||||
}),
|
||||
session.user.email,
|
||||
]
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
githubUsername: githubUser.login,
|
||||
});
|
||||
return NextResponse.json({ success: true, githubUsername: githubUser.login });
|
||||
} catch (error) {
|
||||
console.error('[GitHub Connect] Error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to store GitHub connection' },
|
||||
{ status: 500 }
|
||||
);
|
||||
return NextResponse.json({ error: 'Failed to store GitHub connection' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get GitHub connection status for authenticated user
|
||||
*/
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const authHeader = request.headers.get('Authorization');
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const idToken = authHeader.split('Bearer ')[1];
|
||||
const adminAuth = getAdminAuth();
|
||||
const adminDb = getAdminDb();
|
||||
const rows = await query<{ data: any }>(
|
||||
`SELECT data FROM fs_users WHERE data->>'email' = $1 LIMIT 1`,
|
||||
[session.user.email]
|
||||
);
|
||||
|
||||
let userId: string;
|
||||
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) {
|
||||
if (rows.length === 0 || !rows[0].data?.githubConnected) {
|
||||
return NextResponse.json({ connected: false });
|
||||
}
|
||||
|
||||
const data = connectionDoc.data()!;
|
||||
|
||||
const d = rows[0].data;
|
||||
return NextResponse.json({
|
||||
connected: true,
|
||||
githubUsername: data.githubUsername,
|
||||
githubName: data.githubName,
|
||||
githubAvatarUrl: data.githubAvatarUrl,
|
||||
connectedAt: data.connectedAt,
|
||||
lastSyncedAt: data.lastSyncedAt,
|
||||
githubUsername: d.githubUsername,
|
||||
githubName: d.githubName,
|
||||
githubAvatarUrl: d.githubAvatarUrl,
|
||||
connectedAt: d.githubConnectedAt,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[GitHub Connect] Error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch GitHub connection' },
|
||||
{ status: 500 }
|
||||
);
|
||||
return NextResponse.json({ error: 'Failed to fetch GitHub connection' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect GitHub account
|
||||
*/
|
||||
export async function DELETE(request: Request) {
|
||||
try {
|
||||
const authHeader = request.headers.get('Authorization');
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user?.email) {
|
||||
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 });
|
||||
}
|
||||
|
||||
await adminDb.collection('githubConnections').doc(userId).delete();
|
||||
await query(
|
||||
`UPDATE fs_users
|
||||
SET data = data - 'githubConnected' - 'githubUserId' - 'githubUsername'
|
||||
- 'githubName' - 'githubEmail' - 'githubAvatarUrl'
|
||||
- 'githubAccessToken' - 'githubConnectedAt',
|
||||
updated_at = NOW()
|
||||
WHERE data->>'email' = $1`,
|
||||
[session.user.email]
|
||||
);
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('[GitHub Disconnect] Error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to disconnect GitHub' },
|
||||
{ status: 500 }
|
||||
);
|
||||
return NextResponse.json({ error: 'Failed to disconnect GitHub' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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 type { KnowledgeSourceMeta } from '@/lib/types/knowledge';
|
||||
|
||||
@@ -32,9 +34,12 @@ export async function POST(
|
||||
return NextResponse.json({ error: 'transcript is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const adminDb = getAdminDb();
|
||||
const projectSnap = await adminDb.collection('projects').doc(projectId).get();
|
||||
if (!projectSnap.exists) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user?.email) {
|
||||
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 });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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(
|
||||
request: Request,
|
||||
@@ -8,74 +10,29 @@ export async function GET(
|
||||
try {
|
||||
const { projectId } = await params;
|
||||
|
||||
// Authentication (skip in development if no auth header)
|
||||
const authHeader = request.headers.get('Authorization');
|
||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||
|
||||
if (!isDevelopment || authHeader?.startsWith('Bearer ')) {
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user?.email) {
|
||||
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
|
||||
console.log('[API /knowledge/items] Fetching items for project:', 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 rows = await query<{ id: string; data: any; created_at: string; updated_at: string }>(
|
||||
`SELECT id, data, created_at, updated_at FROM fs_knowledge_items WHERE project_id = $1 ORDER BY created_at DESC LIMIT 100`,
|
||||
[projectId]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 { 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(
|
||||
request: NextRequest,
|
||||
{ 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 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { visionAnswers } = body;
|
||||
|
||||
@@ -20,11 +24,20 @@ 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
|
||||
await adminDb.collection('projects').doc(projectId).set(
|
||||
{
|
||||
if (rows.length === 0) {
|
||||
return NextResponse.json({ error: 'Project not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const current = rows[0].data ?? {};
|
||||
const updated = {
|
||||
...current,
|
||||
visionAnswers: {
|
||||
q1: visionAnswers.q1,
|
||||
q2: visionAnswers.q2,
|
||||
@@ -35,14 +48,17 @@ export async function POST(
|
||||
readyForMVP: true,
|
||||
currentPhase: 'mvp',
|
||||
phaseStatus: 'ready',
|
||||
},
|
||||
{ merge: true }
|
||||
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}`);
|
||||
|
||||
// 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(), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -65,4 +81,3 @@ export async function POST(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,59 +1,72 @@
|
||||
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) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const projectId = searchParams.get('projectId');
|
||||
const limit = parseInt(searchParams.get('limit') || '10');
|
||||
|
||||
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 session = await getServerSession(authOptions);
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json([], { status: 200 });
|
||||
}
|
||||
|
||||
const sessionsSnapshot = await sessionsQuery
|
||||
.orderBy('createdAt', 'desc')
|
||||
.limit(limit)
|
||||
.get();
|
||||
const { searchParams } = new URL(request.url);
|
||||
const projectId = searchParams.get('projectId');
|
||||
const limit = parseInt(searchParams.get('limit') || '50');
|
||||
|
||||
const sessions = sessionsSnapshot.docs.map(doc => {
|
||||
const data = doc.data();
|
||||
let rows: any[];
|
||||
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 {
|
||||
id: doc.id,
|
||||
session_id: doc.id,
|
||||
projectId: data.projectId,
|
||||
userId: data.userId,
|
||||
workspacePath: data.workspacePath,
|
||||
workspaceName: data.workspaceName,
|
||||
startTime: data.startTime,
|
||||
endTime: data.endTime,
|
||||
duration: data.duration,
|
||||
duration_minutes: data.duration ? Math.round(data.duration / 60) : 0,
|
||||
tokensUsed: data.tokensUsed || 0,
|
||||
total_tokens: data.tokensUsed || 0,
|
||||
cost: data.cost || 0,
|
||||
estimated_cost_usd: data.cost || 0,
|
||||
model: data.model || 'unknown',
|
||||
primary_ai_model: data.model || 'unknown',
|
||||
filesModified: data.filesModified || [],
|
||||
summary: data.conversationSummary || null,
|
||||
message_count: data.messageCount || 0,
|
||||
id: row.id,
|
||||
session_id: row.id,
|
||||
projectId: d.projectId,
|
||||
userId: d.userId,
|
||||
workspacePath: d.workspacePath,
|
||||
workspaceName: d.workspaceName,
|
||||
startTime: d.startTime,
|
||||
endTime: d.endTime,
|
||||
duration: d.duration,
|
||||
duration_minutes: d.duration ? Math.round(d.duration / 60) : 0,
|
||||
tokensUsed: d.tokensUsed || 0,
|
||||
total_tokens: d.tokensUsed || 0,
|
||||
cost: d.cost || 0,
|
||||
estimated_cost_usd: d.cost || 0,
|
||||
model: d.model || 'unknown',
|
||||
primary_ai_model: d.model || 'unknown',
|
||||
filesModified: d.filesModified || [],
|
||||
summary: d.conversationSummary || null,
|
||||
message_count: d.messageCount || 0,
|
||||
ide_name: 'Cursor',
|
||||
github_branch: data.githubBranch || null,
|
||||
conversation: data.conversation || [],
|
||||
file_changes: data.fileChanges || [],
|
||||
createdAt: data.createdAt,
|
||||
last_updated: data.updatedAt || data.createdAt,
|
||||
github_branch: d.githubBranch || null,
|
||||
conversation: d.conversation || [],
|
||||
file_changes: d.fileChanges || [],
|
||||
createdAt: row.created_at,
|
||||
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);
|
||||
} catch (error) {
|
||||
console.error('[API] Error fetching sessions:', error);
|
||||
|
||||
@@ -1,76 +1,41 @@
|
||||
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 { FieldValue } from 'firebase-admin/firestore';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
console.log('[API] Getting API key...');
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user?.email) {
|
||||
return NextResponse.json({ error: 'No authorization token provided' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Get the authorization header
|
||||
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 rows = await query<{ data: any }>(
|
||||
`SELECT data FROM fs_users WHERE data->>'email' = $1 LIMIT 1`,
|
||||
[session.user.email]
|
||||
);
|
||||
|
||||
if (rows.length > 0 && rows[0].data?.apiKey) {
|
||||
return NextResponse.json({ apiKey: rows[0].data.apiKey });
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
console.log('[API] Token received, verifying...');
|
||||
|
||||
// Verify the Firebase ID token
|
||||
const decodedToken = await adminAuth.verifyIdToken(token);
|
||||
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
|
||||
console.log('[API] Generating new API key...');
|
||||
// Generate new API key and store it
|
||||
const apiKey = `vibn_${uuidv4().replace(/-/g, '')}`;
|
||||
|
||||
// Store API key document
|
||||
console.log('[API] Storing API key in Firestore...');
|
||||
await adminDb.collection('apiKeys').doc(apiKey).set({
|
||||
key: apiKey,
|
||||
userId,
|
||||
createdAt: FieldValue.serverTimestamp(),
|
||||
isActive: true,
|
||||
});
|
||||
await query(
|
||||
`UPDATE fs_users
|
||||
SET data = data || $1::jsonb, updated_at = NOW()
|
||||
WHERE data->>'email' = $2`,
|
||||
[JSON.stringify({ apiKey, apiKeyCreatedAt: new Date().toISOString() }), session.user.email]
|
||||
);
|
||||
|
||||
// Update user document with API key reference (or create if doesn't exist)
|
||||
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,
|
||||
});
|
||||
return NextResponse.json({ apiKey, isNew: true });
|
||||
} catch (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(
|
||||
{
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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_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(); });
|
||||
"
|
||||
|
||||
|
||||
@@ -205,17 +205,15 @@ export async function buildProjectContextForChat(
|
||||
};
|
||||
|
||||
try {
|
||||
// Query sessions linked to this project
|
||||
const sessionsSnapshot = await adminDb
|
||||
.collection('sessions')
|
||||
.where('projectId', '==', projectId)
|
||||
.orderBy('startTime', 'asc')
|
||||
.get();
|
||||
// Query sessions linked to this project from PostgreSQL
|
||||
const sessionRows = await query<{ id: string; data: any }>(
|
||||
`SELECT id, data FROM fs_sessions WHERE data->>'projectId' = $1 ORDER BY created_at ASC`,
|
||||
[projectId]
|
||||
);
|
||||
|
||||
if (!sessionsSnapshot.empty) {
|
||||
sessionHistory.totalSessions = sessionsSnapshot.size;
|
||||
if (sessionRows.length > 0) {
|
||||
sessionHistory.totalSessions = sessionRows.length;
|
||||
|
||||
// Extract all messages from all sessions in chronological order
|
||||
const allMessages: Array<{
|
||||
role: string;
|
||||
content: string;
|
||||
@@ -223,32 +221,27 @@ export async function buildProjectContextForChat(
|
||||
sessionId: string;
|
||||
}> = [];
|
||||
|
||||
for (const sessionDoc of sessionsSnapshot.docs) {
|
||||
const sessionData = sessionDoc.data();
|
||||
const conversation = sessionData.conversation || [];
|
||||
|
||||
// Add messages from this session
|
||||
for (const row of sessionRows) {
|
||||
const conversation = row.data?.conversation || [];
|
||||
for (const msg of conversation) {
|
||||
if (msg.content && msg.content.trim()) {
|
||||
allMessages.push({
|
||||
role: msg.role || 'unknown',
|
||||
content: msg.content,
|
||||
timestamp: msg.timestamp instanceof Date
|
||||
? msg.timestamp.toISOString()
|
||||
: (typeof msg.timestamp === 'string' ? msg.timestamp : new Date().toISOString()),
|
||||
sessionId: sessionDoc.id,
|
||||
timestamp: typeof msg.timestamp === 'string'
|
||||
? msg.timestamp
|
||||
: new Date().toISOString(),
|
||||
sessionId: row.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort all messages by timestamp (chronological order)
|
||||
allMessages.sort((a, b) =>
|
||||
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
||||
);
|
||||
|
||||
sessionHistory.messages = allMessages;
|
||||
|
||||
console.log(
|
||||
`[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 { FieldValue } from 'firebase-admin/firestore';
|
||||
import { query } from '@/lib/db-postgres';
|
||||
import { randomUUID } from 'crypto';
|
||||
import type {
|
||||
KnowledgeItem,
|
||||
KnowledgeSourceMeta,
|
||||
KnowledgeSourceType,
|
||||
} from '@/lib/types/knowledge';
|
||||
|
||||
const COLLECTION = 'knowledge_items';
|
||||
|
||||
interface CreateKnowledgeItemInput {
|
||||
projectId: string;
|
||||
sourceType: KnowledgeSourceType;
|
||||
@@ -16,59 +14,62 @@ interface CreateKnowledgeItemInput {
|
||||
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(
|
||||
input: CreateKnowledgeItemInput,
|
||||
): Promise<KnowledgeItem> {
|
||||
const adminDb = getAdminDb();
|
||||
const docRef = adminDb.collection(COLLECTION).doc();
|
||||
|
||||
const payload = {
|
||||
id: docRef.id,
|
||||
projectId: input.projectId,
|
||||
const id = randomUUID();
|
||||
const data = {
|
||||
sourceType: input.sourceType,
|
||||
title: input.title ?? null,
|
||||
content: input.content,
|
||||
sourceMeta: input.sourceMeta ?? null,
|
||||
createdAt: FieldValue.serverTimestamp(),
|
||||
updatedAt: FieldValue.serverTimestamp(),
|
||||
};
|
||||
|
||||
await docRef.set(payload);
|
||||
const snapshot = await docRef.get();
|
||||
return snapshot.data() as KnowledgeItem;
|
||||
await query(
|
||||
`INSERT INTO fs_knowledge_items (id, project_id, data) VALUES ($1, $2, $3::jsonb)`,
|
||||
[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(
|
||||
projectId: string,
|
||||
knowledgeItemId: string,
|
||||
): Promise<KnowledgeItem | null> {
|
||||
const adminDb = getAdminDb();
|
||||
const docRef = adminDb.collection(COLLECTION).doc(knowledgeItemId);
|
||||
const snapshot = await docRef.get();
|
||||
|
||||
if (!snapshot.exists) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = snapshot.data() as KnowledgeItem;
|
||||
if (data.projectId !== projectId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return data;
|
||||
const rows = await query<any>(
|
||||
`SELECT id, project_id, data, created_at, updated_at FROM fs_knowledge_items WHERE id = $1 AND project_id = $2`,
|
||||
[knowledgeItemId, projectId]
|
||||
);
|
||||
if (rows.length === 0) return null;
|
||||
return rowToItem(rows[0]);
|
||||
}
|
||||
|
||||
export async function listKnowledgeItems(
|
||||
projectId: string,
|
||||
): Promise<KnowledgeItem[]> {
|
||||
const adminDb = getAdminDb();
|
||||
const querySnapshot = await adminDb
|
||||
.collection(COLLECTION)
|
||||
.where('projectId', '==', projectId)
|
||||
.orderBy('createdAt', 'desc')
|
||||
.get();
|
||||
|
||||
return querySnapshot.docs.map((doc) => doc.data() as KnowledgeItem);
|
||||
const rows = await query<any>(
|
||||
`SELECT id, project_id, data, created_at, updated_at FROM fs_knowledge_items WHERE project_id = $1 ORDER BY created_at DESC`,
|
||||
[projectId]
|
||||
);
|
||||
return rows.map(rowToItem);
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user