feat(identity): phone number confirmation flow (closes #174)#198
feat(identity): phone number confirmation flow (closes #174)#198antosubash wants to merge 2 commits into
Conversation
Mirrors the existing email confirmation surface for phone numbers. Users can now send a verification code to their phone, enter the 6-digit token to atomically save and confirm the number, and remove the number entirely. The Manage profile page shows a verified badge, and admins can see verification state and force re-verification from the user security tab. Introduces ISmsSender + ConsoleSmsSender so real providers (Twilio, etc.) can plug in via DI without touching the endpoints.
Deploying simplemodule-website with
|
| Latest commit: |
0797e28
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://7716b81f.simplemodule-website.pages.dev |
| Branch Preview URL: | https://claude-identity-phone-confir.simplemodule-website.pages.dev |
There was a problem hiding this comment.
Pull request overview
Adds an end-to-end phone number verification flow for identity users, including SMS dispatch, confirmation/removal endpoints, account management UI, and admin visibility/control for phone verification state.
Changes:
- Added
ISmsSender,ConsoleSmsSender, phone confirmation/removal endpoints, and account manage UI updates. - Added admin DTO fields, admin UI phone verification card, and force phone re-verification action.
- Added integration tests for core phone confirmation endpoint behavior.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
packages/SimpleModule.Client/src/routes.ts |
Adds client route helpers for phone confirmation endpoints. |
modules/Users/tests/SimpleModule.Users.Tests/Integration/PhoneConfirmationEndpointTests.cs |
Adds endpoint tests for send, confirm, change, remove, and unauthenticated flows. |
modules/Users/src/SimpleModule.Users/UsersModule.cs |
Registers the default SMS sender. |
modules/Users/src/SimpleModule.Users/UserAdminService.cs |
Adds phone re-verification admin service logic and maps phone fields. |
modules/Users/src/SimpleModule.Users/types.ts |
Adds generated TypeScript phone fields to AdminUserDto. |
modules/Users/src/SimpleModule.Users/Services/ConsoleSmsSender.cs |
Adds console-backed SMS verification sender. |
modules/Users/src/SimpleModule.Users/Pages/Account/Manage/SendPhoneVerificationCodeEndpoint.cs |
Adds endpoint to generate and send phone verification codes. |
modules/Users/src/SimpleModule.Users/Pages/Account/Manage/RemovePhoneNumberEndpoint.cs |
Adds endpoint to clear phone number and confirmation state. |
modules/Users/src/SimpleModule.Users/Pages/Account/Manage/ManageIndexEndpoint.cs |
Supplies phone confirmation state to the manage page. |
modules/Users/src/SimpleModule.Users/Pages/Account/Manage/ManageIndex.tsx |
Reworks profile phone UI for send/verify/remove flow. |
modules/Users/src/SimpleModule.Users/Pages/Account/Manage/ConfirmPhoneNumberEndpoint.cs |
Adds endpoint to verify code and atomically update confirmed phone number. |
modules/Users/src/SimpleModule.Users.Contracts/UsersConstants.cs |
Adds route constants for phone verification endpoints. |
modules/Users/src/SimpleModule.Users.Contracts/IUserAdminContracts.cs |
Extends admin contract with phone re-verification method. |
modules/Users/src/SimpleModule.Users.Contracts/ISmsSender.cs |
Defines SMS verification sender abstraction. |
modules/Users/src/SimpleModule.Users.Contracts/AdminUserDto.cs |
Adds phone number and confirmation fields to admin DTO. |
modules/Admin/src/SimpleModule.Admin/Pages/Admin/UsersEdit.tsx |
Wires admin confirmation dialog/action for phone re-verification. |
modules/Admin/src/SimpleModule.Admin/Pages/Admin/components/UserSecurityTab.tsx |
Adds phone verification status card and action button. |
modules/Admin/src/SimpleModule.Admin/Locales/keys.ts |
Adds localization keys for phone verification admin UI. |
modules/Admin/src/SimpleModule.Admin/Locales/en.json |
Adds English admin phone verification labels/messages. |
modules/Admin/src/SimpleModule.Admin/Endpoints/Admin/AdminUsersEndpoint.cs |
Adds admin force phone re-verification route. |
modules/Admin/src/SimpleModule.Admin.Contracts/AdminConstants.cs |
Adds admin route constant for phone re-verification. |
Comments suppressed due to low confidence (1)
modules/Users/src/SimpleModule.Users/Pages/Account/Manage/SendPhoneVerificationCodeEndpoint.cs:58
- The send-code path is not covered for the behavior that actually dispatches the generated token through
ISmsSender; the current tests only assert the response and database state. A fake/recording SMS sender test would catch regressions where no SMS is sent or the wrong phone/code is passed.
await smsSender.SendVerificationCodeAsync(user, phoneNumber, code);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| user, | ||
| phoneNumber | ||
| ); | ||
| await smsSender.SendVerificationCodeAsync(user, phoneNumber, code); |
There was a problem hiding this comment.
Acknowledged — the security concern is real. Deferring to follow-up #199 so the fix can cover both the new phone endpoints and the existing email flow (Manage/Email, ResendEmailConfirmation), which has the same gap. Landing #174 with parity-to-email as scoped, and #199 will close both gaps together.
Generated by Claude Code
| var result = await userManager.ChangePhoneNumberAsync(user, phoneNumber, code); | ||
| if (!result.Succeeded) |
There was a problem hiding this comment.
Agreed — 6 digits is too narrow a search space to leave unthrottled. Tracking in #199 alongside the SMS resend cooldown, so we add per-user attempt counters once for both email and phone code submission. Out of scope for this PR (phone parity to the existing untrottled email flow).
Generated by Claude Code
| displayName: string; | ||
| email: string; | ||
| emailConfirmed: boolean; | ||
| phoneNumber: string; |
There was a problem hiding this comment.
Correct observation, but this is a pre-existing limitation of SimpleModule.Generator — it doesn't propagate C# nullability into the generated TS types. The neighbouring email field has the exact same shape (string? in C#, string in TS), and so do other DTOs across the codebase. Fixing it properly means teaching the generator about NullableAnnotation, which is its own change and should land for all DTOs at once. Leaving this matching the existing convention so it can be fixed uniformly later.
Generated by Claude Code
| statusMessage, | ||
| }: Props) { | ||
| const [confirmOpen, setConfirmOpen] = useState(false); | ||
| const [inputPhone, setInputPhone] = useState(pendingPhoneNumber ?? phoneNumber ?? ''); |
There was a problem hiding this comment.
Good catch — confirmed Inertia preserves the useState initialiser across the POST response, so the input stayed stale after Remove. Pushed a fix that re-syncs inputPhone via useEffect whenever phoneNumber or pendingPhoneNumber changes.
Generated by Claude Code
| group.MapPost( | ||
| "/{id}/force-phone-reverify", | ||
| async Task<IResult> (string id, IUserAdminContracts userAdmin) => | ||
| { | ||
| await userAdmin.ForcePhoneReverificationAsync(UserId.From(id)); |
There was a problem hiding this comment.
Added ForcePhoneReverify_ValidUser_ClearsPhoneNumberConfirmedAndRedirects to AdminUsersEndpointTests. It seeds a user with a confirmed phone, posts to /admin/users/{id}/force-phone-reverify, and asserts both the 302 redirect and that PhoneNumberConfirmed flips to false while the number itself is preserved.
Generated by Claude Code
… test
- Re-sync `inputPhone` in ManageIndex via useEffect when the server
returns a new phoneNumber (e.g. after Remove). Inertia preserves
component state across POST responses, so the useState initializer
wasn't picking up the cleared number.
- Add integration test asserting POST /admin/users/{id}/force-phone-reverify
clears PhoneNumberConfirmed and redirects, while preserving the phone
number itself.
Addresses Copilot review comments on #198. Resend cooldown and 6-digit
code brute-force throttling tracked in #199 across both email and phone
flows.
Summary
Brings phone numbers up to parity with the existing email confirmation flow.
PhoneNumberConfirmedis now actually reachable — users send a 6-digit code to their phone, enter it, and the number is atomically saved + confirmed viaUserManager.ChangePhoneNumberAsync. Closes #174.ISmsSender(inSimpleModule.Users.Contracts) +ConsoleSmsSenderdefault that logs the code, mirroringConsoleEmailSender. Real providers (Twilio, etc.) plug in via DI replacement./Identity/Account/Manage/:SendPhoneVerificationCode— generates the change-phone-number token and dispatches viaISmsSender. Does not save the number yet, so an interrupted flow leaves the saved phone unchanged.ConfirmPhoneNumber— callsChangePhoneNumberAsync(user, phone, code), which atomically saves and confirms.RemovePhoneNumber— clears phone + confirmation flag, refreshes sign-in.ManageIndex.tsx— phone field shows a green ✓ when confirmed, a "Send verification code" button, an inline code-entry form once a code is in flight, and a "Remove" button. Status messages render as success or danger based on prefix.UserSecurityTabgains a Phone Verification card showing the number + status, plus a "Force Phone Re-verification" button. NewPOST /admin/users/{id}/force-phone-reverifyendpoint clearsPhoneNumberConfirmedviaIUserAdminContracts.ForcePhoneReverificationAsync.AdminUserDtopicks upPhoneNumber+PhoneNumberConfirmedso the admin tab can render state.Test plan
dotnet build— cleandotnet test— full suite green, including 7 newPhoneConfirmationEndpointTestscovering happy path, invalid code (no save, no confirmation), number change from confirmed → new confirmed, remove flow, and the two unauthenticated 401 pathsnpm run check— biome + typecheck clean across all 13 modulesnpm run validate-pages— all module page registries valid (no new view pages needed; the new endpoints re-render the existingUsers/Account/Manage/IndexInertia page)csharpier check— formatted on the touched modulesNotes
SetPhoneNumberAsync(Identity built-in) already clearsPhoneNumberConfirmedwhenever the number changes, so the "number change invalidates confirmation" requirement is enforced both directly (when admin/user clears via Remove) and atomically (viaChangePhoneNumberAsyncswapping number+confirmation in one shot). The integration testConfirmPhoneNumber_ChangingNumberFromConfirmed_ResetsAndReconfirmsForNewNumberexercises this.POST /Identity/Account/Manage(which used to set the phone via the profile form's Save button) is left untouched but no longer called by the frontend — the verification flow replaces it. Keeping it avoids breaking direct posters and leaves a slot for future profile fields.Generated by Claude Code