Skip to content

Email anonymous registrations a confirmation link that signs them in - #4018

Merged
sethherr merged 10 commits into
mainfrom
sethherr/registration-seq-ack-bike-id
Aug 3, 2026
Merged

Email anonymous registrations a confirmation link that signs them in#4018
sethherr merged 10 commits into
mainfrom
sethherr/registration-seq-ack-bike-id

Conversation

@sethherr

@sethherr sethherr commented Aug 1, 2026

Copy link
Copy Markdown
Member

An anonymous register-flow registration can't create its bike — Ownership needs a creator — so step 1 now emails the address a link. Following it proves the address, creates a passwordless account, signs them in, and creates the bike the registration was holding, dropping them on whatever step they're on.

  • The link carries its own single-use token, minted only in the send path, bound to the address it was mailed to, and spent on use. The registration's id_token is already in the registrant's URL before any email goes out, so a click carrying it would prove nothing about who received what. Binding matters because step 1 stays resubmittable: without it, editing owner_email after the link went out would let that link confirm — and sign you in as — an address you never received mail at.
  • BParam::TOKEN_EXPIRATION (90 days) governs both the link and how long the register flow resumes by token, since a link outliving the registration it resumes is a dead end. The confirm lookup applies no window of its own, so an expired link can say so and send a new one rather than dumping them into a fresh registration.
  • The landing page only renders a form that posts itself via Sign users in from emailed links instead of a password #4019's auto-submit controller — a link scanner's GET can't spend the token. Resends are rate limited off a stamp written by the sender, not by the delivering job.
  • The account it makes is a passwordless_user, so it gets Sign users in from emailed links instead of a password #4019's offer to set a password like every other emailed sign in; an address that already has an unconfirmed account gets confirmed, as the magic link does. Creating one moves out of Users::ProcessOrganizationRoleJob into UserServices::PasswordlessCreator, and the non-redirecting half of Sessionable#sign_in_and_redirect becomes sign_in_user, so this lands in the same session state as every other sign in.
  • The email rides on Email::PartialRegistrationJob, which now takes the notification kind alongside the id — it and partial_registration are the two a registration sends before it has a bike, so they share the domain check and the notification. Both are seeded now, since nothing else leaves a b_param mid-flow for the mailer previews.

New accounts still land on the terms-of-service gate the first time they visit their account — same as passwordless org accounts. If step 1's submit should count as acceptance, that's a follow-up.

sethherr and others added 7 commits August 1, 2026 15:33
An anonymous register-flow registration can't create its bike - Ownership needs a
creator - so step 1 now emails the address a link. Following it proves the address,
makes a passwordless account, signs them in and creates the bike they were holding.

The link carries its own single-use token, minted only in the send path: the
registration's id_token is already in the registrant's URL, so a click carrying it
would prove nothing about who received the email.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-seq-ack-bike-id

# Conflicts:
#	app/components/register/step2/component.html.erb
#	app/javascript/controllers/auto_submit_controller.js
#	app/jobs/users/process_organization_role_job.rb
…ign in

#4019 made passwordless users a first-class thing and nudges them to set a
password whenever an emailed link signs them in. The register flow's confirmation
link mints exactly that kind of account, so it shows the same flash.

find_or_create reports whether it created the user - confirming saves again, so
previously_new_record? can't answer that by the time it returns.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
owner_email stays editable after the confirmation link goes out, and
clean_params deep-merges, so the token survived an address change - which
let a registrant point their own link at someone else's account and be
signed in as them. The token now records the address it was minted for and
reads as absent for any other, so the next send mints a fresh one.

Also from the review:

- Confirm an existing unconfirmed account, like sign_in_with_magic_link
  does. Without it the auth cookie was set for a user current_user won't
  resolve, so the registrant got "Logged in!" and then
  please_confirm_email, with the single-use token already spent.
- flash[:notice] rather than flash[:info], which isn't a
  UI::Alerts::Base::Component kind (it raised when rendered).
- bin/rake prepare_translations, for the i18n normalization spec.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Email::BParamNotification existed only to be included in two jobs whose
difference was one string, so the string is a parameter now and both the
module and Email::RegisterConfirmationJob are gone. perform takes a kind
alongside the id, naming both the notification and the mailer method, and
defaults to partial_registration so jobs already queued still run.

Renames the email register_confirmation -> partial_register_confirmation,
matching the job it now rides on.

Also:

- Seed the two registrations that never became bikes - a partial
  registration and an anonymous one awaiting confirmation, both on
  Brakebills - and add the mailer preview they exist for.
- Look the confirmation registration up with BParam.find_by, and compare
  its token with ControllerHelpers#secure_compare? rather than wrapping
  either in a service/model method.
- AGENTS.md: prefer composition over inheritance and include, and never
  revert what bin/lint wrote.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Per AGENTS.md: drop the ones that restate the name below them, and the
clauses that narrate the change or defend a choice against an edit nobody
was going to make. What's left is the why a reader would otherwise get
wrong - token_time flooring so a blank token reads as expired, the sent-at
stamp being written by the caller rather than the delivering job, the
self-posting confirm form defeating link scanners.

find_b_param_for_confirmation's had grown to five lines when the deleted
service method's comment folded into it; now it leads with why it isn't
find_b_param.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
color_and_brand and email_confirmation_email each only forwarded, so the
callers read through to the b_param and its params directly.

PartialRegistrationJob raises ArgumentError for a kind no b_param sends,
rather than returning quietly - it's a programming error, so it should
retry and land in dead. Validated before the record is looked up, so it
fails the same way whether or not the b_param is still around.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
sethherr added a commit that referenced this pull request Aug 3, 2026
Pull some changes that can be shipped independently from #4018
sethherr and others added 2 commits August 3, 2026 09:41
The two components each wrapped it in an identical forwarding method.

Also trims PasswordlessCreator's comment, which read as though the module
confirms an existing account - the link-proved-it confirm is the caller's,
since ProcessOrganizationRoleJob deliberately doesn't want it.

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

sethherr commented Aug 3, 2026

Copy link
Copy Markdown
Member Author

Screenshots

/register?step=2

The "we've sent a confirmation link" banner is unchanged — only the predicate behind it moved to BikeServices::Register.confirmation_email_pending?, so this pair documents visual parity.

Desktop Mobile
main 👆 this branch 👇

/rails/mailers/organized_mailer/partial_register_confirmation

New email, so there's nothing on main to compare against. Rendered off the seeded Brakebills registration, so the org's header/footer snippets are in it.

Desktop Mobile

/register/confirm isn't captured — it auto-submits on load via the auto-submit controller, so a screenshot races the redirect and each attempt spends the single-use token.

@sethherr
sethherr marked this pull request as ready for review August 3, 2026 17:12
@sethherr
sethherr merged commit 126b8e6 into main Aug 3, 2026
9 checks passed
@sethherr
sethherr deleted the sethherr/registration-seq-ack-bike-id branch August 3, 2026 17:12
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.

1 participant