Skip to content

Implement user management features with Keycloak integration#36

Open
zribktad wants to merge 7 commits intofeature/usiness-logic-in-handlersfrom
feature/user
Open

Implement user management features with Keycloak integration#36
zribktad wants to merge 7 commits intofeature/usiness-logic-in-handlersfrom
feature/user

Conversation

@zribktad
Copy link
Copy Markdown
Owner

@zribktad zribktad commented Apr 8, 2026

Enhance user management capabilities by implementing user provisioning, role changes, and CRUD operations. Introduce commands for user creation, updates, deletions, and password resets, along with improved error handling and logging. Add WolverineFx.Http package for dead letter endpoints and update the application configuration accordingly. Document identified issues and proposed fixes for distributed consistency.

tadeas.zribko added 4 commits April 8, 2026 13:45
…tion, updates, and deletions

- Added ChangeUserRoleCommand and its handler for changing user roles.
- Introduced CreateUserCommand and handler for user creation with validation.
- Implemented UpdateUserCommand for updating user details.
- Added DeleteUserCommand for user deletion with Keycloak integration.
- Created GetUserByIdQuery and GetUsersQuery for fetching user details.
- Implemented KeycloakPasswordResetCommand for triggering password reset emails.
- Added specifications for filtering users by email and username.
- Introduced validators for user requests and filters.
- Enhanced error handling and logging throughout user management features.
- Updated global usings and removed unused references in Identity module.
Copilot AI review requested due to automatic review settings April 8, 2026 18:57
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the user creation flow to implement a durable outbox pattern, ensuring distributed consistency between the local database and Keycloak. Key changes include the introduction of the ProvisionKeycloakUserEvent for asynchronous provisioning, the implementation of idempotency in the Keycloak administration service, and the reorganization of the Identity module's namespaces. Additionally, the PR adds comprehensive documentation regarding Wolverine message handling and updates the project's error handling policies. Feedback was provided regarding a version mismatch for the WolverineFx.Http package relative to other WolverineFx dependencies.

<PackageVersion Include="WolverineFx" Version="5.27.0" />
<PackageVersion Include="WolverineFx.EntityFrameworkCore" Version="5.27.0" />
<PackageVersion Include="WolverineFx.FluentValidation" Version="5.22.0" />
<PackageVersion Include="WolverineFx.Http" Version="5.24.0" />
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The version of WolverineFx.Http (5.24.0) is not aligned with other WolverineFx packages in this file, which are at version 5.27.0 (e.g., WolverineFx, WolverineFx.EntityFrameworkCore, WolverineFx.Postgresql). Using mismatched versions of packages from the same library can lead to subtle runtime errors or compatibility issues. It's recommended to use the same version for all related packages to ensure compatibility.

    <PackageVersion Include="WolverineFx.Http" Version="5.27.0" />

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

This PR enhances the Identity module’s user management by shifting Keycloak user provisioning to a Wolverine durable-outbox workflow (DB-first, Keycloak-later), adding dead-letter tooling, and consolidating user feature types under a single namespace to simplify usage and validation.

Changes:

  • Refactors user creation to persist AppUser first and publish a durable ProvisionKeycloakUserEvent, handled asynchronously to create/link the Keycloak account.
  • Adds Wolverine dead-letter HTTP endpoints + retry policy for transient HttpRequestException failures, plus documentation on Wolverine message handling.
  • Normalizes Identity user feature namespaces (DTOs/validators/specifications/mappings) and adds helper validation utilities + unit tests for provisioning/create flows.

Reviewed changes

Copilot reviewed 39 out of 42 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
TODO.md Documents distributed consistency risks and proposed fixes.
tests/APITemplate.Tests/Unit/Identity/ProvisionKeycloakUserHandlerTests.cs Adds unit tests for outbox-driven Keycloak provisioning handler.
tests/APITemplate.Tests/Unit/Identity/CreateUserCommandHandlerTests.cs Adds unit tests for new DB-first user creation + provisioning event emission.
src/Modules/Identity/Security/Keycloak/KeycloakAdminService.cs Makes Keycloak user creation idempotent on 409 conflict via username lookup.
src/Modules/Identity/Repositories/UserRepository.cs Removes now-unneeded using after namespace consolidation.
src/Modules/Identity/Logging/IdentityLogs.cs Updates logging events for provisioning + password reset flows.
src/Modules/Identity/IdentityModule.cs Removes obsolete validation namespace import.
src/Modules/Identity/Identity.Api.GlobalUsings.cs Removes DTOs global using after namespace consolidation.
src/Modules/Identity/Features/User/UpdateUser/UpdateUserRequestValidator.cs Moves validator into consolidated Identity.Features.User namespace.
src/Modules/Identity/Features/User/UpdateUser/UpdateUserRequest.cs Moves request DTO into consolidated namespace.
src/Modules/Identity/Features/User/UpdateUser/UpdateUserCommand.cs Splits validation into ValidateAsync and injects result into HandleAsync.
src/Modules/Identity/Features/User/Shared/UserValidationHelper.cs Introduces shared uniqueness validation helpers for email/username.
src/Modules/Identity/Features/User/Shared/UserResponse.cs Moves response DTO into consolidated namespace.
src/Modules/Identity/Features/User/Shared/UserMappings.cs Moves mappings into consolidated namespace.
src/Modules/Identity/Features/User/Shared/UserByUsernameSpecification.cs Moves specification into consolidated namespace.
src/Modules/Identity/Features/User/Shared/UserByEmailSpecification.cs Moves specification into consolidated namespace.
src/Modules/Identity/Features/User/SetUserActive/SetUserActiveCommand.cs Adds ValidateAsync and injects user into handler.
src/Modules/Identity/Features/User/ProvisionKeycloakUser/ProvisionKeycloakUserHandler.cs Adds durable outbox handler to create Keycloak user + link back to AppUser.
src/Modules/Identity/Features/User/ProvisionKeycloakUser/ProvisionKeycloakUserEvent.cs Adds outbox event contract for Keycloak provisioning.
src/Modules/Identity/Features/User/KeycloakPasswordReset/RequestPasswordResetRequest.cs Moves request DTO into consolidated namespace.
src/Modules/Identity/Features/User/KeycloakPasswordReset/KeycloakPasswordResetCommand.cs Adds password-reset command handler calling Keycloak admin service.
src/Modules/Identity/Features/User/GetUsers/UserSortFields.cs Adds sortable field map for user queries.
src/Modules/Identity/Features/User/GetUsers/UserFilterValidator.cs Moves validator into consolidated namespace.
src/Modules/Identity/Features/User/GetUsers/UserFilterSpecification.cs Moves specification into consolidated namespace and removes redundant using.
src/Modules/Identity/Features/User/GetUsers/UserFilterCriteria.cs Moves criteria helper into consolidated namespace.
src/Modules/Identity/Features/User/GetUsers/UserFilter.cs Moves filter DTO into consolidated namespace.
src/Modules/Identity/Features/User/GetUsers/GetUsersQuery.cs Removes redundant specifications using after consolidation.
src/Modules/Identity/Features/User/GetUserById/UserByIdSpecification.cs Moves specification into consolidated namespace and removes redundant using.
src/Modules/Identity/Features/User/GetUserById/GetUserByIdQuery.cs Removes redundant specifications using after consolidation.
src/Modules/Identity/Features/User/DeleteUser/DeleteUserCommand.cs Adds ValidateAsync and injects user into handler.
src/Modules/Identity/Features/User/CreateUser/CreateUserRequestValidator.cs Moves validator into consolidated namespace.
src/Modules/Identity/Features/User/CreateUser/CreateUserRequest.cs Moves request DTO into consolidated namespace.
src/Modules/Identity/Features/User/CreateUser/CreateUserCommand.cs Replaces create flow with DB-first + outbox provisioning event emission.
src/Modules/Identity/Features/User/Commands/CreateUserCommand.cs Removes old Keycloak-first create flow and compensating delete logic.
src/Modules/Identity/Features/User/ChangeUserRole/ChangeUserRoleRequestValidator.cs Moves validator into consolidated namespace.
src/Modules/Identity/Features/User/ChangeUserRole/ChangeUserRoleRequest.cs Moves request DTO into consolidated namespace.
src/Modules/Identity/Features/User/ChangeUserRole/ChangeUserRoleCommand.cs Adds ValidateAsync and injects user into handler.
src/Modules/Identity/Entities/AppUser.cs Adds LinkKeycloak() to safely set Keycloak linkage once.
src/APITemplate/Api/Program.cs Adds Wolverine HTTP + dead-letter endpoints + retry/dead-letter policy for HttpRequestException.
src/APITemplate/Api/APITemplate.csproj Adds WolverineFx.Http dependency.
docs/wolverine-message-handling.md Documents inline vs outbox execution, retries, and dead-letter behavior.
Directory.Packages.props Pins WolverineFx.Http package version.
Comments suppressed due to low confidence (4)

src/Modules/Identity/Features/User/UpdateUser/UpdateUserCommand.cs:64

  • UpdateUserCommandHandler has been refactored into ValidateAsync + HandleAsync with a new HandleAsync signature (now takes ErrorOr<AppUser> userResult and no longer accepts IMessageBus). There are existing unit tests/callers still invoking the old HandleAsync(command, repository, unitOfWork, bus, ct) signature (e.g., tests/APITemplate.Tests/Unit/Handlers/UserRequestHandlersTests.cs:298+), which will fail to compile until updated to the new pattern.
    src/Modules/Identity/Features/User/SetUserActive/SetUserActiveCommand.cs:28
  • SetUserActiveCommandHandler.HandleAsync signature changed to require an injected ErrorOr<AppUser> userResult (from ValidateAsync) and no longer accepts an IMessageBus. Existing unit tests/callers are still invoking the old signature with a bus parameter (e.g., tests/APITemplate.Tests/Unit/Handlers/UserRequestHandlersTests.cs:399+), which will break compilation until updated to the new validation + handle pattern.
    src/Modules/Identity/Features/User/DeleteUser/DeleteUserCommand.cs:31
  • DeleteUserCommandHandler.HandleAsync signature changed to require an injected ErrorOr<AppUser> userResult (from ValidateAsync) and no longer accepts an IMessageBus. Existing unit tests/callers are still invoking the old signature with a bus parameter (e.g., tests/APITemplate.Tests/Unit/Handlers/UserRequestHandlersTests.cs:521+), which will break compilation until updated.
    src/Modules/Identity/Features/User/ChangeUserRole/ChangeUserRoleCommand.cs:27
  • ChangeUserRoleCommandHandler.HandleAsync signature changed to require an injected ErrorOr<AppUser> userResult (from ValidateAsync) and no longer accepts IMessageBus/ILogger. Existing unit tests/callers are still invoking the old signature with bus/logger parameters (e.g., tests/APITemplate.Tests/Unit/Handlers/UserRequestHandlersTests.cs:470+), which will fail to compile until updated.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +11 to +39
// Wolverine lifecycle method — runs before HandleAsync; result is injected as a parameter
public static async Task<ErrorOr<Success>> ValidateAsync(
CreateUserCommand command,
IUserRepository repository,
CancellationToken ct
)
{
ErrorOr<Success> emailResult = await UserValidationHelper.ValidateEmailUniqueAsync(
repository,
command.Request.Email,
ct
);
if (emailResult.IsError)
return emailResult.Errors;

return await UserValidationHelper.ValidateUsernameUniqueAsync(
repository,
command.Request.Username,
ct
);
}

public static async Task<(ErrorOr<UserResponse>, OutgoingMessages)> HandleAsync(
CreateUserCommand command,
IUserRepository repository,
IUnitOfWork<IdentityDbMarker> unitOfWork,
ErrorOr<Success> validation,
CancellationToken ct
)
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

CreateUserCommandHandler now uses a ValidateAsync lifecycle method and a different HandleAsync signature. There are existing unit tests/callers still using the old signature that accepted IMessageBus, ILogger, and IKeycloakAdminService (e.g., tests/APITemplate.Tests/Unit/Handlers/UserRequestHandlersTests.cs:162+), which will fail to compile until updated to call ValidateAsync and pass its result into HandleAsync (or switch to Wolverine invocation).

Copilot uses AI. Check for mistakes.
Comment on lines 12 to 21
[LoggerMessage(
EventId = 2001,
Level = LogLevel.Error,
Message = "DB save failed after creating Keycloak user {KeycloakUserId}. Attempting compensating delete."
Level = LogLevel.Information,
Message = "Provisioned Keycloak account for AppUser {UserId} — KeycloakUserId={KeycloakUserId}"
)]
public static partial void CreateUserDbSaveFailed(
public static partial void KeycloakUserProvisioned(
this ILogger logger,
Exception exception,
[SensitiveData] string keycloakUserId
Guid userId,
string keycloakUserId
);
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

IdentityLogs.KeycloakUserProvisioned logs KeycloakUserId without [SensitiveData], but other Identity logs treat Keycloak IDs/bodies as sensitive (e.g., DeleteUserDbDeleteFailed uses [SensitiveData]). If log redaction is expected for Keycloak identifiers, mark keycloakUserId as [SensitiveData] here as well to keep telemetry consistent.

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +66
if (response.StatusCode == HttpStatusCode.Conflict)
{
// Idempotent retry: user already exists in Keycloak (e.g. handler ran twice due to
// outbox retry after a DB commit failure). Fetch the existing user ID by username.
keycloakUserId = await GetExistingUserIdByUsernameAsync(username, ct);
_logger.KeycloakUserCreated(username, keycloakUserId);
return keycloakUserId;
}
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

In CreateUserAsync, a 409 Conflict path logs _logger.KeycloakUserCreated(...) even though the user was not created in this execution (it already existed). This makes operational logs misleading during idempotent retries. Consider adding a distinct log message/event for the conflict/idempotent path (e.g., “Keycloak user already exists, resolved id”) or adjusting the existing log message to reflect the actual behavior.

Copilot uses AI. Check for mistakes.
Comment on lines 49 to 53
<PackageVersion Include="WolverineFx" Version="5.27.0" />
<PackageVersion Include="WolverineFx.EntityFrameworkCore" Version="5.27.0" />
<PackageVersion Include="WolverineFx.FluentValidation" Version="5.22.0" />
<PackageVersion Include="WolverineFx.Http" Version="5.24.0" />
<PackageVersion Include="WolverineFx.Postgresql" Version="5.27.0" />
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

WolverineFx.Http is pinned to 5.24.0 while other Wolverine packages are at 5.27.0. This version skew can cause subtle runtime/compile incompatibilities if the packages expect the same shared abstractions. If possible, align WolverineFx.Http to the same WolverineFx minor/patch version used elsewhere (or document why it must differ).

Copilot uses AI. Check for mistakes.
tadeas.zribko added 3 commits April 8, 2026 22:43
…actor user command handlers for consistency
- Introduced a new enum `ProvisioningStatus` to track the provisioning state of external identity provider accounts.
- Updated the `AppUser` entity to include a `ProvisioningStatus` field with a default value of "Pending".
- Created a migration to add the `ProvisioningStatus` field to the `Users` table and ensure proper constraints and indices.
- Added corresponding migration files for the new schema changes.

refactor(productCatalog): enhance logging for orphaned product data cleanup

- Updated logging messages for orphaned product data cleanup to provide clearer context.
- Introduced new logging methods to track the marking and sweeping of orphaned product data.

chore(migrations): add PendingDeletion field to product_data collection

- Created a MongoDB migration to backfill existing `product_data` documents with a `PendingDeletion` field set to false.
- Implemented methods to handle the addition and removal of the `PendingDeletion` field in the database.
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.

2 participants