28a604954806653e7d76ef1cbaf7279bcdb85657
3
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
14dae8e550 |
fix: design critique findings, and the settings card that saved nothing
The critique found a functional bug I had shipped an hour earlier: the whole
e-mail configuration was never sent. `saveSettings` builds its body from a fixed
field list, and none of the smtp_* fields or share_default_days were in it, and
the password input had no state at all. Filling in the card and pressing save
lost everything while the toast said "Gespeichert." That card is the
precondition for password reset, job notifications and mailing share links, so
it would have looked like the whole feature was broken. Now sent, with the
password held separately like the other secrets, and a failed PATCH no longer
reports success.
The public page told recipients how to bypass "view only": the hint about
long-pressing to save was printed regardless of allow_download, two lines under
a caption saying "Nur ansehen". The backend takes that setting seriously enough
to withhold the original even for cutouts; the page undid it. Now the two cases
have separate copy, worded for Android as well as iOS, and the hint sits above
the grid where it is actually read rather than under twenty tiles.
The share dialog had two blue buttons in step two, the exact mistake DESIGN.md
records as having happened once before, and asked five questions before a link
existed when the common case ("one picture for grandma") needs none of them.
One blue button now, and the rarely-used settings collapse behind a summary
that states what currently applies.
/konto was unreachable on a phone: the only way in was the username in the top
bar, which is hidden below 480px. It is a tab bar entry now, with short labels
so eight items still fit at 390px (49x48px each).
Network failures rendered as content: a failed load showed "nothing shared yet"
alongside an invitation to share something, and /konto and /admin hung on
"loading" forever. All three have an error state with a retry now.
Also: the public page nested a second <main> inside the app shell and inherited
padding for a tab bar anonymous visitors never see; admin inputs were 14px, so
iOS zoomed on focus at tablet width; the admin had no media queries at all, so
"Postausgangsserver" got ~156px on a phone; "Jetzt umbenennen" was clickable
without the dry run PRODUCT.md requires; share_default_days did not reach the
dialog; partial mail success was coloured as failure by a regex on the message
text; the passphrase sheet ignored the home indicator; the privacy line claimed
"encrypted" for what is a salted hash.
Added PRODUCT.md and DESIGN.md, written from the code and the wiki rather than
invented, so future design work has something to be measured against.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BNhLunz586g6DeYMa7Qsd1
|
||
|
|
1d78535662 |
fix: findings from the code review of the mail and sharing work
Two reviews, both dimensions. The serious ones, verified before fixing:
Sessions never expired server-side and survived a password reset. `Max-Age` is
a request to the browser, not a rule for the server, and `readSession` only
checked the signature -- 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. Now the
cookie carries a session epoch, `verifySession` checks the account against the
database on every request and takes the role from there, and the epoch is
bumped on password reset and on any role change. This was pre-existing, but the
reset feature promised safety it did not deliver.
"Allow download: off" did nothing for images with transparency -- exactly the
cutouts and stickers this tool exists for. The view path skipped resizing when
`has_alpha`, so the untouched original went out at full resolution, right-click
saveable. Verified: now 1500x2000 instead of 2400x3200, and the download route
returns 403.
The share title went into outgoing mail HTML unescaped while the message was
escaped -- a foreign link could be posted through the account's own
SPF/DKIM-signed mailserver. Both escaped now, plus length limits.
argon2 ran on every request for passphrase-protected links, including every
image: 130 ms and 64 MiB each. A 40-image gallery would have walked into the
memory limit, and a few dozen wrong guesses could have stalled the process.
Checked once, then a short-lived signed cookie scoped to that one link. The
passphrase moved from the query string to POST (it was landing in history and
proxy logs) and wrong guesses are now rate limited.
`days` accepted nonsense and quietly produced an unlimited link -- `Number('abc')`
is NaN and `NaN > 0` is false. `[]` coerced to 0, same result. Now rejected
outright, which matters when the pictures are of children.
Rate limits keyed on `clientAddress`, which is the *first* X-Forwarded-For
value -- set by the caller. Counting from the right instead, with the number of
trusted proxies configurable (TRUSTED_PROXIES, default 1), plus per-username and
per-recipient counters, a real limit on outgoing mail, and a limiter that
expires entries individually instead of clearing everyone's counter.
Also: expired links could still be mailed; missing PUBLIC_BASE_URL sent dead
links while reporting success; the reset endpoint's response time revealed
whether an account existed (send is no longer awaited); invalid UUIDs produced
500s; the email uniqueness race returned an HTML error page; share_views grew
without bound and is now one row per visitor per hour, hashed per link, user
agent reduced to browser and system, deleted after 90 days; double provenance
marking appended a second XMP packet with the older model instead of replacing.
21 new tests (97 total) for shares, mail and provenance -- all three were
completely untested, and that is where both reviews found the most.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BNhLunz586g6DeYMa7Qsd1
|
||
|
|
63d4b7d894 |
feat: e-mail, password reset and shareable links for images and collections
Mail goes out through the account's own mailbox (SMTP) rather than a sending service: the domain lives there anyway, so SPF and DKIM line up by themselves and no third party gets to see the messages. Credentials are AES-256-GCM encrypted in settings like every other secret, never in the repo. nodemailer has zero dependencies and no native postinstall -- checked before adding it, given the history. Password reset: single-use token, only its hash is stored, one hour, and the request endpoint answers identically whether or not the address exists so it cannot be used to enumerate accounts. Rate limited per address. Share links (/s/<slug>) let someone without an account see the pictures and download them at full resolution. These are photos of children, so the defaults lean safe: every link expires by itself (30 days), can be revoked, optionally carries a passphrase, and the page 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 at all. Tracking records when and with what, never who: IP addresses are only stored salted-and-hashed, so repeat visitors can be counted but nobody identified. Sharing goes out by mail (several recipients at once, 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 would need a Meta account, approved templates and per-conversation fees; this works today for free and the API can slot in behind it later. Verified end to end against a local SMTP server: reset issues and consumes its token exactly once, the old password stops working, an unknown address gives the same answer, share mail reaches several recipients. Link behaviour checked for expiry (410), revocation (410), wrong passphrase (401) and download lock (403). Outbound SMTP is blocked in the sandbox, so the real mailbox still needs one test from the live server. Also fixes a regression this introduced: the seventh nav item pushed the logout button off screen at 820px. The bar now shrinks and scrolls, logout never does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BNhLunz586g6DeYMa7Qsd1 |