import { useState, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; import { Skeleton } from '@/components/ui/skeleton'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog'; import { toast } from 'sonner'; import { apiFetch } from '@/lib/api'; import { TeamProGate } from './TeamProGate'; import { TierBadge } from './TierBadge'; import { Zap, Plus, Copy, Trash2, CheckCircle, RefreshCw, Clock } from 'lucide-react'; interface ApiTokenListItem { id: number; name: string; scope: string; created_at: number; last_used_at: number | null; expires_at: number | null; revoked_at: number | null; } const SCOPE_LABELS: Record = { 'read-only': 'Read Only', 'deploy-only': 'Deploy Only', 'full-admin': 'Full Admin', }; const SCOPE_BADGE_VARIANT: Record = { 'read-only': 'default', 'deploy-only': 'secondary', 'full-admin': 'destructive', }; function formatDate(ts: number): string { return new Date(ts).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); } function formatRelative(ts: number): string { const diff = Date.now() - ts; const mins = Math.floor(diff / 60000); if (mins < 1) return 'Just now'; if (mins < 60) return `${mins}m ago`; const hours = Math.floor(mins / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); if (days < 30) return `${days}d ago`; return formatDate(ts); } export function ApiTokensSection() { const [tokens, setTokens] = useState([]); const [loading, setLoading] = useState(true); const [creating, setCreating] = useState(false); const [showForm, setShowForm] = useState(false); const [newToken, setNewToken] = useState<{ id: number; token: string } | null>(null); const [formName, setFormName] = useState(''); const [formScope, setFormScope] = useState('read-only'); const [formExpiry, setFormExpiry] = useState(null); const fetchTokens = async () => { try { const res = await apiFetch('/api-tokens', { localOnly: true }); if (res.ok) { const data: ApiTokenListItem[] = await res.json(); setTokens(data.filter(t => !t.revoked_at)); } } catch { toast.error('Failed to load API tokens.'); } finally { setLoading(false); } }; // eslint-disable-next-line react-hooks/set-state-in-effect useEffect(() => { fetchTokens(); }, []); const handleCreate = async () => { if (!formName.trim()) { toast.error('Token name is required.'); return; } setCreating(true); try { const res = await apiFetch('/api-tokens', { method: 'POST', localOnly: true, body: JSON.stringify({ name: formName.trim(), scope: formScope, expires_in: formExpiry }), }); if (res.ok) { const data = await res.json(); setNewToken({ id: data.id, token: data.token }); setShowForm(false); setFormName(''); setFormScope('read-only'); setFormExpiry(null); fetchTokens(); toast.success('API token created.'); } else { const err = await res.json().catch(() => ({})); toast.error(err?.error || err?.message || 'Failed to create token.'); } } catch (e: unknown) { toast.error((e as Error)?.message || 'Network error.'); } finally { setCreating(false); } }; const handleRevoke = async (id: number) => { try { const res = await apiFetch(`/api-tokens/${id}`, { method: 'DELETE', localOnly: true }); if (res.ok) { toast.success('API token revoked.'); fetchTokens(); } else { const err = await res.json().catch(() => ({})); toast.error(err?.error || err?.message || 'Failed to revoke token.'); } } catch { toast.error('Network error.'); } }; const copyToClipboard = (text: string, label: string) => { navigator.clipboard.writeText(text); toast.success(`${label} copied to clipboard.`); }; return (

API Tokens

Generate scoped tokens for CI/CD pipelines, scripts, and automation.

{/* Create form */} {showForm && (
setFormName(e.target.value)} />
)} {/* Token reveal (shown once after creation) */} {newToken && (
Token created - copy it now

This token will not be shown again. Store it securely.

{newToken.token}
)} {/* Loading state */} {loading && (
)} {/* Empty state */} {!loading && tokens.length === 0 && !showForm && (

No API tokens yet.

Create one to authenticate CI/CD pipelines and scripts.

)} {/* Token list */} {!loading && tokens.map(token => (
{token.name} {SCOPE_LABELS[token.scope] || token.scope}
Revoke API token? Revoking {token.name} will immediately invalidate it. Any pipelines or scripts using this token will stop working. Cancel handleRevoke(token.id)} className="bg-destructive text-destructive-foreground hover:bg-destructive/90"> Revoke
Created {formatDate(token.created_at)} Last used: {token.last_used_at ? formatRelative(token.last_used_at) : 'Never'} {token.expires_at && ( {token.expires_at < Date.now() ? 'Expired' : `Expires ${formatDate(token.expires_at)}`} )}
))}
); }