import { useEffect, useState, type FormEvent } from 'react' import { AlertTriangle, Check, Copy, KeyRound, Loader2, PenLine, Plus, Trash2 } from 'lucide-react' import { api, type NewToken, type TokenRow } from '../api' import { run, useStore } from '../store' import { bytes, zeit } from '../format' import { Feld, Karte, Knopf, Leer, Nachfrage, useKopieren } from '../components/Bausteine' import { TokenBearbeiten } from '../components/TokenBearbeiten' export function Tokens() { const note = useStore((s) => s.note) const [rows, setRows] = useState(null) const [laedt, setLaedt] = useState(true) const [name, setName] = useState('') const [prefix, setPrefix] = useState('') const [darfLoeschen, setDarfLoeschen] = useState(false) const [maxMb, setMaxMb] = useState('') const [quotaMb, setQuotaMb] = useState('') const [gueltigTage, setGueltigTage] = useState('') const [arten, setArten] = useState([]) const [legtAn, setLegtAn] = useState(false) const [frisch, setFrisch] = useState(null) const [weg, setWeg] = useState(null) const [bearbeiten, setBearbeiten] = useState(null) const [loescht, setLoescht] = useState(false) const [ohneToken, setOhneToken] = useState<{ dateien: number; bytes: number } | null>(null) const { kopiert, kopieren } = useKopieren() async function laden() { setLaedt(true) try { const antwort = await api.tokens() setRows(antwort.items) setOhneToken(antwort.ohneToken) } catch (err) { note('fehler', err instanceof Error ? err.message : 'Unbekannt') } finally { setLaedt(false) } } useEffect(() => { void laden() // eslint-disable-next-line react-hooks/exhaustive-deps }, []) async function anlegen(e: FormEvent) { e.preventDefault() setLegtAn(true) const neu = await run( () => api.createToken({ name: name.trim(), prefix: prefix.trim(), canDelete: darfLoeschen, maxMb: maxMb.trim(), quotaMb: quotaMb.trim(), arten, gueltigTage: gueltigTage.trim(), }), (t) => `Token "${t.name}" angelegt.`, ) setLegtAn(false) if (neu) { setFrisch(neu) setName('') setPrefix('') setDarfLoeschen(false) setMaxMb('') setGueltigTage('') setArten([]) void laden() } } async function loeschen() { if (!weg) return setLoescht(true) const ok = await run(() => api.deleteToken(weg.id), `Token "${weg.name}" gelöscht.`) setLoescht(false) setWeg(null) if (ok) void laden() } return (
setName(e.target.value)} /> setPrefix(e.target.value)} />
setMaxMb(e.target.value)} /> setQuotaMb(e.target.value)} /> setGueltigTage(e.target.value)} />
Erlaubte Arten (optional)
{( [ ['bild', 'Bilder'], ['video', 'Video'], ['ton', 'Ton'], ['dokument', 'Dokumente'], ['andere', 'Anderes'], ] as const ).map(([wert, text]) => { const an = arten.includes(wert) return ( ) })}
Nichts gewählt heißt: alles erlaubt. Ein Token, der nur Bilder darf, kann kein Video hochladen — auch wenn er abhandenkommt.
Anlegen } />
{laedt ? (
) : !rows || rows.length === 0 ? ( } text="Noch kein Token angelegt." /> ) : ( {/* Was mit dem Token wirklich abgelegt wurde. `token_id` steht seit dem ersten Tag in jeder Zeile und war bis hierhin NIRGENDS zu sehen — dabei ist es die Auskunft, die vor dem Löschen zählt. */} {rows.map((t) => ( ))}
Name Präfix Rechte GrenzenAbgelegt Angelegt Zuletzt benutzt
{t.name} {t.abgelaufen && ( abgelaufen )} {t.prefix ? `${t.prefix}/` : '— (überall)'} {t.can_delete ? ( schreiben + löschen ) : ( nur schreiben )} {[ t.max_bytes ? `max ${bytes(t.max_bytes)}/Datei` : null, t.quota_bytes ? `${bytes(t.bytes)} von ${bytes(t.quota_bytes)}` : null, t.arten ? t.arten.replaceAll(',', ', ') : null, t.expires_at ? `bis ${zeit(t.expires_at).slice(0, 10)}` : null, ] .filter(Boolean) .join(' · ') || '—'} {t.dateien === 0 ? ( ) : ( {t.dateien} {' '} · {bytes(t.bytes)} )} {zeit(t.created_at)} {zeit(t.last_used_at)}
)} {ohneToken && ohneToken.dateien > 0 && (

Dazu kommen {ohneToken.dateien}{' '} Dateien ({bytes(ohneToken.bytes)}) ohne Token — aus dem Dashboard hochgeladen. Dort gibt es eine Sitzung und keinen Token; ohne diese Zeile ginge die Summe nicht auf.

)}
{/* Der Klartext — genau einmal. Deshalb ein Dialog, den man bewusst schließen muss, und kein Zettel, der nach vier Sekunden verschwindet. */} {frisch && (

Token „{frisch.name}" angelegt

Dies ist das einzige Mal, dass der Token im Klartext zu sehen ist. Danach steht nur noch sein Hash in der Datenbank — auch der Dienst kann ihn nicht mehr zeigen.

{frisch.token} { const ok = await kopieren(frisch.token, 'neu') if (!ok) note( 'fehler', 'Die Zwischenablage ist gesperrt — bitte von Hand markieren.', ) }} kind={ kopiert === 'neu' ? ( <> kopiert ) : ( <> Kopieren ) } />

Präfix: {frisch.prefix || '—'}{' '} · Löschen: {frisch.canDelete ? 'erlaubt' : 'nicht erlaubt'}

setFrisch(null)} kind="Habe ich mir notiert" />
)} {bearbeiten && ( setBearbeiten(null)} onFertig={() => { setBearbeiten(null) void laden() }} /> )} setWeg(null)} onJa={() => void loeschen()} text={ <>

„{weg?.name}" wird sofort ungültig. Was ihn benutzt — etwa der Fotostudio — bekommt ab dann 401 und lädt nichts mehr hoch.

Hochgeladene Dateien bleiben.

} />
) }