diff --git a/CLAUDE.md b/CLAUDE.md index 7e81926..034962b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -93,6 +93,8 @@ Hash-based router (required for Electron `file://` protocol). Routes: `/` (index **Step one advances on success alone and never reports "no such account".** The backend answers `forgot-password` identically for a registered and an unregistered address so that the endpoint cannot be used to test who has one, and a UI that reported the difference would hand that oracle straight back - which is why the copy on step two is conditional ("if an account exists for..."). `AuthService.forgotPassword` resolving true means the request went through, nothing more. +**The signup wizard works the same way, for the same reason.** `send-verification-code` used to answer 409 for an address that already had an account, so signup reported it inline and immediately - which made the care taken in the reset wizard pointless, since the same question was answerable one screen over. The backend now answers 200 either way and mails a code to a free address or a "you already have an account" notice to a taken one, so signup's step two copy is conditional in the same shape ("if x@y.z does not already have an account...") and mentions the notice, because for that user the code they are waiting to paste is never coming. `AuthService.sendVerificationCode` resolving true means the request went through, not that the address is free. + `AuthService.resetPassword` rewrites the stored password behind **two** guards, `rememberMe` and the address matching the remembered one. The login form pre-fills from that store, so skipping the write leaves a filled-in password that has just stopped working; writing it on `rememberMe` alone puts credentials on disk for a user who did not opt in. The address check is specific to reset, the only password flow that runs while signed out and therefore the only one that can be run for an account other than the remembered one - on a shared machine, writing unconditionally would replace someone else's remembered login with this one. That write is wrapped in its own `try`, separate from the request. By the time it runs the password has already changed and the code is spent, so letting a disk failure decide the return value would report a failure for a reset that succeeded and send the user to retry with a code that can no longer work - the same trap the login form avoids when it persists remember-me. `test/password-reset.test.mjs` pins all of it, including the failed-reset case and a store that throws. The final step latches on success. `loading` is already back to false while the two-second redirect runs, so a live button there would let a second click resend a code the backend has just spent, toasting a guaranteed failure over the success still on screen. diff --git a/src/main/services/auth.service.ts b/src/main/services/auth.service.ts index 951d84d..2f29868 100644 --- a/src/main/services/auth.service.ts +++ b/src/main/services/auth.service.ts @@ -18,6 +18,12 @@ export class AuthService { /** * Send an email verification code to a prospective user. + * + * The backend answers the same whether or not the address already has an account, so a + * `true` here means "the request went through", never "this address is free". It mails a + * code to a free address and a "you already have an account" notice to a taken one. + * Reporting anything more specific to the renderer would put back over the UI the + * enumeration the endpoint was changed to remove. */ async sendVerificationCode(email: string): Promise<{ success: boolean; error?: string }> { try { diff --git a/src/renderer/pages/auth/signup.tsx b/src/renderer/pages/auth/signup.tsx index a1f60bc..ecaa08e 100644 --- a/src/renderer/pages/auth/signup.tsx +++ b/src/renderer/pages/auth/signup.tsx @@ -99,8 +99,17 @@ export default function SignupPage() {
+ {/* + Conditional, like the reset wizard's step two, because the backend now answers + the same whether or not the address already has an account. It sends a code to + a free address and a "you already have one" notice to a taken one, so a flat + "we sent a code" is wrong half the time - and stating which happened would put + back over the UI the enumeration the endpoint was changed to remove. + */}

- We sent a verification code to {email}. Paste it below. + If {email} does not already have an account, we sent a verification code to it. + Paste the code below. If it does, we sent a note explaining how to sign in + instead.