Skip to content

feat(identity): split organization and user registration#186

Merged
phuongnse merged 4 commits into
mainfrom
codex/registration-split
Jun 5, 2026
Merged

feat(identity): split organization and user registration#186
phuongnse merged 4 commits into
mainfrom
codex/registration-split

Conversation

@phuongnse

@phuongnse phuongnse commented Jun 5, 2026

Copy link
Copy Markdown
Owner

Summary

Implements the split between organization onboarding and user identity registration. POST /api/organizations now captures organization/contact/legal facts, sends organization-contact verification, starts tenant provisioning after verification, and returns a first-user setup token. POST /api/users/register can consume that setup token to attach the first admin user while standalone user registration remains supported.

Linked spec

  • docs/use-cases/platform-foundation/register-org/README.md
  • docs/use-cases/identity-access/register-user/README.md

Requirements & rules followed

  • Spec -> code - registration behavior matches the split use-case docs; remaining frontend polish gaps are documented in implementation status callouts.
  • Ready review - affected register-org/register-user acceptance paths were mapped before implementation.
  • Path coverage matrix - domain/application/API/frontend happy, validation, expired/used token, provisioning, and setup-token paths are covered by updated tests.
  • Verification gate - python scripts/axis.py verify passed; additional Identity Infrastructure and API Testcontainers suites passed with the WSL Docker TCP endpoint.
  • Docs review - use-case docs, progress docs, OpenAPI, and generated frontend API types were updated.
  • Retrospective review - no durable rule or repeat finding emerged from this change.
  • Workarounds - no P0/P1 workaround was introduced or resolved.
  • No new TODO / FIXME / NotImplementedException / placeholder / stub under src/, tests/, frontend/src/.

Summary by CodeRabbit

Release Notes

  • New Features

    • Separated organization contact verification from user registration; organization contacts now verify their email first and receive a setup token to complete admin user registration.
    • Email verification endpoint now intelligently routes organization contacts to user registration and verified users to workspace provisioning.
  • Documentation

    • Updated implementation status and flow diagrams across registration use-case documentation to reflect the new registration-split architecture.

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 64437a5b-b70a-47d0-96cb-da0ef953d8e4

📥 Commits

Reviewing files that changed from the base of the PR and between 6eeafc0 and 8d8b08c.

📒 Files selected for processing (20)
  • docs/playbooks/patterns.md
  • frontend/src/lib/api-types.ts
  • openapi.json
  • src/Axis.Api/Endpoints/AuthEndpoints.cs
  • src/Axis.Api/Endpoints/UserEndpoints.cs
  • src/Modules/Identity/Axis.Identity.Application/Commands/RegisterOrganization/RegisterOrganizationHandler.cs
  • src/Modules/Identity/Axis.Identity.Application/Commands/RegisterUser/RegisterUserHandler.cs
  • src/Modules/Identity/Axis.Identity.Application/Commands/VerifyEmail/VerifyEmailHandler.cs
  • src/Modules/Identity/Axis.Identity.Application/Commands/VerifyEmail/VerifyEmailSessionEstablishedDto.cs
  • src/Modules/Identity/Axis.Identity.Application/Commands/VerifyEmail/VerifyEmailSuccessDto.cs
  • src/Modules/Identity/Axis.Identity.Application/Services/IOrganizationRegistrationTokenStore.cs
  • src/Modules/Identity/Axis.Identity.Domain/Aggregates/Organization.cs
  • src/Modules/Identity/Axis.Identity.Infrastructure/Repositories/RegistrationIdempotencyRepository.cs
  • src/Modules/Identity/Axis.Identity.Infrastructure/Services/OrganizationRegistrationTokenStore.cs
  • tests/Api/Axis.Api.Tests/Identity/OrganizationEndpointTests.cs
  • tests/Modules/Identity/Axis.Identity.Application.Tests/Commands/RegisterUserHandlerTests.cs
  • tests/Modules/Identity/Axis.Identity.Application.Tests/Commands/VerifyEmailHandlerTests.cs
  • tests/Modules/Identity/Axis.Identity.Domain.Tests/Aggregates/OrganizationTests.cs
  • tests/Modules/Identity/Axis.Identity.Infrastructure.Tests/Repositories/RegistrationIdempotencyRepositoryTests.cs
  • tests/Modules/Identity/Axis.Identity.Infrastructure.Tests/Services/OrganizationRegistrationTokenStoreTests.cs
✅ Files skipped from review due to trivial changes (2)
  • docs/playbooks/patterns.md
  • frontend/src/lib/api-types.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/Axis.Api/Endpoints/UserEndpoints.cs
  • src/Modules/Identity/Axis.Identity.Application/Commands/VerifyEmail/VerifyEmailSessionEstablishedDto.cs
  • tests/Modules/Identity/Axis.Identity.Application.Tests/Commands/VerifyEmailHandlerTests.cs
  • src/Modules/Identity/Axis.Identity.Domain/Aggregates/Organization.cs
  • tests/Modules/Identity/Axis.Identity.Domain.Tests/Aggregates/OrganizationTests.cs
  • src/Modules/Identity/Axis.Identity.Application/Commands/VerifyEmail/VerifyEmailSuccessDto.cs
  • src/Modules/Identity/Axis.Identity.Application/Commands/RegisterUser/RegisterUserHandler.cs
  • openapi.json

📝 Walkthrough

Walkthrough

This PR splits organization contact verification from user registration by introducing organization-scoped registration tokens, bifurcating the email-verification endpoint to route to either user provisioning or user registration with a setup token, and attaching first users to organizations via token consumption during registration.

Changes

Registration Split: Organization Contact Verification and First User Setup

Layer / File(s) Summary
Public contract and schema updates
openapi.json, frontend/src/lib/api-types.ts, src/Axis.Api/Endpoints/RegisterOrganizationRequest.cs, src/Axis.Api/Endpoints/RegisterUserRequest.cs, src/Modules/Identity/Axis.Identity.Application/Commands/VerifyEmail/VerifyEmailSessionEstablishedDto.cs, src/Modules/Identity/Axis.Identity.Application/Commands/VerifyEmail/VerifyEmailSuccessDto.cs
OpenAPI summaries and endpoint/schema updates clarify organization-contact registration, user registration with setup tokens, and next-step routing. RegisterOrganizationRequest removes admin fields and adds organizationContactEmail. RegisterUserRequest adds nullable organizationSetupToken. VerifyEmailSessionEstablishedDto gains organizationSetupToken and enum RegisterUser option; VerifyEmailSuccessDto makes UserId nullable to support org-contact verification without session establishment.
Organization registration token infrastructure
src/Modules/Identity/Axis.Identity.Application/Services/IOrganizationRegistrationTokenStore.cs, src/Modules/Identity/Axis.Identity.Infrastructure/Persistence/Entities/OrganizationRegistrationToken.cs, src/Modules/Identity/Axis.Identity.Infrastructure/Persistence/Configurations/OrganizationRegistrationTokenConfiguration.cs, src/Modules/Identity/Axis.Identity.Infrastructure/Services/OrganizationRegistrationTokenStore.cs, src/Modules/Identity/Axis.Identity.Infrastructure/Persistence/IdentityDbContext.cs, src/Modules/Identity/Axis.Identity.Infrastructure/Extensions/IdentityInfrastructureExtensions.cs, src/Modules/Identity/Axis.Identity.Infrastructure/Migrations/20260605075025_RegistrationSplit.*, src/Modules/Identity/Axis.Identity.Infrastructure/Migrations/IdentityDbContextModelSnapshot.cs
New IOrganizationRegistrationTokenStore interface and EF-backed OrganizationRegistrationTokenStore implementation define token creation and consumption for verification and first-user setup. Entity, configuration, DbContext, DI registration, and migrations/snapshots wire token persistence with expiry, usage, and organization/user tracking.
Register organization as contact-verification flow
src/Modules/Identity/Axis.Identity.Domain/Aggregates/OrganizationStatus.cs, src/Modules/Identity/Axis.Identity.Domain/Aggregates/Organization.cs, src/Modules/Identity/Axis.Identity.Application/Commands/RegisterOrganization/RegisterOrganizationCommand.cs, src/Modules/Identity/Axis.Identity.Application/Commands/RegisterOrganization/RegisterOrganizationCommandValidator.cs, src/Modules/Identity/Axis.Identity.Application/Commands/RegisterOrganization/RegisterOrganizationHandler.cs, src/Axis.Api/Endpoints/OrganizationEndpoints.cs, src/Axis.Api/Endpoints/UserEndpoints.cs
PendingVerification status added to enum. Organization aggregate gains RegisterForContactVerification() factory and RecordLegalAcceptance() to track accepted terms/privacy versions and legal acceptance timestamp. Handler now validates organization contact email, creates pending organizations, and issues organization verification tokens instead of creating users. Endpoints and command/validator updated accordingly.
Verify-email dual path and next-step routing
src/Modules/Identity/Axis.Identity.Application/Commands/VerifyEmail/VerifyEmailHandler.cs, src/Axis.Api/Endpoints/AuthEndpoints.cs, frontend/src/features/auth/hooks/useVerifyEmail.ts
Handler now falls back to organization-contact verification when user email token is not found, resolving organization token to provision and create first-user setup token. Domain provisioning conditional on PendingVerification status. Endpoint gates session establishment on SessionEstablished flag. Frontend hook detects nextStep='RegisterUser' and navigates to /register with setup token instead of PKCE flow.
Register-user setup-token attachment path
src/Modules/Identity/Axis.Identity.Application/Commands/RegisterUser/RegisterUserCommand.cs, src/Modules/Identity/Axis.Identity.Application/Commands/RegisterUser/RegisterUserCommandValidator.cs, src/Modules/Identity/Axis.Identity.Application/Commands/RegisterUser/RegisterUserHandler.cs, src/Axis.Api/Endpoints/UserEndpoints.cs, frontend/src/features/auth/hooks/useRegister.ts, frontend/tests/register-page.test.tsx
Command adds optional organizationSetupToken parameter. Validator enforces 512-char max length. Handler consumes setup token via new AttachFirstUserToOrganizationAsync helper, which hashes the token, resolves organization, validates sign-in readiness, creates admin membership, and persists. Endpoint forwards setup token from request. Frontend hook reads setupToken query param and includes it in registration payload.
Provisioning status and retry with organization tokens
src/Modules/Identity/Axis.Identity.Application/Queries/GetProvisioningStatus/GetProvisioningStatusHandler.cs, src/Modules/Identity/Axis.Identity.Application/Commands/RetryTenantProvisioning/RetryTenantProvisioningHandler.cs
Handlers now attempt organization-token resolution before user-token fallback. GetProvisioningStatusHandler builds status via new BuildStatusAsync helper when org token resolves. RetryTenantProvisioningHandler extracts provisioning-retry logic into RetryForOrganizationAsync and calls it from both org-token and user-membership paths.
Idempotency repository bulk-update refactor
src/Modules/Identity/Axis.Identity.Infrastructure/Repositories/RegistrationIdempotencyRepository.cs, tests/Modules/Identity/Axis.Identity.Infrastructure.Tests/Repositories/RegistrationIdempotencyRepositoryTests.cs
MarkCompletedAsync and MarkFailedAsync now use EF Core ExecuteUpdateAsync for set-based status updates instead of entity loading/mutation, preventing unrelated tracked entities from being persisted. New regression test verifies unrelated user changes are not flushed.
API test helpers and endpoint test updates
tests/Api/Axis.Api.Tests/Helpers/TestRegistrationPayload.cs, tests/Api/Axis.Api.Tests/Helpers/AuthHelper.cs, tests/Api/Axis.Api.Tests/Identity/AuthEndpointTests.cs, tests/Api/Axis.Api.Tests/Identity/OrganizationEndpointTests.cs
TestRegistrationPayload refactored to provide organization-level and user-level payloads separately. AuthHelper introduces RegisterAndVerifyOrganizationAsync, RegisterFirstAdminWithoutUserVerificationAsync, and RegisterAndVerifyAdminAsync helpers orchestrating the split registration flow. Endpoint tests updated to use helpers and assert organization-contact emails instead of hardcoded admin emails.
Application and domain test alignment
tests/Modules/Identity/Axis.Identity.Application.Tests/Commands/RegisterOrganizationHandlerTests.cs, tests/Modules/Identity/Axis.Identity.Application.Tests/Commands/RegisterUserHandlerTests.cs, tests/Modules/Identity/Axis.Identity.Application.Tests/Commands/VerifyEmailHandlerTests.cs, tests/Modules/Identity/Axis.Identity.Application.Tests/Queries/GetProvisioningStatusHandlerTests.cs, tests/Modules/Identity/Axis.Identity.Application.Tests/Commands/RetryTenantProvisioningHandlerTests.cs, tests/Modules/Identity/Axis.Identity.Domain.Tests/Aggregates/OrganizationTests.cs, tests/Modules/Identity/Axis.Identity.Infrastructure.Tests/Services/OrganizationRegistrationTokenStoreTests.cs
Tests now cover pending-organization creation and system-role seeding, organization-contact verification and setup-token emission, user registration with valid/expired setup tokens, organization-token resolution paths in provisioning handlers, PendingVerification status transitions, and token store transactional behavior.
Documentation realignment for split journeys
docs/PROGRESS.md, docs/use-cases/identity-access/README.md, docs/use-cases/identity-access/register-user/README.md, docs/use-cases/platform-foundation/README.md, docs/use-cases/platform-foundation/register-org/README.md, docs/playbooks/patterns.md
Use-case docs updated to describe register-org API handling organization contact verification and tenant provisioning while register-user API handles standalone and setup-token-driven user registration. Screen flow reordered: org email verification → user registration → workspace provisioning. Playbook patterns clarified to warn that token stores and repositories must not call SaveChangesAsync and should use set-based updates to avoid tracked-entity side effects.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • phuongnse/axis#155: Both PRs modify the /api/auth/verify-email contract and session-establishment behavior via PKCE/cookies, with this PR's organization-contact routing building on top of the verify-email session groundwork.
  • phuongnse/axis#177: Both PRs implement the same register-org → register-user split for identity onboarding with standalone user registration and organization setup-token handoff contracts.
  • phuongnse/axis#141: Frontend registration flow changes (setupToken query param forwarding and hook integration) build directly on the registration UI and useRegister logic from this PR.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/registration-split

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
docs/use-cases/platform-foundation/register-org/README.md (1)

175-175: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Align sequence text with the new canonical screen order.

Line 175 still says “Continue to register-user setup link when ready,” but this now contradicts Line 106–107 and Line 129–130, which define register-user before workspace-provisioning. Update this line so the sequence diagram matches the reordered flow.

As per coding guidelines, “don’t leave stale ‘planned/will be wired’ statements; use PROGRESS for status and use-case files for spec-aligned implementation notes.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/use-cases/platform-foundation/register-org/README.md` at line 175,
Update the sequence line that currently reads "Web-->>Rep: Continue to
register-user setup link when ready" so it matches the canonical screen order
where register-user appears before workspace-provisioning: change the message to
reference workspace-provisioning instead of register-user (e.g., "Web-->>Rep:
Continue to workspace-provisioning setup link when ready") and remove any "will
be wired" style wording—if you need a status note use PROGRESS or move
implementation notes into the use-case spec file; update the sequence entry that
mentions register-user / workspace-provisioning accordingly.
tests/Modules/Identity/Axis.Identity.Application.Tests/Commands/RegisterOrganizationHandlerTests.cs (1)

107-121: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

The “without creating anything” test under-asserts side effects.

Line 107’s claim is broader than what’s verified. The test currently checks only _orgRepo and _emailSender; it can still pass if roles/tokens are created or writes are committed.

Suggested assertion coverage
         result.IsFailure.Should().BeTrue();
         await _orgRepo.DidNotReceive().AddAsync(Arg.Any<Organization>(), Arg.Any<CancellationToken>());
+        await _roleRepo.DidNotReceive().AddAsync(Arg.Any<Role>(), Arg.Any<CancellationToken>());
+        await _organizationTokenStore.DidNotReceive().CreateVerificationAsync(
+            Arg.Any<Guid>(), Arg.Any<string>(), Arg.Any<DateTime>(), Arg.Any<CancellationToken>());
+        await _uow.DidNotReceive().SaveChangesAsync(Arg.Any<CancellationToken>());
         await _emailSender.DidNotReceive().SendVerificationEmailAsync(
             Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>());

As per coding guidelines, “the assertions must prove what the test name claims.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@tests/Modules/Identity/Axis.Identity.Application.Tests/Commands/RegisterOrganizationHandlerTests.cs`
around lines 107 - 121, The test
RegisterOrganization_WhenEmailIsInvalid_ReturnsFailureWithoutCreatingAnything
currently only asserts _orgRepo and _emailSender; expand it to prove no other
side effects occur by also asserting that role and token repositories were not
called and no commit happened—e.g., add DidNotReceive checks for
_roleRepo.AddAsync(Arg.Any<Role>(), Arg.Any<CancellationToken>()),
_tokenRepo.AddAsync(Arg.Any<Token>(), Arg.Any<CancellationToken>()), and
_unitOfWork.CommitAsync(Arg.Any<CancellationToken>()) (or the actual
unit-of-work commit method used in the code) so the test name is fully
satisfied.
src/Modules/Identity/Axis.Identity.Application/Commands/RegisterOrganization/RegisterOrganizationHandler.cs (1)

111-125: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Make organization creation and verification-token issuance atomic.

SaveChangesAsync commits the new organization and roles before CreateVerificationAsync, SendVerificationEmailAsync, and MarkCompletedAsync. If any of those later steps fail, you can leave behind a persisted pending organization with no guaranteed verification path, and the catch then marks the idempotency key failed so a retry can create a second pending org. Keep org + verification token in one transaction, then hand email delivery off after commit (for example via an outbox).

Also applies to: 129-133

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/Modules/Identity/Axis.Identity.Application/Commands/RegisterOrganization/RegisterOrganizationHandler.cs`
around lines 111 - 125, The current flow calls uow.SaveChangesAsync before
issuing the verification token and emailing, which can leave a persisted
organization without a token if subsequent steps fail; change
RegisterOrganizationHandler to create the verification token
(OpaqueTokenGenerator.Create) and call
organizationTokenStore.CreateVerificationAsync within the same
unit-of-work/transaction (uow) so org + token are saved atomically, then commit
via uow.SaveChangesAsync; after successful commit, dispatch
emailSender.SendVerificationEmailAsync (or enqueue an outbox entry to send the
email) and only then call MarkIdempotencyCompletedIfNeededAsync; ensure any
exceptions before commit roll back without marking idempotency completed.
src/Modules/Identity/Axis.Identity.Infrastructure/Services/OrganizationIdentityPurger.cs (1)

12-40: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Move external logo deletion after a successful DB transaction.

DeleteLogoAsync runs before the database purge completes. If any subsequent delete fails, storage and DB state drift (logo removed, org still present/partially present).

Suggested transaction/order shape
 public async Task PurgeAsync(Guid organizationId, string? logoUrl, CancellationToken cancellationToken)
 {
-    if (!string.IsNullOrEmpty(logoUrl))
-    {
-        try
-        {
-            await logoStorage.DeleteLogoAsync(logoUrl, cancellationToken);
-        }
-        catch (Exception)
-        {
-            // Best-effort file cleanup during hard delete.
-        }
-    }
+    await using IDbContextTransaction tx = await context.Database.BeginTransactionAsync(cancellationToken);

     IQueryable<Guid> userIds = context.OrganizationMemberships
         .Where(m => m.OrganizationId == organizationId)
         .Select(m => m.UserId);

@@
     await context.Organizations
         .Where(o => o.Id == organizationId)
         .ExecuteDeleteAsync(cancellationToken);
+
+    await tx.CommitAsync(cancellationToken);
+
+    if (!string.IsNullOrEmpty(logoUrl))
+    {
+        try
+        {
+            await logoStorage.DeleteLogoAsync(logoUrl, cancellationToken);
+        }
+        catch (Exception)
+        {
+            // Best-effort post-commit cleanup.
+        }
+    }
 }

As per coding guidelines, “Side effects (external storage, messaging) must not commit before the DB transaction they depend on — flag ordering that can leave drift.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/Modules/Identity/Axis.Identity.Infrastructure/Services/OrganizationIdentityPurger.cs`
around lines 12 - 40, PurgeAsync currently calls logoStorage.DeleteLogoAsync
before performing DB deletes, which can cause storage/DB drift; change the flow
so all DB deletions (the queries using context.OrganizationMemberships,
context.EmailVerificationTokens.ExecuteDeleteAsync,
context.OrganizationRegistrationTokens.ExecuteDeleteAsync,
context.PasswordResetTokens.ExecuteDeleteAsync) are executed inside a single
database transaction and, only after that transaction completes successfully,
call logoStorage.DeleteLogoAsync; keep the existing best-effort catch around
DeleteLogoAsync but move it to after the committed transaction so external side
effects occur only on successful DB commit.
🧹 Nitpick comments (1)
tests/Modules/Identity/Axis.Identity.Application.Tests/Commands/RegisterUserHandlerTests.cs (1)

85-111: ⚡ Quick win

Keep organization IDs consistent in the setup-token fixture.

Line 85 defines organizationId, but Lines 88-92 create an Organization with a different organization.Id. This impossible state makes the test brittle and implementation-coupled.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@tests/Modules/Identity/Axis.Identity.Application.Tests/Commands/RegisterUserHandlerTests.cs`
around lines 85 - 111, The test creates a separate organizationId Guid but then
constructs an Organization whose Id doesn't match that Guid, causing impossible
state; fix by making the Organization and the organizationId consistent—either
pass the pre-generated organizationId into Organization.Create (or use an
overload/ctor that accepts an id) so the created Organization.Id equals
organizationId, or remove the standalone organizationId and set organizationId =
organization.Id after Organization.Create; update uses in the test
(_organizationTokenStore.ConsumeFirstUserSetupAsync,
_organizationRepo.GetByIdAsync, Role.CreateSystem) to reference the single
consistent organizationId/Organization instance.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@openapi.json`:
- Around line 7523-7525: Operation summaries for the affected endpoints are out
of date: update the OpenAPI operation.summary and relevant descriptions for
/api/users/register and /api/auth/verify-email (and duplicate entries in the
block around 8037-8062) to reflect the new setup-token branch—specifically note
that registration is not "standalone-only" anymore and email verification no
longer always establishes a session; edit the operation objects for the paths
"/api/users/register" and "/api/auth/verify-email" (and their corresponding
operationId entries) to clearly state the new behaviors so the generated docs,
frontend types, and tests remain consistent.

In `@src/Axis.Api/Endpoints/AuthEndpoints.cs`:
- Around line 141-152: The endpoint currently computes business logic (deriving
nextStep using VerifyEmailNextStep and
result.Value.OrganizationSetupToken/OrganizationId) before returning
VerifyEmailSessionEstablishedDto; move this logic into the application layer by
having the handler (the mediator response) include a precomputed NextStep
property or by adding a factory on VerifyEmailSessionEstablishedDto that accepts
the handler result and computes NextStep there, then update the endpoint to only
call mediator.Send and map the response to Results.Ok without conditional logic
(remove references to nextStep computation from the endpoint).

In
`@src/Modules/Identity/Axis.Identity.Application/Commands/RegisterUser/RegisterUserHandler.cs`:
- Around line 65-72: The handler currently returns early on setup-token failures
(inside the setupToken is not null block) and skips idempotency cleanup, leaving
the idempotency key acquired; update RegisterUserHandler so that before
returning any failure from AttachFirstUserToOrganizationAsync (or other
setup-token business failures) you call a helper like
MarkIdempotencyFailedIfNeededAsync to release the idempotency record; implement
MarkIdempotencyFailedIfNeededAsync to call
idempotencyRepo.MarkFailedAsync(idempotencyKey, cancellationToken) when
idempotencyKey is not null and acquire result ==
RegistrationIdempotencyAcquireResult.Acquired, otherwise return completed task,
and invoke this helper in the failure path right before each early return (e.g.,
after AttachFirstUserToOrganizationAsync returns IsFailure).

In
`@src/Modules/Identity/Axis.Identity.Application/Commands/VerifyEmail/VerifyEmailHandler.cs`:
- Around line 88-93: The verification flow in VerifyEmailHandler currently calls
uow.SaveChangesAsync to persist the organization status and provisioning rows
before calling CreateFirstUserSetupAsync, which can leave the tenant stranded if
token creation fails; change the sequence so that CreateFirstUserSetupAsync is
invoked and its resulting setup-token is created/validated before committing
changes, or wrap both the setup-token creation and the org status/provisioning
updates in a single transactional unit (e.g., the existing unit-of-work
transaction scope) so that either all of: (a) setup token creation, (b)
organization.Status transition from PendingVerification, and (c) provisioning
rows are committed together; update code paths around VerifyEmailHandler
(including the pre-check for organization.Status and the later
uow.SaveChangesAsync calls) to use the single transaction and only return
success after the transaction completes.

In
`@src/Modules/Identity/Axis.Identity.Application/Services/OrganizationRegistrationTokenResults.cs`:
- Around line 3-25: Replace the bespoke records/enums with the shared
Result/Result<T> pattern: remove OrganizationVerificationTokenResolveResult and
OrganizationVerificationTokenState and have the verification flow return
Result<Guid> (successful Result contains the OrganizationId); similarly remove
OrganizationSetupTokenConsumeResult and OrganizationSetupTokenState and return
Result<Guid> for setup consumption. Map the previous enum cases (NotFound,
Expired, AlreadyUsed) to domain failure Results (use appropriate failure
reasons/messages from your shared failure contract) so callers receive Result
failures instead of custom enums; update all methods referencing
OrganizationVerificationTokenResolveResult and
OrganizationSetupTokenConsumeResult to use Result/Result<Guid> accordingly.

In `@src/Modules/Identity/Axis.Identity.Domain/Aggregates/Organization.cs`:
- Around line 166-177: The method BeginProvisioningAfterContactVerification
currently raises OrganizationVerified even when no status change occurs; update
BeginProvisioningAfterContactVerification so OrganizationVerified is only raised
when the organization actually transitions to OrganizationStatus.Provisioning
(i.e., if Status == OrganizationStatus.PendingVerification set Provisioning and
raise, otherwise call BeginProvisioning only when Status !=
OrganizationStatus.Provisioning and raise OrganizationVerified after that
successful transition); reference BeginProvisioningAfterContactVerification,
BeginProvisioning, Status, OrganizationStatus.Provisioning and the
OrganizationVerified domain event to locate and implement the conditional check
that prevents emitting the event when no status change happened.

In
`@src/Modules/Identity/Axis.Identity.Infrastructure/Services/OrganizationRegistrationTokenStore.cs`:
- Around line 148-166: The CreateAsync helper in
OrganizationRegistrationTokenStore is performing a premature commit by calling
context.SaveChangesAsync; remove the SaveChangesAsync call so CreateAsync only
constructs and AddAsync the OrganizationRegistrationToken (use the existing
context.Set<OrganizationRegistrationToken>().AddAsync(token, ct)) and let the
caller control transaction/SaveChanges; if the caller needs the new entity id or
object, return the created OrganizationRegistrationToken (or its Id) from
CreateAsync so callers can persist in their transaction boundary.

In `@tests/Api/Axis.Api.Tests/Identity/OrganizationEndpointTests.cs`:
- Around line 49-50: Replace the legacy admin-email assertions with checks
against the actual contact-email side effect: update the two ctx.Users.Count
checks that use Email.Create("adminidem1a@test.com") and
Email.Create("adminidem1b@test.com") to assert against the
organizationContactEmail value produced by the test flow (use the
organizationContactEmail variable/property passed or created in the test and
Email.Create(...) on it) so the test verifies users are/are-not created for the
contact email rather than the old admin emails.

---

Outside diff comments:
In `@docs/use-cases/platform-foundation/register-org/README.md`:
- Line 175: Update the sequence line that currently reads "Web-->>Rep: Continue
to register-user setup link when ready" so it matches the canonical screen order
where register-user appears before workspace-provisioning: change the message to
reference workspace-provisioning instead of register-user (e.g., "Web-->>Rep:
Continue to workspace-provisioning setup link when ready") and remove any "will
be wired" style wording—if you need a status note use PROGRESS or move
implementation notes into the use-case spec file; update the sequence entry that
mentions register-user / workspace-provisioning accordingly.

In
`@src/Modules/Identity/Axis.Identity.Application/Commands/RegisterOrganization/RegisterOrganizationHandler.cs`:
- Around line 111-125: The current flow calls uow.SaveChangesAsync before
issuing the verification token and emailing, which can leave a persisted
organization without a token if subsequent steps fail; change
RegisterOrganizationHandler to create the verification token
(OpaqueTokenGenerator.Create) and call
organizationTokenStore.CreateVerificationAsync within the same
unit-of-work/transaction (uow) so org + token are saved atomically, then commit
via uow.SaveChangesAsync; after successful commit, dispatch
emailSender.SendVerificationEmailAsync (or enqueue an outbox entry to send the
email) and only then call MarkIdempotencyCompletedIfNeededAsync; ensure any
exceptions before commit roll back without marking idempotency completed.

In
`@src/Modules/Identity/Axis.Identity.Infrastructure/Services/OrganizationIdentityPurger.cs`:
- Around line 12-40: PurgeAsync currently calls logoStorage.DeleteLogoAsync
before performing DB deletes, which can cause storage/DB drift; change the flow
so all DB deletions (the queries using context.OrganizationMemberships,
context.EmailVerificationTokens.ExecuteDeleteAsync,
context.OrganizationRegistrationTokens.ExecuteDeleteAsync,
context.PasswordResetTokens.ExecuteDeleteAsync) are executed inside a single
database transaction and, only after that transaction completes successfully,
call logoStorage.DeleteLogoAsync; keep the existing best-effort catch around
DeleteLogoAsync but move it to after the committed transaction so external side
effects occur only on successful DB commit.

In
`@tests/Modules/Identity/Axis.Identity.Application.Tests/Commands/RegisterOrganizationHandlerTests.cs`:
- Around line 107-121: The test
RegisterOrganization_WhenEmailIsInvalid_ReturnsFailureWithoutCreatingAnything
currently only asserts _orgRepo and _emailSender; expand it to prove no other
side effects occur by also asserting that role and token repositories were not
called and no commit happened—e.g., add DidNotReceive checks for
_roleRepo.AddAsync(Arg.Any<Role>(), Arg.Any<CancellationToken>()),
_tokenRepo.AddAsync(Arg.Any<Token>(), Arg.Any<CancellationToken>()), and
_unitOfWork.CommitAsync(Arg.Any<CancellationToken>()) (or the actual
unit-of-work commit method used in the code) so the test name is fully
satisfied.

---

Nitpick comments:
In
`@tests/Modules/Identity/Axis.Identity.Application.Tests/Commands/RegisterUserHandlerTests.cs`:
- Around line 85-111: The test creates a separate organizationId Guid but then
constructs an Organization whose Id doesn't match that Guid, causing impossible
state; fix by making the Organization and the organizationId consistent—either
pass the pre-generated organizationId into Organization.Create (or use an
overload/ctor that accepts an id) so the created Organization.Id equals
organizationId, or remove the standalone organizationId and set organizationId =
organization.Id after Organization.Create; update uses in the test
(_organizationTokenStore.ConsumeFirstUserSetupAsync,
_organizationRepo.GetByIdAsync, Role.CreateSystem) to reference the single
consistent organizationId/Organization instance.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a3eb8efc-2a3a-4111-bdd2-43d84e5c59e3

📥 Commits

Reviewing files that changed from the base of the PR and between d439e81 and 6eeafc0.

📒 Files selected for processing (51)
  • docs/PROGRESS.md
  • docs/use-cases/identity-access/README.md
  • docs/use-cases/identity-access/register-user/README.md
  • docs/use-cases/platform-foundation/README.md
  • docs/use-cases/platform-foundation/register-org/README.md
  • frontend/src/features/auth/hooks/useRegister.ts
  • frontend/src/features/auth/hooks/useVerifyEmail.ts
  • frontend/src/lib/api-types.ts
  • frontend/tests/register-page.test.tsx
  • frontend/tests/verify-email-page.test.tsx
  • openapi.json
  • src/Axis.Api/Endpoints/AuthEndpoints.cs
  • src/Axis.Api/Endpoints/OrganizationEndpoints.cs
  • src/Axis.Api/Endpoints/RegisterOrganizationRequest.cs
  • src/Axis.Api/Endpoints/RegisterUserRequest.cs
  • src/Axis.Api/Endpoints/UserEndpoints.cs
  • src/Modules/Identity/Axis.Identity.Application/Commands/RegisterOrganization/RegisterOrganizationCommand.cs
  • src/Modules/Identity/Axis.Identity.Application/Commands/RegisterOrganization/RegisterOrganizationCommandValidator.cs
  • src/Modules/Identity/Axis.Identity.Application/Commands/RegisterOrganization/RegisterOrganizationHandler.cs
  • src/Modules/Identity/Axis.Identity.Application/Commands/RegisterUser/RegisterUserCommand.cs
  • src/Modules/Identity/Axis.Identity.Application/Commands/RegisterUser/RegisterUserCommandValidator.cs
  • src/Modules/Identity/Axis.Identity.Application/Commands/RegisterUser/RegisterUserHandler.cs
  • src/Modules/Identity/Axis.Identity.Application/Commands/RetryTenantProvisioning/RetryTenantProvisioningHandler.cs
  • src/Modules/Identity/Axis.Identity.Application/Commands/VerifyEmail/VerifyEmailHandler.cs
  • src/Modules/Identity/Axis.Identity.Application/Commands/VerifyEmail/VerifyEmailSessionEstablishedDto.cs
  • src/Modules/Identity/Axis.Identity.Application/Commands/VerifyEmail/VerifyEmailSuccessDto.cs
  • src/Modules/Identity/Axis.Identity.Application/Queries/GetProvisioningStatus/GetProvisioningStatusHandler.cs
  • src/Modules/Identity/Axis.Identity.Application/Services/IOrganizationRegistrationTokenStore.cs
  • src/Modules/Identity/Axis.Identity.Application/Services/OrganizationRegistrationTokenResults.cs
  • src/Modules/Identity/Axis.Identity.Domain/Aggregates/Organization.cs
  • src/Modules/Identity/Axis.Identity.Domain/Aggregates/OrganizationStatus.cs
  • src/Modules/Identity/Axis.Identity.Infrastructure/Extensions/IdentityInfrastructureExtensions.cs
  • src/Modules/Identity/Axis.Identity.Infrastructure/Migrations/20260605075025_RegistrationSplit.Designer.cs
  • src/Modules/Identity/Axis.Identity.Infrastructure/Migrations/20260605075025_RegistrationSplit.cs
  • src/Modules/Identity/Axis.Identity.Infrastructure/Migrations/IdentityDbContextModelSnapshot.cs
  • src/Modules/Identity/Axis.Identity.Infrastructure/Persistence/Configurations/OrganizationConfiguration.cs
  • src/Modules/Identity/Axis.Identity.Infrastructure/Persistence/Configurations/OrganizationRegistrationTokenConfiguration.cs
  • src/Modules/Identity/Axis.Identity.Infrastructure/Persistence/Entities/OrganizationRegistrationToken.cs
  • src/Modules/Identity/Axis.Identity.Infrastructure/Persistence/IdentityDbContext.cs
  • src/Modules/Identity/Axis.Identity.Infrastructure/Services/OrganizationIdentityPurger.cs
  • src/Modules/Identity/Axis.Identity.Infrastructure/Services/OrganizationRegistrationTokenStore.cs
  • tests/Api/Axis.Api.Tests/Helpers/AuthHelper.cs
  • tests/Api/Axis.Api.Tests/Helpers/TestRegistrationPayload.cs
  • tests/Api/Axis.Api.Tests/Identity/AuthEndpointTests.cs
  • tests/Api/Axis.Api.Tests/Identity/OrganizationEndpointTests.cs
  • tests/Modules/Identity/Axis.Identity.Application.Tests/Commands/RegisterOrganizationHandlerTests.cs
  • tests/Modules/Identity/Axis.Identity.Application.Tests/Commands/RegisterUserHandlerTests.cs
  • tests/Modules/Identity/Axis.Identity.Application.Tests/Commands/RetryTenantProvisioningHandlerTests.cs
  • tests/Modules/Identity/Axis.Identity.Application.Tests/Commands/VerifyEmailHandlerTests.cs
  • tests/Modules/Identity/Axis.Identity.Application.Tests/Queries/GetProvisioningStatusHandlerTests.cs
  • tests/Modules/Identity/Axis.Identity.Domain.Tests/Aggregates/OrganizationTests.cs

Comment thread openapi.json
Comment thread src/Axis.Api/Endpoints/AuthEndpoints.cs Outdated
Comment thread tests/Api/Axis.Api.Tests/Identity/OrganizationEndpointTests.cs Outdated
@phuongnse phuongnse merged commit c6b9f4b into main Jun 5, 2026
9 checks passed
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.

1 participant