Repository navigation
ECHO-1046 feat: login says what went wrong and when to try again - #1367
Open
JorimTheuns wants to merge 7 commits into
Open
JorimTheuns wants to merge 7 commits into
JorimTheuns wants to merge 7 commits into
Conversation
The limiter's counters already know when their window ends; check() now puts that in retry_after_seconds on rate_limit.exceeded. The dashboard may read Better Auth's X-Retry-After (exposed through CORS). Registration's email shape rule moves to its own module so the form can mirror it, with a test that fails if the two differ. No limit, window, key or store changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BKdjPZBaxnCMragWmqzZTL
…again Every login failure goes through describeAuthError with its own message: wrong credentials (kept generic), unverified (with resend), rate limited (countdown from the server's wait, Log in off until it ends), blocked before reaching dembrane (other network or browser, the emailed code, support), offline and server. A failure after the sign-in worked is no longer a failed login: the person is told they are in and can carry on. user_login_failed gains cause and http_status; login_rate_limit_shown and login_blocked_shown are new. Forgot your password is always shown. Registration applies the server's email rule, "Change email" goes back to the email field with the other answers kept, the fields carry password manager hints, and password length counts code points as the server does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BKdjPZBaxnCMragWmqzZTL
Seven new strings, written by hand in each catalog's form of address and recorded in machine-translations.json for review. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BKdjPZBaxnCMragWmqzZTL
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BKdjPZBaxnCMragWmqzZTL
JorimTheuns
force-pushed
the
echo-1046-login-messages
branch
from
November 10, 2026 17:35
c8152cc to
66cd01e
Compare
While the blocked notice shows, it carries "Email me a sign-in code instead" and the same link under Log in steps aside, as every notice carries its own way out (the signed-in notice's Continue). Forgot your password stays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BKdjPZBaxnCMragWmqzZTL
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BKdjPZBaxnCMragWmqzZTL
The seven German login and recovery messages this PR adds address the reader as du, as the rest of German does once #1371 lands (model-written, in the ledger). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BKdjPZBaxnCMragWmqzZTL
This branch has not been deployed
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.
Stacked on #1362
ECHO-1046. Between 12 Oct and 10 Nov, people who could not log in mostly saw "Something went wrong". Now each failure says what happened and what to do next.
What people see now
Every login error goes through
describeAuthError. The cause comes fromloginFailureCause.INVALID_EMAIL_OR_PASSWORD,INVALID_EMAIL, wrong codesEMAIL_NOT_VERIFIEDResendVerificationEmailrate_limit.exceeded,TOO_MANY_ATTEMPTSLoginRateLimitNoticeshows a countdown, and "Log in" stays disabled until it ends. The wait comes from Better Auth'sX-Retry-After, fromRetry-After, or from the platform'sparams.retry_after_seconds. If none of these is present, it waits 30 s (Better Auth's sign-in window). For a password login the message never mentions a code; the code flow keeps "request a new code".HTTP_403)LoginBlockedNoticesays the login was stopped before it reached dembrane and suggests another network or browser. It offers "Email me a sign-in code instead" and support@dembrane.com, and it does not guess the cause.try, separate from what runs after it (the device check and the landing page). If a later step fails,SignedInRecoveryNoticesays "You are logged in..." and has a Continue button. It is not counted inuser_login_failed. The emailed-code sign-in is split the same way.reg@exampleare refused on the form instead of being dropped silently by the server. The rule lives inplatform/packages/account/src/email.tsand is mirrored infrontend/src/lib/emailRule.ts. A test on each side reads the other file and fails if the two patterns differ.passwordProblemscounts it on the server, so an emoji counts as one character. The rule itself is unchanged.given-name,family-name,usernameon the email field, andnew-passwordon both password fields. On the password step a hiddenusernamefield carries the email, because the email field is on another step. Login usesusernameandcurrent-password.?email=: this comes from/registerand from invite links (AcceptInviteRoute). It still locks the address and setsoff/new-password. That is deliberate: it stops a password manager from filling in a different saved account (QA audit 2026-04-23). The intent is now documented in the code.Platform
RateLimiter.check: a refusal now includesretry_after_secondswhen the counter knows when its window ends. Both counters do: the memory counter, and the Postgres counter through the same upsert, which now also returnsreset_at. Counters without that information still refuse without a wait. No limit, window, key, store, password rule, session lifetime or verification requirement changed.X-Retry-AfterandRetry-Afterare added to the exposed headers. Without this, the dashboard (on another origin) cannot read Better Auth's wait.Analytics
user_login_failednow carries{ error_code, cause, http_status }.login_rate_limit_shown { retry_after_s }andlogin_blocked_shown.wrong_credentials.Blocks
These are new in
components/auth/:LoginRateLimitNotice,LoginBlockedNoticeandSignedInRecoveryNotice. They take props in and send callbacks out, use Lingui strings anddata-testid, and are tested inLoginNotices.test.tsx. I reuseduseResendCooldown, adding an optionalstart(seconds), and reusedResendVerificationEmailandPasswordRequirements. I did not useErrorNoticeon the login page, because it presents platform catalog codes and these are Better Auth errors. Register still uses it.Translations
There are 7 new strings in
src/locales, written by hand for nl, de, fr, es, it, uk and cs, each in the catalog's existing form of address. All of them are recorded insrc/locales/machine-translations.jsonfor review.Verification
check-path-case,pnpm lint,pnpm exec tsc,check-translations --strict,sync-error-codes --check,sync-accounts-contract --check, andvitest run(all: 241 files, 2014 tests passed, 1 skipped as before).biome ci(one existing warning, in pricing),typecheck,packages check, andbun testfor auth, account, ratelimit andapps/api/test/app.test.tsagainst a local Postgres 16. Result: 386 passed and 9 skipped. The skipped tests are the auth parity tests, which need the parity template, and they were not run.e2e/register-verify-login.spec.ts: register, try logging in unverified, verify, then log in. I could not read the verification email from the test, so the spec mints the same HS256 token withE2E_AUTH_SECRETand skips when that is not set. I ran it once against a local API and dashboard and it passed. It is not a PR gate.Out of scope
Found while running the e2e: Better Auth's own invalid-origin refusal is a 403 that carries a code. It reads as "server" here, not "blocked".
Fix round
--app-disabled: #ffd166instyles/rules.css, applied instyles/button.module.cssto every disabled control since frontend: the design system, theme layer and the first workstreams on it #1139), not a focus ring or a warning. Left as is; it is the same on Create project and Continue.Generated by Claude Code