feat: add user and role view sheets with detailed information display
- Created UserViewSheet component to display user details including username, full name, role, status, and timestamps. - Created RoleViewSheet component to display role details including name, level, status, creation date, and associated permissions. - Introduced permissions configuration in a new permissions.ts file, defining categories and permission codes for better management. - Implemented middleware for authentication, ensuring access control for protected routes.
This commit is contained in:
@@ -1,21 +0,0 @@
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace Distribution.Infra.Authorization;
|
||||
|
||||
/// <summary>
|
||||
/// Proxies own-user auth calls (login/register/getUserDetails/logout) to the external AuthHex
|
||||
/// identity service. AuthHex owns credentials and issues the RS256-signed JWT this app validates
|
||||
/// (see JwtTokenService's replacement — token generation is no longer done locally); this app
|
||||
/// never sees or stores a plaintext/hashed password for the AuthHex-authenticated flow.
|
||||
/// </summary>
|
||||
public class AuthHexClient(HttpClient http) : IAuthHexClient
|
||||
{
|
||||
public async Task<AuthHexResponse> CallAsync(string functionName, object? payload, string? reference = null, CancellationToken ct = default)
|
||||
{
|
||||
var body = new { functionName, payload, reference };
|
||||
var response = await http.PostAsJsonAsync("/api/user", body, ct);
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<AuthHexResponse>(cancellationToken: ct);
|
||||
return result ?? new AuthHexResponse((int)response.StatusCode, false, "Empty response from AuthHex.", null);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Distribution.Infra.Authorization;
|
||||
|
||||
/// <summary>Raw response shape returned by AuthHex's `/api/user` endpoint for every functionName.</summary>
|
||||
public record AuthHexResponse(
|
||||
[property: JsonPropertyName("statusCode")] int StatusCode,
|
||||
[property: JsonPropertyName("success")] bool Success,
|
||||
[property: JsonPropertyName("message")] string? Message,
|
||||
[property: JsonPropertyName("data")] JsonElement? Data);
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace Distribution.Infra.Authorization;
|
||||
|
||||
public interface IAuthHexClient
|
||||
{
|
||||
Task<AuthHexResponse> CallAsync(string functionName, object? payload, string? reference = null, CancellationToken ct = default);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace Distribution.Infra.Authorization;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||
public class AuthenticatedAttribute : Attribute, IAuthorizationFilter
|
||||
{
|
||||
public void OnAuthorization(AuthorizationFilterContext context)
|
||||
{
|
||||
if (context.HttpContext.User.Identity?.IsAuthenticated != true)
|
||||
context.Result = new UnauthorizedResult();
|
||||
}
|
||||
}
|
||||
+21
-1
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Distribution.Infra.Authorization;
|
||||
|
||||
@@ -17,6 +17,14 @@ public class AuthorizationController(
|
||||
return Ok(new PermissionCheckResponse(result));
|
||||
}
|
||||
|
||||
[HttpGet("my-permissions")]
|
||||
public async Task<IActionResult> GetMyPermissions()
|
||||
{
|
||||
if (currentUser.RoleId is not { } roleId) return Unauthorized();
|
||||
var permissions = await permissionService.GetRolePermissionsAsync(roleId);
|
||||
return Ok(new RolePermissionsResponse(permissions));
|
||||
}
|
||||
|
||||
[HttpGet("scopes/stores/{storeId:guid}")]
|
||||
public async Task<IActionResult> CheckStoreScope(Guid storeId)
|
||||
{
|
||||
@@ -49,3 +57,15 @@ public class AuthorizationController(
|
||||
return Ok(new AccessibleStoresResponse(result));
|
||||
}
|
||||
}
|
||||
|
||||
[ApiController]
|
||||
[Route("api/permissions")]
|
||||
public class PermissionController(IPermissionService permissionService) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetAll()
|
||||
{
|
||||
var permissions = await permissionService.GetAllAsync();
|
||||
return Ok(permissions.Select(p => new { permissionId = p.Id, p.Code, p.Description }));
|
||||
}
|
||||
}
|
||||
+2
@@ -2,6 +2,8 @@ namespace Distribution.Infra.Authorization;
|
||||
|
||||
public record PermissionCheckResponse(bool HasPermission);
|
||||
|
||||
public record RolePermissionsResponse(List<string> Permissions);
|
||||
|
||||
public record StoreScopeCheckResponse(bool HasScope);
|
||||
|
||||
public record RouteScopeCheckResponse(bool HasScope);
|
||||
@@ -1,46 +0,0 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Distribution.Infra.Authorization;
|
||||
|
||||
public class CurrentUserService : ICurrentUserService
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
public CurrentUserService(IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
private System.Security.Claims.ClaimsPrincipal? User => _httpContextAccessor.HttpContext?.User;
|
||||
|
||||
public bool IsAuthenticated => User?.Identity?.IsAuthenticated ?? false;
|
||||
|
||||
public Guid? UserId
|
||||
{
|
||||
get
|
||||
{
|
||||
var value = User?.FindFirst("sub")?.Value ?? User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
||||
return Guid.TryParse(value, out var id) ? id : null;
|
||||
}
|
||||
}
|
||||
|
||||
public Guid? RoleId
|
||||
{
|
||||
get
|
||||
{
|
||||
var value = User?.FindFirst("role_id")?.Value;
|
||||
return Guid.TryParse(value, out var id) ? id : null;
|
||||
}
|
||||
}
|
||||
|
||||
public string? Username => User?.FindFirst("username")?.Value;
|
||||
|
||||
public int RoleLevel
|
||||
{
|
||||
get
|
||||
{
|
||||
var value = User?.FindFirst("role_level")?.Value;
|
||||
return int.TryParse(value, out var level) ? level : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Security.Claims;
|
||||
using Distribution.Data;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
namespace Distribution.Infra.Authorization;
|
||||
|
||||
public class CurrentUserService(IHttpContextAccessor http, AppDBContext db, IMemoryCache cache) : ICurrentUserService
|
||||
{
|
||||
private ClaimsPrincipal? User => http.HttpContext?.User;
|
||||
|
||||
public bool IsAuthenticated => User?.Identity?.IsAuthenticated ?? false;
|
||||
public Guid? UserId => Guid.TryParse(User?.FindFirst("UserId")?.Value, out var id) ? id : null;
|
||||
public Guid? RoleId => Guid.TryParse(User?.FindFirst("RoleId")?.Value, out var id) ? id : null;
|
||||
|
||||
public async Task<string?> GetUsernameAsync()
|
||||
{
|
||||
if (UserId is not { } id) return null;
|
||||
var key = $"user_rolename_{id}";
|
||||
if (cache.TryGetValue(key, out string? cached)) return cached;
|
||||
var user = await db.Users.AsNoTracking().FirstOrDefaultAsync(u => u.Id == id);
|
||||
cache.Set(key, user?.Username, TimeSpan.FromMinutes(10));
|
||||
return user?.Username;
|
||||
}
|
||||
|
||||
public async Task<string?> GetRoleCodeAsync()
|
||||
{
|
||||
if (RoleId is not { } id) return null;
|
||||
var key = $"role_name_{id}";
|
||||
if (cache.TryGetValue(key, out string? cached)) return cached;
|
||||
var role = await db.Roles.AsNoTracking().FirstOrDefaultAsync(r => r.Id == id);
|
||||
cache.Set(key, role?.Name, TimeSpan.FromMinutes(10));
|
||||
return role?.Name;
|
||||
}
|
||||
|
||||
public async Task<int> GetRoleLevelAsync()
|
||||
{
|
||||
if (RoleId is not { } id) return 0;
|
||||
var key = $"role_level_{id}";
|
||||
if (cache.TryGetValue(key, out int cached)) return cached;
|
||||
var role = await db.Roles.AsNoTracking().FirstOrDefaultAsync(r => r.Id == id);
|
||||
var level = role?.Level ?? 0;
|
||||
cache.Set(key, level, TimeSpan.FromMinutes(10));
|
||||
return level;
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -4,7 +4,8 @@ public interface ICurrentUserService
|
||||
{
|
||||
Guid? UserId { get; }
|
||||
Guid? RoleId { get; }
|
||||
string? Username { get; }
|
||||
int RoleLevel { get; }
|
||||
bool IsAuthenticated { get; }
|
||||
Task<string?> GetUsernameAsync();
|
||||
Task<string?> GetRoleCodeAsync();
|
||||
Task<int> GetRoleLevelAsync();
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Distribution.Infra.Authorization;
|
||||
|
||||
/// <summary>
|
||||
/// Parses an XML-format RSA public key (`<RSAKeyValue>...</RSAKeyValue>`, as issued by
|
||||
/// AuthHex) into an <see cref="RSA"/> instance. Written by hand instead of relying on
|
||||
/// RSA.FromXmlString, which is Windows-only in .NET (throws PlatformNotSupportedException on
|
||||
/// Linux/macOS) — this keeps token validation portable across hosts.
|
||||
/// </summary>
|
||||
public static class RsaXmlKeyReader
|
||||
{
|
||||
public static RSA ReadPublicKey(string xml)
|
||||
{
|
||||
var root = XElement.Parse(xml);
|
||||
var modulus = root.Element("Modulus")?.Value
|
||||
?? throw new InvalidOperationException("RSA XML key is missing <Modulus>.");
|
||||
var exponent = root.Element("Exponent")?.Value
|
||||
?? throw new InvalidOperationException("RSA XML key is missing <Exponent>.");
|
||||
|
||||
var parameters = new RSAParameters
|
||||
{
|
||||
Modulus = Convert.FromBase64String(modulus),
|
||||
Exponent = Convert.FromBase64String(exponent)
|
||||
};
|
||||
|
||||
var rsa = RSA.Create();
|
||||
rsa.ImportParameters(parameters);
|
||||
return rsa;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace Distribution.Infra.Authorization.Middleware;
|
||||
|
||||
/// <summary>Stand-in for OSMCore's OMSCore.Common.Models.IOs.ApiResponse<T>, which has no equivalent in this project.</summary>
|
||||
public sealed record ApiResponse<T>
|
||||
{
|
||||
public int StatusCode { get; init; }
|
||||
public bool Success { get; init; }
|
||||
public string? Message { get; init; }
|
||||
public T? Data { get; init; }
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
namespace Distribution.Infra.Authorization.Middleware;
|
||||
|
||||
/// <summary>
|
||||
/// Ported from OSMCore's System/Authentication/JwtOptions.cs. Binds to the same "Jwt" config
|
||||
/// section already used by Program.cs's AddJwtBearer setup (Jwt:PublicKey/Audience/Issuer) — no
|
||||
/// new config keys needed.
|
||||
/// </summary>
|
||||
public sealed record JwtOptions
|
||||
{
|
||||
public string PublicKey { get; init; } = string.Empty;
|
||||
public string Audience { get; init; } = string.Empty;
|
||||
public string Issuer { get; init; } = string.Empty;
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
using Distribution.Models;
|
||||
|
||||
namespace Distribution.Infra.Authorization;
|
||||
|
||||
public interface IPermissionRepository
|
||||
{
|
||||
Task<bool> HasPermissionAsync(Guid roleId, string permissionCode);
|
||||
Task<List<string>> GetRolePermissionsAsync(Guid roleId);
|
||||
Task<List<Permission>> GetAllAsync();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
using Distribution.Models;
|
||||
|
||||
namespace Distribution.Infra.Authorization;
|
||||
|
||||
public interface IPermissionService
|
||||
{
|
||||
Task<bool> HasPermissionAsync(Guid roleId, string permissionCode);
|
||||
Task<List<string>> GetRolePermissionsAsync(Guid roleId);
|
||||
Task<List<Permission>> GetAllAsync();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Distribution.Data;
|
||||
using Distribution.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Distribution.Infra.Authorization;
|
||||
@@ -10,4 +11,15 @@ public class PermissionRepository(AppDBContext context) : IPermissionRepository
|
||||
.Where(rp => rp.FK_RoleId == roleId && rp.IsActive)
|
||||
.Join(context.Permissions.Where(p => p.IsActive), rp => rp.FK_PermissionId, p => p.Id, (rp, p) => p.Code)
|
||||
.AnyAsync(code => code == permissionCode);
|
||||
|
||||
public Task<List<string>> GetRolePermissionsAsync(Guid roleId) =>
|
||||
context.RolePermissions
|
||||
.Where(rp => rp.FK_RoleId == roleId && rp.IsActive)
|
||||
.Select(rp => rp.Permission)
|
||||
.Where(p => p.IsActive)
|
||||
.Select(p => p.Code)
|
||||
.ToListAsync();
|
||||
|
||||
public Task<List<Permission>> GetAllAsync() =>
|
||||
context.Permissions.Where(p => p.IsActive).ToListAsync();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
using Distribution.Models;
|
||||
|
||||
namespace Distribution.Infra.Authorization;
|
||||
|
||||
public class PermissionService(IPermissionRepository repo) : IPermissionService
|
||||
{
|
||||
public Task<bool> HasPermissionAsync(Guid roleId, string permissionCode) =>
|
||||
repo.HasPermissionAsync(roleId, permissionCode);
|
||||
|
||||
public Task<List<string>> GetRolePermissionsAsync(Guid roleId) =>
|
||||
repo.GetRolePermissionsAsync(roleId);
|
||||
|
||||
public Task<List<Permission>> GetAllAsync() =>
|
||||
repo.GetAllAsync();
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ public class PermissionAuthorizationHandler : AuthorizationHandler<PermissionReq
|
||||
protected override async Task HandleRequirementAsync(
|
||||
AuthorizationHandlerContext context, PermissionRequirement requirement)
|
||||
{
|
||||
var roleIdClaim = context.User.FindFirst("role_id")?.Value;
|
||||
var roleIdClaim = context.User.FindFirst("RoleId")?.Value;
|
||||
if (!Guid.TryParse(roleIdClaim, out var roleId))
|
||||
return;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user