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.
|
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
|
## 2026-08-21 — Unique, chronologically sortable filenames
|
||||||
|
|
||||||
### Fixed
|
### 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**.
|
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
|
Zeitzone `KLARBILD_TZ` (Standard `Europe/Berlin`). Nachbenennung der Bibliothek und erneute Auslieferung
|
||||||
im Admin unter „Dateinamen" (`src/lib/renames.ts`), idempotent.
|
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
|
## Datenmodell / Migrationen
|
||||||
Nummerierte SQL in `migrations/`, beim Start automatisch (kein ORM-Push). Wichtige Spalten:
|
Nummerierte SQL in `migrations/`, beim Start automatisch (kein ORM-Push). Wichtige Spalten:
|
||||||
`items.has_alpha`, `jobs.status` (inkl. `failed`), `jobs.finished_at`,
|
`items.has_alpha`, `jobs.status` (inkl. `failed`), `jobs.finished_at`,
|
||||||
`settings.output_ext` / `recipes.output_ext`, `settings.*_metadata_sidecar`, `delivery_targets.*`,
|
`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
|
## Tests
|
||||||
`npm test` — Node-Testrunner mit nativem Type-Stripping (`node --experimental-strip-types`).
|
`npm test` — Node-Testrunner mit nativem Type-Stripping (`node --experimental-strip-types`).
|
||||||
|
|||||||
@@ -9,6 +9,38 @@ import Base from '../layouts/Base.astro';
|
|||||||
</div>
|
</div>
|
||||||
<p class="lead">Was in Klarbild neu ist. Neueste Änderungen oben.</p>
|
<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>
|
<section>
|
||||||
<div class="ver"><span class="tag">21.08.2026</span><h2>Eindeutige Dateinamen — Bilder überschreiben sich nicht mehr</h2></div>
|
<div class="ver"><span class="tag">21.08.2026</span><h2>Eindeutige Dateinamen — Bilder überschreiben sich nicht mehr</h2></div>
|
||||||
<ul>
|
<ul>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const BODY = `# Klarbild
|
|||||||
|
|
||||||
Basis-URL: https://klarbild.heidrich-digital.de
|
Basis-URL: https://klarbild.heidrich-digital.de
|
||||||
Öffentliche Vorstellung des Druckmoduls (ohne Login, reine Beschreibung): /vorstellung
|
Ö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.
|
Sprache der Oberfläche: Deutsch. Stack: Astro 5 (SSR) · Postgres · sharp · pg-boss.
|
||||||
|
|
||||||
## Authentifizierung (für Agenten)
|
## 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.
|
- 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.
|
- 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
|
## 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\`.
|
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.
|
- Der Zeitstempel steht vorn, deshalb sortiert jede Dateiliste (Picdrop, NAS, Finder) von allein chronologisch.
|
||||||
|
|||||||
Reference in new issue
Block a user