215 lines
6.3 KiB
TypeScript
215 lines
6.3 KiB
TypeScript
/**
|
|
* Manage user's third-party API keys (OpenAI, GitHub, etc.)
|
|
*/
|
|
|
|
import { NextResponse } from 'next/server';
|
|
import { getAdminAuth, getAdminDb } from '@/lib/firebase/admin';
|
|
import { FieldValue } from 'firebase-admin/firestore';
|
|
import * as crypto from 'crypto';
|
|
|
|
// Encryption helpers
|
|
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || 'vibn-default-encryption-key-change-me!!';
|
|
const ALGORITHM = 'aes-256-cbc';
|
|
|
|
function encrypt(text: string): { encrypted: string; iv: string } {
|
|
const key = crypto.createHash('sha256').update(ENCRYPTION_KEY).digest();
|
|
const iv = crypto.randomBytes(16);
|
|
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
let encrypted = cipher.update(text, 'utf8', 'hex');
|
|
encrypted += cipher.final('hex');
|
|
return { encrypted, iv: iv.toString('hex') };
|
|
}
|
|
|
|
function decrypt(encrypted: string, ivHex: string): string {
|
|
const key = crypto.createHash('sha256').update(ENCRYPTION_KEY).digest();
|
|
const iv = Buffer.from(ivHex, 'hex');
|
|
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
decrypted += decipher.final('utf8');
|
|
return decrypted;
|
|
}
|
|
|
|
// GET - List all keys (metadata only, not actual values)
|
|
export async function GET(request: Request) {
|
|
try {
|
|
const authHeader = request.headers.get('Authorization');
|
|
if (!authHeader?.startsWith('Bearer ')) {
|
|
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 keysSnapshot = await adminDb
|
|
.collection('userKeys')
|
|
.where('userId', '==', userId)
|
|
.get();
|
|
|
|
const keys = keysSnapshot.docs.map(doc => {
|
|
const data = doc.data();
|
|
return {
|
|
id: doc.id,
|
|
service: data.service,
|
|
name: data.name,
|
|
createdAt: data.createdAt,
|
|
lastUsed: data.lastUsed,
|
|
// Don't send the actual key
|
|
};
|
|
});
|
|
|
|
return NextResponse.json({ keys });
|
|
} catch (error) {
|
|
console.error('Error fetching keys:', error);
|
|
return NextResponse.json(
|
|
{ error: 'Failed to fetch keys', details: error instanceof Error ? error.message : String(error) },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|
|
// POST - Add or update a key
|
|
export async function POST(request: Request) {
|
|
try {
|
|
const authHeader = request.headers.get('Authorization');
|
|
if (!authHeader?.startsWith('Bearer ')) {
|
|
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 { service, name, keyValue } = await request.json();
|
|
|
|
if (!service || !keyValue) {
|
|
return NextResponse.json({ error: 'Service and key value are required' }, { status: 400 });
|
|
}
|
|
|
|
// Encrypt the key
|
|
const { encrypted, iv } = encrypt(keyValue);
|
|
|
|
// Check if key already exists for this service
|
|
const existingKeysSnapshot = await adminDb
|
|
.collection('userKeys')
|
|
.where('userId', '==', userId)
|
|
.where('service', '==', service)
|
|
.limit(1)
|
|
.get();
|
|
|
|
if (!existingKeysSnapshot.empty) {
|
|
// Update existing key
|
|
const keyDoc = existingKeysSnapshot.docs[0];
|
|
await keyDoc.ref.update({
|
|
name: name || service,
|
|
encryptedKey: encrypted,
|
|
iv,
|
|
updatedAt: FieldValue.serverTimestamp(),
|
|
});
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
message: `${service} key updated`,
|
|
id: keyDoc.id,
|
|
});
|
|
} else {
|
|
// Create new key
|
|
const keyRef = await adminDb.collection('userKeys').add({
|
|
userId,
|
|
service,
|
|
name: name || service,
|
|
encryptedKey: encrypted,
|
|
iv,
|
|
createdAt: FieldValue.serverTimestamp(),
|
|
updatedAt: FieldValue.serverTimestamp(),
|
|
lastUsed: null,
|
|
});
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
message: `${service} key added`,
|
|
id: keyRef.id,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error saving key:', error);
|
|
return NextResponse.json(
|
|
{ error: 'Failed to save key', details: error instanceof Error ? error.message : String(error) },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|
|
// DELETE - Remove a key
|
|
export async function DELETE(request: Request) {
|
|
try {
|
|
const authHeader = request.headers.get('Authorization');
|
|
if (!authHeader?.startsWith('Bearer ')) {
|
|
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 { service } = await request.json();
|
|
|
|
if (!service) {
|
|
return NextResponse.json({ error: 'Service is required' }, { status: 400 });
|
|
}
|
|
|
|
// Find and delete the key
|
|
const keysSnapshot = await adminDb
|
|
.collection('userKeys')
|
|
.where('userId', '==', userId)
|
|
.where('service', '==', service)
|
|
.get();
|
|
|
|
if (keysSnapshot.empty) {
|
|
return NextResponse.json({ error: 'Key not found' }, { status: 404 });
|
|
}
|
|
|
|
const batch = adminDb.batch();
|
|
keysSnapshot.docs.forEach(doc => {
|
|
batch.delete(doc.ref);
|
|
});
|
|
await batch.commit();
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
message: `${service} key deleted`,
|
|
});
|
|
} catch (error) {
|
|
console.error('Error deleting key:', error);
|
|
return NextResponse.json(
|
|
{ error: 'Failed to delete key', details: error instanceof Error ? error.message : String(error) },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|