Skip to content

feat(auth): reset passwords through verified recovery email - #719

Merged
anneoneone merged 2 commits into
mainfrom
feat/711-email-password-reset
Oct 10, 2026
Merged

anneoneone merged 2 commits into
mainfrom
feat/711-email-password-reset

Conversation

@anneoneone

Copy link
Copy Markdown
Collaborator
  • Let Host and Admin users verify a recovery email and reset a forgotten password.
  • Add recovery settings, public reset pages and an admin clear action.
  • Revoke old reset links on password changes and sign out sessions after an email reset.
  • Keep mail disabled until the Resend sender and Bitwarden configuration are ready.

Closes: #711

Implementation and checks
  • Store verified addresses separately, with normalized unique emails. Keep a replacement pending until verified.
  • Use hashed, single-use tokens with a 30-minute expiry and atomic SQLite transactions.
  • Allow unexpired Host and Admin accounts. Keep Guest and Superadmin recovery with administrators.
  • Limit requests by IP and email. Use a configured HTTPS URL for emailed links.
  • Send mail after commit with bounded concurrency and timeouts. Report delivery failures through logs and the existing Telegram admin channel. No durable queue.
  • Preserve active broadcasts when reset deletes sessions. New connections require login.
  • Setup and delivery limits: docs/password-recovery.md. The production workflow still needs the actual Bitwarden secret mapping before activation.

Validation:

  • Backend: 110 tests passed; 2 existing live-storage tests ignored.
  • Final focused recovery run: 17 passed, including real HTTP enrollment, emailed link verification, reset and username login.
  • Frontend: 174 tests passed; typecheck, lint and production build passed. Focused tests also passed after the final route-state change.
  • Cargo clippy passed with existing warnings outside the recovery code.
  • Independent code review found no blockers.

Live delivery and production deployment were not performed.

Let Host and Admin accounts verify an email and reset a forgotten
password. Revoke reset links on every password change and sessions
on email reset. Keep delivery disabled until mail is configured.

Refs: #711
@vercel

vercel Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
live-moafunk-de Ready Ready Preview Oct 10, 2026 11:00pm UTC

@anneoneone

Copy link
Copy Markdown
Collaborator Author

Review

Verdict: good to merge once marked ready. No blocking bugs. One setting must be added before mail is turned on.

Before enabling mail

Set RECOVERY_TRUST_PROXY=true in prod. Prod runs the backend behind nginx, so every request comes from the same address. With the default false, all users share one IP limit: 20 public requests per 15 minutes, for request, verify and complete together. One person spamming the form blocks recovery for everyone.

The docs explain the setting, but the activation steps do not list it. Add it to step 3 of docs/password-recovery.md, together with the three mail settings, when the Bitwarden mapping goes into backend.yml.

This does not block the merge: mail is off by default, so the public endpoints return 503 until setup.

Note

Backend tests do not run on PRs. backend.yml only runs on push to main, so only the frontend workflow ran here. Ran cargo test locally on a8d3edf: 110 passed, 2 ignored. This matches the PR body. The CI gap is older than this PR; a follow-up could run cargo test on PRs.

Checked and fine

  • Tokens: random, stored as SHA-256, epoch expiry, one live token per kind. All writes use BEGIN IMMEDIATE; a busy database returns 503.
  • Complete reset: checks the link before Argon2, checks again in the transaction, then deletes sessions, clears the forced-change flag and drops all tokens in one commit. The double-submit test passes.
  • Password writers: change-password, initial password and admin reset all use update_password, so they kill open reset links.
  • Eligibility: explicit Host/Admin allow-list plus an expiry check.
  • Reset request: same answer for unknown, invalid and ineligible addresses. The email limit counts unknown addresses too. Mail is sent after the commit.
  • Mail: no tokens in logs, fixed provider URL, no redirects. The link matches the hash router.
  • Frontend: recovery pages are public and skip the forced-change redirect. Opening a link only shows a confirm page. The invalid-link page offers a new link.
  • Admin clear: follows the same role rules as admin password reset.
Code references

Checked at a8d3edf.

  • Shared IP key: public_limit in backend/src/handlers/recovery.rs (lines 122-127) uses one ip: key with limit 20 for verify, request and complete. RecoveryControls::ip (lines 86-98) uses the peer address unless RECOVERY_TRUST_PROXY=true.
  • Backend bound to localhost behind nginx: backend/docker-compose.prod.yml:7. nginx sets X-Real-IP: backend/scripts/deploy_hetzner.sh:467.
  • Backend workflow triggers: .github/workflows/backend.yml lines 3-10 (push to main and manual runs only).
  • Token and transaction logic: backend/src/recovery.rs (begin, issue_token, find_token, complete_reset, update_password).
  • Host expiry is a date (frontend/src/admin/pages/UsersPage.vue:320). unixepoch() makes it end at the start of that day, the same as the login check.

List the production proxy setting with the mail activation settings
so users behind nginx do not share one recovery request limit.

Refs: #711
@anneoneone
anneoneone marked this pull request as ready for review October 10, 2026 23:00
@anneoneone

Copy link
Copy Markdown
Collaborator Author

Approved at c0439d1.

  • The proxy-trust step is now in the activation docs (RECOVERY_TRUST_PROXY=true in production behind nginx).
  • No code change since the reviewed commit. Backend tests (cargo test, run locally): 110 passed, 2 ignored. Frontend CI is green.
  • Before enabling mail: add the Bitwarden mapping and the four settings in backend.yml, as described in docs/password-recovery.md.

GitHub does not allow an approving review from the PR author, so this approval is a comment.

@anneoneone

Copy link
Copy Markdown
Collaborator Author

Review: APPROVED by Claude at c0439d1

@anneoneone
anneoneone merged commit 1bae514 into main Oct 10, 2026
4 of 5 checks passed
@anneoneone
anneoneone deleted the feat/711-email-password-reset branch October 10, 2026 23:09
@anneoneone

Copy link
Copy Markdown
Collaborator Author

This PR is merged but not deployed.

anneoneone added a commit that referenced this pull request Oct 11, 2026
#719 left this block
unformatted on main, which has no format check. Interim's backend-ci
runs cargo fmt --check. This is rustfmt 1.98.0 output only.

Refs: #731

This branch was successfully deployed

1 active deployment
Preview — c0439d1e Deployed Oct 10, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(auth): reset passwords through an emailed link

1 participant