docs: sharing, mail and provenance in changelog, README and llms.txt
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BNhLunz586g6DeYMa7Qsd1
This commit is contained in:
4 files changed
+173
-2
No files matched your search
@@ -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/<slug>`) 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
|
||||
|
||||
@@ -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/<slug>`, ö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`).
|
||||
|
||||
@@ -9,6 +9,38 @@ import Base from '../layouts/Base.astro';
|
||||
</div>
|
||||
<p class="lead">Was in Klarbild neu ist. Neueste Änderungen oben.</p>
|
||||
|
||||
<section>
|
||||
<div class="ver"><span class="tag">01.09.2026</span><h2>Bilder teilen, E-Mail, Passwort vergessen</h2></div>
|
||||
<ul>
|
||||
<li><b>Teilen-Links.</b> In der Bibliothek auf <b>Teilen</b> — einzeln oder für mehrere markierte
|
||||
Bilder auf einmal. Es entsteht ein Link, den auch jemand <b>ohne Klarbild-Konto</b> öffnen kann;
|
||||
von dort lässt sich das Bild in voller Auflösung herunterladen.</li>
|
||||
<li><b>Weitergeben per WhatsApp</b> (öffnet WhatsApp mit fertigem Text), über dein eigenes
|
||||
Mailprogramm, oder direkt aus Klarbild heraus per E-Mail an mehrere Empfänger auf einmal.</li>
|
||||
<li><b>Sicher voreingestellt:</b> Jeder Link <b>läuft von selbst ab</b> (Standard 30 Tage, im Admin
|
||||
änderbar), lässt sich jederzeit <b>abschalten</b> und wieder freigeben, auf Wunsch mit einem
|
||||
<b>Zugangswort</b> schützen, und Herunterladen kann man ganz sperren. Suchmaschinen bekommen die
|
||||
Seiten nie zu sehen. Bilder aus privaten Sitzungen lassen sich gar nicht teilen.</li>
|
||||
<li><b>Übersicht unter <a href="/teilen">Geteilte Links</a>:</b> Aufrufe, Downloads und die Zahl der
|
||||
Besuchenden je Link. Festgehalten wird <b>wann</b> und mit welcher Art Gerät geöffnet wurde —
|
||||
nicht <b>wer</b>. Nach 90 Tagen wird das gelöscht.</li>
|
||||
<li><b>E-Mail-Versand</b> über dein eigenes Postfach (Admin → E-Mail). Damit funktionieren:
|
||||
<b>Passwort vergessen</b>, ein <b>Hinweis wenn ein Auftrag fertig ist</b> (unter
|
||||
<a href="/konto">Mein Konto</a> einschaltbar) und der Versand von Teilen-Links.</li>
|
||||
<li><b>Passwort vergessen?</b> Auf der Anmeldeseite unten. Der Link aus der Mail gilt eine Stunde
|
||||
und lässt sich nur einmal verwenden. Dafür muss unter <b>Mein Konto</b> eine E-Mail-Adresse
|
||||
hinterlegt sein.</li>
|
||||
<li><b>Behoben:</b> 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.</li>
|
||||
<li><b>Behoben:</b> Bei freigestellten Motiven wirkte „Herunterladen sperren" nicht.</li>
|
||||
<li><b>KI-Bilder tragen jetzt eine maschinenlesbare Kennzeichnung</b> (IPTC/XMP) samt verwendetem
|
||||
Modell — direkt in der Bilddatei.</li>
|
||||
<li>Kleinigkeiten: farbige Markierung liegt nicht mehr auf dem Auswahlkreis; die Kopfleiste passt
|
||||
wieder aufs Tablet; der Admin-Bereich ist am Handy bedienbar.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="ver"><span class="tag">21.08.2026</span><h2>Eindeutige Dateinamen — Bilder überschreiben sich nicht mehr</h2></div>
|
||||
<ul>
|
||||
|
||||
@@ -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/<slug> (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/<slug>\` 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/<slug>/file?item=<uuid>\` — 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/<id>\` — 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_<motiv>_<format>_<kürzel>.<ext>\` — 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.
|
||||
|
||||
Reference in new issue
Block a user