Commit Graph
8 Commits
Author SHA1 Message Date
bvandeusenandClaude Opus 5.5 c0de50d85d Email: one branded HTML layout with a text part, and invites can be emailed (#5917)
Each email was a subject and a plain-text f-string at its call site.
mail_templates.py now holds one layout: the site name, a heading, the
message, a button for the link and a footer saying who sent it and why.
Every email (password reset, test, invite) is a function returning its
subject, text and HTML. mailer.send sends them as multipart/alternative,
text first.

The HTML is email-shaped: tables, inline styles, no images or fonts, and
every value from settings or a person escaped.

POST /api/invites takes send_email. It is refused before anything is made
when email is off or the invite names no address. The link is built from
the public address. A failed send leaves the invite standing with
email_error, so the admin still gets the link once. The invite form offers
"Email the link to …" when email is on and an address is filled in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:43:13 -04:00
bvandeusenandClaude Opus 5.5 498b23789c DRY pass #3: one one-time secret reveal (#5373)
AccountView's device-token reveal was OneTimeLink's markup and copy handler
again. OneTimeLink becomes OneTimeSecret (value, note, noun = link | token),
and the account page shows a new token through it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 15:13:18 -04:00
bvandeusenandClaude Opus 5.5 6e4c4930cb DRY pass #2, batch 5, F16: the admin lists load through useLoad (#5372)
Accounts, groups, invites and activity each wrote the same first load:
clear the error, try, put the server's reason or a fallback in error,
and stop loading. useLoad (composables/useAction.ts) is that, and its
load() is also the retry.

Kept: ShareDialog and Settings. Their load sets loading back to true on a
reload, which the lists never did, so moving them would change what a
retry shows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 14:46:14 -04:00
bvandeusenandClaude Opus 5.5 52d53fb78a InviteList: load() keeps its own error line; revoke toasts
ebbe4a6 converted the wrong try block in InviteList: the edit matched from
load()'s try down to revoke()'s catch, which left load() half-converted.
load() goes back to its own try/catch/finally. revoke() uses
toastOnFailure, as intended.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 14:45:19 -04:00
bvandeusenandClaude Opus 5.5 ebbe4a678a DRY pass #2, batch 5, F16: one way an action reports its failure (#5372)
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 4s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 4s
Android / Build, or is the channel already serving this? (push) Successful in 8s
CI & Build / Web typecheck and unit tests (push) Failing after 11s
CI & Build / Python tests (push) Successful in 19s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Failing after 29s
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
Android / Core and FFI clippy and tests (push) Successful in 44s
CI & Build / integration (push) Successful in 1m32s
CI & Build / Build & push image (push) Skipped
Android / Kotlin + Rust (APK) (push) Failing after 4m11s
Android / Build the server image (push) Successful in 1s
composables/useAction.ts: toastOnFailure runs an action and toasts the
server's reason or a fallback; useAction adds the busy flag a button
waits on; useRowAction keeps the id of the row whose action is running.
Import, export, the menu entry, the integration prompt, sign out
elsewhere, the account reset link, the group actions (whose local act()
it replaces), invite revoke and sync disconnect each wrote that
try/catch/finally out.

Kept: AccountView's device revoke. It shows a fixed message rather than
the server's reason, and moving it would change the text. ShareDialog
shows its errors inline, not in a toast.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 14:44:43 -04:00
bvandeusenandClaude Opus 5.5 b481e6857c DRY pass #2, batch 5, F14: the web's repeated class lists become component classes (#5372)
style.css gains the classes the views spelled out in full: .section-label
(17 sites), .hint (20), .form-error (13), .alert-error (5), .row-card (5),
.list-empty (5), .field (5), .page-shell (3), and the small row action
.btn-sm (4) / .btn-sm-danger (3). Only exact runs moved, so nothing renders
differently; spacing a site adds beyond a run stays a utility beside it.

Kept: the Reminders and Timeline small buttons. They carry no text colour
and inherit it, so putting them on .btn-sm would recolour them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 14:41:34 -04:00
bvandeusenandClaude Opus 5.5 3dd0b44cb9 password reset: an admin makes a one-hour link, and using it signs the account out everywhere
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 3s
Android / Kotlin + Rust (APK) (push) Skipped
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Web typecheck and unit tests (push) Successful in 9s
CI & Build / Python tests (push) Failing after 12s
CI & Build / integration (push) Successful in 49s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 1m41s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m4s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m5s
Desktop (Tauri) / Update manifest (push) Successful in 5s
There is no mail path, so a forgotten password needed a hand on the database
(#2939 §2). Settings → People lists the accounts; Reset password makes a link
that works once within an hour, shown once for the admin to hand over. Making
another link for the same account closes the earlier one.

Using it (/reset-password) sets the password, deletes the account's device
tokens, and moves users.session_epoch on. Sessions are signed cookies the
server can't delete, so each now carries the epoch it signed in under and
login_required reads the account's epoch by primary key. A cookie from before
this has no epoch and reads as 0, the starting value, so the upgrade signs
nobody out. A deleted account's session now stops working too.

The one-time link reveal moves out of InviteList into OneTimeLink, and the
link-building into router/links.ts, shared by invites and resets.

Migration 0033. #5173.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:35:58 -04:00
bvandeusenandClaude Opus 5.5 28fa8badcb invites: an admin lets one person register while registration stays closed
CI & Build / Python lint (push) Successful in 2s
Android / Build, or is the channel already serving this? (push) Successful in 2s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
Android / Kotlin + Rust (APK) (push) Skipped
CI & Build / Web typecheck and unit tests (push) Successful in 8s
CI & Build / Python tests (push) Successful in 11s
CI & Build / integration (push) Successful in 47s
CI & Build / Build & push image (push) Successful in 54s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 2m8s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m28s
Desktop (Tauri) / Update manifest (push) Successful in 4s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m14s
Until now adding a second person meant re-opening registration to the
whole internet while they signed up (#2939 §1). An admin now makes an
invite in Settings: a link that works once, expires (7 days by default,
1 to 30), and can be pinned to one email address. Only the token's hash
is stored, so the link is shown once.

POST /api/auth/register takes `invite`. Redemption is one conditional
UPDATE inside the transaction that creates the account, so two people
racing one link can't both get in, and a taken email leaves the invite
unused. Every refusal says "invalid or expired invite". The register
page reads ?invite= and opens even while registration is closed.

Refs #5172

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:13:03 -04:00