Skip to content

feat(identity): phone number confirmation flow (closes #174)#198

Open
antosubash wants to merge 2 commits into
mainfrom
claude/identity-phone-confirmation
Open

feat(identity): phone number confirmation flow (closes #174)#198
antosubash wants to merge 2 commits into
mainfrom
claude/identity-phone-confirmation

Conversation

@antosubash
Copy link
Copy Markdown
Owner

Summary

Brings phone numbers up to parity with the existing email confirmation flow. PhoneNumberConfirmed is now actually reachable — users send a 6-digit code to their phone, enter it, and the number is atomically saved + confirmed via UserManager.ChangePhoneNumberAsync. Closes #174.

  • ISmsSender (in SimpleModule.Users.Contracts) + ConsoleSmsSender default that logs the code, mirroring ConsoleEmailSender. Real providers (Twilio, etc.) plug in via DI replacement.
  • Three new view endpoints under /Identity/Account/Manage/:
    • SendPhoneVerificationCode — generates the change-phone-number token and dispatches via ISmsSender. Does not save the number yet, so an interrupted flow leaves the saved phone unchanged.
    • ConfirmPhoneNumber — calls ChangePhoneNumberAsync(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.
  • Admin UIUserSecurityTab gains a Phone Verification card showing the number + status, plus a "Force Phone Re-verification" button. New POST /admin/users/{id}/force-phone-reverify endpoint clears PhoneNumberConfirmed via IUserAdminContracts.ForcePhoneReverificationAsync.
  • AdminUserDto picks up PhoneNumber + PhoneNumberConfirmed so the admin tab can render state.

Test plan

  • dotnet build — clean
  • dotnet test — full suite green, including 7 new PhoneConfirmationEndpointTests covering happy path, invalid code (no save, no confirmation), number change from confirmed → new confirmed, remove flow, and the two unauthenticated 401 paths
  • npm run check — biome + typecheck clean across all 13 modules
  • npm run validate-pages — all module page registries valid (no new view pages needed; the new endpoints re-render the existing Users/Account/Manage/Index Inertia page)
  • csharpier check — formatted on the touched modules
  • Manual: send code → confirm → see badge; change number → reset+reconfirm; admin force-reverify clears the badge

Notes

  • SetPhoneNumberAsync (Identity built-in) already clears PhoneNumberConfirmed whenever the number changes, so the "number change invalidates confirmation" requirement is enforced both directly (when admin/user clears via Remove) and atomically (via ChangePhoneNumberAsync swapping number+confirmation in one shot). The integration test ConfirmPhoneNumber_ChangingNumberFromConfirmed_ResetsAndReconfirmsForNewNumber exercises this.
  • The legacy 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

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.
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 13, 2026

Deploying simplemodule-website with  Cloudflare Pages  Cloudflare Pages

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

View logs

@antosubash antosubash requested a review from Copilot May 14, 2026 06:56
@antosubash antosubash marked this pull request as ready for review May 14, 2026 06:56
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +54 to +55
var result = await userManager.ChangePhoneNumberAsync(user, phoneNumber, code);
if (!result.Succeeded)
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ?? '');
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +178 to +182
group.MapPost(
"/{id}/force-phone-reverify",
async Task<IResult> (string id, IUserAdminContracts userAdmin) =>
{
await userAdmin.ForcePhoneReverificationAsync(UserId.From(id));
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Identity: phone number confirmation flow

3 participants