feat(account): allow password users to change their email address#313
Open
stophecom wants to merge 3 commits into
Open
feat(account): allow password users to change their email address#313stophecom wants to merge 3 commits into
stophecom wants to merge 3 commits into
Conversation
Adds a self-serve "change email" flow on the account profile page. Because the auth verifier is salted with the user's email (deriveAuthVerifier), changing the email requires re-deriving and re-storing the credential — otherwise password login would break. The encryption master key is unaffected (random pdkSalt), so existing secrets stay decryptable and the key is not re-wrapped. Flow: - Step 1 (requestEmailChange): re-authenticate with the current password and email a 6-digit OTP to the new address. - Step 2 (confirmEmailChange): verify the OTP, re-check the password, then commit the new email + the verifier re-derived with the new email salt. Also syncs userSettings.email, the Stripe customer, and the Resend audience. Eligibility is limited to accounts that have a password and are not linked to Google — a Google-linked account re-matches on Google's email on the next OAuth login, which would fork into a second account, so the field stays read-only for those users. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
The confirm step's submit silently no-op'd: `email` was a required field set programmatically (not rendered), and after step 1 the request form's default `invalidateAll` re-ran the page load, handing the confirm superForm a fresh empty form and wiping the email back to "". Client-side validation then failed on the invisible field and superforms cancelled the submit with no visible error. - Make `email` client-optional in the confirm schema (it's always attached in onSubmit from in-memory state); the server now guards its presence. - Disable `invalidateAll` on both change-email forms (the confirm success path calls invalidateAll manually) so state isn't reset mid-flow and the code field isn't cleared on a wrong-code error. - Strip the invalid email `pattern` attribute on the new-email input to clear the console regex warning (matches signin-form). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…field - Move the post-success invalidateAll out of the confirm form's onResult and into onSuccess (after the dialog closes). Reloading while the form was mounted re-synced it and fired the success toast a second time. - Restore the plaintext password in the request form's onUpdated/onError: jsonData() replaces the store field with the auth verifier and the server echoes it back, so a wrong-password error was repainting the input with the 64-char hash. Mirrors the signin-form pattern. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
What
Adds a self-serve change email address flow on the account profile page (
/account/profile). Previously the email field was permanently read-only for everyone.Why it's not a one-line change
The zero-knowledge auth verifier is salted with the user's email (
deriveAuthVerifier(password, email)inpackages/core/src/key-management.ts). The client sends this verifier to the server in place of the password, and it's hashed intouser.passwordHash. So if the email changes, the same password derives a different verifier → password login breaks.The flow therefore re-derives and re-stores the credential. Critically, the encryption master key is untouched — it's wrapped with a random
pdkSalt, independent of email — so existing secrets stay decryptable and the key is not re-wrapped (unlike a password change).Flow
requestEmailChange: user enters new email + current password. Server re-authenticates (current-email verifier vspasswordHash), checks the new address is free, and emails a 6-digit OTP to it (reuses the existingemail_verification_requestflow).confirmEmailChange: user enters the OTP. Client re-derives the verifier with the new email salt. Server verifies the OTP, re-checks the password (so step 2 can't bypass step 1's password gate), then commitsuser.email+ newpasswordHashin a transaction and syncsuserSettings.email. External systems (Stripe customer email, Resend audience) are best-effort synced after.The plaintext password is held in memory across the two steps (same pattern as the encryption flows) so the client can re-derive verifiers; it never leaves the browser.
Eligibility (deliberate limitation)
Only accounts that have a password and are not linked to Google can change their email. A Google-linked account re-matches on Google's email at the next OAuth login (
createOrUpdateUserconflicts onemail), which would fork into a second account. For those users the field stays read-only with an explanatory tooltip. The session already exposeshasPasswordandgoogleId, so no schema change was needed.No DB migration
user.emailalready exists and is unique.Files
formSchemas.ts—changeEmailRequestFormSchema,changeEmailConfirmFormSchemaactions.ts—requestEmailChange,confirmEmailChangechange-email-form.svelte— new two-step dialog formaccount-card.svelte— editable email for eligible usersprofile/+page.server.ts/+page.svelte— wire actions + formsmessages/*.json— 16 new keys, translated into all 13 locales via/translate-keysVerification
pnpm check— no errors in changed files (only pre-existing repo errors remain)pnpm lint— cleanpnpm build— succeedsThe end-to-end OTP flow wasn't exercised in a live browser (needs a seeded password account + Resend email delivery); worth a manual smoke test on the preview deploy.
Security notes / follow-ups
userId). A "your email was changed" notification to the old address would be a good hardening follow-up.🤖 Generated with Claude Code