Email anonymous registrations a confirmation link that signs them in - #4018
Merged
Conversation
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
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>
Member
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.






An anonymous register-flow registration can't create its bike —
Ownershipneeds 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.id_tokenis 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, editingowner_emailafter 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.auto-submitcontroller — 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.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 ofUsers::ProcessOrganizationRoleJobintoUserServices::PasswordlessCreator, and the non-redirecting half ofSessionable#sign_in_and_redirectbecomessign_in_user, so this lands in the same session state as every other sign in.Email::PartialRegistrationJob, which now takes the notification kind alongside the id — it andpartial_registrationare 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.