Commit Graph
8 Commits
Author SHA1 Message Date
Florian Wilde 4046b72626 Erscheinungsbild im Admin-Bereich anpassbar
Name, Zusatz in der Fusszeile, Akzentfarbe, Logo und Favicon lassen sich
unter Verwaltung -> Erscheinungsbild einstellen. Sie gelten fuer die ganze
Installation und liegen in einer eigenen Tabelle mit genau einer Zeile.

Zur Farbe: Tailwind 4 uebersetzt bg-indigo-600 zu
background-color: var(--color-indigo-600). Die Anwendung schreibt diese
Variablen zur Laufzeit neu — damit folgen alle rund 60 eingefaerbten Stellen
der gewaehlten Farbe, ohne dass eine einzige Klasse angefasst werden muss.

Uebernommen werden nur Farbton und Saettigung, nicht die Helligkeit. Die
Helligkeitskurve stammt aus der Tailwind-Vorlage. Sonst wuerde ein helles
Gelb die Kopfzeile unlesbar machen, denn auf Stufe 600 steht weisse Schrift.
Die Umrechnung laeuft ueber OKLCH und trifft Tailwinds eigenes Indigo exakt.

Logo und Favicon sind Uploads mit eigener Groessengrenze (2 MB bzw. 512 KB)
und eigener Rechtepruefung: instance:manage statt codes:create. Uploads haben
jetzt ein Feld "purpose" — sonst haette die Aufraeumroutine fuer herrenlose
Dateien Logo und Favicon nach einem Tag geloescht. Ein ersetztes Bild wird
gelaescht, und eine Datei aus einem QR-Code laesst sich nicht als Logo
einhaengen.

Auf den Landingpages gelten Farbe und Favicon, aber weder Logo noch
Navigation — wer scannt, soll die Seite des Inhalts sehen.

Ein erster Entwurf hielt das Erscheinungsbild 15 Sekunden im Speicher, um die
Abfrage je Seitenaufruf zu sparen. Das ging schief: API-Route und Layout
laufen in getrennten Modulinstanzen, das Verwerfen beim Speichern erreichte
die andere Instanz nicht — nach dem Speichern blieb die Oberflaeche
unveraendert. Jetzt ohne Puffer.
2026-08-16 02:18:49 +02:00
Florian Wilde 75252b32b4 Anmeldung und Sitzungen nach OWASP durchgesehen und gehaertet
Zehn Befunde, alle behoben. Die wichtigsten:

scrypt lief mit N=2^15; das OWASP Password Storage Cheat Sheet nennt 2^17.
Jetzt 2^17, alte Hashes bleiben gueltig und werden beim naechsten Anmelden
still erneuert — das ist der einzige Moment, in dem das Klartextpasswort
vorliegt.

Passwoerter hatten keine Obergrenze. Ein Passwort von mehreren Megabyte haette
scrypt beschaeftigt, und zwar an einem Endpunkt ohne Anmeldung. Jetzt 128
Zeichen, geprueft vor dem Hashen.

Als CSRF-Schutz gab es nur SameSite=Lax. Das Merkblatt sagt ausdruecklich,
dass das allein nicht genuegt — besonders beim Upload, der multipart
entgegennimmt und damit von einem fremden Formular erreichbar waere. Jetzt
wird bei jedem schreibenden Zugriff der Origin gegen den Host der Anfrage
geprueft (nicht gegen BASE_URL, die Anwendung laeuft unter mehreren Domains).

Es gab keinen einzigen Schutzheader — die Verwaltung liess sich einbetten und
per Clickjacking fernsteuern. Jetzt frame-ancestors none, X-Frame-Options,
nosniff, Referrer-Policy und Permissions-Policy; X-Powered-By ist weg.

Sitzungen hatten keine absolute Obergrenze: eine gestohlene Sitzung haette
durch regelmaessige Nutzung unbegrenzt weitergelebt. Jetzt SESSION_MAX_DAYS
neben SESSION_IDLE_DAYS, beide einstellbar.

Dazu: Secure auch hinter einem TLS-Proxy, weniger Auskunft ueber fremde
Konten bei der Anmeldung, eine Bremse je Absender gegen breites
Durchprobieren, Ersteinrichtung ohne Wettlauf, und abgelaufene Sitzungen
werden aufgeraeumt.

Die Datei heisst proxy.ts statt middleware.ts — Next 16 hat die Konvention
umbenannt. /api/uploads bleibt davon ausgenommen: Der Proxy puffert
Anfragekoerper nur bis 10 MB, eine Datei an der Groessengrenze kaeme sonst
unvollstaendig an. Die Herkunftspruefung macht die Route dort selbst.

Befund, Begruendungen und was bewusst offen bleibt: docs/sicherheit.md
2026-08-15 08:31:29 +02:00
Florian Wilde c8d60d8006 Anmeldung ueber Authentik und andere OIDC-Anbieter
Anmeldefluss mit PKCE (S256), State und Nonce. Das ID-Token wird gegen den
JWKS des Anbieters geprueft — Signatur, Aussteller, Empfaenger, Ablauf und
Nonce; alles Noetige kommt aus dem Discovery-Dokument, damit die Anbindung
nicht auf Authentik festgelegt ist.

Wiedererkannt wird jemand an der Kennung des Anbieters (sub), nicht an der
E-Mail-Adresse — Adressen aendern sich. Ein bestehendes Konto mit derselben
Adresse wird verknuepft statt verdoppelt, aber nur bei bestaetigter Adresse:
sonst koennte sich beim Anbieter jemand eine fremde Adresse eintragen und
damit ein hiesiges Konto uebernehmen.

Uebernommen wird bewusst nur, wer jemand ist — plus die Instanz-Administration,
wenn OIDC_ADMIN_GROUP gesetzt ist. Gruppen und Rollen bleiben Sache dieser
Anwendung: die Gruppen im Verzeichnis bedeuten etwas anderes als die hier.

Konten aus dem Verzeichnis haben kein Passwort mehr (passwordHash ist jetzt
optional); Konto-Bereich und Passwort-Anmeldung sagen das statt an einer
leeren Pruefung zu scheitern.

Anleitung mit allen Feldern in Authentik: docs/authentik.md
2026-08-15 00:12:21 +02:00
Florian Wilde e296b1e6c2 Phase C: die letzten sechs Typen — PDF, Bild, Feedback und Datei-Upload
Damit sind alle 15 QR-Typen gebaut.

PDF und Bild brauchen eine hochgeladene Datei. Die liegt in der Datenbank,
nicht auf der Platte: damit bleibt es bei einem Volumen, das gesichert werden
muss — ein zweites wird beim Sichern zu leicht vergessen. Geprueft wird der
Dateiinhalt, nicht die Endung; eine Textdatei namens harmlos.pdf kommt nicht
durch. SVG ist bewusst nicht erlaubt, weil es Skripte enthalten darf und von
derselben Adresse wie die Anwendung ausgeliefert wuerde. Ausgeliefert wird mit
nosniff und einer CSP-Sandbox, damit auch ein PDF mit Skripten nichts anrichtet.

Dateien haengen am Code: wird er geloescht, geht sie mit. Eine ausgetauschte
oder nie gespeicherte Datei wird nach 24 Stunden weggeraeumt. Eine fremde
Datei laesst sich nicht an den eigenen Code haengen — sonst koennte man sie
einem anderen Code entziehen.

Feedback sammelt Note und Kommentar, ohne Name, IP oder Geraetekennung. Die
Auswertung mit Durchschnitt, Verteilung und Kommentaren ist aus der Codeliste
erreichbar — ein Feedback-Code ohne Auswertung waere nur die halbe Sache.
2026-08-14 23:34:28 +02:00
Florian Wilde 2a80f15e20 Phase B2: mehrere Domains fuer Kurz-URLs
Eine Domain kann mehreren Gruppen dienen (n:m ueber DomainGrant), und der
Slug eines Codes ist nur noch je Domain eindeutig - derselbe sprechende
Slug darf also auf zwei Domains liegen.

Sicherheitsrelevanter Kern: die Weiterleitung liest den Hostnamen aus
X-Forwarded-Host bzw. Host und prueft ihn gegen die Domain-Tabelle. Ein
unbekannter Hostname endet mit 404, faellt also auf die Standard-Domain
zurueck und findet dort nichts. Ueber einen gefaelschten Header laesst sich
damit keine fremde Domain ansprechen. Auch der Hostname fuer die Vorschau
kommt aus der Datenbank, nicht aus der Anfrage.

- Migration legt die Spalte erst nullable an, fuellt sie mit der
  Standard-Domain und macht sie danach verpflichtend; der globale
  Slug-Index weicht dem zusammengesetzten aus (domainId, slug).
- Verwaltung -> Domains: registrieren, Gruppen zuteilen, Voreinstellung je
  Gruppe, abschalten. Domains mit Codes lassen sich weder loeschen noch
  einer Gruppe entziehen, die noch Codes darunter hat - beides wuerde
  gedruckte Codes ins Leere laufen lassen.

Ein Fehler dabei gefunden und behoben: die Migration kennt BASE_URL nicht
und legt die Standard-Domain mit einem Platzhalter-Hostnamen an. Korrigiert
wurde er bisher nur beim Oeffnen der Domain-Verwaltung - auf einer frischen
Installation waeren also Kurz-URLs auf 'standard.invalid' entstanden.
ensureDefaultDomain() laeuft jetzt vor jeder Verwendung.

Geprueft: 34 neue Pruefungen fuer Host-Aufloesung, Spoofing, Slug je Domain
und Zuteilung; dazu 280 bestehende bei ausgeschalteter Anmeldung.
2026-08-14 13:10:12 +02:00
Florian Wilde f1cdb3da13 Phase B1: Anmeldung, Benutzer, Gruppen und Rollen
Umschaltbar ueber AUTH_ENABLED in der .env. Steht sie auf false, verhaelt
sich alles wie bisher - geprueft mit den 200 bestehenden Pruefungen.

Kern ist eine Actor- und Policy-Schicht: getActor() liefert bei
ausgeschalteter Anmeldung einen system-Actor mit Vollzugriff, sonst den
angemeldeten Benutzer. Dadurch braucht keine einzige Route ein
'if (anmeldungAktiv)' - sie fragen alle nur can() bzw. requirePermission().

- Datenmodell: User, Session, Group, Membership; Codes bekommen Gruppe und
  Ersteller. Die Migration legt eine Standardgruppe an und ordnet ihr
  bestehende Codes zu, bevor die Spalte verpflichtend wird - sonst
  scheiterte sie auf jeder bereits benutzten Installation.
- Passwoerter mit scrypt aus der Node-Standardbibliothek. Bewusst kein
  Argon2: das braeuchte ein natives Modul passend zur Container-Plattform.
- Sitzungen in der Datenbank, im Cookie nur ein Zufallstoken; gespeichert
  wird dessen SHA-256. Deaktivieren oder ein neues Passwort beendet alle
  offenen Sitzungen sofort.
- Aussperrschutz: der letzte aktive Administrator kann sich nicht selbst
  entmachten, deaktivieren oder loeschen. Der Schalter liegt in der .env,
  nicht in der Oberflaeche - so bleibt der Weg zurueck immer offen.
- Ersteinrichtung legt den ersten Administrator an und schliesst sich danach.
- Die Weiterleitung /r/{slug} bleibt oeffentlich, sonst liesse sich kein
  gedruckter Code mehr scannen.

Zwei Fehler dabei gefunden und behoben:

- 'return handleLogin(...)' innerhalb eines try-Blocks gibt das Promise
  zurueck, bevor es sich aufloest. Die Ablehnung kam nie im catch an und
  wurde zu einem nackten 500er statt einer 401. Jetzt mit await.
- Die Fehlerklassen wurden ueber instanceof erkannt. type-module.ts landet
  aber sowohl im Server- als auch im Client-Buendel, es gibt also zwei
  Klassenidentitaeten - saubere 400er wurden dadurch still zu 500ern. Die
  Erkennung laeuft jetzt ueber eine Eigenschaft.

Geprueft: 51 neue Pruefungen fuer Anmeldung und die vollstaendige
Rollenmatrix, dazu die 200 bestehenden bei ausgeschalteter Anmeldung.
2026-08-14 12:31:16 +02:00
Florian Wilde c6da20f419 Verwaltung: QR-Typen einzeln freigeben oder ausblenden
Neue Seite Verwaltung -> QR-Typen mit einem Schalter je Typ. Abgeschaltete
Typen verschwinden vollstaendig von der Startseite und lassen sich nicht
mehr neu anlegen.

- Neue Tabelle QrTypeSetting; fehlender Eintrag heisst freigegeben, damit
  eine frische Installation ohne Vorbelegung laeuft und spaetere Typen
  automatisch erscheinen.
- Registry-Feld 'active' heisst jetzt 'implemented' und beschreibt nur noch,
  ob der Typ ausprogrammiert ist. Ob er erscheint, entscheidet zusaetzlich
  die Freigabe aus der Datenbank (src/lib/qr/availability.ts).
- Bestehende dynamische Codes bleiben von einer Abschaltung unberuehrt: die
  Weiterleitung funktioniert weiter und Bearbeiten bleibt moeglich. Ein
  gedruckter Code darf durch einen Schalter nicht ungueltig werden.
- resolveQrRequest ist dadurch asynchron geworden; QrRequestError liegt jetzt
  in type-module.ts, sonst gaebe es einen Ringschluss mit availability.ts.
- Startseite meldet jetzt auch den Fall, dass nur noch Platzhalter
  freigegeben sind - vorher standen dort 14 tote Kacheln ohne Erklaerung.
- Kopfzeile: Schriftzug auf schmalen Geraeten ausgeblendet, sonst passte die
  dritte Navigationsschaltflaeche nicht mehr in die Zeile (7px Ueberlauf).
2026-08-14 10:48:21 +02:00
Florian Wilde ad4cd92f8e V1: Next.js-App mit QR-Typ-Registry, Website-Generator und dynamischen Codes
- Erweiterbare Registry aller 15 QR-Typen; nur "Website" ist aktiv, die
  uebrigen erscheinen als Platzhalter mit Badge "Bald verfuegbar".
- defineQrType() kapselt Zod-Schema, Kodierung und Redirect-Ziel je Typ,
  sodass ein V2-Typ nur aus einem Registry-Eintrag plus Formular besteht.
- Serverseitige QR-Erzeugung (PNG/SVG) mit Farben, Groesse, Fehlerkorrektur
  und Rand; Live-Vorschau ueber /api/qr/preview.
- Dynamische Codes: Kurz-URL {BASE_URL}/r/{slug}, 302-Redirect mit
  Scan-Zaehler, CRUD unter /api/codes.
- Prisma 7 mit pg-Driver-Adapter und Initial-Migration fuer PostgreSQL.
- Deutsche Oberflaeche mit Tailwind CSS, helles Theme, responsiv.
2026-08-13 15:54:47 +02:00