import React, { useEffect, useState } from 'react' import { KeyRound, Plus, Trash2, Copy, Check } from 'lucide-react' import Section from './Section' import { useTranslation } from '../../i18n' import { useToast } from '../shared/Toast' import { authApi } from '../../api/client' /** * Keys for the public API — the credential a user hands to other software that * should read their trips. * * Its own section rather than a third tab under MCP: an API key is not an MCP * credential, it does not need the MCP addon, and burying it under a heading * about AI assistants is how people end up minting the wrong kind. The two look * alike deliberately (name, prefix, shown once) because they are the same * gesture; what differs is which door they open. * * State lives here rather than in the tab's shared hook so the section works on * an instance with MCP switched off. */ interface ApiKey { id: number name: string token_prefix: string created_at: string last_used_at: string | null } export default function ApiKeysSection(): React.ReactElement { const { t, locale } = useTranslation() const toast = useToast() const [keys, setKeys] = useState([]) const [modalOpen, setModalOpen] = useState(false) const [newName, setNewName] = useState('') const [creating, setCreating] = useState(false) /** The raw key, held only until the modal closes — the server keeps a hash. */ const [created, setCreated] = useState(null) const [deleteId, setDeleteId] = useState(null) const [copied, setCopied] = useState(false) useEffect(() => { authApi.apiKeys.list().then(d => setKeys(d.tokens || [])).catch(() => {}) }, []) const handleCreate = async () => { if (!newName.trim() || creating) return setCreating(true) try { const d = await authApi.apiKeys.create(newName.trim()) setCreated(d.token.raw_token) setKeys(prev => [ { id: d.token.id, name: d.token.name, token_prefix: d.token.token_prefix, created_at: d.token.created_at, last_used_at: null }, ...prev, ]) setNewName('') } catch { toast.error(t('settings.apiKeys.createFailed')) } finally { setCreating(false) } } const handleDelete = async (id: number) => { try { await authApi.apiKeys.delete(id) setKeys(prev => prev.filter(k => k.id !== id)) toast.success(t('settings.apiKeys.deleted')) } catch { toast.error(t('settings.apiKeys.deleteFailed')) } finally { setDeleteId(null) } } const handleCopy = () => { if (!created) return navigator.clipboard.writeText(created) setCopied(true) setTimeout(() => setCopied(false), 2000) } const closeModal = () => { setModalOpen(false) setCreated(null) setNewName('') } return ( <>

{t('settings.apiKeys.description')}

{keys.length === 0 ? (

{t('settings.apiKeys.empty')}

) : (
{keys.map((key, i) => (

{key.name}

{key.token_prefix}... {t('settings.apiKeys.createdAt')} {new Date(key.created_at).toLocaleDateString(locale)} {key.last_used_at && ( · {t('settings.apiKeys.usedAt')} {new Date(key.last_used_at).toLocaleDateString(locale)} )}

))}
)}

{t('settings.apiKeys.docsHint')}

{modalOpen && (
{ if (e.target === e.currentTarget && !created) closeModal() }}>
{!created ? ( <>

{t('settings.apiKeys.modal.createTitle')}

setNewName(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleCreate()} placeholder={t('settings.apiKeys.modal.namePlaceholder')} className="w-full px-3 py-2.5 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-slate-400 border-edge bg-surface-secondary text-content" autoFocus />

{t('settings.apiKeys.modal.nameHint')}

) : ( <>

{t('settings.apiKeys.modal.createdTitle')}

⚠

{t('settings.apiKeys.modal.createdWarning')}

                    {created}
                  
)}
)} {deleteId !== null && (
{ if (e.target === e.currentTarget) setDeleteId(null) }}>

{t('settings.apiKeys.deleteTitle')}

{t('settings.apiKeys.deleteMessage')}

)} ) }