From 5a438e2c1903c4665ef2d78afbde9544f9dd7d76 Mon Sep 17 00:00:00 2001 From: Till Heidrich Date: Tue, 1 Sep 2026 03:43:04 +0000 Subject: [PATCH] docs: sharing, mail and provenance in changelog, README and llms.txt Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01BNhLunz586g6DeYMa7Qsd1 --- CHANGELOG.md | 72 +++++++++++++++++++++++++++++++++++++++ README.md | 30 ++++++++++++++-- src/pages/changelog.astro | 32 +++++++++++++++++ src/pages/llms.txt.ts | 41 ++++++++++++++++++++++ 4 files changed, 173 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac2ca99..14dd10b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,78 @@ All notable changes to Klarbild are documented here. Newest first. +## 2026-09-01 — Sharing, e-mail, password reset, and a security pass + +### Added +- **Share links** (`/s/`) for a single image or a collection: someone without an + account can view them and download at full resolution. These are photos of children, + so the defaults lean safe — every link expires by itself (30 days, configurable), can + be revoked and un-revoked, optionally carries a passphrase, download can be switched + off entirely, and pages and files are noindex. Access is bound to the link, not the + image: swapping in another item id on a valid link returns 404. Private-session + results can never be shared. +- **Passing it on** by e-mail (several recipients, no attachment — a link can be + withdrawn later, an attachment cannot) or via `wa.me`, which opens WhatsApp with the + text prepared. The WhatsApp Business API needs a Meta account, approved templates and + per-conversation fees; this works today for free and the API can slot in behind it. +- **E-mail through the account's own mailbox** (SMTP, credentials AES-256-GCM encrypted + like every other secret). Powers password reset, optional job-done notifications, and + mailing share links. nodemailer has zero dependencies and no native postinstall — + checked before adding it, given the history. +- **Password reset**: single-use token, only its hash stored, one hour, identical + response whether or not the address exists, rate limited per address and per sender. +- **`/konto`** for the e-mail address and notifications, **`/teilen`** for managing links + with view and download counts. +- **IPTC/XMP provenance** on AI results (`trainedAlgorithmicMedia`, or + `compositeWithTrainedAlgorithmicMedia` for edited photos) plus the model, written by + hand into PNG iTXt and JPEG APP1 rather than pulling in a sharp upgrade. exiftool reads + it back from both formats; pixels, dimensions and dpi provably untouched. +- **CSP with real hashes** for every inline script, plus nosniff, referrer-policy, + X-Frame-Options and a permissions-policy. `style-src` stays open because React sets + styles as attributes and attributes cannot be hashed — measured, the hash-only policy + breaks the print preview. +- `mem_limit: 3g` in compose. sharp can spike; without a limit an outlier takes the whole + container down instead of just the process. + +### Fixed +- **Sessions never expired server-side and survived a password reset.** `Max-Age` is a + request to the browser, not a rule for the server. A leaked cookie stayed valid + indefinitely, and the very reset meant to rescue a compromised account left the + intruder logged in; a demoted admin also kept admin, because the role came from the + cookie. Cookies now carry a session epoch, checked against the database on every + request, and the epoch is bumped on password reset and on any role change. +- **"Allow download: off" did nothing for images with transparency** — exactly the + cutouts and stickers this tool exists for. Verified: now 1500x2000 instead of the + 2400x3200 original, and the download route returns 403. +- **The e-mail settings card saved nothing** while reporting success. Found by the design + critique an hour after shipping. +- **The public page explained how to bypass "view only"**, printing the long-press hint + regardless of the setting. +- The share title went into outgoing mail HTML unescaped while the message was escaped. +- argon2 ran on every request for protected links, including every image (130 ms, 64 MiB + each). Checked once now, then a short-lived signed cookie scoped to that link. The + passphrase moved from the query string to POST. +- `days` accepted nonsense and quietly produced an unlimited link (`Number('abc')` is + NaN, `NaN > 0` is false; `[]` coerces to 0). +- Rate limits keyed on the *first* X-Forwarded-For value, which the caller sets. Counting + from the right now, trusted-proxy count configurable. +- **The path-escape hole in delivery** the wiki had flagged twice as open. +- The crop maths lived twice with nothing holding the two copies together. One module + now, 60000 randomised cases asserting both entry points agree — which immediately + found a real bug: the zoom limits also clamped the *initial* crop, so an extreme format + silently broke the aspect ratio. +- Smaller: expired links could still be mailed; missing PUBLIC_BASE_URL sent dead links + while reporting success; the reset response time revealed whether an account existed; + `share_views` grew without bound; double provenance marking appended a second packet + with the older model; the colour flag covered the selection circle in the library; the + seventh nav item pushed logout off screen at 820px; `/konto` was unreachable on a + phone; network failures rendered as empty states. + +### Changed +- 42 new tests (97 total). `shares`, `mail` and `provenance` had none, and that is where + both reviews found the most. +- `PRODUCT.md` and `DESIGN.md` added, written from the code and the wiki. + ## 2026-08-21 — Unique, chronologically sortable filenames ### Fixed diff --git a/README.md b/README.md index 34b27e4..8b3b04c 100644 --- a/README.md +++ b/README.md @@ -57,13 +57,39 @@ Migrationen und Seeds laufen beim Serverstart automatisch. Erststart-Passwörter chronologisch; Kürzel aus der Item-ID → zwei Bilder können sich beim Ausliefern **nicht mehr überschreiben**. Zeitzone `KLARBILD_TZ` (Standard `Europe/Berlin`). Nachbenennung der Bibliothek und erneute Auslieferung im Admin unter „Dateinamen" (`src/lib/renames.ts`), idempotent. -- **Admin:** Key/Picdrop/Modelle/Nutzer/Kostendeckel/Presets/Speicherverwaltung, Dateinamen, Picdrop-Diagnose. +- **Teilen (`/s/`, öffentlich):** ein Bild oder eine Sammlung ohne Konto zugänglich machen — + ansehen und in voller Auflösung herunterladen. Voreinstellungen bewusst vorsichtig: **Ablauf** + (Standard 30 Tage), **widerrufbar**, optionales **Zugangswort** (argon2), **Download abschaltbar**, + `noindex`. Zugriff hängt am **Link**, nicht am Bild (fremde `item`-Kennung ⇒ 404); private Sitzungen + lassen sich nie teilen. Verwaltung unter `/teilen` mit Aufrufen und Downloads. Gezählt wird *wann* + und *womit*, nicht *wer*: IP nur gehasht, gesalzen mit Server-Geheimnis **und** Link-Kennung, eine + Zeile je Besucher und Stunde, Löschung nach 90 Tagen. Kern: `src/lib/shares.ts`. +- **Weitergeben:** per E-Mail aus Klarbild heraus (mehrere Empfänger, **kein Anhang** — ein Link lässt + sich zurückziehen), über das eigene Mailprogramm oder per `wa.me`-Link, der WhatsApp mit fertigem + Text öffnet. (Server-seitiger WhatsApp-Versand bräuchte die Business-API: Meta-Konto, genehmigte + Vorlagen, Gebühren je Konversation.) +- **E-Mail (`src/lib/mail.ts`):** SMTP über das **eigene Postfach** — SPF/DKIM stimmen dadurch von + selbst und kein Versanddienst sieht mit. Zugang AES-256-GCM in `settings`. Trägt Passwort-Reset + (Einmal-Token, nur Hash gespeichert, 60 min, gleiche Antwort für unbekannte Adressen), + Auftragshinweise (je Nutzer, `/konto`) und den Versand von Teilen-Links. +- **Sitzungen:** signiertes Cookie **plus** serverseitige Prüfung (`session_epoch`). Passwortwechsel und + Rollenänderung entwerten alle offenen Anmeldungen; die Rolle kommt aus der Datenbank, nicht aus dem + Cookie. +- **KI-Kennzeichnung (`src/lib/provenance.ts`):** IPTC `DigitalSourceType` als XMP direkt in der Datei + (`trainedAlgorithmicMedia` bzw. `compositeWithTrainedAlgorithmicMedia`) samt Modell — von Hand in + PNG-`iTXt` und JPEG-`APP1` geschrieben, ohne neue native Abhängigkeit. Mit exiftool prüfbar. +- **Sicherheits-Header:** CSP mit echten Hashes für jedes inline-Skript (`experimental.csp`), dazu + `nosniff`, `referrer-policy`, `X-Frame-Options`, `permissions-policy`. `style-src` bleibt offen, weil + React Stile als Attribute setzt und Attribute sich nicht hashen lassen. +- **Admin:** Key/Picdrop/Modelle/Nutzer/Kostendeckel/Presets/Speicherverwaltung, E-Mail, Teilen-Links, + Dateinamen, Picdrop-Diagnose. ## Datenmodell / Migrationen Nummerierte SQL in `migrations/`, beim Start automatisch (kein ORM-Push). Wichtige Spalten: `items.has_alpha`, `jobs.status` (inkl. `failed`), `jobs.finished_at`, `settings.output_ext` / `recipes.output_ext`, `settings.*_metadata_sidecar`, `delivery_targets.*`, -`print_presets` (Bogen-Vorlagen, `013`). +`print_presets` (Bogen-Vorlagen, `013`), `shares`/`share_items`/`share_views` + `settings.smtp_*` + +`users.email`/`notify_jobs` (`014`), `users.session_epoch` (`015`). ## Tests `npm test` — Node-Testrunner mit nativem Type-Stripping (`node --experimental-strip-types`). diff --git a/src/pages/changelog.astro b/src/pages/changelog.astro index b038998..07b1f47 100644 --- a/src/pages/changelog.astro +++ b/src/pages/changelog.astro @@ -9,6 +9,38 @@ import Base from '../layouts/Base.astro';

Was in Klarbild neu ist. Neueste Änderungen oben.

+
+
01.09.2026

Bilder teilen, E-Mail, Passwort vergessen

+
    +
  • Teilen-Links. In der Bibliothek auf Teilen — einzeln oder für mehrere markierte + Bilder auf einmal. Es entsteht ein Link, den auch jemand ohne Klarbild-Konto öffnen kann; + von dort lässt sich das Bild in voller Auflösung herunterladen.
  • +
  • Weitergeben per WhatsApp (öffnet WhatsApp mit fertigem Text), über dein eigenes + Mailprogramm, oder direkt aus Klarbild heraus per E-Mail an mehrere Empfänger auf einmal.
  • +
  • Sicher voreingestellt: Jeder Link läuft von selbst ab (Standard 30 Tage, im Admin + änderbar), lässt sich jederzeit abschalten und wieder freigeben, auf Wunsch mit einem + Zugangswort schützen, und Herunterladen kann man ganz sperren. Suchmaschinen bekommen die + Seiten nie zu sehen. Bilder aus privaten Sitzungen lassen sich gar nicht teilen.
  • +
  • Übersicht unter Geteilte Links: Aufrufe, Downloads und die Zahl der + Besuchenden je Link. Festgehalten wird wann und mit welcher Art Gerät geöffnet wurde — + nicht wer. Nach 90 Tagen wird das gelöscht.
  • +
  • E-Mail-Versand über dein eigenes Postfach (Admin → E-Mail). Damit funktionieren: + Passwort vergessen, ein Hinweis wenn ein Auftrag fertig ist (unter + Mein Konto einschaltbar) und der Versand von Teilen-Links.
  • +
  • Passwort vergessen? Auf der Anmeldeseite unten. Der Link aus der Mail gilt eine Stunde + und lässt sich nur einmal verwenden. Dafür muss unter Mein Konto eine E-Mail-Adresse + hinterlegt sein.
  • +
  • Behoben: Ein neues Passwort beendet jetzt auch alle noch offenen Anmeldungen. Vorher + blieb eine bestehende Sitzung bis zu 30 Tage gültig — wer sein Passwort zurücksetzte, weil + jemand anderes hineingekommen war, hatte davon also nichts.
  • +
  • Behoben: Bei freigestellten Motiven wirkte „Herunterladen sperren" nicht.
  • +
  • KI-Bilder tragen jetzt eine maschinenlesbare Kennzeichnung (IPTC/XMP) samt verwendetem + Modell — direkt in der Bilddatei.
  • +
  • Kleinigkeiten: farbige Markierung liegt nicht mehr auf dem Auswahlkreis; die Kopfleiste passt + wieder aufs Tablet; der Admin-Bereich ist am Handy bedienbar.
  • +
+
+
21.08.2026

Eindeutige Dateinamen — Bilder überschreiben sich nicht mehr

    diff --git a/src/pages/llms.txt.ts b/src/pages/llms.txt.ts index c759462..6030745 100644 --- a/src/pages/llms.txt.ts +++ b/src/pages/llms.txt.ts @@ -19,6 +19,7 @@ const BODY = `# Klarbild Basis-URL: https://klarbild.heidrich-digital.de Öffentliche Vorstellung des Druckmoduls (ohne Login, reine Beschreibung): /vorstellung +Öffentliche Teilen-Links: /s/ (ohne Login, nur was der Link freigibt) Sprache der Oberfläche: Deutsch. Stack: Astro 5 (SSR) · Postgres · sharp · pg-boss. ## Authentifizierung (für Agenten) @@ -120,6 +121,46 @@ Werkzeug \`klarbild\` (mcp/klarbild-mcp.mjs), nutzt denselben API-Token: - exact_size — OHNE KI: ein Bild exakt auf ein Maß bringen, Datei lokal speichern. - print_sheet — OHNE KI: Druckbogen (mehrere Bilder × Stückzahl, Schnittmarken) als PDF lokal speichern. +## Teilen-Links (öffentlich, ohne Konto) +Ein Bild oder eine Sammlung unter \`/s/\` zugänglich machen — der Empfänger braucht kein Konto. +- Voreinstellungen sind bewusst vorsichtig (auf den Bildern sind Kinder): **Ablauf** (Standard 30 Tage, + \`settings.share_default_days\`), jederzeit **widerrufbar** und wieder freigebbar, optionales + **Zugangswort** (argon2), **Herunterladen abschaltbar**, Seiten und Dateien tragen \`X-Robots-Tag: noindex\`. +- Der Zugriff hängt am **Link**, nicht am Bild: Eine fremde \`item\`-Kennung an einem gültigen Link + liefert 404. Bilder aus privaten Sitzungen lassen sich gar nicht teilen. +- Zustände: 200 (gültig) · 401 (Zugangswort nötig oder falsch) · 404 (unbekannt) · 410 (abgelaufen oder + widerrufen). Das Zugangswort geht per **POST**, danach quittiert ein kurzlebiges Cookie je Link. +- \`GET /api/s//file?item=\` — Ansichtsfassung (auf 2000 px verkleinert). + \`&download=1\` liefert das Original; bei gesperrtem Download 403. \`&thumb=1\` für die Übersicht. +- \`GET|POST /api/shares\` — eigene Links auflisten bzw. anlegen. Body: \`{ itemIds[], title?, note?, + days?, allowDownload?, password? }\`. \`days\` muss eine Zahl von 0–3650 sein (0 = unbegrenzt); + alles andere wird **abgelehnt**, nicht als „unbegrenzt" gedeutet. +- \`GET|PATCH|DELETE /api/shares/\` — Zugriffe ansehen, ändern (\`action: 'revoke'|'unrevoke'\`, + \`title\`, \`note\`, \`days\`, \`allowDownload\`, \`password\`), löschen. +- \`POST /api/shares/send\` — Link per Mail verschicken: \`{ shareId, to, message? }\`, höchstens 10 + Empfänger je Anfrage, 50 Mails je Stunde und Absender. **Kein Anhang** — ein Link lässt sich + zurückziehen, ein Anhang nicht. +- WhatsApp läuft über einen \`wa.me\`-Link im Browser (kein Server-Versand; die WhatsApp-Business-API + bräuchte Meta-Konto, genehmigte Vorlagen und Gebühren). +- Gezählt wird **wann** und mit welcher Art Gerät geöffnet wurde, nicht **wer**: von der IP nur ein + Hash, gesalzen mit Server-Geheimnis **und** Link-Kennung (also nicht über Links hinweg verknüpfbar), + höchstens eine Zeile je Besucher und Stunde, Löschung nach 90 Tagen. + +## E-Mail +Versand über das eigene Postfach (SMTP, Zugang verschlüsselt in \`settings\`; Admin → E-Mail). +Gebraucht für: Passwort zurücksetzen, Hinweis bei fertigen Aufträgen (je Nutzer schaltbar unter +\`/konto\`), Teilen-Links verschicken. Ohne eingerichteten Versand melden die betroffenen Routen 503. +- \`POST /api/auth/reset { email }\` — Rücksetz-Link anfordern. Antwortet **immer gleich**, egal ob es + die Adresse gibt. \`POST /api/auth/reset { token, password }\` — neues Passwort setzen; Token gilt + 60 Minuten, genau einmal, und entwertet alle offenen Sitzungen des Kontos. +- \`GET|PATCH /api/me\` — eigenes Konto: \`email\`, \`notify_jobs\`. + +## KI-Kennzeichnung +Ergebnisse tragen IPTC \`DigitalSourceType\` als XMP in der Datei: +\`trainedAlgorithmicMedia\` (aus Text erzeugt) bzw. \`compositeWithTrainedAlgorithmicMedia\` +(vorhandenes Foto verändert), dazu \`xmp:CreatorTool\` mit dem verwendeten Modell. +Auslesbar mit exiftool. Das Druckmodul erzeugt nichts mit KI und kennzeichnet folglich nichts. + ## Dateinamen der Ergebnisse Schema: \`JJJJ-MM-TT_HHMMSS___.\` — z. B. \`2026-08-20_143207_lea-badge_the-frame_a3f9.jpg\`. - Der Zeitstempel steht vorn, deshalb sortiert jede Dateiliste (Picdrop, NAS, Finder) von allein chronologisch.