veza/apps/web/src/components/developer/modals/CreateAPIKeyModal.tsx

200 lines
7.1 KiB
TypeScript
Raw Normal View History

import React, { useState } from 'react';
import { Button } from '../../ui/button';
import { Input } from '../../ui/input';
import { X, Key, Copy, Check, Loader2 } from 'lucide-react';
import { useToast } from '@/hooks/useToast';
interface CreateAPIKeyModalProps {
onClose: () => void;
onCreate: (keyData: { name: string; scopes: string[] }) => Promise<{ key?: string; id: string; name: string; prefix: string }>;
}
const SCOPES = [
{ id: 'user.read', label: 'Read User Data' },
{ id: 'user.write', label: 'Update User Profile' },
{ id: 'tracks.read', label: 'Read Tracks' },
{ id: 'tracks.upload', label: 'Upload Tracks' },
{ id: 'sales.read', label: 'Read Sales Data' },
];
export const CreateAPIKeyModal: React.FC<CreateAPIKeyModalProps> = ({
onClose,
onCreate,
}) => {
const toast = useToast();
const [step, setStep] = useState(1);
const [name, setName] = useState('');
const [selectedScopes, setSelectedScopes] = useState<string[]>(['user.read']);
const [generatedKey, setGeneratedKey] = useState('');
const [isGenerating, setIsGenerating] = useState(false);
const toggleScope = (id: string) => {
setSelectedScopes((prev) =>
prev.includes(id) ? prev.filter((s) => s !== id) : [...prev, id],
);
};
const handleGenerate = async () => {
// Validate form
if (!name.trim()) {
toast.error('Please enter a name for your API key');
return;
}
if (selectedScopes.length === 0) {
toast.error('Please select at least one permission scope');
return;
}
setIsGenerating(true);
try {
// Call the parent's onCreate handler which makes the API call
const result = await onCreate({ name: name.trim(), scopes: selectedScopes });
// If the API returns a full key, use it; otherwise generate a mock for display
if (result.key) {
setGeneratedKey(result.key);
} else {
// Fallback: generate a mock key for display (in case backend doesn't return full key)
const mockKey = `vz_${Math.random().toString(36).substr(2, 8)}_${Math.random().toString(36).substr(2, 16)}`;
setGeneratedKey(mockKey);
}
setStep(2);
} catch (error) {
// Error is already handled by the parent component
// Just reset the loading state
setIsGenerating(false);
}
};
const copyKey = () => {
navigator.clipboard.writeText(generatedKey);
toast.success('API Key copied to clipboard');
};
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
<div
className="absolute inset-0 bg-kodo-void/90 backdrop-blur-sm"
onClick={onClose}
></div>
<div className="relative w-full max-w-lg bg-kodo-graphite border border-kodo-steel rounded-xl shadow-2xl animate-scaleIn overflow-hidden">
<div className="p-4 border-b border-kodo-steel bg-kodo-ink flex justify-between items-center">
<h3 className="font-bold text-white flex items-center gap-2">
<Key className="w-4 h-4 text-kodo-gold" />{' '}
{step === 1 ? 'Create API Key' : 'API Key Generated'}
</h3>
<button onClick={onClose}>
<X className="w-5 h-5 text-kodo-content-dim hover:text-white" />
</button>
</div>
<div className="p-6">
{step === 1 ? (
<div className="space-y-6">
<div>
<label className="block text-xs font-bold text-kodo-content-dim uppercase mb-2">
Key Name
</label>
<Input
placeholder="e.g. Production Server, Mobile App"
value={name}
onChange={(e) => setName(e.target.value)}
autoFocus
disabled={isGenerating}
/>
</div>
<div>
<label className="block text-xs font-bold text-kodo-content-dim uppercase mb-3">
Permissions (Scopes)
</label>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{SCOPES.map((scope) => (
<label
key={scope.id}
className="flex items-center gap-4 p-4 bg-kodo-ink rounded border border-kodo-steel cursor-pointer hover:border-kodo-steel transition-colors"
>
<input
type="checkbox"
className="rounded border-kodo-steel bg-transparent text-kodo-gold focus:ring-0"
checked={selectedScopes.includes(scope.id)}
onChange={() => toggleScope(scope.id)}
/>
<span className="text-sm text-kodo-text-main">
{scope.label}
</span>
</label>
))}
</div>
</div>
</div>
) : (
<div className="text-center space-y-6">
<div className="w-16 h-16 bg-kodo-lime/20 rounded-full flex items-center justify-center mx-auto text-kodo-lime">
<Check className="w-8 h-8" />
</div>
<div>
<h4 className="text-xl font-bold text-white mb-2">
Key Created Successfully
</h4>
<p className="text-sm text-kodo-content-dim max-w-xs mx-auto">
Please copy your API key now. For security reasons, you won't
be able to see it again.
</p>
</div>
<div className="bg-black border border-kodo-steel rounded-lg p-4 flex items-center gap-2 relative group">
<code className="text-kodo-gold font-mono text-sm flex-1 break-all">
{generatedKey}
</code>
<Button
variant="ghost"
size="icon"
onClick={copyKey}
className="hover:text-white"
>
<Copy className="w-4 h-4" />
</Button>
</div>
</div>
)}
</div>
<div className="p-4 border-t border-kodo-steel bg-kodo-ink flex justify-end gap-4">
{step === 1 ? (
<>
<Button
variant="ghost"
onClick={onClose}
disabled={isGenerating}
>
Cancel
</Button>
<Button
variant="primary"
onClick={handleGenerate}
disabled={isGenerating || !name.trim() || selectedScopes.length === 0}
>
{isGenerating ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Generating...
</>
) : (
'Generate Key'
)}
</Button>
</>
) : (
<Button variant="primary" onClick={onClose}>
Done
</Button>
)}
</div>
</div>
</div>
);
};