feat: customer password reset via email round-trip (#32) #43

Merged
bermudalamb merged 2 commits from feature/password-reset into main 2026-08-17 19:15:58 -05:00
Owner

Adds "Forgot password?" to the login page, a request page, and a reset page reached by a one-hour, single-use token delivered by email. Reuses customer_tokens with a new password_reset kind alongside verify_email.

The request endpoint always answers 200, whether or not the address has an account, so it cannot be used to test addresses for membership. Note/register still reveals existence through its 409 on a duplicate, so this protection is currently partial; closing that is its own change.

Completing a reset deletes every session for that customer. A reset prompted by a compromise has to evict the intruder, and leaving a 30-day cookie alive would defeat the point. It also marks the address verified, since receiving the mail is exactly what verification proves, and supersedes any outstanding token so an older link in the inbox cannot be resurrected.

Introduces the first rate limiting in the codebase, on the request endpoint only. The limiter is keyed on caller and submitted address: keying on IP alone would let one person lock out everyone behind the same proxy, and everything arrives via Nginx Proxy Manager. Applying that same limiter to the reset endpoint, which carries no address, collapsed every caller into one shared bucket -- so that endpoint is deliberately unlimited instead, protected by a 32-byte single-use token whose bcrypt work only runs after the token matches.

The e2e tests read the issued token directly from Postgres rather than through a test-support endpoint. An endpoint returning a reset token for an arbitrary address is account takeover for every customer if it is ever reachable, and an environment gate is thin protection against that.

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com

Adds "Forgot password?" to the login page, a request page, and a reset page reached by a one-hour, single-use token delivered by email. Reuses customer_tokens with a new password_reset kind alongside verify_email. The request endpoint always answers 200, whether or not the address has an account, so it cannot be used to test addresses for membership. Note/register still reveals existence through its 409 on a duplicate, so this protection is currently partial; closing that is its own change. Completing a reset deletes every session for that customer. A reset prompted by a compromise has to evict the intruder, and leaving a 30-day cookie alive would defeat the point. It also marks the address verified, since receiving the mail is exactly what verification proves, and supersedes any outstanding token so an older link in the inbox cannot be resurrected. Introduces the first rate limiting in the codebase, on the request endpoint only. The limiter is keyed on caller *and* submitted address: keying on IP alone would let one person lock out everyone behind the same proxy, and everything arrives via Nginx Proxy Manager. Applying that same limiter to the reset endpoint, which carries no address, collapsed every caller into one shared bucket -- so that endpoint is deliberately unlimited instead, protected by a 32-byte single-use token whose bcrypt work only runs after the token matches. The e2e tests read the issued token directly from Postgres rather than through a test-support endpoint. An endpoint returning a reset token for an arbitrary address is account takeover for every customer if it is ever reachable, and an environment gate is thin protection against that. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
bermudalamb added this to the Initial Build project 2026-08-17 18:55:32 -05:00
bermudalamb added 1 commit 2026-08-17 18:55:33 -05:00
feat: customer password reset via email round-trip (#32)
SonarQube Analysis / sonarqube (pull_request) Successful in 2m57s
Tests / backend-unit (pull_request) Successful in 53s
Tests / frontend-e2e (pull_request) Failing after 7m40s
Tests / backend-integration (pull_request) Failing after 3h14m41s
db7c61c89d
Adds "Forgot password?" to the login page, a request page, and a reset
page reached by a one-hour, single-use token delivered by email. Reuses
customer_tokens with a new password_reset kind alongside verify_email.

The request endpoint always answers 200, whether or not the address has
an account, so it cannot be used to test addresses for membership. Note
/register still reveals existence through its 409 on a duplicate, so this
protection is currently partial; closing that is its own change.

Completing a reset deletes every session for that customer. A reset
prompted by a compromise has to evict the intruder, and leaving a 30-day
cookie alive would defeat the point. It also marks the address verified,
since receiving the mail is exactly what verification proves, and
supersedes any outstanding token so an older link in the inbox cannot be
resurrected.

Introduces the first rate limiting in the codebase, on the request
endpoint only. The limiter is keyed on caller *and* submitted address:
keying on IP alone would let one person lock out everyone behind the same
proxy, and everything arrives via Nginx Proxy Manager. Applying that same
limiter to the reset endpoint, which carries no address, collapsed every
caller into one shared bucket -- so that endpoint is deliberately
unlimited instead, protected by a 32-byte single-use token whose bcrypt
work only runs after the token matches.

The e2e tests read the issued token directly from Postgres rather than
through a test-support endpoint. An endpoint returning a reset token for
an arbitrary address is account takeover for every customer if it is ever
reachable, and an environment gate is thin protection against that.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
bermudalamb requested review from calamb 2026-08-17 18:55:33 -05:00
bermudalamb moved this to In Progress in Initial Build on 2026-08-17 18:56:06 -05:00
bermudalamb moved this to Ready for Review in Initial Build on 2026-08-17 18:56:23 -05:00
bermudalamb added 1 commit 2026-08-17 19:12:14 -05:00
ci: manual workflow to build and publish the QA image (#25)
SonarQube Analysis / sonarqube (pull_request) Successful in 2m34s
Tests / backend-unit (pull_request) Successful in 36s
Tests / backend-integration (pull_request) Failing after 3h3m42s
Tests / frontend-e2e (pull_request) Failing after 7m32s
2a20c0e05b
Adds a workflow_dispatch job that builds a chosen ref, pushes it to the
Gitea container registry as :qa plus a commit-sha tag, and emails when it
is ready. It deliberately does not restart the QA stack — redeploying
stays a human action in Portainer.

The build runs against a Docker-in-Docker service rather than the NAS's
Docker socket. Mounting the host socket into the runner would give every
workflow on every branch root-equivalent control of the NAS, production
included; pushing to a registry means the image does not need to survive
in the build daemon.

The QA stack now pulls that image instead of requiring a local build. The
previous arrangement meant the image existed only if someone remembered
to build it, which produced two confusing failures already: a Docker Hub
"pull access denied" when the tag was missing, and a silent stale-image
deploy when the build had not been rerun.

Two runner capabilities cannot be verified from here — privileged service
containers for dind, and a docker CLI in the runner image. The workflow
checks both and fails with an explanation rather than a connection
refused, and validates all five required secrets and variables up front
rather than part-way through a build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
bermudalamb merged commit 42152e791c into main 2026-08-17 19:15:58 -05:00
bermudalamb deleted branch feature/password-reset 2026-08-17 19:15:58 -05:00
bermudalamb moved this to Review in Initial Build on 2026-08-18 11:09:46 -05:00
bermudalamb moved this to Ready for Release in Initial Build on 2026-08-18 11:09:53 -05:00
bermudalamb moved this to Released in Initial Build on 2026-08-18 11:18:12 -05:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bermudalamb/redefined-designs#43