import { useCallback, useEffect, useState } from 'react'; interface TokenView { id: string; label: string; prefix: string; createdAt: string; } interface CreatedToken extends TokenView { token: string; // plaintext, surfaced only once } function formatDate(iso: string): string { try { return new Date(iso).toLocaleString(); } catch { return iso; } } export function WebdavTokens() { const [tokens, setTokens] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [newLabel, setNewLabel] = useState(''); const [creating, setCreating] = useState(false); const [justCreated, setJustCreated] = useState(null); const [copied, setCopied] = useState(false); const refresh = useCallback(async () => { try { const res = await fetch('/api/webdav-tokens'); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); setTokens(data.tokens ?? []); setError(null); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setLoading(false); } }, []); useEffect(() => { refresh(); }, [refresh]); const create = async () => { setCreating(true); try { const res = await fetch('/api/webdav-tokens', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ label: newLabel }), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const created: CreatedToken = await res.json(); setJustCreated(created); setNewLabel(''); setCopied(false); await refresh(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setCreating(false); } }; const revoke = async (id: string, label: string) => { if (!confirm(`Revoke token "${label}"? Devices using it will lose WebDAV access immediately.`)) return; try { const res = await fetch(`/api/webdav-tokens/${id}`, { method: 'DELETE' }); if (!res.ok && res.status !== 204) throw new Error(`HTTP ${res.status}`); await refresh(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } }; const copyToken = async () => { if (!justCreated) return; try { await navigator.clipboard.writeText(justCreated.token); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { // Clipboard API may be unavailable on http; fall back silently. } }; if (loading) return
Loading tokens…
; return (

Generate one token per device and paste it as the basic-auth password when you add the WebDAV mount in Plex / Finder / Windows Explorer / etc. Any username works — the password is what authenticates. Revoke a token to cut off that single device without affecting the others.

{error &&
{error}
} {justCreated && (

New token — copy it now

This is the only time the full token will be shown. After you close this banner only the prefix will appear in the list.

{justCreated.token}
)}
Generate token
setNewLabel(e.target.value)} placeholder="Label (e.g. 'Plex on living-room TV')" maxLength={200} style={{ flex: 1, padding: '0.5rem' }} onKeyDown={(e) => { if (e.key === 'Enter' && !creating) create(); }} />
{tokens.length === 0 ? (
No tokens yet — generate one to enable WebDAV access.
) : ( tokens.map((t) => (
{t.label}
mfwd_{t.prefix}… · created {formatDate(t.createdAt)}
)) )}
); }