From 2ee7a49122be6f336ebb47bffe584db900746973 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 10 Oct 2026 10:12:13 +0200 Subject: [PATCH] Added user management endpoints --- .../GetTenantOrganisationsEndpoint.cs | 2 +- .../Endpoints/Users/AddUserClaimEndpoint.cs | 35 ++- .../Endpoints/Users/AddUserRoleEndpoint.cs | 34 ++- .../Endpoints/Users/CreateUserEndpoint.cs | 29 ++- .../Users/DeleteUserClaimEndpoint.cs | 35 ++- .../Endpoints/Users/DeleteUserRoleEndpoint.cs | 33 ++- .../Endpoints/Users/GetSystemRolesEndpoint.cs | 34 ++- .../Endpoints/Users/GetUserClaimsEndpoint.cs | 35 ++- .../Endpoints/Users/GetUserEndpoint.cs | 4 +- .../Endpoints/Users/GetUserRolesEndpoint.cs | 34 ++- .../Endpoints/Users/LogUserOutEndpoint.cs | 33 ++- .../Endpoints/Users/SetUserLockoutEndpoint.cs | 33 ++- .../Abstractions/Services/IUserService.cs | 40 +++- .../Extensions/Security.cs | 5 +- .../Services/Users/UserCacheService.cs | 106 ++++++++- .../Services/Users/UserService.cs | 210 +++++++++++++++++- .../Abstractions/ISkorubaUserService.cs | 9 +- .../Models/Security/Contracts.cs | 11 +- .../Security/SkorubaUserService.cs | 6 +- 19 files changed, 688 insertions(+), 40 deletions(-) diff --git a/PostFundManagement.Api/Endpoints/Governance/GetTenantOrganisationsEndpoint.cs b/PostFundManagement.Api/Endpoints/Governance/GetTenantOrganisationsEndpoint.cs index 80dc27c..70a3e62 100644 --- a/PostFundManagement.Api/Endpoints/Governance/GetTenantOrganisationsEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Governance/GetTenantOrganisationsEndpoint.cs @@ -1,9 +1,9 @@ using PostFundManagement.Application.Abstractions.Services; using PostFundManagement.Application.Extensions; using PostFundManagement.Domain.Abstractions; -using PostFundManagement.Domain.Entities; using PostFundManagement.Domain.Errors; using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Models; namespace PostFundManagement.Api.Endpoints.Governance; diff --git a/PostFundManagement.Api/Endpoints/Users/AddUserClaimEndpoint.cs b/PostFundManagement.Api/Endpoints/Users/AddUserClaimEndpoint.cs index ace3d84..4cc6cdb 100644 --- a/PostFundManagement.Api/Endpoints/Users/AddUserClaimEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Users/AddUserClaimEndpoint.cs @@ -1,5 +1,36 @@ -namespace PostFundManagement.Api.Endpoints.Users; +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Models.Security; -public class AddUserClaimEndpoint +namespace PostFundManagement.Api.Endpoints.Users; + +[ApiVersionTarget(1)] +public sealed class AddUserClaimEndpoint : IEndpoint { + public void Map(IEndpointRouteBuilder builder) + { + builder.MapPatch("api/users/user/claims", async (AddUserClaimRequest request, HttpContext context, + IUserService userService, CancellationToken cancellationToken = default) => + await userService.AddUserClaimAsync(request, userId: context.GetUserId(), cancellationToken) switch + { + { IsSuccess: true } => Results.Ok(), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r when r.HasError() => Results.NotFound(r.Errors.ToProblems()), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r => Results.InternalServerError(r.Errors.ToProblems()), + }) + .WithDescription("Adds a claim to a user") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(AddUserClaimEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Users); + } } diff --git a/PostFundManagement.Api/Endpoints/Users/AddUserRoleEndpoint.cs b/PostFundManagement.Api/Endpoints/Users/AddUserRoleEndpoint.cs index 489399e..31109ec 100644 --- a/PostFundManagement.Api/Endpoints/Users/AddUserRoleEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Users/AddUserRoleEndpoint.cs @@ -1,5 +1,35 @@ -namespace PostFundManagement.Api.Endpoints.Users; +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; -public class AddUserRoleEndpoint +namespace PostFundManagement.Api.Endpoints.Users; + +[ApiVersionTarget(1)] +public sealed class AddUserRoleEndpoint : IEndpoint { + public void Map(IEndpointRouteBuilder builder) + { + builder.MapPatch("api/users/{userId:Guid}/roles/{roleId}", async (Guid userId, string roleId, HttpContext context, + IUserService userService, CancellationToken cancellationToken = default) => + await userService.AddUserRoleAsync(userId, roleId, context.GetUserId(), cancellationToken) switch + { + { IsSuccess: true } => Results.Ok(), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r when r.HasError() => Results.NotFound(r.Errors.ToProblems()), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r => Results.InternalServerError(r.Errors.ToProblems()), + }) + .WithDescription("Adds a role to a user") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(AddUserRoleEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Users); + } } diff --git a/PostFundManagement.Api/Endpoints/Users/CreateUserEndpoint.cs b/PostFundManagement.Api/Endpoints/Users/CreateUserEndpoint.cs index e994f7e..520b95d 100644 --- a/PostFundManagement.Api/Endpoints/Users/CreateUserEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Users/CreateUserEndpoint.cs @@ -1,4 +1,9 @@ -using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Models.Security; namespace PostFundManagement.Api.Endpoints.Users; @@ -7,6 +12,26 @@ public sealed class CreateUserEndpoint : IEndpoint { public void Map(IEndpointRouteBuilder builder) { - throw new NotImplementedException(); + builder.MapPost("/api/users/{tenantId}/{idNumber}", async (string tenantId, string idNumber, CreateUserRequest request, HttpContext context, IUserService userService, + CancellationToken cancellationToken = default) => + await userService.CreateUserAsync(tenantId, idNumber, request, context.GetUserId(), cancellationToken) switch + { + { IsSuccess: true } => Results.Ok(), + var r when r.HasError() => Results.Forbid(), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r => Results.InternalServerError(r.Errors.ToProblems()), + }) + .WithDescription("Creates a new user in the system") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(CreateUserEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Users); } } diff --git a/PostFundManagement.Api/Endpoints/Users/DeleteUserClaimEndpoint.cs b/PostFundManagement.Api/Endpoints/Users/DeleteUserClaimEndpoint.cs index c96246d..fd8d4fa 100644 --- a/PostFundManagement.Api/Endpoints/Users/DeleteUserClaimEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Users/DeleteUserClaimEndpoint.cs @@ -1,6 +1,35 @@ -namespace PostFundManagement.Api.Endpoints.Users; +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; -public class DeleteUserClaimEndpoint +namespace PostFundManagement.Api.Endpoints.Users; + +[ApiVersionTarget(1)] +public sealed class DeleteUserClaimEndpoint : IEndpoint { - + public void Map(IEndpointRouteBuilder builder) + { + builder.MapDelete("api/users/{targetUserId:Guid}/claims/{claimId}", async (string claimId, Guid targetUserId, HttpContext context, + IUserService service, CancellationToken cancellationToken = default) => + await service.DeleteUserClaimAsync(claimIdHash: claimId, targetUserId: targetUserId, userId: context.GetUserId(), cancellationToken) switch + { + { IsSuccess: true } => Results.Ok(), + var r when r.HasError() => Results.Forbid(), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r when r.HasError() => Results.NotFound(r.Errors.ToProblems()), + var r => Results.InternalServerError(r.Errors.ToProblems()), + }) + .WithDescription("Deletes a claim from a user") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(DeleteUserClaimEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Users); + } } diff --git a/PostFundManagement.Api/Endpoints/Users/DeleteUserRoleEndpoint.cs b/PostFundManagement.Api/Endpoints/Users/DeleteUserRoleEndpoint.cs index e756806..4977002 100644 --- a/PostFundManagement.Api/Endpoints/Users/DeleteUserRoleEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Users/DeleteUserRoleEndpoint.cs @@ -1,5 +1,34 @@ -namespace PostFundManagement.Api.Endpoints.Users; +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; -public class DeleteUserRoleEndpoint +namespace PostFundManagement.Api.Endpoints.Users; + +[ApiVersionTarget(1)] +public sealed class DeleteUserRoleEndpoint : IEndpoint { + public void Map(IEndpointRouteBuilder builder) + { + builder.MapDelete("api/users/{userId:Guid}/roles/{roleId}", async (Guid userId, string roleId, HttpContext context, + IUserService service, CancellationToken cancellationToken = default) => + await service.DeleteUserRoleAsync(userId, roleId, context.GetUserId(), cancellationToken) switch + { + { IsSuccess: true } => Results.Ok(), + var r when r.HasError() => Results.NotFound(r.Errors.ToProblems()), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Delete user role") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(DeleteUserRoleEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Users); + } } diff --git a/PostFundManagement.Api/Endpoints/Users/GetSystemRolesEndpoint.cs b/PostFundManagement.Api/Endpoints/Users/GetSystemRolesEndpoint.cs index fd50ffc..d289bc5 100644 --- a/PostFundManagement.Api/Endpoints/Users/GetSystemRolesEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Users/GetSystemRolesEndpoint.cs @@ -1,5 +1,35 @@ -namespace PostFundManagement.Api.Endpoints.Users; +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Models.Security; -public class GetSystemRolesEndpoint +namespace PostFundManagement.Api.Endpoints.Users; + +[ApiVersionTarget(1)] +public sealed class GetSystemRolesEndpoint : IEndpoint { + public void Map(IEndpointRouteBuilder builder) + { + builder.MapGet("api/users/system/roles", async (HttpContext context, + IUserService service, CancellationToken cancellationToken = default) => + await service.GetSystemRolesAsync(context.GetUserId(), cancellationToken) switch + { + { IsSuccess: true, Value: var roles } => Results.Ok(roles), + var r when r.HasError() => Results.NotFound(r.Errors.ToProblems()), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Get system roles") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(GetSystemRolesEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Users); + } } diff --git a/PostFundManagement.Api/Endpoints/Users/GetUserClaimsEndpoint.cs b/PostFundManagement.Api/Endpoints/Users/GetUserClaimsEndpoint.cs index d425540..4a2d4d4 100644 --- a/PostFundManagement.Api/Endpoints/Users/GetUserClaimsEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Users/GetUserClaimsEndpoint.cs @@ -1,6 +1,35 @@ -namespace PostFundManagement.Api.Endpoints.Users; +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; +using Claim = PostFundManagement.Domain.Models.Security.Claim; -public class GetUserClaimsEndpoint +namespace PostFundManagement.Api.Endpoints.Users; + +[ApiVersionTarget(1)] +public sealed class GetUserClaimsEndpoint : IEndpoint { - + public void Map(IEndpointRouteBuilder builder) + { + builder.MapGet("api/users/{userId:Guid}/claims", async (Guid userId, HttpContext context, + IUserService service, CancellationToken cancellationToken = default) => + await service.GetUserClaimsAsync(userId, context.GetUserId(), cancellationToken) switch + { + { IsSuccess: true, Value: var claims } => Results.Ok(claims), + var r when r.HasError() => Results.NotFound(r.Errors.ToProblems()), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Get user claims") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(GetUserClaimsEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Users); + } } diff --git a/PostFundManagement.Api/Endpoints/Users/GetUserEndpoint.cs b/PostFundManagement.Api/Endpoints/Users/GetUserEndpoint.cs index fde821b..43749cf 100644 --- a/PostFundManagement.Api/Endpoints/Users/GetUserEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Users/GetUserEndpoint.cs @@ -17,7 +17,7 @@ public sealed class GetUserEndpoint : IEndpoint await service.GetUserAsync(userId, cancellationToken) switch { { IsSuccess: true, Value: var user } => Results.Ok(user), - var r when r.HasError() => Results.NotFound(), + var r when r.HasError() => Results.NotFound(r.Errors.ToProblems()), var r => Results.InternalServerError(r.Errors.ToProblems()) }) .WithDescription("Gets the metadata of a single user.") @@ -25,8 +25,8 @@ public sealed class GetUserEndpoint : IEndpoint .WithName(typeof(GetUserEndpoint).ToEndpointName()) .MapToApiVersion(new ApiVersion(1)) .Produces(StatusCodes.Status200OK) - .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status500InternalServerError) .WithTags(EndpointTags.Users) .CacheOutput(); diff --git a/PostFundManagement.Api/Endpoints/Users/GetUserRolesEndpoint.cs b/PostFundManagement.Api/Endpoints/Users/GetUserRolesEndpoint.cs index 3eb3739..367de1d 100644 --- a/PostFundManagement.Api/Endpoints/Users/GetUserRolesEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Users/GetUserRolesEndpoint.cs @@ -1,5 +1,35 @@ -namespace PostFundManagement.Api.Endpoints.Users; +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; +using PostFundManagement.Domain.Models.Security; -public class GetUserRolesEndpoint +namespace PostFundManagement.Api.Endpoints.Users; + +[ApiVersionTarget(1)] +public sealed class GetUserRolesEndpoint : IEndpoint { + public void Map(IEndpointRouteBuilder builder) + { + builder.MapGet("api/users/{userId:Guid}/roles", async (Guid userId, HttpContext context, + IUserService service, CancellationToken cancellationToken = default) => + await service.GetUserRolesAsync(userId, context.GetUserId(), cancellationToken) switch + { + { IsSuccess: true, Value: var roles } => Results.Ok(roles), + var r when r.HasError() => Results.NotFound(r.Errors.ToProblems()), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Get user roles") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(GetUserRolesEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Users); + } } diff --git a/PostFundManagement.Api/Endpoints/Users/LogUserOutEndpoint.cs b/PostFundManagement.Api/Endpoints/Users/LogUserOutEndpoint.cs index d9f9905..1d00f5a 100644 --- a/PostFundManagement.Api/Endpoints/Users/LogUserOutEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Users/LogUserOutEndpoint.cs @@ -1,5 +1,34 @@ -namespace PostFundManagement.Api.Endpoints.Users; +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; -public class LogUserOutEndpoint +namespace PostFundManagement.Api.Endpoints.Users; + +[ApiVersionTarget(1)] +public sealed class LogUserOutEndpoint : IEndpoint { + public void Map(IEndpointRouteBuilder builder) + { + builder.MapPut("api/users/{userId:Guid}/session/logout", async (Guid userId, HttpContext context, + IUserService service, CancellationToken cancellationToken = default) => + await service.LogoutUserAsync(userId, context.GetUserId(), cancellationToken) switch + { + { IsSuccess: true } => Results.Ok(), + var r when r.HasError() => Results.NotFound(r.Errors.ToProblems()), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Logout user session") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(LogUserOutEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Users); + } } diff --git a/PostFundManagement.Api/Endpoints/Users/SetUserLockoutEndpoint.cs b/PostFundManagement.Api/Endpoints/Users/SetUserLockoutEndpoint.cs index b8173d0..c58aac9 100644 --- a/PostFundManagement.Api/Endpoints/Users/SetUserLockoutEndpoint.cs +++ b/PostFundManagement.Api/Endpoints/Users/SetUserLockoutEndpoint.cs @@ -1,5 +1,34 @@ -namespace PostFundManagement.Api.Endpoints.Users; +using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Extensions; +using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Errors; +using PostFundManagement.Domain.Extensions; -public class SetUserLockoutEndpoint +namespace PostFundManagement.Api.Endpoints.Users; + +[ApiVersionTarget(1)] +public sealed class SetUserLockoutEndpoint : IEndpoint { + public void Map(IEndpointRouteBuilder builder) + { + builder.MapPut("api/users/{userId:Guid}/session/lockout/{disable:bool}", async (Guid userId, bool disable, HttpContext context, + IUserService service, CancellationToken cancellationToken = default) => + await service.SetUserLockoutAsync(userId, context.GetUserId(), disable, cancellationToken) switch + { + { IsSuccess: true } => Results.Ok(), + var r when r.HasError() => Results.NotFound(r.Errors.ToProblems()), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r => Results.InternalServerError(r.Errors.ToProblems()) + }) + .WithDescription("Lock user session") + .RequireAuthorization(Constants.MainScope) + .WithName(typeof(SetUserLockoutEndpoint).ToEndpointName()) + .MapToApiVersion(new ApiVersion(1)) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status500InternalServerError) + .WithTags(EndpointTags.Users); + } } diff --git a/PostFundManagement.Application/Abstractions/Services/IUserService.cs b/PostFundManagement.Application/Abstractions/Services/IUserService.cs index bb206cb..a6e7974 100644 --- a/PostFundManagement.Application/Abstractions/Services/IUserService.cs +++ b/PostFundManagement.Application/Abstractions/Services/IUserService.cs @@ -2,6 +2,7 @@ using PostFundManagement.Domain; using PostFundManagement.Domain.Attributes; using PostFundManagement.Domain.Models; +using PostFundManagement.Domain.Models.Security; namespace PostFundManagement.Application.Abstractions.Services; @@ -19,5 +20,42 @@ public interface IUserService Task UpdateUserStatusAsync(Guid userId, ActivityStatus status, CancellationToken cancellationToken = default); [Log] - Task> GetUserAsync(Guid userId, CancellationToken cancellationToken); + Task> GetUserAsync(Guid userId, CancellationToken cancellationToken = default); + + [Log] + [Audit] + Task CreateUserAsync(string tenantIdHash, string idNumberHash, CreateUserRequest request, Guid userId, CancellationToken cancellationToken = default); + + [Log] + [Audit] + Task AddUserClaimAsync(AddUserClaimRequest request, Guid userId, CancellationToken cancellationToken = default); + + [Log] + [Audit] + Task AddUserRoleAsync(Guid targetUserId, string roleId, Guid userId, CancellationToken cancellationToken = default); + + [Log] + [Audit] + Task DeleteUserClaimAsync(string claimIdHash, Guid targetUserId, Guid userId, CancellationToken cancellationToken = default); + + [Log] + [Audit] + Task SetUserLockoutAsync(Guid targetUserId, Guid userId, bool disable, CancellationToken cancellationToken = default); + + [Log] + [Audit] + Task LogoutUserAsync(Guid targetUserId, Guid userId, CancellationToken cancellationToken = default); + + [Log] + Task> GetUserRolesAsync(Guid targetUserId, Guid userId, CancellationToken cancellationToken = default); + + [Log] + Task> GetUserClaimsAsync(Guid targetUserId, Guid userId, CancellationToken cancellationToken = default); + + [Log] + Task> GetSystemRolesAsync(Guid userId, CancellationToken cancellationToken = default); + + [Log] + [Audit] + Task DeleteUserRoleAsync(Guid targetUserId, string roleId, Guid userId, CancellationToken cancellationToken = default); } diff --git a/PostFundManagement.Application/Extensions/Security.cs b/PostFundManagement.Application/Extensions/Security.cs index 434220b..029c3dc 100644 --- a/PostFundManagement.Application/Extensions/Security.cs +++ b/PostFundManagement.Application/Extensions/Security.cs @@ -32,12 +32,15 @@ public static class Security client.ClientSecret = ClientSecret.Parse(authOptions.ClientSecret!); client.Scope = Duende.AccessTokenManagement.Scope.ParseOrDefault(authOptions.Scope); }); - + services.AddHttpClient(SkorubaHttpClient, client => { client.BaseAddress = new Uri(configuration.GetConnectionString("SkorubaApi")!); }).AddClientCredentialsTokenHandler(ClientCredentialsClientName.Parse(SkorubaCredentialClient)); + services.AddKeyedScoped(SkorubaHttpClient, (provider, key) => + provider.GetRequiredService()); + services.AddScoped(); services.AddScoped(); services.AddSingleton(); diff --git a/PostFundManagement.Application/Services/Users/UserCacheService.cs b/PostFundManagement.Application/Services/Users/UserCacheService.cs index 312383b..f4e5022 100644 --- a/PostFundManagement.Application/Services/Users/UserCacheService.cs +++ b/PostFundManagement.Application/Services/Users/UserCacheService.cs @@ -3,12 +3,54 @@ using PostFundManagement.Application.Extensions; using PostFundManagement.Application.Services.Shared; using PostFundManagement.Domain; using PostFundManagement.Domain.Models; +using PostFundManagement.Domain.Models.Security; +using Claim = PostFundManagement.Domain.Models.Security.Claim; namespace PostFundManagement.Application.Services.Users; -public sealed class UserCacheService(UserService service, IDistributedCache cache, +public sealed class UserCacheService(UserService service, IDistributedCache cache, IFeatureManager featureManager) : ServiceCacheBase, IUserService { + public async Task AddUserClaimAsync(AddUserClaimRequest request, Guid userId, CancellationToken cancellationToken = default) => + await service.AddUserClaimAsync(request, userId, cancellationToken); + + public async Task AddUserRoleAsync(Guid targetUserId, string roleId, Guid userId, CancellationToken cancellationToken = default) => + await service.AddUserRoleAsync(targetUserId, roleId, userId, cancellationToken); + + public async Task CreateUserAsync(string tenantIdHash, string idNumberHash, CreateUserRequest request, Guid userId, CancellationToken cancellationToken = default) => + await service.CreateUserAsync(tenantIdHash, idNumberHash, request, userId, cancellationToken); + + public async Task DeleteUserClaimAsync(string claimIdHash, Guid targetUserId, Guid userId, CancellationToken cancellationToken = default) => + await service.DeleteUserClaimAsync(claimIdHash, targetUserId, userId, cancellationToken); + + public async Task DeleteUserRoleAsync(Guid targetUserId, string roleId, Guid userId, CancellationToken cancellationToken = default) => + await service.DeleteUserRoleAsync(targetUserId, roleId, userId, cancellationToken); + + public async Task> GetSystemRolesAsync(Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetSystemRolesAsync(userId, cancellationToken); + + var cacheKey = HashService.ToMd5Hash($"getsystemroles-{userId}").Value; + + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (string.IsNullOrWhiteSpace(cached)) + { + var rolesResult = await service.GetSystemRolesAsync(userId, cancellationToken); + + if (rolesResult.IsFailed) return rolesResult; + + await cache.SetStringAsync(cacheKey, rolesResult.Value.ToJson(), + cacheOptions, cancellationToken); + + return rolesResult; + } + + return Result.Ok(cached.FromJson()); + })); + public async Task> GetUserAsync(Guid userId, CancellationToken cancellationToken) => await Result.Try((Func>>)(async () => { @@ -19,7 +61,7 @@ public sealed class UserCacheService(UserService service, IDistributedCache cach var cached = await cache.GetStringAsync(cacheKey, cancellationToken); - if(string.IsNullOrWhiteSpace(cached)) + if (string.IsNullOrWhiteSpace(cached)) { var userResult = await service.GetUserAsync(userId, cancellationToken); @@ -33,6 +75,56 @@ public sealed class UserCacheService(UserService service, IDistributedCache cach return Result.Ok(cached.FromJson()); })); + public async Task> GetUserClaimsAsync(Guid targetUserId, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetUserClaimsAsync(targetUserId, userId, cancellationToken); + + var cacheKey = HashService.ToMd5Hash($"getuserclaims-{userId}").Value; + + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (string.IsNullOrWhiteSpace(cached)) + { + var claimsResult = await service.GetUserClaimsAsync(targetUserId, userId, cancellationToken); + + if (claimsResult.IsFailed) return claimsResult; + + await cache.SetStringAsync(cacheKey, claimsResult.Value.ToJson(), + cacheOptions, cancellationToken); + + return claimsResult; + } + + return Result.Ok(cached.FromJson()); + })); + + public async Task> GetUserRolesAsync(Guid targetUserId, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + if (!await featureManager.IsEnabledAsync(ServiceCachingFeature)) + return await service.GetUserRolesAsync(targetUserId, userId, cancellationToken); + + var cacheKey = HashService.ToMd5Hash($"getuserroles-{userId}").Value; + + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); + + if (string.IsNullOrWhiteSpace(cached)) + { + var rolesResult = await service.GetUserRolesAsync(targetUserId, userId, cancellationToken); + + if (rolesResult.IsFailed) return rolesResult; + + await cache.SetStringAsync(cacheKey, rolesResult.Value.ToJson(), + cacheOptions, cancellationToken); + + return rolesResult; + } + + return Result.Ok(cached.FromJson()); + })); + public async Task> GetUsersAsync(Pagination pagination, UserFilterRequest request, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { @@ -50,7 +142,7 @@ public sealed class UserCacheService(UserService service, IDistributedCache cach if (usersResult.IsFailed) return usersResult; - await cache.SetStringAsync(cacheKey, usersResult.Value.ToJson(), + await cache.SetStringAsync(cacheKey, usersResult.Value.ToJson(), cacheOptions, cancellationToken); return usersResult; @@ -59,9 +151,15 @@ public sealed class UserCacheService(UserService service, IDistributedCache cach return Result.Ok(cached.FromJson()); })); + public async Task LogoutUserAsync(Guid targetUserId, Guid userId, CancellationToken cancellationToken = default) => + await service.LogoutUserAsync(targetUserId, userId, cancellationToken); + + public async Task SetUserLockoutAsync(Guid targetUserId, Guid userId, bool disable, CancellationToken cancellationToken = default) => + await service.SetUserLockoutAsync(targetUserId, userId, disable, cancellationToken); + public async Task SyncUserAsync(Guid userId, string email, string role, CancellationToken cancellationToken = default) => await service.SyncUserAsync(userId, email, role, cancellationToken); - + public async Task UpdateUserStatusAsync(Guid userId, ActivityStatus status, CancellationToken cancellationToken = default) => await service.UpdateUserStatusAsync(userId, status, cancellationToken); } diff --git a/PostFundManagement.Application/Services/Users/UserService.cs b/PostFundManagement.Application/Services/Users/UserService.cs index 755ddc6..76527f9 100644 --- a/PostFundManagement.Application/Services/Users/UserService.cs +++ b/PostFundManagement.Application/Services/Users/UserService.cs @@ -1,14 +1,18 @@ using PostFundManagement.Application.Abstractions.Services; +using PostFundManagement.Application.Services.Shared; using PostFundManagement.Domain; using PostFundManagement.Domain.Abstractions; using PostFundManagement.Domain.Errors; using PostFundManagement.Domain.Extensions; using PostFundManagement.Domain.Models; +using PostFundManagement.Domain.Models.Security; using PostFundManagement.Infrastructure.Database; +using Claim = PostFundManagement.Domain.Models.Security.Claim; namespace PostFundManagement.Application.Services.Users; -public sealed class UserService(IDbContextFactory contextFactory, ITenantContextService tenantContext) : IUserService +public sealed class UserService(IDbContextFactory contextFactory, ITenantContextService tenantContext, + ISkorubaUserService skorubaService, HashService hashService) : IUserService { public async Task SyncUserAsync(Guid userId, string email, string role, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => @@ -113,4 +117,208 @@ public sealed class UserService(IDbContextFactory contextF return Result.FailIf(tenantUser == null, NotFoundError.Create("User not found.")) .ToResult(tenantUser!.User?.Map()!); })); + + public async Task CreateUserAsync(string tenantIdHash, string idNumberHash, CreateUserRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + var tenantIdHashDecode = hashService.DecodeLongIdHash(tenantIdHash); + + if (tenantIdHashDecode.IsFailed) + return Result.Fail(IdDecodeError.Create() + .CausedBy(tenantIdHashDecode.Errors)); + + var tenantId = tenantIdHashDecode.Value; + + var idNumberHashDecode = await hashService.DecodeHexHash(idNumberHash); + + if (idNumberHashDecode.IsFailed) + return Result.Fail(IdDecodeError.Create() + .CausedBy(idNumberHashDecode.Errors)); + + var idNumber = idNumberHashDecode.Value.Trim(); + + var email = request.Email?.Trim().ToLowerInvariant(); + + if (string.IsNullOrWhiteSpace(email)) + return Result.Fail(ValidationError.Create("Email is required.")); + + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (await context.Users.AnyAsync(u => u.Email == email, cancellationToken)) + return Result.Fail(ValidationError.Create("User with this email already exists.")); + + var createUserResult = await skorubaService.CreateUserAsync(request, userId, cancellationToken); + + if (createUserResult.IsFailed) + return Result.Fail(CreateError.Create("Failed to create user.")); + + var newUserId = Guid.Parse(createUserResult.Value.Trim()); + + var availableRolesResult = await skorubaService.GetAvailableRolesAsync(cancellationToken: cancellationToken); + + if (availableRolesResult.IsFailed) + return Result.Fail(NotFoundError.Create("Failed to retrieve available roles.")); + + var defaultRole = availableRolesResult.Value.FirstOrDefault(r => r.Name == "Contributor"); + + if (defaultRole == null) + return Result.Fail(NotFoundError.Create("Role 'Contributor' not found.")); + + var roleAssignmentResult = await skorubaService.AddRoleAsync(newUserId, defaultRole.Id, cancellationToken); + + if (roleAssignmentResult.IsFailed) + return Result.Fail(UpdateError.Create("Failed to assign role to user.")); + + var newUser = context.Users.Add(new Domain.Entities.User + { + Id = newUserId, + Email = email, + Status = ActivityStatus.Active, + LastLoginAt = DateTime.UtcNow, + IdNumber = idNumber, + Role = defaultRole.Name, + }); + + context.TenantUsers.Add(new Domain.Entities.TenantUser + { + CreatedAt = DateTime.UtcNow, + CreatedBy = userId, + Status = ActivityStatus.Active, + TenantId = tenantId, + UserId = newUserId, + }); + + return Result.OkIf(await context.SaveChangesAsync(cancellationToken) > 0, CreateError.Create("Failed to create user, contact support")); + })); + + public async Task DeleteUserClaimAsync(string claimIdHash, Guid targetUserId, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + var claimIdHashDecode = hashService.DecodeIntIdHash(claimIdHash); + + if (claimIdHashDecode.IsFailed) + return Result.Fail(IdDecodeError.Create() + .CausedBy(claimIdHashDecode.Errors)); + + var claimId = claimIdHashDecode.Value; + + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + if (!await context.Users.AnyAsync(u => u.Id == targetUserId, cancellationToken)) + return Result.Fail(NotFoundError.Create("User not found.")); + + var claimDeleteResult = await skorubaService.RemoveClaimAsync(targetUserId, claimId, cancellationToken); + + return Result.OkIf(claimDeleteResult.IsSuccess, ValidationError.Create("Failed to delete user claim.") + .CausedBy(claimDeleteResult.Errors)); + })); + + public async Task SetUserLockoutAsync(Guid targetUserId, Guid userId, bool disable, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var lockoutResult = await skorubaService.SetUserLockoutAsync(targetUserId, disable, cancellationToken); + + if(lockoutResult.IsFailed) + return Result.Fail(UpdateError.Create("Failed to set user lockout.") + .CausedBy(lockoutResult.Errors)); + + var changedTotal = await context.Users.Where(f => f.Id == targetUserId) + .ExecuteUpdateAsync(f => f.SetProperty(u => u.Status, disable ? ActivityStatus.Disabled : ActivityStatus.Active), cancellationToken); + + changedTotal += await context.TenantUsers.Where(f => f.UserId == targetUserId) + .ExecuteUpdateAsync(f => f.SetProperty(u => u.Status, disable ? ActivityStatus.Disabled : ActivityStatus.Active), cancellationToken); + + return Result.OkIf(lockoutResult.IsSuccess && changedTotal > 0, ValidationError.Create("Failed to set user lockout.") + .CausedBy(lockoutResult.Errors)); + })); + + public async Task LogoutUserAsync(Guid targetUserId, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + var logoutResult = await skorubaService.InvalidateUserSessionAsync(targetUserId, cancellationToken); + + return Result.OkIf(logoutResult.IsSuccess, ValidationError.Create("Failed to logout user.") + .CausedBy(logoutResult.Errors)); + })); + + public async Task> GetUserRolesAsync(Guid targetUserId, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + await skorubaService.GetUserRolesAsync(targetUserId, cancellationToken: cancellationToken))); + + public async Task> GetUserClaimsAsync(Guid targetUserId, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + await skorubaService.GetUserClaimsAsync(targetUserId, cancellationToken: cancellationToken))); + + public async Task> GetSystemRolesAsync(Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + await skorubaService.GetAvailableRolesAsync(cancellationToken: cancellationToken))); + + public async Task DeleteUserRoleAsync(Guid targetUserId, string roleId, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var changedTotal = await context.Users.Where(f => f.Id == targetUserId) + .ExecuteUpdateAsync(f => f.SetProperty(u => u.Role, string.Empty), cancellationToken); + + var roleDeleteResult = await skorubaService.RemoveRoleAsync(targetUserId, roleId, cancellationToken); + + return Result.OkIf(roleDeleteResult.IsSuccess && changedTotal > 0, ValidationError.Create("Failed to delete user role.") + .CausedBy(roleDeleteResult.Errors)); + })); + + public async Task AddUserClaimAsync(AddUserClaimRequest request, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + var addClaimResult = await skorubaService.AddClaimAsync(request.UserId, request.ClaimType, request.ClaimValue, cancellationToken); + + return Result.OkIf(addClaimResult.IsSuccess, ValidationError.Create("Failed to add user claim.") + .CausedBy(addClaimResult.Errors)); + })); + + public async Task AddUserRoleAsync(Guid targetUserId, string roleId, Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + var userRolesResult = await skorubaService.GetUserRolesAsync(targetUserId, cancellationToken: cancellationToken); + + if (userRolesResult.IsFailed) + return Result.Fail(NotFoundError.Create("Failed to retrieve user roles.") + .CausedBy(userRolesResult.Errors)); + + foreach (var role in userRolesResult.Value) + { + var deleteRoleResult = await skorubaService.RemoveRoleAsync(targetUserId, role.Id, cancellationToken); + + if (deleteRoleResult.IsFailed) + return Result.Fail(UpdateError.Create("Failed to remove existing user role.") + .CausedBy(deleteRoleResult.Errors)); + } + + var roleAddResult = await skorubaService.AddRoleAsync(targetUserId, roleId, cancellationToken); + + if(roleAddResult.IsFailed) + return Result.Fail(UpdateError.Create("Failed to add user role.") + .CausedBy(roleAddResult.Errors)); + + var systemRoles = await skorubaService.GetAvailableRolesAsync(cancellationToken: cancellationToken); + + if (systemRoles.IsFailed) + return Result.Fail(NotFoundError.Create("Failed to retrieve system roles.") + .CausedBy(systemRoles.Errors)); + + var targetRole = systemRoles.Value.FirstOrDefault(r => r.Id == roleId); + + if (targetRole == null) + return Result.Fail(NotFoundError.Create("Role not found.")); + + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + + var changedTotal = await context.Users.Where(f => f.Id == targetUserId) + .ExecuteUpdateAsync(f => f.SetProperty(u => u.Role, targetRole.Name), cancellationToken); + + return Result.OkIf(roleAddResult.IsSuccess && changedTotal > 0, ValidationError.Create("Failed to add user role.") + .CausedBy(roleAddResult.Errors)); + })); } diff --git a/PostFundManagement.Domain/Abstractions/ISkorubaUserService.cs b/PostFundManagement.Domain/Abstractions/ISkorubaUserService.cs index 8cf3968..3294d03 100644 --- a/PostFundManagement.Domain/Abstractions/ISkorubaUserService.cs +++ b/PostFundManagement.Domain/Abstractions/ISkorubaUserService.cs @@ -7,7 +7,7 @@ public interface ISkorubaUserService { [Log] [Audit] - Task> CreateUserAsync(CreateUserRequest request, CancellationToken cancellationToken = default); + Task> CreateUserAsync(CreateUserRequest request, Guid userId, CancellationToken cancellationToken = default); [Log] Task> GetUserAsync(Guid userId, CancellationToken cancellationToken = default); @@ -40,8 +40,11 @@ public interface ISkorubaUserService Task> GetAvailableRolesAsync(string? searchText = null, int page = 1, int pageSize = 50, CancellationToken cancellationToken = default); [Log] - Task> GetUserRolesAsync(string userId, int page = 1, int pageSize = 50, CancellationToken cancellationToken = default); + Task> GetUserRolesAsync(Guid userId, int page = 1, int pageSize = 50, CancellationToken cancellationToken = default); [Log] - Task> GetUserClaimsAsync(string userId, int page = 1, int pageSize = 50, CancellationToken cancellationToken = default); + Task> GetUserClaimsAsync(Guid userId, int page = 1, int pageSize = 50, CancellationToken cancellationToken = default); + + [Log] + Task InvalidateUserSessionAsync(Guid userId, CancellationToken cancellationToken = default); } diff --git a/PostFundManagement.Domain/Models/Security/Contracts.cs b/PostFundManagement.Domain/Models/Security/Contracts.cs index 8c8588f..e4d1260 100644 --- a/PostFundManagement.Domain/Models/Security/Contracts.cs +++ b/PostFundManagement.Domain/Models/Security/Contracts.cs @@ -1,8 +1,15 @@ -namespace PostFundManagement.Domain.Models.Security; +using System.ComponentModel.DataAnnotations; + +namespace PostFundManagement.Domain.Models.Security; + +public sealed record AddUserClaimRequest([Required] Guid UserId, + [Required] int ClaimId, + [Required][MaxLength(LabelLength)] string ClaimType, + [Required][MaxLength(LabelLength)] string ClaimValue); public sealed record CreateUserRequest(string UserName, string Email, string Password, bool EmailConfirmed = true, string? PhoneNumber = null, bool PhoneNumberConfirmed = false); -public sealed record UpdateUserRequest(string Id, string UserName, string Email, bool EmailConfirmed, string? PhoneNumber, bool PhoneNumberConfirmed, bool LockoutEnabled, DateTimeOffset? LockoutEnd, int AccessFailedCount, bool TwoFactorEnabled); +public sealed record UpdateUserRequest(string UserId, string UserName, string Email, bool EmailConfirmed, string? PhoneNumber, bool PhoneNumberConfirmed, bool LockoutEnabled, DateTimeOffset? LockoutEnd, int AccessFailedCount, bool TwoFactorEnabled); public sealed record Claim(string ClaimType, string ClaimValue, int? ClaimId = null); diff --git a/PostFundManagement.Infrastructure/Security/SkorubaUserService.cs b/PostFundManagement.Infrastructure/Security/SkorubaUserService.cs index 1def22d..f070cfe 100644 --- a/PostFundManagement.Infrastructure/Security/SkorubaUserService.cs +++ b/PostFundManagement.Infrastructure/Security/SkorubaUserService.cs @@ -6,7 +6,7 @@ namespace PostFundManagement.Infrastructure.Security; public class SkorubaUserService([FromKeyedServices(SkorubaHttpClient)] IHttpClientFactory httpClientFactory) : ISkorubaUserService { - public async Task> CreateUserAsync(CreateUserRequest request, CancellationToken cancellationToken = default) => + public async Task> CreateUserAsync(CreateUserRequest request, Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { var httpClient = httpClientFactory.CreateClient(); @@ -80,7 +80,7 @@ public class SkorubaUserService([FromKeyedServices(SkorubaHttpClient)] IHttpClie return Result.OkIf(response.IsSuccessStatusCode, CreateError.Create("Failed to remove user claim.")); })); - public async Task> GetUserClaimsAsync(string userId, int page = 1, int pageSize = 50, CancellationToken cancellationToken = default) => + public async Task> GetUserClaimsAsync(Guid userId, int page = 1, int pageSize = 50, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { var httpClient = httpClientFactory.CreateClient(); @@ -111,7 +111,7 @@ public class SkorubaUserService([FromKeyedServices(SkorubaHttpClient)] IHttpClie return Result.OkIf(response.IsSuccessStatusCode, CreateError.Create("Failed to remove user role.")); })); - public async Task> GetUserRolesAsync(string userId, int page = 1, int pageSize = 50, CancellationToken cancellationToken = default) => + public async Task> GetUserRolesAsync(Guid userId, int page = 1, int pageSize = 50, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { var httpClient = httpClientFactory.CreateClient();