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
876 lines
34 KiB
Markdown
876 lines
34 KiB
Markdown
# 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 **alle 15 QR-Typen**. Welche davon in der Oberfläche erscheinen,
|
||
entscheidet die Verwaltung — nicht gebrauchte lassen sich 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)
|
||
- [Anmeldung über Authentik (OIDC)](#anmeldung-über-authentik-oidc)
|
||
- [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 — 15
|
||
Kacheln sind viel, wenn ihr ohnehin nur drei davon braucht. Ausgeblendete
|
||
Typen verschwinden aus der Auswahl; bereits erstellte Codes dieses Typs
|
||
funktionieren unverändert weiter.
|
||
|
||
### 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. Alle 15 Typen sind umgesetzt.
|
||
|
||
| 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 | ✓ | ✓ |
|
||
| Video | die Videoadresse — dynamisch mit Player-Seite | ✓ | ✓ |
|
||
| vCard | vCard 3.0 | ✓ | — |
|
||
| WLAN | `WIFI:` | ✓ | — |
|
||
| App-Link | Weiterleitung je nach Gerät | — | ✓ |
|
||
| Zahlung | GiroCode (EPC069-12) oder Zahlungslink | ✓ | nur Link |
|
||
| Mehrere Links | eigene Seite mit Linkliste | — | ✓ |
|
||
| Social Media | eigene Seite mit den Profilen | — | ✓ |
|
||
| PDF | hinterlegte Datei auf eigener Seite | — | ✓ |
|
||
| Bild | hinterlegte Bilder auf eigener Seite | — | ✓ |
|
||
| Feedback | Bewertungsformular auf eigener Seite | — | ✓ |
|
||
|
||
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.
|
||
- **Mehrere Links, Social Media, PDF, Bild und Feedback** brauchen eine Seite,
|
||
die die Anwendung selbst ausliefert — eine Linkliste oder ein Formular passt
|
||
nicht in einen einzelnen Code. Diese Seiten liegen unter `/s/{slug}`; der
|
||
gedruckte Code zeigt weiterhin auf `/r/{slug}` und wird dorthin
|
||
weitergeleitet. Der Scan-Zähler zählt dabei den Aufruf des Codes, nicht das
|
||
Neuladen der Seite.
|
||
|
||
> **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**.
|
||
|
||
**Liste oder Karten.** Voreingestellt ist die **Liste** — eine Zeile je Code,
|
||
gedacht für viele Projekte nebeneinander. Über die beiden Schalter rechts neben
|
||
der Sortierung wechselst du zur **Kartenansicht** mit großer Vorschau. Die Wahl
|
||
merkt sich der Browser.
|
||
|
||
Dazu kommen
|
||
|
||
- ein **Suchfeld** über Name, Ziel, Kurz-URL und Typ,
|
||
- eine **Sortierung** nach Datum, Name oder Scan-Anzahl,
|
||
- ein **Gruppenfilter**, sobald Codes in mehr als einer Gruppe liegen.
|
||
|
||
Auf schmalen Bildschirmen blendet die Liste die hinteren Spalten aus; die
|
||
Kurz-URL rückt dann unter den Namen. Zum Herunterladen dient dort die
|
||
Kartenansicht.
|
||
|
||
Statische Codes erscheinen hier nicht, weil sie nicht gespeichert werden.
|
||
|
||
Bei einem **Feedback-Code** führt aus der Zeile ein Stern-Symbol zur
|
||
Auswertung: Durchschnitt, Verteilung der Noten und die abgegebenen Kommentare.
|
||
|
||
---
|
||
|
||
### 7. Eigene Seiten: Links, Profile, Dateien, Feedback
|
||
|
||
Fünf Typen haben kein fremdes Ziel, auf das sich weiterleiten ließe — sie
|
||
brauchen eine Seite. Die liefert die Anwendung selbst aus, unter
|
||
`{BASE_URL}/s/{slug}`:
|
||
|
||
| Typ | Was die Seite zeigt |
|
||
| --- | --- |
|
||
| Mehrere Links | Überschrift, kurzer Text, bis zu 20 beschriftete Links |
|
||
| Social Media | dieselbe Form, aber mit Netzwerk-Auswahl aus einem Katalog |
|
||
| PDF | das hinterlegte Dokument mit „Öffnen“ und „Herunterladen“ |
|
||
| Bild | ein Bild oder eine Galerie aus bis zu 10 Bildern mit Bildunterschriften |
|
||
| Feedback | ein Formular mit Sternen oder Gesichtern und optionalem Kommentar |
|
||
|
||
Diese Seiten sind **öffentlich** und zeigen bewusst nichts von der Anwendung:
|
||
keine Navigation, kein „Verwaltung“, kein Hinweis auf den Generator. Wer einen
|
||
Aushang scannt, soll nur den Inhalt sehen.
|
||
|
||
Der gedruckte Code zeigt weiterhin auf `/r/{slug}`; von dort geht es auf die
|
||
Seite. Der Scan-Zähler zählt dabei den Aufruf des Codes — ein Neuladen der
|
||
Seite zählt nicht mit.
|
||
|
||
**Bei Social Media** genügt als Eingabe das Kürzel: aus `@firma` wird beim
|
||
Speichern `https://instagram.com/firma`. Eine vollständige Adresse bleibt
|
||
unverändert, damit auch abweichende Profil-Adressen möglich sind.
|
||
|
||
**Beim Video-Typ** wird der Player von YouTube oder Vimeo erst nach einem Klick
|
||
geladen, und davor steht, dass dabei Daten an den Anbieter gehen. Ungefragt
|
||
einen Drittanbieter einzubinden wäre auf einer Seite, die Fremde nach einem
|
||
Scan zu sehen bekommen, nicht in Ordnung.
|
||
|
||
#### Hochgeladene Dateien
|
||
|
||
PDFs und Bilder liegen **in der Datenbank**, nicht auf der Platte. Damit gibt
|
||
es weiterhin genau ein Volumen zu sichern: Wer die Datenbank sichert, hat
|
||
alles.
|
||
|
||
- Die Grenze steht in der `.env` unter `MAX_UPLOAD_MB` (Voreinstellung 10).
|
||
- Erlaubt sind PDF sowie PNG, JPEG, WebP, GIF und AVIF. **SVG bewusst nicht** —
|
||
eine SVG-Datei darf Skripte enthalten und würde von derselben Adresse wie die
|
||
Anwendung ausgeliefert.
|
||
- Geprüft wird der **Inhalt**, nicht die Dateiendung. Eine Textdatei mit dem
|
||
Namen `harmlos.pdf` wird abgewiesen.
|
||
- Wird ein Code gelöscht, verschwindet seine Datei mit ihm. Eine ausgetauschte
|
||
Datei und eine hochgeladene, aber nie gespeicherte werden nach 24 Stunden
|
||
aufgeräumt.
|
||
|
||
#### Rückmeldungen
|
||
|
||
Ein Feedback-Eintrag besteht aus einer Note von 1 bis 5 und einem optionalen
|
||
Text. Gespeichert wird **kein Name, keine IP-Adresse, keine Gerätekennung** —
|
||
wer einen Aushang scannt und eine Note vergibt, bleibt anonym. Gegen Fluten
|
||
gibt es eine Bremse von 10 Rückmeldungen pro Minute und Absender; die Adresse
|
||
dient dabei nur als Schlüssel im Arbeitsspeicher und wird nirgends gespeichert.
|
||
|
||
> Ohne eingeschaltete Anmeldung sieht und ändert jeder, der die Anwendung
|
||
> erreicht, alle Codes. Für den Einzelplatz- oder LAN-Betrieb ist das gewollt;
|
||
> ins offene Internet gehört die Anwendung dann nicht. Mit `AUTH_ENABLED=true`
|
||
> gibt es Benutzer, Gruppen und Rollen — siehe unten.
|
||
|
||
---
|
||
|
||
## 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.
|
||
|
||
---
|
||
## Anmeldung über Authentik (OIDC)
|
||
|
||
Statt Passwörter hier zu pflegen, lässt sich die Anmeldung an einen
|
||
OpenID-Connect-Anbieter abgeben — getestet mit **Authentik**, ebenso nutzbar
|
||
mit Keycloak, Zitadel oder Entra ID.
|
||
|
||
```bash
|
||
AUTH_ENABLED=true
|
||
OIDC_ENABLED=true
|
||
OIDC_ISSUER=https://auth.example.com/application/o/qrcode-generator/
|
||
OIDC_CLIENT_ID=…
|
||
OIDC_CLIENT_SECRET=…
|
||
```
|
||
|
||
Auf der Anmeldeseite erscheint dann ein Knopf für den Anbieter; das
|
||
Passwortfeld bleibt daneben stehen, bis `OIDC_ONLY=true` gesetzt wird.
|
||
|
||
**Übernommen wird, wer jemand ist** — Kennung, E-Mail-Adresse, Anzeigename —
|
||
und auf Wunsch über `OIDC_ADMIN_GROUP` das Recht zur Verwaltung. Gruppen und
|
||
Rollen bleiben Sache dieser Anwendung: Die Gruppen im Verzeichnis bedeuten
|
||
etwas anderes als die hier, und eine automatische Übersetzung wäre eine Quelle
|
||
stiller Überraschungen.
|
||
|
||
Bestehende Konten mit Passwort werden beim ersten Anmelden anhand der
|
||
**bestätigten** E-Mail-Adresse verknüpft, nicht verdoppelt.
|
||
|
||
→ **Vollständige Anleitung mit allen Feldern in Authentik, Fehlerbildern und
|
||
dem Weg zurück: [docs/authentik.md](docs/authentik.md)**
|
||
|
||
---
|
||
|
||
## 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.
|