Nachgesehen, was bei Herabstufung, Rollenentzug, Rauswurf und Austritt passiert. Für Requests war alles abgedeckt: der Worker löscht die Sitzungen, ein Trigger verwirft die zwischengespeicherten Rechte, getCurrentUser prüft bei jedem Request Sitzungszeile, isActive und Mitgliedschaft nach. Die Zeile in der Datenbank ist der Nachweis, nicht das Cookie — und eine Zeile lässt sich zurückziehen. Eine Stelle fiel dabei durch: der Ereignisstrom stellt keine neuen Requests. Er prüfte einmal beim Verbinden und lief danach mit einer eingefrorenen Rechtekopie weiter. Wer mit offenem Tab hinausgeworfen wurde, bekam weiter Chatnachrichten zugestellt, mit vollem Text — jeder Seitenaufruf wäre abgemeldet gewesen, aber es machte ja keiner mehr einen. /api/stream löst Sitzung und Rechte jetzt bei jedem Ereignis und bei jedem Herzschlag neu auf. Ändern sich die Rechte, werden auch die erlaubten Themen neu bestimmt: ein Ereignis zu einem Verlaufseintrag trägt nur eine Kennung, das Thema ist dort die einzige Schranke. Ist die Sitzung weg, geht ein revoked hinaus und die Verbindung wird geschlossen — Schliessen allein genügt nicht, EventSource baut sofort wieder auf. Fünf Tests für die Web-Hälfte der Kette; die Discord-Hälfte deckte worker/tests/mitgliedschaft.test.ts schon ab.
76 lines
2.5 KiB
TypeScript
76 lines
2.5 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useRef } from 'react'
|
|
|
|
/**
|
|
* Eine Verbindung zum Ereignisstrom, so lange die Ansicht offen ist.
|
|
*
|
|
* `EventSource` baut nach einem Abriss von selbst wieder auf und schickt dabei
|
|
* die zuletzt gesehene Kennung mit, sodass der Server nachliefern kann. Das
|
|
* Aufräumen bei Tabwechsel übernimmt der Browser: eine Verbindung ohne
|
|
* Verkehr kostet nichts.
|
|
*
|
|
* Die Rückrufe stehen in einem Ref, damit ein neu erzeugter Handler nicht die
|
|
* ganze Verbindung neu aufbaut — sonst reisst der Strom bei jedem Tastendruck
|
|
* im Eingabefeld ab.
|
|
*/
|
|
export type StreamHandlers = Record<string, (data: unknown) => void>
|
|
|
|
export function useStream(topics: string[], handlers: StreamHandlers): void {
|
|
const aktuell = useRef(handlers)
|
|
aktuell.current = handlers
|
|
|
|
// Die Themen als Zeichenkette, damit ein neues Array mit gleichem Inhalt
|
|
// keine neue Verbindung auslöst.
|
|
const schluessel = topics.filter(Boolean).sort().join(',')
|
|
|
|
useEffect(() => {
|
|
if (!schluessel) return
|
|
|
|
const quelle = new EventSource(`/api/stream?topics=${encodeURIComponent(schluessel)}`)
|
|
const angemeldet: [string, EventListener][] = []
|
|
|
|
for (const name of Object.keys(aktuell.current)) {
|
|
const listener: EventListener = (event) => {
|
|
const nachricht = event as MessageEvent<string>
|
|
try {
|
|
aktuell.current[name]?.(JSON.parse(nachricht.data))
|
|
} catch {
|
|
// Kaputte Nutzlast ignorieren; der nächste Seitenaufbau hat den Stand.
|
|
}
|
|
}
|
|
quelle.addEventListener(name, listener)
|
|
angemeldet.push([name, listener])
|
|
}
|
|
|
|
// Der Server meldet einen Entzug ausdrücklich, statt die Verbindung nur
|
|
// fallen zu lassen: `EventSource` baut sonst sofort wieder auf, und man
|
|
// arbeitet auf einer Seite weiter, für die man keine Rechte mehr hat.
|
|
// Neu laden führt in die Anmeldung — dorthin, wo man jetzt hingehört.
|
|
const entzogen: EventListener = () => {
|
|
quelle.close()
|
|
window.location.reload()
|
|
}
|
|
quelle.addEventListener('revoked', entzogen)
|
|
angemeldet.push(['revoked', entzogen])
|
|
|
|
return () => {
|
|
for (const [name, listener] of angemeldet) quelle.removeEventListener(name, listener)
|
|
quelle.close()
|
|
}
|
|
// Absichtlich nur an den Themen: die Rückrufe hängen im Ref.
|
|
}, [schluessel])
|
|
}
|
|
|
|
export type ScanEvent = {
|
|
id: string
|
|
scanStatus: 'PENDING' | 'CLEAN' | 'BLOCKED' | 'FAILED'
|
|
scanMessage: string | null
|
|
mimeType: string
|
|
sizeBytes: number
|
|
blurhash: string | null
|
|
width: number | null
|
|
height: number | null
|
|
hasThumb: boolean
|
|
}
|