2026-01-07 09:31:02 +00:00
|
|
|
import React, { useState } from 'react';
|
|
|
|
|
import { Button } from '../../ui/button';
|
|
|
|
|
import { Input } from '../../ui/input';
|
2026-01-18 13:46:16 +00:00
|
|
|
import { X, Key, Copy, Check, Loader2 } from 'lucide-react';
|
2026-01-18 12:54:32 +00:00
|
|
|
import { useToast } from '@/hooks/useToast';
|
2026-01-07 09:31:02 +00:00
|
|
|
|
|
|
|
|
interface CreateAPIKeyModalProps {
|
|
|
|
|
onClose: () => void;
|
2026-01-18 13:46:16 +00:00
|
|
|
onCreate: (keyData: { name: string; scopes: string[] }) => Promise<{ key?: string; id: string; name: string; prefix: string }>;
|
2026-01-07 09:31:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const SCOPES = [
|
2026-01-13 18:47:57 +00:00
|
|
|
{ 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' },
|
2026-01-07 09:31:02 +00:00
|
|
|
];
|
|
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
export const CreateAPIKeyModal: React.FC<CreateAPIKeyModalProps> = ({
|
|
|
|
|
onClose,
|
|
|
|
|
onCreate,
|
|
|
|
|
}) => {
|
2026-01-18 12:54:32 +00:00
|
|
|
const toast = useToast();
|
2026-01-07 09:31:02 +00:00
|
|
|
const [step, setStep] = useState(1);
|
|
|
|
|
const [name, setName] = useState('');
|
|
|
|
|
const [selectedScopes, setSelectedScopes] = useState<string[]>(['user.read']);
|
|
|
|
|
const [generatedKey, setGeneratedKey] = useState('');
|
2026-01-18 13:46:16 +00:00
|
|
|
const [isGenerating, setIsGenerating] = useState(false);
|
2026-01-07 09:31:02 +00:00
|
|
|
|
|
|
|
|
const toggleScope = (id: string) => {
|
2026-01-13 18:47:57 +00:00
|
|
|
setSelectedScopes((prev) =>
|
|
|
|
|
prev.includes(id) ? prev.filter((s) => s !== id) : [...prev, id],
|
|
|
|
|
);
|
2026-01-07 09:31:02 +00:00
|
|
|
};
|
|
|
|
|
|
2026-01-18 13:46:16 +00:00
|
|
|
const handleGenerate = async () => {
|
|
|
|
|
// Validate form
|
|
|
|
|
if (!name.trim()) {
|
|
|
|
|
toast.error('Please enter a name for your API key');
|
2026-01-13 18:47:57 +00:00
|
|
|
return;
|
|
|
|
|
}
|
2026-01-18 13:46:16 +00:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
2026-01-07 09:31:02 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const copyKey = () => {
|
2026-01-13 18:47:57 +00:00
|
|
|
navigator.clipboard.writeText(generatedKey);
|
2026-01-18 12:54:32 +00:00
|
|
|
toast.success('API Key copied to clipboard');
|
2026-01-07 09:31:02 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
|
2026-01-13 18:47:57 +00:00
|
|
|
<div
|
|
|
|
|
className="absolute inset-0 bg-kodo-void/90 backdrop-blur-sm"
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
></div>
|
2026-01-07 09:31:02 +00:00
|
|
|
<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">
|
2026-01-13 18:47:57 +00:00
|
|
|
<Key className="w-4 h-4 text-kodo-gold" />{' '}
|
|
|
|
|
{step === 1 ? 'Create API Key' : 'API Key Generated'}
|
2026-01-07 09:31:02 +00:00
|
|
|
</h3>
|
2026-01-13 18:47:57 +00:00
|
|
|
<button onClick={onClose}>
|
2026-01-16 00:59:31 +00:00
|
|
|
<X className="w-5 h-5 text-kodo-content-dim hover:text-white" />
|
2026-01-13 18:47:57 +00:00
|
|
|
</button>
|
2026-01-07 09:31:02 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="p-6">
|
2026-01-13 18:47:57 +00:00
|
|
|
{step === 1 ? (
|
|
|
|
|
<div className="space-y-6">
|
2026-01-18 13:46:16 +00:00
|
|
|
<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>
|
2026-01-13 18:47:57 +00:00
|
|
|
|
|
|
|
|
<div>
|
2026-01-16 00:59:31 +00:00
|
|
|
<label className="block text-xs font-bold text-kodo-content-dim uppercase mb-3">
|
2026-01-13 18:47:57 +00:00
|
|
|
Permissions (Scopes)
|
|
|
|
|
</label>
|
aesthetic-improvements: align spacing to 8px grid (Action 11.2.1.3)
- Created automated script (scripts/align-8px-grid.py) to align all spacing to 8px grid
- Replaced non-8px-aligned spacing: gap-3/p-3/m-3 (12px) → gap-4/p-4/m-4 (16px), gap-5/p-5/m-5 (20px) → gap-6/p-6/m-6 (24px), gap-10/p-10/m-10 (40px) → gap-12/p-12/m-12 (48px), gap-20/p-20/m-20 (80px) → gap-24/p-24/m-24 (96px)
- Preserved: 4px values (gap-1, p-1, m-1) as they may be intentional fine-tuning, responsive breakpoints (sm:, md:, lg:), test files, documentation
- Modified files across all components to ensure consistent 8px grid alignment
- Action 11.2.1.3: Align all elements to 8px grid - COMPLETE
2026-01-16 10:50:46 +00:00
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
2026-01-13 18:47:57 +00:00
|
|
|
{SCOPES.map((scope) => (
|
|
|
|
|
<label
|
|
|
|
|
key={scope.id}
|
aesthetic-improvements: align spacing to 8px grid (Action 11.2.1.3)
- Created automated script (scripts/align-8px-grid.py) to align all spacing to 8px grid
- Replaced non-8px-aligned spacing: gap-3/p-3/m-3 (12px) → gap-4/p-4/m-4 (16px), gap-5/p-5/m-5 (20px) → gap-6/p-6/m-6 (24px), gap-10/p-10/m-10 (40px) → gap-12/p-12/m-12 (48px), gap-20/p-20/m-20 (80px) → gap-24/p-24/m-24 (96px)
- Preserved: 4px values (gap-1, p-1, m-1) as they may be intentional fine-tuning, responsive breakpoints (sm:, md:, lg:), test files, documentation
- Modified files across all components to ensure consistent 8px grid alignment
- Action 11.2.1.3: Align all elements to 8px grid - COMPLETE
2026-01-16 10:50:46 +00:00
|
|
|
className="flex items-center gap-4 p-4 bg-kodo-ink rounded border border-kodo-steel cursor-pointer hover:border-kodo-steel transition-colors"
|
2026-01-13 18:47:57 +00:00
|
|
|
>
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
2026-01-16 00:59:31 +00:00
|
|
|
className="rounded border-kodo-steel bg-transparent text-kodo-gold focus:ring-0"
|
2026-01-13 18:47:57 +00:00
|
|
|
checked={selectedScopes.includes(scope.id)}
|
|
|
|
|
onChange={() => toggleScope(scope.id)}
|
|
|
|
|
/>
|
2026-01-16 00:59:31 +00:00
|
|
|
<span className="text-sm text-kodo-text-main">
|
2026-01-13 18:47:57 +00:00
|
|
|
{scope.label}
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
))}
|
2026-01-07 09:31:02 +00:00
|
|
|
</div>
|
2026-01-13 18:47:57 +00:00
|
|
|
</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>
|
2026-01-16 00:59:31 +00:00
|
|
|
<p className="text-sm text-kodo-content-dim max-w-xs mx-auto">
|
2026-01-13 18:47:57 +00:00
|
|
|
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>
|
|
|
|
|
)}
|
2026-01-07 09:31:02 +00:00
|
|
|
</div>
|
|
|
|
|
|
aesthetic-improvements: align spacing to 8px grid (Action 11.2.1.3)
- Created automated script (scripts/align-8px-grid.py) to align all spacing to 8px grid
- Replaced non-8px-aligned spacing: gap-3/p-3/m-3 (12px) → gap-4/p-4/m-4 (16px), gap-5/p-5/m-5 (20px) → gap-6/p-6/m-6 (24px), gap-10/p-10/m-10 (40px) → gap-12/p-12/m-12 (48px), gap-20/p-20/m-20 (80px) → gap-24/p-24/m-24 (96px)
- Preserved: 4px values (gap-1, p-1, m-1) as they may be intentional fine-tuning, responsive breakpoints (sm:, md:, lg:), test files, documentation
- Modified files across all components to ensure consistent 8px grid alignment
- Action 11.2.1.3: Align all elements to 8px grid - COMPLETE
2026-01-16 10:50:46 +00:00
|
|
|
<div className="p-4 border-t border-kodo-steel bg-kodo-ink flex justify-end gap-4">
|
2026-01-13 18:47:57 +00:00
|
|
|
{step === 1 ? (
|
|
|
|
|
<>
|
2026-01-18 13:46:16 +00:00
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
disabled={isGenerating}
|
|
|
|
|
>
|
2026-01-13 18:47:57 +00:00
|
|
|
Cancel
|
|
|
|
|
</Button>
|
2026-01-18 13:46:16 +00:00
|
|
|
<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'
|
|
|
|
|
)}
|
2026-01-13 18:47:57 +00:00
|
|
|
</Button>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<Button variant="primary" onClick={onClose}>
|
|
|
|
|
Done
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
2026-01-07 09:31:02 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|