diff --git a/PostFundManagement.Api/Endpoints/Users/AddUserClaimEndpoint.cs b/PostFundManagement.Api/Endpoints/Users/AddUserClaimEndpoint.cs new file mode 100644 index 0000000..ace3d84 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Users/AddUserClaimEndpoint.cs @@ -0,0 +1,5 @@ +namespace PostFundManagement.Api.Endpoints.Users; + +public class AddUserClaimEndpoint +{ +} diff --git a/PostFundManagement.Api/Endpoints/Users/AddUserRoleEndpoint.cs b/PostFundManagement.Api/Endpoints/Users/AddUserRoleEndpoint.cs new file mode 100644 index 0000000..489399e --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Users/AddUserRoleEndpoint.cs @@ -0,0 +1,5 @@ +namespace PostFundManagement.Api.Endpoints.Users; + +public class AddUserRoleEndpoint +{ +} diff --git a/PostFundManagement.Api/Endpoints/Users/CreateUserEndpoint.cs b/PostFundManagement.Api/Endpoints/Users/CreateUserEndpoint.cs new file mode 100644 index 0000000..fca9d81 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Users/CreateUserEndpoint.cs @@ -0,0 +1,5 @@ +namespace PostFundManagement.Api.Endpoints.Users; + +public class CreateUserEndpoint +{ +} diff --git a/PostFundManagement.Api/Endpoints/Users/DeleteUserClaimEndpoint.cs b/PostFundManagement.Api/Endpoints/Users/DeleteUserClaimEndpoint.cs new file mode 100644 index 0000000..c96246d --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Users/DeleteUserClaimEndpoint.cs @@ -0,0 +1,6 @@ +namespace PostFundManagement.Api.Endpoints.Users; + +public class DeleteUserClaimEndpoint +{ + +} diff --git a/PostFundManagement.Api/Endpoints/Users/DeleteUserRoleEndpoint.cs b/PostFundManagement.Api/Endpoints/Users/DeleteUserRoleEndpoint.cs new file mode 100644 index 0000000..e756806 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Users/DeleteUserRoleEndpoint.cs @@ -0,0 +1,5 @@ +namespace PostFundManagement.Api.Endpoints.Users; + +public class DeleteUserRoleEndpoint +{ +} diff --git a/PostFundManagement.Api/Endpoints/Users/GetSystemRolesEndpoint.cs b/PostFundManagement.Api/Endpoints/Users/GetSystemRolesEndpoint.cs new file mode 100644 index 0000000..fd50ffc --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Users/GetSystemRolesEndpoint.cs @@ -0,0 +1,5 @@ +namespace PostFundManagement.Api.Endpoints.Users; + +public class GetSystemRolesEndpoint +{ +} diff --git a/PostFundManagement.Api/Endpoints/Users/GetUserClaimsEndpoint.cs b/PostFundManagement.Api/Endpoints/Users/GetUserClaimsEndpoint.cs new file mode 100644 index 0000000..d425540 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Users/GetUserClaimsEndpoint.cs @@ -0,0 +1,6 @@ +namespace PostFundManagement.Api.Endpoints.Users; + +public class GetUserClaimsEndpoint +{ + +} diff --git a/PostFundManagement.Api/Endpoints/Users/GetUserRolesEndpoint.cs b/PostFundManagement.Api/Endpoints/Users/GetUserRolesEndpoint.cs new file mode 100644 index 0000000..3eb3739 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Users/GetUserRolesEndpoint.cs @@ -0,0 +1,5 @@ +namespace PostFundManagement.Api.Endpoints.Users; + +public class GetUserRolesEndpoint +{ +} diff --git a/PostFundManagement.Api/Endpoints/Users/LogUserOutEndpoint.cs b/PostFundManagement.Api/Endpoints/Users/LogUserOutEndpoint.cs new file mode 100644 index 0000000..d9f9905 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Users/LogUserOutEndpoint.cs @@ -0,0 +1,5 @@ +namespace PostFundManagement.Api.Endpoints.Users; + +public class LogUserOutEndpoint +{ +} diff --git a/PostFundManagement.Api/Endpoints/Users/SetUserLockoutEndpoint.cs b/PostFundManagement.Api/Endpoints/Users/SetUserLockoutEndpoint.cs new file mode 100644 index 0000000..b8173d0 --- /dev/null +++ b/PostFundManagement.Api/Endpoints/Users/SetUserLockoutEndpoint.cs @@ -0,0 +1,5 @@ +namespace PostFundManagement.Api.Endpoints.Users; + +public class SetUserLockoutEndpoint +{ +} diff --git a/PostFundManagement.Domain/Abstractions/ISkorubaUserService.cs b/PostFundManagement.Domain/Abstractions/ISkorubaUserService.cs index b4aff97..8cf3968 100644 --- a/PostFundManagement.Domain/Abstractions/ISkorubaUserService.cs +++ b/PostFundManagement.Domain/Abstractions/ISkorubaUserService.cs @@ -10,7 +10,7 @@ public interface ISkorubaUserService Task> CreateUserAsync(CreateUserRequest request, CancellationToken cancellationToken = default); [Log] - Task> GetUserAsync(string userId, CancellationToken cancellationToken = default); + Task> GetUserAsync(Guid userId, CancellationToken cancellationToken = default); [Log] [Audit] @@ -18,13 +18,30 @@ public interface ISkorubaUserService [Log] [Audit] - Task SetUserLockoutAsync(string userId, bool disabled, CancellationToken cancellationToken = default); + Task SetUserLockoutAsync(Guid userId, bool disabled, CancellationToken cancellationToken = default); [Log] [Audit] - Task AddClaimAsync(string userId, string claimType, string claimValue, CancellationToken cancellationToken = default); + Task AddClaimAsync(Guid userId, string claimType, string claimValue, CancellationToken cancellationToken = default); [Log] [Audit] - Task RemoveClaimAsync(string userId, int claimId, CancellationToken cancellationToken = default); + Task RemoveClaimAsync(Guid userId, int claimId, CancellationToken cancellationToken = default); + + [Log] + [Audit] + Task AddRoleAsync(Guid userId, string roleId, CancellationToken cancellationToken = default); + + [Log] + [Audit] + Task RemoveRoleAsync(Guid userId, string roleId, CancellationToken cancellationToken = default); + + [Log] + 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); + + [Log] + Task> GetUserClaimsAsync(string userId, int page = 1, int pageSize = 50, CancellationToken cancellationToken = default); } diff --git a/PostFundManagement.Domain/Models/Security/Contracts.cs b/PostFundManagement.Domain/Models/Security/Contracts.cs index e67f680..bfbf504 100644 --- a/PostFundManagement.Domain/Models/Security/Contracts.cs +++ b/PostFundManagement.Domain/Models/Security/Contracts.cs @@ -4,6 +4,12 @@ public sealed record CreateUserRequest(string UserName, string Email, string Pas 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 UserClaim(string ClaimType, string ClaimValue, int? ClaimId = null); +public sealed record Claim(string ClaimType, string ClaimValue, int? ClaimId = null); -public sealed record UserResponse(string Id, string UserName, string Email, bool IsLockedOut, bool LockoutEnabled); \ No newline at end of file +public record ClaimsResponse(int TotalCount, int PageSize, ICollection Claims); + +public sealed record UserResponse(string Id, string UserName, string Email, bool IsLockedOut, bool LockoutEnabled); + +public record Role(string Id, string Name); + +public record RolesResponse(int TotalCount, int PageSize, ICollection Roles); \ No newline at end of file diff --git a/PostFundManagement.Infrastructure/Security/SkorubaUserService.cs b/PostFundManagement.Infrastructure/Security/SkorubaUserService.cs index 62d393d..8389656 100644 --- a/PostFundManagement.Infrastructure/Security/SkorubaUserService.cs +++ b/PostFundManagement.Infrastructure/Security/SkorubaUserService.cs @@ -13,7 +13,8 @@ public class SkorubaUserService([FromKeyedServices(SkorubaHttpClient)] IHttpClie var httpClient = httpClientFactory.CreateClient(); var response = await httpClient.PostAsJsonAsync("api/users", request, cancellationToken); - response.EnsureSuccessStatusCode(); + if (!response.IsSuccessStatusCode) + return Result.Fail(CreateError.Create("Failed to create user.")); var created = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); @@ -21,7 +22,7 @@ public class SkorubaUserService([FromKeyedServices(SkorubaHttpClient)] IHttpClie .ToResult(created?.Id!); })); - public async Task> GetUserAsync(string userId, CancellationToken cancellationToken = default) => + public async Task> GetUserAsync(Guid userId, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { var httpClient = httpClientFactory.CreateClient(); @@ -40,13 +41,13 @@ public class SkorubaUserService([FromKeyedServices(SkorubaHttpClient)] IHttpClie return Result.OkIf(response.IsSuccessStatusCode, UpdateError.Create("Failed to update user.")); })); - public async Task SetUserLockoutAsync(string userId, bool disabled, CancellationToken cancellationToken = default) => + public async Task SetUserLockoutAsync(Guid userId, bool disabled, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { var httpClient = httpClientFactory.CreateClient(); var user = await httpClient.GetFromJsonAsync($"api/users/{userId}", cancellationToken); - if(user is null) + if (user is null) return Result.Fail(NotFoundError.Create($"User {userId} not found.")); var updated = user with @@ -60,7 +61,7 @@ public class SkorubaUserService([FromKeyedServices(SkorubaHttpClient)] IHttpClie return Result.OkIf(response.IsSuccessStatusCode, UpdateError.Create("Failed to update user lockout status.")); })); - public async Task AddClaimAsync(string userId, string claimType, string claimValue, CancellationToken cancellationToken = default) => + public async Task AddClaimAsync(Guid userId, string claimType, string claimValue, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { var httpClient = httpClientFactory.CreateClient(); @@ -71,7 +72,7 @@ public class SkorubaUserService([FromKeyedServices(SkorubaHttpClient)] IHttpClie return Result.OkIf(response.IsSuccessStatusCode, CreateError.Create("Failed to add user claim.")); })); - public async Task RemoveClaimAsync(string userId, int claimId, CancellationToken cancellationToken = default) => + public async Task RemoveClaimAsync(Guid userId, int claimId, CancellationToken cancellationToken = default) => await Result.Try((Func>)(async () => { var httpClient = httpClientFactory.CreateClient(); @@ -79,4 +80,76 @@ 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) => + await Result.Try((Func>>)(async () => + { + var httpClient = httpClientFactory.CreateClient(); + + var result = await httpClient.GetFromJsonAsync($"api/users/{userId}/claims?page={page}&pageSize={pageSize}", cancellationToken); + + return Result.FailIf(result is null, NotFoundError.Create($"Claims for user {userId} could not be retrieved.")) + .ToResult(result?.Claims.ToArray() ?? []); + })); + + public async Task AddRoleAsync(Guid userId, string roleId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + var httpClient = httpClientFactory.CreateClient(); + var payload = new { UserId = userId, RoleId = roleId }; + + var response = await httpClient.PostAsJsonAsync($"api/users/{userId}/roles", payload, cancellationToken); + + return Result.OkIf(response.IsSuccessStatusCode, CreateError.Create("Failed to add user role.")); + })); + + public async Task RemoveRoleAsync(Guid userId, string roleId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + var httpClient = httpClientFactory.CreateClient(); + var response = await httpClient.DeleteAsync($"api/users/{userId}/roles/{roleId}", cancellationToken); + + 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) => + await Result.Try((Func>>)(async () => + { + var httpClient = httpClientFactory.CreateClient(); + + var result = await httpClient.GetFromJsonAsync($"api/users/{userId}/roles?page={page}&pageSize={pageSize}", cancellationToken); + + return Result.FailIf(result is null, NotFoundError.Create($"Roles for user {userId} could not be retrieved.")) + .ToResult(result?.Roles?.ToArray() ?? []); + })); + + public async Task InvalidateUserSessionAsync(Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + var httpClient = httpClientFactory.CreateClient(); + var user = await httpClient.GetFromJsonAsync($"api/users/{userId}", cancellationToken); + + if (user is null) + return Result.Fail(NotFoundError.Create($"User {userId} not found.")); + + var response = await httpClient.PutAsJsonAsync("api/users", user, cancellationToken); + + return Result.OkIf(response.IsSuccessStatusCode, UpdateError.Create("Failed to invalidate user session.")); + })); + + public async Task> GetAvailableRolesAsync(string? searchText = null, int page = 1, int pageSize = 50, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + var httpClient = httpClientFactory.CreateClient(); + + var query = $"api/roles?page={page}&pageSize={pageSize}"; + + if (!string.IsNullOrWhiteSpace(searchText)) + query += $"&searchText={Uri.EscapeDataString(searchText)}"; + + var result = await httpClient.GetFromJsonAsync(query, cancellationToken); + + return Result.FailIf(result is null, NotFoundError.Create("Available roles could not be retrieved.")) + .ToResult(result?.Roles?.ToArray() ?? []); + })); } \ No newline at end of file