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.
931 lines
37 KiB
Markdown
931 lines
37 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.
|
||
|
||
---
|
||
|
||
## Was die Anwendung kann — kurz
|
||
|
||
- **15 QR-Typen:** Website, vCard, App-Link, Mehrere Links, Social Media,
|
||
WhatsApp, PDF, Bild, Video, E-Mail, SMS, Standort, Zahlung, WLAN, Feedback.
|
||
- **Statisch oder dynamisch.** Dynamische Codes zeigen auf eine Kurz-URL:
|
||
Ziel jederzeit änderbar, ohne neu zu drucken — mit Scan-Zähler.
|
||
- **Eigene Seiten** für Linksammlungen, Profile, hinterlegte PDFs und Bilder
|
||
sowie Feedback-Formulare — öffentlich, ohne einen Hinweis auf die Anwendung.
|
||
- **Gestaltung:** freie Farben mit Kontrastwarnung, 256–2048 px,
|
||
Fehlerkorrektur L/M/Q/H, einstellbarer Rand. Download als PNG und SVG.
|
||
- **Meine Codes:** Liste oder Karten, Suche, Sortierung, Gruppenfilter,
|
||
Ziel ändern, löschen.
|
||
- **Feedback auswerten:** Durchschnitt, Verteilung und Kommentare — anonym
|
||
erhoben, ohne Name und IP-Adresse.
|
||
- **Anmeldung abschaltbar.** Mit Anmeldung: Benutzer, Gruppen und die Rollen
|
||
Administrator, Bearbeiter und Betrachter.
|
||
- **Mehrere Domains** für Kurz-URLs, den Gruppen zugeteilt.
|
||
- **Single Sign-on** über Authentik oder einen anderen OIDC-Anbieter.
|
||
- **Typen abschaltbar:** Was ihr nicht braucht, erscheint gar nicht erst.
|
||
- **Eigenes Erscheinungsbild:** Name, Logo, Favicon und Akzentfarbe im
|
||
Admin-Bereich einstellbar.
|
||
- **Zwei Container, kein Reverse Proxy nötig.** Migrationen laufen beim Start
|
||
von selbst; alles Nötige steht in einer `.env`.
|
||
|
||
→ Ausführlich: **[Alle Funktionen im Detail](docs/funktionen.md)**
|
||
|
||
---
|
||
|
||
## Anleitungen
|
||
|
||
- **[Bedienungsanleitung mit Bildern](docs/anleitung.md)** — vom ersten Code
|
||
bis zur Auswertung von Rückmeldungen
|
||
- **[Anmeldung über Authentik (OIDC)](docs/authentik.md)** — Einrichtung,
|
||
Rollen und der Weg zurück
|
||
- **[Alle Funktionen im Detail](docs/funktionen.md)** — die lange Fassung
|
||
- **[Roadmap](docs/roadmap.md)** — was gebaut ist und was kommen könnte
|
||
- **[Anmeldung und Sitzungen nach OWASP](docs/sicherheit.md)** — Durchsicht,
|
||
Härtungen und was bewusst offen bleibt
|
||
|
||
---
|
||
|
||
## 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)
|
||
- [Einen weiteren Typ ergänzen](#einen-weiteren-typ-ergänzen)
|
||
|
||
---
|
||
|
||
## 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"
|
||
}'
|
||
```
|
||
|
||
---
|
||
|
||
## Einen weiteren Typ ergänzen
|
||
|
||
Alle 15 vorgesehenen Typen sind gebaut. Kommt ein sechzehnter dazu, besteht er
|
||
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` und in
|
||
`src/components/qr-forms/index.ts`,
|
||
4. **einer Landingpage** in `src/components/landing/`, falls er `hasLandingPage`
|
||
meldet.
|
||
|
||
Am übrigen Code ist dafür nichts zu ändern: Vorschau, Download, Gestaltung,
|
||
Speicherung, Weiterleitung, Liste und Verwaltung arbeiten für alle Typen
|
||
gleich. Die Kennung gehört zusätzlich in `QR_TYPE_IDS` in
|
||
`src/lib/qr/type-module.ts`.
|
||
|
||
Für einen angekündigten, aber noch nicht gebauten Typ gibt es
|
||
`defineComingSoonType`: Er erscheint als Kachel mit dem Badge „Bald
|
||
verfügbar“ und lässt sich nicht öffnen.
|
||
|
||
Braucht der Typ eine hochgeladene Datei, meldet er ihre Ids über `uploadIds` —
|
||
daran hängt, was beim Löschen des Codes mitgeht und was als verwaist
|
||
weggeräumt wird.
|
||
|
||
### Was als Nächstes sinnvoll wäre
|
||
|
||
- **Gruppen aus dem Verzeichnis übernehmen.** Die OIDC-Anbindung übernimmt
|
||
heute bewusst nur die Identität. Eine Zuordnung von Verzeichnisgruppen zu
|
||
Gruppen dieser Anwendung wäre möglich, braucht aber eine klare Regel dafür,
|
||
was mit von Hand vergebenen Rollen passiert.
|
||
- **Statistik über die Zeit.** Der Zähler weiß heute nur, wie oft und wann
|
||
zuletzt. Ein Verlauf je Tag wäre eine eigene Tabelle — und die Frage, wie
|
||
lange man ihn aufbewahrt.
|
||
- **Codes exportieren.** Eine CSV aller Codes einer Gruppe, für die Übergabe
|
||
an Druckereien.
|