import type { ApiTokenCreatedView, ApiTokenView, PondView } from '@dorfteich/shared'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { FormError } from '../components/forms'; import { apiDelete, apiGet, apiPost } from '../lib/api'; const TOKENS_QUERY_KEY = ['users', 'me', 'api-tokens'] as const; /** * Personal-access-token management in the user settings (issue #104): * create (scope, optional expiry, optional pond restriction), the one-time * secret reveal, and the list with revoke. */ export function ApiTokensSection(): React.JSX.Element { const { t } = useTranslation('apiTokens'); const tokens = useQuery({ queryKey: TOKENS_QUERY_KEY, queryFn: () => apiGet('/users/me/api-tokens'), }); return (

{t('section.title')}

{t('section.intro')}

{tokens.data && tokens.data.length === 0 &&

{t('section.empty')}

} {tokens.data && tokens.data.length > 0 && }
); } function CreateTokenForm(): React.JSX.Element { const { t } = useTranslation('apiTokens'); const queryClient = useQueryClient(); const [name, setName] = useState(''); const [scope, setScope] = useState<'read' | 'write'>('read'); const [expiresAt, setExpiresAt] = useState(''); const [pondIds, setPondIds] = useState([]); const [created, setCreated] = useState(null); const [copied, setCopied] = useState(false); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); const ponds = useQuery({ queryKey: ['ponds'], queryFn: () => apiGet('/ponds'), }); const submit = async (event: React.FormEvent): Promise => { event.preventDefault(); setError(null); setCreated(null); setCopied(false); setBusy(true); try { const view = await apiPost('/users/me/api-tokens', { name, scope, expiresAt: expiresAt ? new Date(expiresAt).toISOString() : null, pondIds, }); setCreated(view); setName(''); setExpiresAt(''); setPondIds([]); await queryClient.invalidateQueries({ queryKey: TOKENS_QUERY_KEY }); } catch (err) { setError(err); } finally { setBusy(false); } }; return (
void submit(event)}>
{t('fields.ponds')}

{t('fields.pondsHint')}

{(ponds.data ?? []).map((pond) => ( ))}
{created && (

{t('create.createdTitle')}

{t('create.createdHint')}

{created.token}
)} ); } function TokenList({ tokens }: { tokens: ApiTokenView[] }): React.JSX.Element { const { t } = useTranslation('apiTokens'); const queryClient = useQueryClient(); const [error, setError] = useState(null); const revoke = async (id: string): Promise => { if (!window.confirm(t('list.revokeConfirm'))) return; setError(null); try { await apiDelete(`/users/me/api-tokens/${id}`); await queryClient.invalidateQueries({ queryKey: TOKENS_QUERY_KEY }); } catch (err) { setError(err); } }; const statusOf = (token: ApiTokenView): 'active' | 'revoked' | 'expired' => { if (token.revokedAt) return 'revoked'; if (token.expiresAt && new Date(token.expiresAt).getTime() <= Date.now()) return 'expired'; return 'active'; }; return ( <> {tokens.map((token) => ( ))}
{t('fields.name')} {t('fields.scope')} {t('fields.ponds')} {t('list.created')} {t('list.lastUsed')} {t('list.expires')} {t('list.status')} {t('common:tableActions')}
{token.name} {token.scope === 'write' ? t('fields.scopeWrite') : t('fields.scopeRead')} {token.ponds.length === 0 ? t('list.allPonds') : token.ponds.map((pond) => pond.name).join(', ')} {new Date(token.createdAt).toLocaleDateString()} {token.lastUsedAt ? new Date(token.lastUsedAt).toLocaleString() : t('list.never')} {token.expiresAt ? new Date(token.expiresAt).toLocaleDateString() : '—'} {t(`list.${statusOf(token)}`)} {!token.revokedAt && ( )}
); }