Commit Graph
18 Commits
Author SHA1 Message Date
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.
v1.1.0
2026-08-14 23:34:28 +02:00
Florian Wilde fdf6452b35 Typen Social Media und Video
Social Media buendelt Profile auf einer Seite. Wer „@firma" eintippt, meint
das Profil beim gewaehlten Netzwerk — daraus wird die volle Adresse. Statt
nachgezeichneter Markenlogos steht ein Kreis in der Hausfarbe mit Monogramm;
ein schlecht nachgebautes Logo sieht schlechter aus als ein sauberes Zeichen.

Video erkennt YouTube, Vimeo und direkte Dateien. Der Rahmen zum Anbieter
laedt erst auf Klick, und davor steht, dass dabei Daten uebertragen werden —
diese Seite bekommen Fremde nach einem Scan zu sehen, ungefragt einen
Drittanbieter einzubinden waere nicht in Ordnung. Videodateien laden mit
preload="none": wer unterwegs scannt, zahlt das Volumen.
2026-08-14 22:44:23 +02:00
Florian Wilde e627f7c372 Landingpages als eigene Gattung, erster Typ: Mehrere Links
Die sechs noch offenen Typen haben kein fremdes Ziel, auf das man
weiterleiten koennte — sie brauchen eine Seite. Dafuer bekommt der
Typ-Vertrag `hasLandingPage`: /r/[slug] schickt solche Codes auf /s/[slug],
statt `redirectTarget` zu befragen.

Die oeffentlichen Seiten duerfen die Navigation der Anwendung nicht zeigen —
wer einen Code scannt, hat mit „Verwaltung" nichts zu tun. Deshalb liegt das
App-Chrome jetzt in der Routengruppe (app); das Wurzel-Layout traegt nur noch
html und body. Die URLs aendern sich dadurch nicht.
2026-08-14 22:27:49 +02:00
Florian Wilde b3d386b0d2 Meine Codes: Listenansicht mit Suche, Sortierung und Gruppenfilter
Bei vielen Projekten ist die Kartenansicht unuebersichtlich. Die Liste ist
jetzt die Voreinstellung, die Karten bleiben ueber einen Schalter erreichbar;
die Wahl merkt sich der Browser.

Dazu Suche ueber Name, Ziel, Kurz-URL und Typ, Sortierung nach Datum, Name
oder Scans sowie ein Gruppenfilter, sobald Codes in mehr als einer Gruppe
liegen. Die Seite ist breiter als die uebrigen, damit die Namen Platz haben.

Die Tabelle hat feste Spaltenbreiten: mit automatischem Layout hat ihre
Eigenbreite die ganze Seite seitlich scrollbar gemacht, auch wenn der
Container sie eigentlich beschnitten hat. Auf schmalen Schirmen weichen die
hinteren Spalten, die Kurz-URL rueckt unter den Namen.
2026-08-14 15:35:05 +02:00
Florian Wilde cdda55f9b0 AUTH_ENABLED an den Container durchreichen
Der Schalter blieb wirkungslos: AUTH_ENABLED stand zwar in der .env und im
README, fehlte aber im environment-Block des app-Dienstes. Docker Compose
liest die .env nur, um Platzhalter wie \ zu ersetzen - in den
Container gelangt ausschliesslich, was unter environment: aufgefuehrt ist.
Wer die Anmeldung einschalten wollte, blieb deshalb ohne jede Rueckmeldung
im Zustand 'ohne Anmeldung' haengen.

Gefunden hat das Florian beim Benutzen. Meine Pruefungen liefen alle gegen
'next start' mit direkt gesetzter Umgebungsvariable und konnten die Luecke
zwischen .env und Container deshalb gar nicht sehen.

- AUTH_ENABLED steht jetzt im environment des app-Dienstes, mit Hinweis im
  docker-compose.yml, warum das Ergaenzen dort nicht vergessen werden darf.
- Neue Pruefung compose-env.mjs: vergleicht die dokumentierten Variablen mit
  dem aufgeloesten Compose-Setup und mit der Umgebung des laufenden
  Containers. Genau diese Klasse von Fehler faellt damit kuenftig auf.
- README: Hinweis, dass die .env von git pull nicht angefasst wird und die
  Zeile in aelteren Installationen fehlt, plus ein Befehl zum Nachpruefen.

Geprueft gegen den Container in beiden Stellungen: 63 Pruefungen fuer
Anmeldung und Rollen sowie 34 fuer Multi-Domain mit AUTH_ENABLED=true,
dazu 293 mit false.
2026-08-14 14:29:39 +02:00
Florian Wilde 6b2baa66c5 Passwoerter verdeckt, Kontobereich mit eigenem Passwortwechsel
Zwei berechtigte Rueckmeldungen von Florian:

1. In der Benutzerverwaltung standen die Passwortfelder auf type='text',
   das Passwort war also im Klartext lesbar - vor fremden Augen und in
   jeder Bildschirmuebertragung. Alle Passwortfelder nutzen jetzt ein
   gemeinsames PasswordInput: verdeckt als Voreinstellung, mit Auge zum
   Aufdecken. Weil ein Administrator das Passwort weitergeben muss, gibt es
   daneben einen Generator fuer ein zufaelliges, vorlesbares Passwort in
   Vierergruppen.

2. Es gab keinen Kontobereich. Neu ist /konto mit Profil, den eigenen
   Gruppen samt Rollenerklaerung, Passwortwechsel und Abmelden. Im
   Kopfbereich fuehrt ein Namenskuerzel dorthin. Ist die Anmeldung
   ausgeschaltet, steht dort 'Ohne Anmeldung' und die Seite erklaert, wie
   man sie einschaltet - vorher suchte man den Abmelden-Knopf vergeblich.

Dabei eine echte Luecke geschlossen: ein Benutzer konnte sein Passwort gar
nicht selbst aendern, nur ein Administrator konnte es setzen. Der Wechsel
verlangt das bisherige Passwort, lehnt ein identisches ab und beendet alle
anderen Sitzungen - die eigene bleibt bestehen.

Nebenbei: die Betriebsart-Knoepfe im Generator haben jetzt data-mode. Ueber
aria-pressed waren sie nicht mehr eindeutig, seit das Passwortfeld einen
eigenen Umschalter hat.

Geprueft: 63 Pruefungen fuer Anmeldung inkl. Passwortwechsel und Konto,
dazu 280 bestehende bei ausgeschalteter Anmeldung.
2026-08-14 13:46:21 +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 94a373ff48 README: Schritt-fuer-Schritt-Anleitung zum Start
Acht nummerierte Schritte vom leeren Server bis zum ersten QR-Code, mit
den erwarteten Ausgaben von 'docker compose ps' und den Protokollzeilen,
an denen man erkennt, dass die Migration durchgelaufen ist.

Dazu zwei Abschnitte, die vorher fehlten:

- Betrieb hinter dem Nginx Proxy Manager samt der Felder, die im Proxy Host
  einzutragen sind, und dem Hinweis, dass BASE_URL dann die oeffentliche
  https-Adresse sein muss.
- Fehlersuche fuer die Faelle, die tatsaechlich vorkommen: fehlende .env,
  belegter Port 3000, unhealthy Datenbank, Passwort in DATABASE_URL passt
  nicht zu POSTGRES_PASSWORD, und der haeufigste Fehler ueberhaupt - eine
  BASE_URL auf localhost, wodurch dynamische Codes auf dem Handy ins Leere
  fuehren.
2026-08-14 11:47:44 +02:00
Florian Wilde 7f7cb6fa20 Phase A: acht weitere QR-Typen
E-Mail, SMS, WhatsApp, WLAN, Standort, vCard, Zahlung und App-Link sind
umgesetzt. Damit sind 9 der 15 Typen nutzbar.

Erweiterungen am Typ-Vertrag, die dafuer noetig waren:

- supportsStatic: nicht jeder Typ kann statisch. Beim App-Link steht das
  Ziel erst beim Scannen fest, also gibt es ihn nur dynamisch.
- RedirectContext: die Weiterleitung reicht den User-Agent durch, damit der
  App-Link iOS, Android und alles Uebrige unterscheiden kann.
- assertDynamicPossible(): supportsDynamic ist eine Eigenschaft des Typs,
  aber nicht jede Eingabe taugt dafuer. Ein GiroCode ist kein Web-Ziel,
  obwohl der Typ 'Zahlung' als Zahlungslink sehr wohl dynamisch sein kann.
  Ohne diese Probe liess sich ein Code speichern, der erst beim Scannen
  aufgefallen waere - also nach dem Drucken. Greift beim Anlegen und beim
  Bearbeiten.

Sorgfalt bei den Formaten, gesammelt in src/lib/qr/encoding.ts:

- WLAN maskiert ; , : \\ und setzt rein hexadezimale Werte in
  Anfuehrungszeichen, sonst deuten Geraete sie als Hexzahl.
- vCard 3.0 mit CRLF, Maskierung und Faltung nach 75 Oktetten, oktettweise
  gezaehlt, damit kein mehrbytiges Zeichen zerrissen wird.
- GiroCode nach EPC069-12 mit fester Feldreihenfolge, IBAN-Pruefsumme nach
  Modulo 97 und der 331-Byte-Grenze.
- Rufnummern werden nach E.164 normalisiert, '(0)' faellt weg.
- Standort nimmt Koordinaten auch aus einem Google-Maps- oder Apple-Link an.

Formular-Bausteine (Select, Textarea, Checkbox, FieldRow, FieldSet) fuer die
mehrfeldrigen Typen ergaenzt.

Geprueft: 225 Pruefungen, davon 80 neu - Dekodierung je Typ aus dem fertigen
PNG, Validierung, Betriebsarten, geraeteabhaengige Weiterleitung und die
Formulare im Browser.
2026-08-14 11:30:09 +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 1e9e37f08d public/ mitversionieren, sonst scheitert der Docker-Build im frischen Klon
Das Verzeichnis war nach dem Entfernen der Next.js-Boilerplate leer, und
leere Verzeichnisse legt git nicht an. Lokal existierte es noch, im Klon
nicht — dort brach 'COPY --from=builder /app/public ./public' ab.
v1.0.0
2026-08-13 18:46:55 +02:00
Florian Wilde 27c463beb4 Merge-Ausgabe von git im Update-Skript unterdruecken 2026-08-13 18:15:11 +02:00
Florian Wilde c731c22160 Update-Skript fuer Linux/macOS und Windows
update.sh bzw. update.ps1 holen den neuen Stand und bauen den Stack neu:

- Bricht ab, wenn git, docker compose oder die .env fehlen.
- Bricht ab, wenn es lokale Aenderungen gibt — ein "git pull" wuerde dort
  im Konflikt enden.
- Holt den Stand mit "git merge --ff-only" und zeigt die neuen Commits.
- Baut nur neu, wenn es tatsaechlich Aenderungen gab (--force erzwingt es).
- Wartet, bis die Anwendung auf APP_PORT antwortet, und gibt bei Zeitablauf
  das Container-Protokoll aus, statt stillschweigend Erfolg zu melden.
- Zeigt zum Schluss die Migrationszeilen aus dem Start und den Status.
- --prune raeumt auf Wunsch verwaiste Images auf, die bei jedem Neubau
  zurueckbleiben.

README um einen Abschnitt "Aktualisieren" ergaenzt.
2026-08-13 18:07:03 +02:00
Florian Wilde b583c88855 .gitattributes: Zeilenenden auf LF festlegen
docker/entrypoint.sh muss LF-Zeilenenden behalten. Beim Auschecken auf
einem Rechner mit core.autocrlf=true wuerde daraus CRLF, und der Kernel
faende den Interpreter aus der Shebang-Zeile nicht mehr — der Container
startet dann nicht.
2026-08-13 17:17:12 +02:00
Florian Wilde 4c8adf3f4a Vorschau-Layout und Hintergrund der Vorschauflaeche verbessert
Auf dem Handy stand die Vorschau ganz unten hinter der Gestaltung — man
musste an allen Reglern vorbeiscrollen, um den Code zu sehen. Die
Rasterplaetze werden jetzt ab "lg" ausdruecklich vergeben: auf dem Handy
folgt die Vorschau direkt auf die Eingabe, auf dem Desktop steht sie
weiterhin rechts daneben.

Das Schachbrettmuster hinter der Vorschau entfaellt. Es sollte
Transparenz sichtbar machen, aber jeder erzeugte Code bringt eine
deckende Hintergrundfarbe mit — es war nur Unruhe.
2026-08-13 16:59:49 +02:00
Florian Wilde 5806dc4ce0 Docker-Stack, README und Korrekturen aus dem ersten Durchlauf
- Dockerfile (4 Stufen) und docker-compose.yml mit Postgres-Healthcheck,
  Volume und direktem Port-Mapping 3000:3000, ohne Reverse Proxy.
- Entrypoint wendet beim Start "prisma migrate deploy" an; die Prisma-CLI
  bekommt eine eigene Build-Stufe, weil sie mehr als nur @prisma/* braucht.
- prisma.config.ts durch prisma.config.js ersetzt: laedt ohne TypeScript und
  ohne dotenv, damit sie im Laufzeit-Image funktioniert.
- Prisma-Client wird erst beim ersten Zugriff erzeugt. Vorher scheiterte
  "next build" beim Einsammeln der Routen-Konfiguration ohne DATABASE_URL.
- Postgres-18-Volume auf /var/lib/postgresql gemountet (neues Datenlayout).
- npm 11 blockiert Installationsskripte: erlaubte Pakete stehen jetzt im
  Feld "allowScripts" der package.json.
- Lokale Variable "module" ueberall in "qrType" umbenannt (verdeckte sonst
  die CommonJS-Variable) und Vorschau-Effekt ohne synchrones setState.
- README.md auf Deutsch: Setup, Konfiguration, Bedienung, Aufbau, API und
  Ausblick auf Version 2.
2026-08-13 16:48:28 +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