Files
vibn-frontend/app/[workspace]/keys/page.tsx

413 lines
14 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { auth } from '@/lib/firebase/config';
import { toast } from 'sonner';
import { Key, Plus, Trash2, Eye, EyeOff, ExternalLink, Save } from 'lucide-react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
interface ApiKey {
id: string;
service: string;
name: string;
createdAt: any;
lastUsed: any;
}
const SUPPORTED_SERVICES = [
{
id: 'openai',
name: 'OpenAI',
description: 'For ChatGPT imports and AI features',
placeholder: 'sk-...',
helpUrl: 'https://platform.openai.com/api-keys',
},
{
id: 'github',
name: 'GitHub',
description: 'Personal access token for repository access',
placeholder: 'ghp_...',
helpUrl: 'https://github.com/settings/tokens',
},
{
id: 'anthropic',
name: 'Anthropic (Claude)',
description: 'For Claude AI integrations',
placeholder: 'sk-ant-...',
helpUrl: 'https://console.anthropic.com/settings/keys',
},
];
export default function KeysPage() {
const [keys, setKeys] = useState<ApiKey[]>([]);
const [loading, setLoading] = useState(true);
const [showAddDialog, setShowAddDialog] = useState(false);
const [selectedService, setSelectedService] = useState('');
const [keyValue, setKeyValue] = useState('');
const [showKey, setShowKey] = useState(false);
const [saving, setSaving] = useState(false);
useEffect(() => {
loadKeys();
}, []);
const loadKeys = async () => {
try {
const user = auth.currentUser;
if (!user) return;
const token = await user.getIdToken();
const response = await fetch('/api/keys', {
headers: {
'Authorization': `Bearer ${token}`,
},
});
if (response.ok) {
const data = await response.json();
setKeys(data.keys);
}
} catch (error) {
console.error('Error loading keys:', error);
toast.error('Failed to load API keys');
} finally {
setLoading(false);
}
};
const handleAddKey = async () => {
if (!selectedService || !keyValue) {
toast.error('Please select a service and enter a key');
return;
}
setSaving(true);
try {
const user = auth.currentUser;
if (!user) {
toast.error('Please sign in');
return;
}
const token = await user.getIdToken();
const service = SUPPORTED_SERVICES.find(s => s.id === selectedService);
const response = await fetch('/api/keys', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
service: selectedService,
name: service?.name,
keyValue,
}),
});
if (response.ok) {
toast.success(`${service?.name} key saved successfully`);
setShowAddDialog(false);
setSelectedService('');
setKeyValue('');
loadKeys();
} else {
const error = await response.json();
toast.error(error.error || 'Failed to save key');
}
} catch (error) {
console.error('Error saving key:', error);
toast.error('Failed to save key');
} finally {
setSaving(false);
}
};
const handleDeleteKey = async (service: string, name: string) => {
try {
const user = auth.currentUser;
if (!user) return;
const token = await user.getIdToken();
const response = await fetch('/api/keys', {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ service }),
});
if (response.ok) {
toast.success(`${name} key deleted`);
loadKeys();
} else {
toast.error('Failed to delete key');
}
} catch (error) {
console.error('Error deleting key:', error);
toast.error('Failed to delete key');
}
};
const getServiceConfig = (serviceId: string) => {
return SUPPORTED_SERVICES.find(s => s.id === serviceId);
};
return (
<div className="flex h-full flex-col overflow-auto">
<div className="flex-1 p-8 space-y-8 max-w-4xl">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-4xl font-bold mb-2">API Keys</h1>
<p className="text-muted-foreground text-lg">
Manage your third-party API keys for Vibn integrations
</p>
</div>
<Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>
<DialogTrigger asChild>
<Button>
<Plus className="mr-2 h-4 w-4" />
Add Key
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Add API Key</DialogTitle>
<DialogDescription>
Add a third-party API key for Vibn to use on your behalf
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="service">Service</Label>
<Select value={selectedService} onValueChange={setSelectedService}>
<SelectTrigger>
<SelectValue placeholder="Select a service" />
</SelectTrigger>
<SelectContent>
{SUPPORTED_SERVICES.map(service => (
<SelectItem key={service.id} value={service.id}>
{service.name}
</SelectItem>
))}
</SelectContent>
</Select>
{selectedService && (
<p className="text-xs text-muted-foreground">
{getServiceConfig(selectedService)?.description}
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="key">API Key</Label>
<div className="flex gap-2">
<div className="relative flex-1">
<Input
id="key"
type={showKey ? 'text' : 'password'}
placeholder={getServiceConfig(selectedService)?.placeholder || 'Enter API key'}
value={keyValue}
onChange={(e) => setKeyValue(e.target.value)}
className="pr-10"
/>
<Button
variant="ghost"
size="icon"
className="absolute right-0 top-0 h-full"
onClick={() => setShowKey(!showKey)}
>
{showKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</Button>
</div>
</div>
{selectedService && (
<a
href={getServiceConfig(selectedService)?.helpUrl}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-primary hover:underline inline-flex items-center gap-1"
>
Get your API key <ExternalLink className="h-3 w-3" />
</a>
)}
</div>
<div className="rounded-lg border bg-muted/50 p-3">
<p className="text-sm text-muted-foreground">
<strong>🔐 Secure Storage:</strong> Your API key will be encrypted and stored securely.
Vibn will only use it when you explicitly request actions that require it.
</p>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowAddDialog(false)}>
Cancel
</Button>
<Button onClick={handleAddKey} disabled={saving || !selectedService || !keyValue}>
{saving ? 'Saving...' : 'Save Key'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
{/* Keys List */}
{loading ? (
<Card>
<CardContent className="pt-6">
<p className="text-center text-muted-foreground">Loading your API keys...</p>
</CardContent>
</Card>
) : keys.length === 0 ? (
<Card>
<CardContent className="pt-6 text-center space-y-4">
<div className="flex justify-center">
<div className="h-16 w-16 rounded-full bg-muted flex items-center justify-center">
<Key className="h-8 w-8 text-muted-foreground" />
</div>
</div>
<div>
<h3 className="text-lg font-semibold mb-2">No API keys yet</h3>
<p className="text-sm text-muted-foreground mb-4">
Add your third-party API keys to enable Vibn features like ChatGPT imports and AI analysis
</p>
<Button onClick={() => setShowAddDialog(true)}>
<Plus className="mr-2 h-4 w-4" />
Add Your First Key
</Button>
</div>
</CardContent>
</Card>
) : (
<div className="space-y-4">
{keys.map((key) => {
const serviceConfig = getServiceConfig(key.service);
return (
<Card key={key.id}>
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-lg bg-primary/10 flex items-center justify-center">
<Key className="h-5 w-5 text-primary" />
</div>
<div>
<CardTitle className="text-base">{key.name}</CardTitle>
<CardDescription>
{serviceConfig?.description || key.service}
</CardDescription>
</div>
</div>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="ghost" size="icon">
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete API Key?</AlertDialogTitle>
<AlertDialogDescription>
This will remove your {key.name} API key. Features using this key will stop working until you add a new one.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={() => handleDeleteKey(key.service, key.name)}>
Delete Key
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between text-sm">
<div className="space-y-1">
<p className="text-muted-foreground">
Added: {key.createdAt ? new Date(key.createdAt._seconds * 1000).toLocaleDateString() : 'Unknown'}
</p>
{key.lastUsed && (
<p className="text-muted-foreground">
Last used: {new Date(key.lastUsed._seconds * 1000).toLocaleDateString()}
</p>
)}
</div>
{serviceConfig && (
<a
href={serviceConfig.helpUrl}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-primary hover:underline inline-flex items-center gap-1"
>
Manage on {serviceConfig.name} <ExternalLink className="h-3 w-3" />
</a>
)}
</div>
</CardContent>
</Card>
);
})}
</div>
)}
{/* Info Card */}
<Card className="border-blue-500/20 bg-blue-500/5">
<CardHeader>
<CardTitle className="text-base">How API Keys Work</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm text-muted-foreground">
<p>
<strong>🔐 Encrypted Storage:</strong> All API keys are encrypted before being stored in the database.
</p>
<p>
<strong>🎯 Automatic Usage:</strong> When you use Vibn features (like ChatGPT import), we'll automatically use your stored keys instead of asking each time.
</p>
<p>
<strong>🔄 Easy Updates:</strong> Add a new key with the same service name to replace an existing one.
</p>
<p>
<strong>🗑 Full Control:</strong> Delete keys anytime - you can always add them back later.
</p>
</CardContent>
</Card>
</div>
</div>
);
}