# 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 . 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 `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. > **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.