/** * TokensSection — personal access token CRUD * * Create, list, and revoke PATs for programmatic API access. * Token value is shown once on creation; stored as SHA-256 hash server-side. */ const { html } = window; const { useState, useEffect, useCallback } = hooks; function esc(s) { return s == null ? '' : String(s); } export function TokensSection() { const [tokens, setTokens] = useState(null); const [availablePerms, setAvailablePerms] = useState([]); const [showForm, setShowForm] = useState(false); const [createdToken, setCreatedToken] = useState(null); const [form, setForm] = useState({ name: '', permissions: [], expires_at: '' }); const [saving, setSaving] = useState(false); const [copied, setCopied] = useState(false); const load = useCallback(async () => { try { const resp = await sw.api.get('/api/v1/auth/tokens'); setTokens(resp || []); } catch (e) { setTokens([]); } }, []); const loadPerms = useCallback(async () => { try { const resp = await sw.api.get('/api/v1/profile/permissions'); setAvailablePerms(resp?.permissions || []); } catch (_) {} }, []); useEffect(() => { load(); loadPerms(); }, [load, loadPerms]); const openCreate = useCallback(() => { setCreatedToken(null); setForm({ name: '', permissions: [], expires_at: '' }); setShowForm(true); }, []); const cancel = useCallback(() => { setShowForm(false); setCreatedToken(null); }, []); const togglePerm = useCallback((perm) => { setForm(f => { const perms = f.permissions.includes(perm) ? f.permissions.filter(p => p !== perm) : [...f.permissions, perm]; return { ...f, permissions: perms }; }); }, []); const create = useCallback(async () => { if (!form.name) { sw.emit('toast', { message: 'Token name is required', variant: 'error' }); return; } setSaving(true); try { const body = { name: form.name, permissions: form.permissions }; if (form.expires_at) body.expires_at = new Date(form.expires_at).toISOString(); const resp = await sw.api.post('/api/v1/auth/tokens', body); setCreatedToken(resp.token); setShowForm(false); load(); sw.emit('toast', { message: 'Token created', variant: 'success' }); } catch (e) { sw.emit('toast', { message: e.message || 'Failed to create token', variant: 'error' }); } finally { setSaving(false); } }, [form, load]); const revoke = useCallback(async (id, name) => { if (!await sw.confirm(`Revoke token "${esc(name)}"? This action cannot be undone.`)) return; try { await sw.api.del(`/api/v1/auth/tokens/${id}`); sw.emit('toast', { message: 'Token revoked', variant: 'success' }); load(); } catch (e) { sw.emit('toast', { message: e.message || 'Failed to revoke', variant: 'error' }); } }, [load]); const copyToken = useCallback(async () => { if (!createdToken) return; try { await navigator.clipboard.writeText(createdToken); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch (_) { sw.emit('toast', { message: 'Copy failed — select and copy manually', variant: 'error' }); } }, [createdToken]); const dismissToken = useCallback(() => { setCreatedToken(null); }, []); // ── Render ────────────────────────────── if (tokens === null) return html`

Loading…

`; return html`
${createdToken && html`
Token created — copy it now. It won't be shown again.
${createdToken}
`}

Personal access tokens provide programmatic API access. Treat tokens like passwords.

${showForm && html`
setForm(f => ({ ...f, name: e.target.value }))} />
setForm(f => ({ ...f, expires_at: e.target.value }))} />

Select the permissions this token should have. Leave empty for read-only access.

${availablePerms.map(p => html` `)}
`} ${tokens.length === 0 && !showForm && html`

No tokens yet

Create a personal access token for CI, scripts, or API integration.

`} ${tokens.length > 0 && html` ${tokens.map(t => html` `)}
Name Token Permissions Expires Last Used
${esc(t.name)} arm_pat_${esc(t.prefix)}… ${(t.permissions || []).length === 0 ? html`none` : (t.permissions || []).map(p => html` ${p} `) } ${t.expires_at ? new Date(t.expires_at).toLocaleDateString() : '—'} ${t.last_used_at ? new Date(t.last_used_at).toLocaleDateString() : 'Never'}
`}
`; } export default TokensSection;