import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Key, Plus, Copy, Check, Trash2, Eye, EyeOff, Clock, Shield, AlertTriangle, ExternalLink, ChevronDown, ChevronUp, X, } from 'lucide-react'; import { useApiTokens, useCreateApiToken, useRevokeApiToken, useUpdateApiToken, API_SCOPES, SCOPE_PRESETS, APIToken, APITokenCreateResponse, } from '../hooks/useApiTokens'; interface NewTokenModalProps { isOpen: boolean; onClose: () => void; onTokenCreated: (token: APITokenCreateResponse) => void; } const NewTokenModal: React.FC = ({ isOpen, onClose, onTokenCreated }) => { const { t } = useTranslation(); const [name, setName] = useState(''); const [selectedScopes, setSelectedScopes] = useState([]); const [expiresIn, setExpiresIn] = useState('never'); const [showAdvanced, setShowAdvanced] = useState(false); const createMutation = useCreateApiToken(); const handlePresetSelect = (presetKey: keyof typeof SCOPE_PRESETS) => { setSelectedScopes(SCOPE_PRESETS[presetKey].scopes); }; const toggleScope = (scope: string) => { setSelectedScopes(prev => prev.includes(scope) ? prev.filter(s => s !== scope) : [...prev, scope] ); }; const calculateExpiryDate = (): string | null => { if (expiresIn === 'never') return null; const now = new Date(); switch (expiresIn) { case '7d': now.setDate(now.getDate() + 7); break; case '30d': now.setDate(now.getDate() + 30); break; case '90d': now.setDate(now.getDate() + 90); break; case '1y': now.setFullYear(now.getFullYear() + 1); break; default: return null; } return now.toISOString(); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!name.trim() || selectedScopes.length === 0) return; try { const result = await createMutation.mutateAsync({ name: name.trim(), scopes: selectedScopes, expires_at: calculateExpiryDate(), }); onTokenCreated(result); setName(''); setSelectedScopes([]); setExpiresIn('never'); } catch (error) { console.error('Failed to create token:', error); } }; if (!isOpen) return null; return (

Create API Token

{/* Token Name */}
setName(e.target.value)} placeholder="e.g., Website Integration, Mobile App" className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-transparent" required />

Choose a descriptive name to identify this token's purpose

{/* Scope Presets */}
{Object.entries(SCOPE_PRESETS).map(([key, preset]) => ( ))}
{/* Advanced: Individual Scopes */}
{showAdvanced && (
{API_SCOPES.map((scope) => ( ))}
)}
{/* Expiration */}
{/* Selected Scopes Summary */} {selectedScopes.length > 0 && (
Selected permissions ({selectedScopes.length})
{selectedScopes.map((scope) => ( {scope} ))}
)} {/* Actions */}
); }; interface TokenCreatedModalProps { token: APITokenCreateResponse | null; onClose: () => void; } const TokenCreatedModal: React.FC = ({ token, onClose }) => { const [copied, setCopied] = useState(false); const [showToken, setShowToken] = useState(false); const handleCopy = () => { if (token) { navigator.clipboard.writeText(token.key); setCopied(true); setTimeout(() => setCopied(false), 2000); } }; if (!token) return null; return (

Token Created

{token.name}

Important: Copy your token now. You won't be able to see it again!
); }; interface TokenRowProps { token: APIToken; onRevoke: (id: string, name: string) => void; isRevoking: boolean; } const TokenRow: React.FC = ({ token, onRevoke, isRevoking }) => { const [expanded, setExpanded] = useState(false); const formatDate = (dateString: string | null) => { if (!dateString) return 'Never'; return new Date(dateString).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', }); }; const isExpired = token.expires_at && new Date(token.expires_at) < new Date(); return (
{token.name} {(!token.is_active || isExpired) && ( {isExpired ? 'Expired' : 'Revoked'} )}
{token.key_prefix}••••••••
{token.is_active && !isExpired && ( )}
{expanded && (
Created
{formatDate(token.created_at)}
Last Used
{formatDate(token.last_used_at)}
Expires
{formatDate(token.expires_at)}
{token.created_by && (
Created By
{token.created_by.full_name || token.created_by.username}
)}
Permissions
{token.scopes.map((scope) => ( {scope} ))}
)}
); }; const ApiTokensSection: React.FC = () => { const { t } = useTranslation(); const { data: tokens, isLoading, error } = useApiTokens(); const revokeMutation = useRevokeApiToken(); const [showNewTokenModal, setShowNewTokenModal] = useState(false); const [createdToken, setCreatedToken] = useState(null); const [tokenToRevoke, setTokenToRevoke] = useState<{ id: string; name: string } | null>(null); const handleTokenCreated = (token: APITokenCreateResponse) => { setShowNewTokenModal(false); setCreatedToken(token); }; const handleRevokeClick = (id: string, name: string) => { setTokenToRevoke({ id, name }); }; const confirmRevoke = async () => { if (!tokenToRevoke) return; setTokenToRevoke(null); await revokeMutation.mutateAsync(tokenToRevoke.id); }; const activeTokens = tokens?.filter(t => t.is_active) || []; const revokedTokens = tokens?.filter(t => !t.is_active) || []; return ( <> {/* Revoke Confirmation Modal */} {tokenToRevoke && (

Revoke API Token?

Are you sure you want to revoke {tokenToRevoke.name}?

This action cannot be undone. Applications using this token will immediately lose access.

)}

API Tokens

Create and manage API tokens for third-party integrations

API Docs
{isLoading ? (
) : error ? (

Failed to load API tokens. Please try again later.

) : tokens && tokens.length === 0 ? (

No API tokens yet

Create your first API token to start integrating with external services and applications.

) : (
{activeTokens.length > 0 && (

Active Tokens ({activeTokens.length})

{activeTokens.map((token) => ( ))}
)} {revokedTokens.length > 0 && (

Revoked Tokens ({revokedTokens.length})

{revokedTokens.map((token) => ( ))}
)}
)}
{/* Modals */} setShowNewTokenModal(false)} onTokenCreated={handleTokenCreated} /> setCreatedToken(null)} /> ); }; export default ApiTokensSection;