feat(users): add self-service account unlock via email#187
Merged
antosubash merged 3 commits intoMay 10, 2026
Merged
Conversation
When users get locked out from failed sign-ins, they can now request an unlock link by email instead of waiting or contacting an admin. - Add SendUnlockEmailEndpoint (POST with rate limiting, no email enumeration leak) - Add UnlockAccountEndpoint (token verification, security stamp rotation) - Add IAccountUnlockEmailSender with console and production implementations - Add UserSelfUnlockedEvent for audit trail - Update Lockout page with link to unlock flow - Add integration tests for happy path, unknown email, invalid token Closes #181
- Unify error messages and add base64 decode guard in UnlockAccount to close a user-enumeration leak and prevent 500s on malformed input. - Require confirmed email in SendUnlockEmail, matching ForgotPassword. - Extract the AccountUnlock token-purpose string to a constant so the two endpoints can't drift. - Use the typed routes helper in Lockout.tsx instead of a hardcoded path. - Strengthen integration tests: recording email-sender fake, branches for healthy/unconfirmed/locked users, malformed-base64 guard, rate- limit smoke test, and GET-page smoke tests for the new pages. - Re-format auto-generated routes.ts to satisfy biome.
Deploying simplemodule-website with
|
| Latest commit: |
37393d7
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://1857c7cc.simplemodule-website.pages.dev |
| Branch Preview URL: | https://emdash-feat-self-service-acc.simplemodule-website.pages.dev |
…l-summary-181-hhs5c
3 tasks
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.
Summary
SendUnlockEmail,SendUnlockEmailConfirmation,UnlockAccount) gate onIsEmailConfirmedAsync+IsLockedOutAsync, return identical error messages on every failure path to prevent user enumeration, and never throw on malformed input.auth-strictpolicy (10/min/IP); GET pages and the confirmation redirect are unauthenticated.Notable details
IAccountUnlockEmailSendercontract lives inSimpleModule.Users.Contractswith aConsoleAccountUnlockEmailSenderfallback in the Users module and a realIdentityAccountUnlockEmailSenderregistered by the Email module.UserSelfUnlockedEventis published on successful unlock; auditing flows through the existingAuditingMessageBusdecorator, so no extra handler is required."AccountUnlock"is centralised inUsersConstants.TokenPurposesso both endpoints can't drift.Base64Url.DecodeFromCharswithOperationStatus, which is genuinely non-throwing on invalid input (unlikeTryDecodeFromChars).Test plan
dotnet test— all 17 projects pass (52 inUsers.Tests, including 12 new integration tests).npx biome check .— clean.npm run validate-pages— new Inertia component names registered inPages/index.ts.npm run validate:i18n,npm run typecheck, framework-scope validation — clean.dotnet build SimpleModule.slnx— 0 errors, 0 warnings./Identity/Account/Lockout→ follow the unlock link → confirm 200 page + login works.