VIBN Frontend for Coolify deployment
This commit is contained in:
214
app/api/keys/route.ts
Normal file
214
app/api/keys/route.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user