Files
qrcode-generator/README.md
T
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

757 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# QR-Code-Generator
Eine Webanwendung zum Erzeugen von QR-Codes — als Docker-Compose-Stack aus
zwei Containern: der Next.js-Anwendung und einer PostgreSQL-Datenbank.
Umgesetzt sind **9 der 15 QR-Typen**. Die übrigen sechs sind in der Oberfläche
sichtbar, als Platzhalter markiert und lassen sich über die Verwaltung
ausblenden.
---
## Inhalt
- [Schritt für Schritt zum laufenden Stack](#schritt-für-schritt-zum-laufenden-stack)
- [Hinter einem Reverse Proxy (Nginx Proxy Manager)](#hinter-einem-reverse-proxy-nginx-proxy-manager)
- [Wenn etwas nicht läuft](#wenn-etwas-nicht-läuft)
- [Aktualisieren](#aktualisieren)
- [Konfiguration](#konfiguration)
- [Bedienung](#bedienung)
- [Anmeldung, Gruppen und Rollen](#anmeldung-gruppen-und-rollen)
- [Technischer Aufbau](#technischer-aufbau)
- [Entwicklung ohne Docker](#entwicklung-ohne-docker)
- [API](#api)
- [Ausblick Version 2](#ausblick-version-2)
---
## Schritt für Schritt zum laufenden Stack
Vom leeren Server bis zum ersten QR-Code. Alle Befehle laufen auf dem
Rechner, auf dem die Anwendung später läuft.
### Schritt 1 — Voraussetzungen prüfen
Gebraucht werden Docker mit Compose v2 und Git:
```bash
docker compose version # erwartet: Docker Compose version v2.x oder höher
git --version
```
Kommt `docker: command not found`, fehlt Docker — die
[offizielle Anleitung](https://docs.docker.com/engine/install/) installiert
Engine und Compose in einem Rutsch.
### Schritt 2 — Projekt holen
```bash
git clone https://git.wilde-server.de/florian.wilde/qrcode-generator.git
cd qrcode-generator
```
### Schritt 3 — Konfiguration anlegen
```bash
cp .env.example .env
nano .env
```
Zwei Werte **musst** du anfassen:
| Wert | Was eintragen |
| --- | --- |
| `POSTGRES_PASSWORD` | Ein eigenes Passwort. Es steht nur in dieser Datei und wird nirgends sonst gebraucht. |
| `DATABASE_URL` | Dasselbe Passwort noch einmal — es steckt mitten im Verbindungsstring. |
Und einen solltest du bewusst setzen:
| Wert | Was eintragen |
| --- | --- |
| `BASE_URL` | Die Adresse, unter der die Anwendung **von außen** erreichbar ist. |
Beispiel für ein eigenes Passwort `MeinGeheimes123`:
```bash
POSTGRES_PASSWORD=MeinGeheimes123
DATABASE_URL=postgresql://qrcode:MeinGeheimes123@db:5432/qrcode?schema=public
BASE_URL=http://192.168.1.50:3000
```
> **Warum `BASE_URL` so wichtig ist:** Ein dynamischer QR-Code enthält nicht
> das eigentliche Ziel, sondern die Kurz-URL `{BASE_URL}/r/{slug}`. Steht dort
> `localhost`, funktioniert der Code **nur auf dem Server selbst** — ein Handy
> läuft ins Leere. Trage deshalb die LAN-Adresse des Servers oder die
> öffentliche Domain ein. Bereits gedruckte Codes lassen sich später nicht
> mehr umbiegen, also am besten vor dem ersten produktiven Code festlegen.
Der Hostname `db` im `DATABASE_URL` bleibt unverändert — das ist der
Servicename aus der `docker-compose.yml`, nicht dein Server.
### Schritt 4 — Starten
```bash
docker compose up -d --build
```
Der erste Durchlauf baut das Image und dauert je nach Maschine **5 bis 15
Minuten**. Spätere Starts brauchen nur wenige Sekunden.
### Schritt 5 — Prüfen, ob alles läuft
```bash
docker compose ps
```
Erwartet werden zwei Container, `db` mit dem Zusatz `(healthy)`:
```
NAME SERVICE STATUS
qrcode-generator-app-1 app Up 30 seconds
qrcode-generator-db-1 db Up 45 seconds (healthy)
```
Und im Protokoll:
```bash
docker compose logs app
```
```
→ Datenbankmigrationen werden angewendet …
All migrations have been successfully applied.
→ Anwendung wird gestartet …
▲ Next.js 16.3.0
✓ Ready
```
Die Datenbank richtet sich beim ersten Start selbst ein — es gibt keinen
Extraschritt dafür.
### Schritt 6 — Aufrufen
Im Browser die Adresse öffnen, die du als `BASE_URL` eingetragen hast, zum
Beispiel <http://192.168.1.50:3000>. Es erscheint „Was möchtest du
erstellen?“ mit den Typ-Kacheln.
### Schritt 7 — Ersten Code erstellen
1. Kachel **Website** anklicken.
2. Adresse eingeben, etwa `https://example.com` — die Vorschau erscheint
sofort.
3. **PNG** oder **SVG** herunterladen. Fertig.
Für einen Code, dessen Ziel du später ändern kannst:
1. Unter „Art des Codes“ auf **Dynamisch** wechseln.
2. Optional einen Namen vergeben, damit du ihn wiederfindest.
3. **Dynamischen Code speichern** — erst dabei entsteht die Kurz-URL, deshalb
ist der Download bis dahin gesperrt.
4. Danach herunterladen. Unter **Meine Codes** lässt sich das Ziel jederzeit
ändern, ohne den gedruckten Code anzufassen.
### Schritt 8 — Aufräumen, was du nicht brauchst
Unter **Verwaltung → QR-Typen** kannst du einzelne Typen ausblenden — etwa
die sechs, die noch nicht umgesetzt sind, damit sie gar nicht erst
auftauchen.
### Die wichtigsten Befehle im Betrieb
```bash
docker compose logs -f app # Protokoll mitlesen (Abbruch mit Strg+C)
docker compose ps # Status beider Container
docker compose restart app # Anwendung neu starten
docker compose down # Stack stoppen — die Daten bleiben erhalten
docker compose up -d # wieder starten
docker compose down -v # Stack stoppen UND die Datenbank löschen
```
---
## Hinter einem Reverse Proxy (Nginx Proxy Manager)
Soll die Anwendung unter einer eigenen Domain mit HTTPS laufen, übernimmt der
Proxy die Zertifikate; die Anwendung selbst bleibt unverändert.
**1. Port nur lokal veröffentlichen.** Wenn Proxy und Anwendung auf demselben
Rechner laufen, muss Port 3000 nicht nach außen offen sein. In der `.env`:
```bash
APP_PORT=3000
BASE_URL=https://qr.deine-domain.de
```
**2. Im Nginx Proxy Manager** einen *Proxy Host* anlegen:
| Feld | Wert |
| --- | --- |
| Domain Names | `qr.deine-domain.de` |
| Scheme | `http` |
| Forward Hostname / IP | IP des Docker-Hosts, z. B. `192.168.1.50` |
| Forward Port | `3000` |
| Websockets Support | an |
| Block Common Exploits | an |
| SSL | Zertifikat anfordern, *Force SSL* aktivieren |
Läuft der Proxy als Container im selben Docker-Netz wie dieser Stack, kann
statt der IP auch der Servicename `app` mit Port `3000` stehen — dann muss
das Port-Mapping gar nicht nach außen offen sein.
**3. `BASE_URL` auf die öffentliche Adresse setzen** — mit `https://` und ohne
Port. Danach:
```bash
docker compose up -d
```
> `BASE_URL` muss zur Adresse passen, unter der Besucher die Anwendung
> aufrufen. Steht dort weiterhin `http://192.168.1.50:3000`, zeigen neue
> dynamische Codes ins interne Netz statt auf die Domain.
### Mehrere Domains
Sollen Kurz-URLs unter verschiedenen Adressen laufen — etwa je Kunde eine —,
lege sie unter **Verwaltung → Domains** an und richte im Proxy für jede einen
eigenen Proxy Host auf dieselbe Anwendung ein.
Die Weiterleitung erkennt am `Host`-Header, welche Domain gemeint ist, und
prüft ihn gegen die registrierten Domains. Ein unbekannter Hostname endet mit
404 — über einen gefälschten Header lässt sich also keine fremde Domain
ansprechen.
Zwei Dinge, die daraus folgen:
- Der **Slug ist je Domain eindeutig**, nicht global. Derselbe sprechende
Slug darf auf zwei Domains liegen und zeigt dort auf verschiedene Ziele.
- Eine Domain kann **mehreren Gruppen** dienen. Das heißt aber nicht, dass
diese die Codes der jeweils anderen sehen — die Übersicht bleibt strikt
nach Gruppe gefiltert.
`BASE_URL` bleibt dabei wichtig: sie liefert die Standard-Domain für Aufrufe
über eine nicht registrierte Adresse (etwa direkt über die IP im LAN) und
bestimmt, ob Kurz-URLs mit `http` oder `https` gebildet werden.
---
## Wenn etwas nicht läuft
**`POSTGRES_PASSWORD fehlt — bitte .env aus .env.example anlegen`**
Die `.env` fehlt oder ist leer. Schritt 3 nachholen.
**`ports are not available: ... bind: address already in use`**
Port 3000 ist belegt. Entweder den anderen Dienst beenden oder in der `.env`
einen anderen Port setzen, z. B. `APP_PORT=3100`, dann `docker compose up -d`.
**Der Container `db` bleibt `unhealthy` oder startet immer neu**
Meist ein Datenverzeichnis aus einer älteren PostgreSQL-Version. Protokoll
ansehen mit `docker compose logs db`. Wenn dort von einem inkompatiblen
Datenlayout die Rede ist und du die Daten nicht brauchst:
`docker compose down -v` — Achtung, das löscht die Datenbank.
**Der QR-Code führt auf dem Handy ins Leere**
`BASE_URL` zeigt auf `localhost`. Auf die LAN-Adresse oder Domain ändern,
`docker compose up -d`, und die betroffenen dynamischen Codes neu
herunterladen. Statische Codes sind davon nicht betroffen.
**Die Anwendung antwortet nicht, `docker compose ps` zeigt sie aber als `Up`**
`docker compose logs app` ansehen. Kommt dort ein Datenbankfehler, stimmt
meist das Passwort in `DATABASE_URL` nicht mit `POSTGRES_PASSWORD` überein —
beide Werte müssen identisch sein.
**Nach einem Update fehlt etwas oder die Seite ist leer**
`docker compose up -d --build` erzwingt einen Neubau. Hilft das nicht:
`docker compose down && docker compose up -d --build`.
---
## Aktualisieren
Für neue Versionen liegt ein Update-Skript bei — einmal für Linux/macOS und
einmal für Windows. Es holt den aktuellen Stand aus Git, baut den Stack neu
und wartet, bis die Anwendung wieder antwortet.
```bash
./update.sh # Linux / macOS
```
```powershell
.\update.ps1 # Windows
```
Beim ersten Aufruf unter Linux muss das Skript einmal ausführbar gemacht
werden: `chmod +x update.sh`.
Was das Skript tut:
1. prüft, ob `git`, `docker compose` und die `.env` vorhanden sind,
2. bricht ab, wenn es **lokale Änderungen** gibt — sonst würde `git pull` im
Konflikt enden,
3. holt den neuen Stand mit `git merge --ff-only` und zeigt die neuen Commits,
4. baut den Stack nur neu, wenn es tatsächlich Änderungen gab,
5. wartet, bis die Anwendung auf `APP_PORT` antwortet, und zeigt sonst das
Protokoll,
6. gibt die Migrationszeilen aus dem Start-Protokoll und den Containerstatus
aus.
Optionen:
| Option | Wirkung |
| ------------------- | -------------------------------------------------------------- |
| `--force` / `-Force` | Auch ohne neue Commits neu bauen |
| `--prune` / `-Prune` | Danach verwaiste Docker-Images entfernen (`docker image prune`) |
| `--help` | Hilfe anzeigen |
Die Datenbank bleibt beim Update erhalten — neue Migrationen wendet der
App-Container beim Start automatisch an. Wer lieber von Hand aktualisiert:
```bash
git pull
docker compose up -d --build
```
---
## Konfiguration
Alle Einstellungen stehen in der `.env` im Projektverzeichnis (Vorlage:
`.env.example`).
| Variable | Bedeutung | Standard |
| ------------------- | ------------------------------------------------------------- | ------------------------ |
| `BASE_URL` | Öffentliche Adresse der App — landet in dynamischen QR-Codes | `http://localhost:3000` |
| `APP_PORT` | Host-Port, auf dem die App veröffentlicht wird | `3000` |
| `AUTH_ENABLED` | Anmeldung mit Benutzern, Gruppen und Rollen | `false` |
| `DATABASE_URL` | Verbindungsstring der App zur Datenbank | — |
| `POSTGRES_USER` | Datenbankbenutzer | `qrcode` |
| `POSTGRES_PASSWORD` | Datenbankpasswort (**muss gesetzt sein**) | — |
| `POSTGRES_DB` | Name der Datenbank | `qrcode` |
### Wichtig: `BASE_URL` richtig setzen
Ein dynamischer QR-Code enthält nicht das eigentliche Ziel, sondern die
Kurz-URL `{BASE_URL}/r/{slug}`. Diese Adresse muss von den Geräten erreichbar
sein, die den Code später scannen.
- Nur am selben Rechner testen → `http://localhost:3000` genügt.
- Im LAN mit dem Handy scannen → die LAN-Adresse des Servers eintragen, z. B.
`http://192.168.1.50:3000`.
Wird `BASE_URL` nachträglich geändert, zeigen bereits **gedruckte** dynamische
Codes weiterhin auf die alte Adresse. Am besten also vor dem ersten
produktiven Code festlegen.
Nach einer Änderung an der `.env`:
```bash
docker compose up -d
```
Ändern sich `POSTGRES_*`-Werte, muss `DATABASE_URL` entsprechend mitgezogen
werden — der Hostname darin bleibt `db` (der Servicename aus
`docker-compose.yml`).
---
## Bedienung
### 1. Typ wählen
Die Startseite zeigt unter „Was möchtest du erstellen?“ ein Kachel-Grid der
freigegebenen QR-Typen. Umgesetzte Typen sind anklickbar, die übrigen tragen
das Badge „Bald verfügbar“.
| Typ | Kodiert als | Statisch | Dynamisch |
| --- | --- | :-: | :-: |
| Website | die URL selbst | ✓ | ✓ |
| E-Mail | `mailto:` mit Betreff und Text | ✓ | ✓ |
| SMS | `SMSTO:` | ✓ | ✓ |
| WhatsApp | `https://wa.me/…` | ✓ | ✓ |
| Standort | `geo:` — dynamisch über OpenStreetMap | ✓ | ✓ |
| vCard | vCard 3.0 | ✓ | — |
| WLAN | `WIFI:` | ✓ | — |
| App-Link | Weiterleitung je nach Gerät | — | ✓ |
| Zahlung | GiroCode (EPC069-12) oder Zahlungslink | ✓ | nur Link |
Warum manche Typen nur eine Betriebsart können:
- **vCard und WLAN** werden vom Gerät direkt ausgewertet — die Kamera legt
einen Kontakt an oder richtet das Netz ein. Über eine Kurz-URL würde
stattdessen nur ein Browser aufgehen.
- **App-Link** kann nicht statisch sein: welches Ziel richtig ist, steht erst
beim Scannen fest. Die Weiterleitung wertet den User-Agent aus und schickt
iOS in den App Store, Android zu Google Play und alles Übrige auf die
Ausweichadresse.
- **GiroCode** ist kein Web-Ziel, sondern ein Datensatz für Banking-Apps. Als
dynamischer Code funktioniert daher nur die Variante „Zahlungslink“ — die
Anwendung weist das beim Speichern ausdrücklich ab, statt einen Code
entstehen zu lassen, der beim Scannen ins Leere führt.
> **Zum WLAN-Code:** Das Passwort steht im Klartext im Code — wer ihn scannt
> oder abfotografiert, kennt es. Für Gästenetze ist das gewollt. Besteht ein
> Netzwerkname oder Passwort ausschließlich aus Hex-Ziffern (etwa `abc123`),
> setzt die Anwendung ihn wie in der Spezifikation vorgesehen in
> Anführungszeichen — sonst würden manche Geräte den Wert als Hexzahl deuten.
### 2. Statisch oder dynamisch
| | Statisch | Dynamisch |
| ---------------------- | -------------------------------------------- | ---------------------------------------------------- |
| Im Code kodiert | die Ziel-URL selbst | die Kurz-URL `{BASE_URL}/r/{slug}` |
| Ziel später änderbar | nein | ja, jederzeit |
| Braucht diesen Server | nein | ja — er leitet weiter |
| Scans werden gezählt | nein | ja |
| Wird gespeichert | nein | ja, in PostgreSQL |
Ein dynamischer Code muss einmal **gespeichert** werden, denn erst dabei
entsteht seine Kurz-URL. Bis dahin zeigt die Vorschau einen Platzhalter und
der Download ist gesperrt — so kann kein Code entstehen, der ins Leere führt.
### 3. Gestalten
- **Farben**: Vorder- und Hintergrund per Farbwähler oder als Hex-Wert.
Fünf Voreinstellungen und ein Knopf zum Tauschen der Farben stehen bereit.
- **Kontrastwarnung**: Die Anwendung berechnet das Kontrastverhältnis und
warnt bei schlecht scanbaren Kombinationen — ebenso bei hellem Vordergrund
auf dunklem Grund, den viele Scanner-Apps nicht erkennen.
- **Größe**: 256 bis 2048 px (gilt für das PNG; das SVG skaliert verlustfrei).
- **Fehlerkorrektur**: L, M (Standard), Q oder H. Höhere Level machen den Code
robuster gegen Beschädigung, aber auch dichter.
- **Rand**: Breite der Ruhezone in Modulen. Weniger als 4 kann das Scannen
erschweren.
Die Vorschau wird bei jeder Änderung serverseitig neu erzeugt — sie zeigt also
exakt das, was auch heruntergeladen wird.
### 4. Herunterladen
**PNG** für Druck und Office-Dokumente, **SVG** für verlustfreie Skalierung
(Plakate, Schilder, Druckvorstufe).
### 5. Verwaltung: welche Typen erscheinen
Unter **Verwaltung → QR-Typen** lässt sich jeder der 15 Typen einzeln
ein- und ausblenden. Ausgeblendete Typen verschwinden vollständig von der
Startseite und lassen sich nicht mehr neu anlegen — praktisch, um die noch
nicht umgesetzten Typen gar nicht erst zu zeigen.
Zwei Eigenschaften sind dabei zu unterscheiden:
| | Bedeutung | Woher |
| --- | --- | --- |
| **umgesetzt** | Der Typ ist ausprogrammiert. In Version 1 gilt das nur für „Website“. | Registry im Code |
| **freigegeben** | Die Verwaltung zeigt den Typ an. | Datenbank, jederzeit änderbar |
Nur umgesetzt **und** freigegeben heißt benutzbar. Freigegeben, aber nicht
umgesetzt, erscheint als Kachel „Bald verfügbar“.
> **Bereits gespeicherte Codes bleiben unberührt.** Schaltet man einen Typ ab,
> leiten vorhandene dynamische Codes weiter wie bisher und lassen sich weiter
> bearbeiten. Ein gedruckter Code wird durch das Abschalten seines Typs also
> nicht ungültig — gesperrt ist nur das Anlegen neuer Codes.
Ohne Eintrag in der Datenbank gilt ein Typ als freigegeben. Eine frische
Installation zeigt also alles, und Typen aus späteren Versionen erscheinen
automatisch.
### 6. Meine Codes
Unter „Meine Codes“ stehen alle dynamischen Codes mit Vorschaubild,
Kurz-URL, aktuellem Ziel, Scan-Anzahl, Erstellungsdatum und letztem Scan.
Dort lässt sich
- das **Ziel ändern** — die Kurz-URL und damit der gedruckte Code bleiben
gültig,
- der Code **löschen** (mit Bestätigungsdialog),
- das Bild erneut als PNG oder SVG **herunterladen**.
Statische Codes erscheinen hier nicht, weil sie nicht gespeichert werden.
> Version 1 ist für den Einzelplatz-/LAN-Betrieb gedacht und hat **keine
> Benutzerverwaltung**. Wer die Anwendung erreicht, sieht und ändert alle
> Codes. Die App gehört daher nicht ungeschützt ins offene Internet.
---
## Anmeldung, Gruppen und Rollen
Standardmäßig ist die Anmeldung **aus**: wer die Anwendung erreicht, darf
alles. Für den Einzelplatz- oder LAN-Betrieb ist das gewollt. Sobald mehrere
Leute damit arbeiten, lässt sie sich einschalten.
### Einschalten
In der `.env`:
```bash
AUTH_ENABLED=true
```
Danach:
```bash
docker compose up -d
```
Beim nächsten Aufruf erscheint die **Ersteinrichtung** und du legst den
ersten Administrator an. Diese Seite schließt sich, sobald es einen Benutzer
gibt — danach führt sie zur normalen Anmeldung.
> **Fehlt die Zeile in deiner `.env`?** Die `.env` wird von `git pull` nicht
> angefasst — sie gehört bewusst nicht ins Repository. Wurde sie vor
> Einführung der Anmeldung angelegt, fehlt `AUTH_ENABLED` darin. Einfach
> ergänzen; ohne die Zeile gilt `false`.
Ob es angekommen ist, zeigt:
```bash
docker compose exec app env | grep AUTH_ENABLED
```
> **Aussperren ist nicht möglich.** Der Schalter steht bewusst in der `.env`
> und nicht in der Oberfläche. Wer den Zugang verliert, setzt
> `AUTH_ENABLED=false`, startet neu und kommt wieder an alles heran.
Bestehende Codes gehen dabei nicht verloren: die Migration legt eine Gruppe
**Standard** an und ordnet ihnen diese zu.
### Die vier Ebenen
| Ebene | Darf |
| --- | --- |
| **Instanz-Administrator** | alles: Benutzer, Gruppen, QR-Typen und sämtliche Codes |
| **Administrator** einer Gruppe | alles in seiner Gruppe, einschließlich Mitglieder und Rollen |
| **Bearbeiter** | Codes der Gruppe anlegen, ändern, löschen |
| **Betrachter** | Codes der Gruppe ansehen und herunterladen — sonst nichts |
Ein Benutzer kann in mehreren Gruppen sein, mit unterschiedlicher Rolle je
Gruppe. Er sieht ausschließlich die Codes der Gruppen, in denen er Mitglied
ist.
| Aktion | Betrachter | Bearbeiter | Admin der Gruppe | Instanz-Admin |
| --- | :-: | :-: | :-: | :-: |
| Codes sehen, Bilder herunterladen | ✓ | ✓ | ✓ | ✓ |
| Statische Codes erzeugen | ✓ | ✓ | ✓ | ✓ |
| Dynamische Codes anlegen und ändern | – | ✓ | ✓ | ✓ |
| Codes löschen | – | ✓ | ✓ | ✓ |
| Mitglieder und Rollen der Gruppe | – | – | ✓ | ✓ |
| Gruppen und Benutzer anlegen | – | – | – | ✓ |
| QR-Typen freigeben | – | – | – | ✓ |
### Was unabhängig von der Anmeldung bleibt
Die Weiterleitung unter `/r/{slug}` ist **immer öffentlich** — sonst könnte
niemand einen gedruckten Code scannen. Sie verrät nur das Ziel, nicht die
Verwaltung.
### Benutzer verwalten
Unter **Verwaltung → Benutzer**: Konten anlegen, Passwörter setzen, Konten
deaktivieren oder löschen.
- Passwörter werden mit **scrypt** gespeichert (Node-Standardbibliothek, kein
natives Modul — dadurch keine Abhängigkeit, die zur Container-Plattform
passen muss).
- Ein neues Passwort oder das Deaktivieren beendet **alle offenen Sitzungen**
des Betroffenen sofort.
- Nach acht Fehlversuchen ist ein Konto 15 Minuten gesperrt; ein Administrator
kann die Sperre vorzeitig aufheben.
- Der **letzte aktive Administrator** kann sich weder selbst deaktivieren noch
sich die Rechte entziehen noch gelöscht werden.
- Passwörter kann nur ein Administrator setzen. Einen Versand per E-Mail gibt
es bewusst nicht — das spart einen SMTP-Zugang.
### Gruppen verwalten
Unter **Verwaltung → Gruppen**: Gruppen anlegen und Mitglieder mit Rollen
zuweisen. Eine Gruppe, die noch Codes enthält, lässt sich nicht löschen —
sonst führten gedruckte Codes ins Leere.
---
## Technischer Aufbau
### Stack
| Baustein | Verwendet |
| ------------ | ------------------------------------------------------- |
| Framework | Next.js 16 (App Router, TypeScript), Frontend + API |
| Oberfläche | React 19, Tailwind CSS 4 |
| Datenbank | PostgreSQL 18 |
| ORM | Prisma 7 mit `@prisma/adapter-pg` |
| QR-Erzeugung | `qrcode`, serverseitig, Ausgabe als PNG und SVG |
| Validierung | Zod 4, im Browser und noch einmal auf dem Server |
Der Stack läuft ohne Reverse Proxy: Der App-Container wird direkt per
Port-Mapping veröffentlicht.
### Verzeichnisse
```
prisma/
schema.prisma Datenmodell (Modell QrCode)
migrations/ Versionierte Migrationen
docker/
entrypoint.sh Migration + Serverstart im Container
update.sh / update.ps1 Update-Skript (git pull + Neubau + Prüfung)
src/
app/
page.tsx Startseite mit dem Kachel-Grid
erstellen/[type]/ Generator je QR-Typ
meine-codes/ Übersicht der dynamischen Codes
r/[slug]/route.ts Weiterleitung dynamischer Codes (302)
api/
qr/preview/ Live-Vorschau (SVG als Data-URL)
qr/download/ Download als PNG oder SVG
codes/ Anlegen und Auflisten dynamischer Codes
codes/[id]/ Ändern und Löschen
codes/[id]/qr/ Bild eines gespeicherten Codes
components/
qr-forms/ Formulare je QR-Typ + deren Registry
qr-generator.tsx Generator-Oberfläche
codes-list.tsx „Meine Codes“
design-panel.tsx Farben, Größe, Fehlerkorrektur, Rand
lib/
qr/
type-module.ts Vertrag eines QR-Typs (defineQrType)
registry.ts Registry aller 15 Typen
types/website.ts Der einzige aktive Typ in V1
design.ts Gestaltungsoptionen + Zod-Schema
contrast.ts Kontrastbewertung
render.ts PNG-/SVG-Erzeugung
request.ts Anfrage-Schema und Auflösung des Code-Inhalts
db.ts Prisma-Client
env.ts Laufzeit-Konfiguration
```
### Datenmodell
Nur dynamische Codes werden gespeichert. Das Modell `QrCode` hält neben Slug,
Zähler und Gestaltung ein generisches `data`-Feld (JSONB) sowie den Typ — so
brauchen die Typen aus Version 2 **keine** Schemaänderung.
### Wie ein QR-Typ aufgebaut ist
Jeder Typ ist ein Modul, das über `defineQrType` erzeugt wird und vier Dinge
beisteuert: Metadaten (Titel, Beschreibung, Icon), ein Zod-Schema, die
Kodierung des Inhalts und eine Kurzfassung für Listen.
```ts
export const websiteType = defineQrType({
id: "website",
title: "Website",
description: "Verlinke eine beliebige Webadresse.",
icon: "globe",
active: true,
supportsDynamic: true,
schema: websiteDataSchema,
defaultData: { url: "" },
encode: (data) => data.url,
summary: (data) => describe(data.url),
});
```
Innerhalb des Moduls arbeitet man mit dem echten, aus dem Zod-Schema
abgeleiteten Datentyp; nach außen zeigt die Registry eine einheitliche
Schnittstelle. Deshalb kennen Vorschau, Download, Speicherung und
Weiterleitung keinen einzigen typspezifischen Sonderfall.
### Warum 302 und kein 301?
Die Weiterleitung unter `/r/{slug}` antwortet mit **302 (temporär)** und
`Cache-Control: no-store`. Ein permanenter Redirect würde in Browsern hängen
bleiben: Zieländerungen kämen nicht an und der Scan-Zähler bliebe stehen.
---
## Entwicklung ohne Docker
Voraussetzung: Node.js 20.9+ und eine erreichbare PostgreSQL-Instanz.
```bash
npm install
# Datenbank z. B. als Einzelcontainer starten
docker run -d --name qr-db -p 5432:5432 \
-e POSTGRES_USER=qrcode -e POSTGRES_PASSWORD=devpass -e POSTGRES_DB=qrcode \
postgres:18-alpine
# .env anlegen und DATABASE_URL auf localhost zeigen lassen:
# DATABASE_URL=postgresql://qrcode:devpass@localhost:5432/qrcode?schema=public
npm run db:migrate # Migrationen anwenden und Prisma Client erzeugen
npm run dev # http://localhost:3000
```
Weitere Skripte:
```bash
npm run build # Produktionsbuild
npm run typecheck # TypeScript prüfen
npm run lint # ESLint
npm run db:studio # Prisma Studio (Datenbank ansehen)
```
> **Hinweis zu npm 11:** Installationsskripte sind standardmäßig blockiert.
> Die Pakete, die welche ausführen dürfen (Prisma lädt darüber seine
> Engine-Binaries), stehen im Feld `allowScripts` der `package.json`.
---
## API
Alle Endpunkte antworten im Fehlerfall mit `{ "error": "…" }` und einer
deutschen Meldung.
| Methode | Pfad | Zweck |
| -------- | ----------------------- | ---------------------------------------------- |
| `POST` | `/api/qr/preview` | Vorschau als SVG-Data-URL |
| `POST` | `/api/qr/download` | PNG- oder SVG-Datei |
| `GET` | `/api/codes` | Alle dynamischen Codes |
| `POST` | `/api/codes` | Dynamischen Code anlegen |
| `GET` | `/api/codes/{id}` | Einzelnen Code lesen |
| `PATCH` | `/api/codes/{id}` | Ziel, Name oder Gestaltung ändern |
| `DELETE` | `/api/codes/{id}` | Code löschen |
| `GET` | `/api/codes/{id}/qr` | Bild des Codes (`?format=png\|svg&download=1`) |
| `GET` | `/r/{slug}` | Weiterleitung, erhöht den Scan-Zähler |
| `GET` | `/api/admin/types` | Zustand aller QR-Typen |
| `PATCH` | `/api/admin/types` | Typ freigeben oder abschalten |
Beispiel:
```bash
curl -X POST http://localhost:3000/api/codes \
-H 'Content-Type: application/json' \
-d '{
"type": "website",
"data": { "url": "https://example.com" },
"design": { "fgColor": "#0F172A", "bgColor": "#FFFFFF",
"size": 512, "ecLevel": "M", "margin": 4 },
"label": "Plakat Eingang"
}'
```
---
## Ausblick Version 2
Geplant sind die übrigen 14 Typen: vCard, App-Link, Mehrere Links, Social
Media, WhatsApp, PDF, Bild, Video, E-Mail, SMS, Standort, Zahlung, WLAN und
Feedback.
Die Architektur ist darauf ausgelegt — ein neuer Typ besteht aus:
1. **einem Modul** in `src/lib/qr/types/` (Zod-Schema, Kodierung, Kurzfassung),
2. **einer Formular-Komponente** in `src/components/qr-forms/`,
3. **je einem Eintrag** in `src/lib/qr/registry.ts` (ersetzt dort den
`defineComingSoonType`-Platzhalter) und in
`src/components/qr-forms/index.ts`.
Am bestehenden Code aus Version 1 ist dafür nichts zu ändern: Vorschau,
Download, Gestaltung, Speicherung dynamischer Codes und die Weiterleitung
arbeiten für alle Typen gleich.
Typen, die statt einer Weiterleitung eine eigene Landingpage brauchen (etwa
„Mehrere Links“ oder „Feedback“), setzen dafür `redirectTarget` auf eine
interne Adresse — auch dafür ist keine Änderung am Datenmodell nötig, weil
`data` als JSONB gespeichert wird.