feat(permissions): implement full-stack role permission management
Backend (IAM Service):
- New GetRolePermissionsQuery + Handler: reads permissions from role_claims
- New UpdateRolePermissionsCommand + Handler: validates permission names
against StaffPermissions enum, replaces role_claims, blocks system roles
- New endpoints: GET/PUT /api/v1/roles/{id}/permissions
- GetRolesQuery: batch-fetch permissions per role via role_claims join
- RoleResponse: add Permissions field to API response
- Seeded role_claims for Admin (7), Merchant (7), MerchantAdmin (6),
MerchantStaff (2), SuperAdmin (All), Support (2)
Frontend (Blazor WASM):
- IamApiService: add Permissions to RoleDto, UpdateRolePermissionsAsync()
- RolePermissions.razor: replace hardcoded GetPermissionsForRole() with
API-driven permission toggles from role_claims data
- Editable toggles for non-system roles, disabled for system roles
- Save/Cancel buttons appear when permissions modified
- 7 permission types matching StaffPermissions enum
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -51,7 +51,7 @@ else
|
||||
@foreach (var role in _roles)
|
||||
{
|
||||
<button class="admin-role-card @(_selectedId == role.Id ? "admin-role-card--active" : "")"
|
||||
@onclick="@(() => _selectedId = role.Id)">
|
||||
@onclick="@(() => SelectRole(role.Id))">
|
||||
<div style="display:flex;align-items:center;gap:10px;">
|
||||
<div class="admin-kpi-card__icon" style="width:36px;height:36px;border-radius:10px;background-color:@(GetRoleColor(role.Name))20;">
|
||||
<i data-lucide="@GetRoleIcon(role.Name)" style="color:@GetRoleColor(role.Name);width:18px;height:18px;"></i>
|
||||
@@ -130,21 +130,39 @@ else
|
||||
</div>
|
||||
}
|
||||
|
||||
@* EN: Permission display — reflects actual backend authorization policies per role.
|
||||
System roles have platform-level access; business roles have shop-level access.
|
||||
VI: Hiển thị quyền — phản ánh chính xác backend authorization policies theo role.
|
||||
System roles có quyền platform; business roles có quyền cửa hàng. *@
|
||||
<div style="font-size:12px;font-weight:700;color:var(--admin-text-tertiary);text-transform:uppercase;letter-spacing:0.05em;">
|
||||
Quyền hạn
|
||||
@* EN: Permissions loaded from role_claims via API. Editable for non-system roles.
|
||||
VI: Permissions được tải từ role_claims qua API. Có thể sửa cho non-system roles. *@
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<div style="font-size:12px;font-weight:700;color:var(--admin-text-tertiary);text-transform:uppercase;letter-spacing:0.05em;">
|
||||
Quyền hạn
|
||||
</div>
|
||||
@if (selected.IsSystem)
|
||||
{
|
||||
<span style="font-size:10px;color:var(--admin-text-tertiary);background:var(--admin-bg-elevated);padding:2px 8px;border-radius:4px;">Vai trò hệ thống — không thể thay đổi</span>
|
||||
}
|
||||
</div>
|
||||
@foreach (var perm in GetPermissionsForRole(selected.Name))
|
||||
@foreach (var perm in _permToggles)
|
||||
{
|
||||
var p = perm;
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:10px 12px;background:var(--admin-bg-interactive);border-radius:10px;">
|
||||
<div style="display:flex;flex-direction:column;">
|
||||
<span style="font-size:13px;font-weight:500;">@perm.Label</span>
|
||||
<span style="font-size:11px;color:var(--admin-text-tertiary);">@perm.Desc</span>
|
||||
<span style="font-size:13px;font-weight:500;">@p.Label</span>
|
||||
<span style="font-size:11px;color:var(--admin-text-tertiary);">@p.Desc</span>
|
||||
</div>
|
||||
<MudSwitch T="bool" Value="perm.Granted" Color="@(perm.Granted ? Color.Success : Color.Default)" Disabled="true" />
|
||||
<MudSwitch T="bool" Value="p.Granted" ValueChanged="@(v => OnPermissionToggled(p.Key, v))"
|
||||
Color="@(p.Granted ? Color.Success : Color.Default)"
|
||||
Disabled="@(selected.IsSystem)" />
|
||||
</div>
|
||||
}
|
||||
@if (_permsDirty && !selected.IsSystem)
|
||||
{
|
||||
<div style="display:flex;gap:8px;padding-top:4px;">
|
||||
<button class="admin-btn-primary" style="font-size:12px;padding:8px 16px;" @onclick="SavePermissions" disabled="@_savingPerms">
|
||||
@if (_savingPerms) { <span>Đang lưu...</span> } else { <span>Lưu quyền hạn</span> }
|
||||
</button>
|
||||
<button style="font-size:12px;padding:8px 16px;border:1px solid var(--admin-border-subtle);border-radius:8px;background:transparent;color:var(--admin-text-tertiary);cursor:pointer;" @onclick="ResetPermissions">
|
||||
Hủy
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -205,6 +223,25 @@ else
|
||||
private Guid? _selectedId;
|
||||
private List<IamApiService.RoleDto> _roles = new();
|
||||
|
||||
// EN: Permission toggle state (loaded from API, editable for non-system roles)
|
||||
// VI: Trạng thái toggle quyền hạn (tải từ API, có thể sửa cho non-system roles)
|
||||
private List<PermToggle> _permToggles = new();
|
||||
private bool _permsDirty;
|
||||
private bool _savingPerms;
|
||||
|
||||
// EN: Master list of all permissions matching StaffPermissions enum
|
||||
// VI: Danh sách gốc tất cả permissions khớp StaffPermissions enum
|
||||
private static readonly List<(string Key, string Label, string Desc)> AllPermissions = new()
|
||||
{
|
||||
("ViewSales", "Xem doanh thu", "Xem dữ liệu bán hàng và đơn hàng"),
|
||||
("ProcessPayment", "Xử lý thanh toán", "Thu tiền và xác nhận thanh toán"),
|
||||
("RefundOrder", "Hoàn tiền đơn hàng", "Hoàn tiền và hủy đơn hàng"),
|
||||
("ManageInventory", "Quản lý tồn kho", "Xem và cập nhật tồn kho"),
|
||||
("ViewReports", "Xem báo cáo", "Truy cập báo cáo và phân tích"),
|
||||
("ManageStaff", "Quản lý nhân viên", "Thêm/sửa/xóa nhân viên"),
|
||||
("ManageSettings", "Quản lý cài đặt", "Thay đổi cài đặt cửa hàng"),
|
||||
};
|
||||
|
||||
// EN: Dialog state for create/edit
|
||||
// VI: Trạng thái dialog cho tạo/sửa
|
||||
private bool _showDialog = false;
|
||||
@@ -229,6 +266,11 @@ else
|
||||
finally
|
||||
{
|
||||
_selectedId ??= _roles.FirstOrDefault()?.Id;
|
||||
if (_selectedId.HasValue)
|
||||
{
|
||||
var role = _roles.FirstOrDefault(r => r.Id == _selectedId);
|
||||
BuildPermToggles(role?.Permissions);
|
||||
}
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
@@ -368,93 +410,82 @@ else
|
||||
_ => "shield"
|
||||
};
|
||||
|
||||
// EN: Permission definitions — reflects actual backend [Authorize(Policy)] enforcement
|
||||
// and StaffPermissions bitmask from MerchantService domain.
|
||||
// VI: Định nghĩa quyền — phản ánh [Authorize(Policy)] thực tế trên backend
|
||||
// và StaffPermissions bitmask từ MerchantService domain.
|
||||
private record PermDef(string Label, string Desc, bool Granted);
|
||||
// EN: Permission toggle record for UI state
|
||||
// VI: Record toggle quyền hạn cho trạng thái UI
|
||||
private class PermToggle
|
||||
{
|
||||
public string Key { get; set; } = "";
|
||||
public string Label { get; set; } = "";
|
||||
public string Desc { get; set; } = "";
|
||||
public bool Granted { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Returns real permissions for a role based on backend authorization policies:
|
||||
/// - SuperAdmin/Admin: RequireAdmin policy → full platform access
|
||||
/// - Merchant/MerchantAdmin: shop owner/admin → full shop access
|
||||
/// - MerchantStaff: StaffPermissions(ViewSales|ProcessPayment) → POS only
|
||||
/// - Support/Auditor: RequireAuditor policy → read-only system access
|
||||
/// - User/PremiumUser: no admin access
|
||||
/// VI: Trả về quyền thật cho role dựa trên authorization policies backend.
|
||||
/// EN: Select a role and load its permissions from API (role_claims).
|
||||
/// VI: Chọn role và tải permissions từ API (role_claims).
|
||||
/// </summary>
|
||||
private static PermDef[] GetPermissionsForRole(string roleName) => roleName switch
|
||||
private void SelectRole(Guid roleId)
|
||||
{
|
||||
"SuperAdmin" => new[]
|
||||
_selectedId = roleId;
|
||||
_permsDirty = false;
|
||||
var role = _roles.FirstOrDefault(r => r.Id == roleId);
|
||||
BuildPermToggles(role?.Permissions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Build permission toggles from API data (role.Permissions list).
|
||||
/// VI: Tạo permission toggles từ dữ liệu API (role.Permissions list).
|
||||
/// </summary>
|
||||
private void BuildPermToggles(List<string>? granted)
|
||||
{
|
||||
var grantedSet = new HashSet<string>(granted ?? new(), StringComparer.OrdinalIgnoreCase);
|
||||
var hasAll = grantedSet.Contains("All");
|
||||
_permToggles = AllPermissions.Select(p => new PermToggle
|
||||
{
|
||||
new PermDef("Quản lý toàn hệ thống", "Truy cập đầy đủ mọi tính năng platform", true),
|
||||
new PermDef("Quản lý người dùng", "Tạo/sửa/xóa tài khoản, gán vai trò", true),
|
||||
new PermDef("Quản lý cửa hàng", "Tạo/xóa/cấu hình tất cả cửa hàng", true),
|
||||
new PermDef("Xem báo cáo hệ thống", "Truy cập báo cáo doanh thu toàn platform", true),
|
||||
new PermDef("Nhật ký hệ thống", "Xem audit logs và hoạt động hệ thống", true),
|
||||
new PermDef("Cấu hình hệ thống", "Thay đổi cài đặt platform và tích hợp", true),
|
||||
},
|
||||
"Admin" => new[]
|
||||
Key = p.Key,
|
||||
Label = p.Label,
|
||||
Desc = p.Desc,
|
||||
Granted = hasAll || grantedSet.Contains(p.Key)
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private void OnPermissionToggled(string key, bool value)
|
||||
{
|
||||
var toggle = _permToggles.FirstOrDefault(t => t.Key == key);
|
||||
if (toggle != null) { toggle.Granted = value; _permsDirty = true; }
|
||||
}
|
||||
|
||||
private void ResetPermissions()
|
||||
{
|
||||
var role = _roles.FirstOrDefault(r => r.Id == _selectedId);
|
||||
BuildPermToggles(role?.Permissions);
|
||||
_permsDirty = false;
|
||||
}
|
||||
|
||||
private async Task SavePermissions()
|
||||
{
|
||||
if (_selectedId == null) return;
|
||||
_savingPerms = true;
|
||||
StateHasChanged();
|
||||
try
|
||||
{
|
||||
new PermDef("Quản lý người dùng", "Tạo/sửa/xóa tài khoản, gán vai trò", true),
|
||||
new PermDef("Quản lý cửa hàng", "Tạo/xóa/cấu hình tất cả cửa hàng", true),
|
||||
new PermDef("Xem báo cáo hệ thống", "Truy cập báo cáo doanh thu toàn platform", true),
|
||||
new PermDef("Nhật ký hệ thống", "Xem audit logs và hoạt động hệ thống", true),
|
||||
new PermDef("Cấu hình hệ thống", "Thay đổi cài đặt platform", false),
|
||||
},
|
||||
"Merchant" => new[]
|
||||
{
|
||||
new PermDef("Quản lý cửa hàng", "Tạo và cấu hình cửa hàng của mình", true),
|
||||
new PermDef("POS bán hàng", "Tạo đơn hàng và xử lý thanh toán", true),
|
||||
new PermDef("Quản lý nhân viên", "Thêm/sửa/xóa nhân viên cửa hàng", true),
|
||||
new PermDef("Xem báo cáo", "Truy cập báo cáo doanh thu cửa hàng", true),
|
||||
new PermDef("Quản lý sản phẩm", "Thêm/sửa menu và sản phẩm", true),
|
||||
new PermDef("Thiết lập cửa hàng", "Thay đổi cài đặt và tính năng cửa hàng", true),
|
||||
},
|
||||
"MerchantAdmin" => new[]
|
||||
{
|
||||
new PermDef("POS bán hàng", "Tạo đơn hàng và xử lý thanh toán", true),
|
||||
new PermDef("Quản lý nhân viên", "Thêm/sửa nhân viên trong cửa hàng", true),
|
||||
new PermDef("Xem báo cáo", "Truy cập báo cáo doanh thu", true),
|
||||
new PermDef("Quản lý sản phẩm", "Thêm/sửa menu và sản phẩm", true),
|
||||
new PermDef("Áp dụng giảm giá", "Giảm giá và khuyến mãi cho đơn hàng", true),
|
||||
new PermDef("Thiết lập cửa hàng", "Thay đổi cài đặt cửa hàng", false),
|
||||
},
|
||||
"MerchantStaff" => new[]
|
||||
{
|
||||
new PermDef("POS bán hàng", "Tạo đơn hàng qua POS", true),
|
||||
new PermDef("Xử lý thanh toán", "Thu tiền và xác nhận thanh toán", true),
|
||||
new PermDef("Áp dụng giảm giá", "Giảm giá cho đơn hàng", false),
|
||||
new PermDef("Hoàn tiền đơn hàng", "Hoàn tiền và hủy đơn", false),
|
||||
new PermDef("Xem báo cáo", "Truy cập báo cáo doanh thu", false),
|
||||
new PermDef("Quản lý tồn kho", "Xem và cập nhật tồn kho", false),
|
||||
},
|
||||
"Support" => new[]
|
||||
{
|
||||
new PermDef("Xem người dùng", "Tra cứu thông tin tài khoản", true),
|
||||
new PermDef("Nhật ký hệ thống", "Xem audit logs và hoạt động", true),
|
||||
new PermDef("Hỗ trợ khách hàng", "Xử lý yêu cầu và ticket hỗ trợ", true),
|
||||
new PermDef("Quản lý người dùng", "Tạo/sửa/xóa tài khoản", false),
|
||||
new PermDef("Cấu hình hệ thống", "Thay đổi cài đặt platform", false),
|
||||
},
|
||||
"PremiumUser" => new[]
|
||||
{
|
||||
new PermDef("Tính năng premium", "Truy cập tính năng nâng cao", true),
|
||||
new PermDef("Ưu đãi đặc biệt", "Nhận khuyến mãi và giảm giá riêng", true),
|
||||
new PermDef("Quản lý cửa hàng", "Truy cập admin cửa hàng", false),
|
||||
new PermDef("Quản lý người dùng", "Truy cập quản trị hệ thống", false),
|
||||
},
|
||||
"User" => new[]
|
||||
{
|
||||
new PermDef("Tài khoản cá nhân", "Quản lý thông tin tài khoản", true),
|
||||
new PermDef("Xem cửa hàng", "Duyệt và tìm kiếm cửa hàng", true),
|
||||
new PermDef("Đặt hàng", "Đặt hàng qua ứng dụng", true),
|
||||
new PermDef("Quản lý cửa hàng", "Truy cập admin cửa hàng", false),
|
||||
new PermDef("Quản lý người dùng", "Truy cập quản trị hệ thống", false),
|
||||
},
|
||||
_ => new[]
|
||||
{
|
||||
new PermDef("Quyền cơ bản", "Quyền truy cập mặc định", true),
|
||||
var granted = _permToggles.Where(t => t.Granted).Select(t => t.Key).ToList();
|
||||
var (ok, err) = await IamService.UpdateRolePermissionsAsync(_selectedId.Value, granted);
|
||||
if (ok)
|
||||
{
|
||||
Snackbar.Add("Đã lưu quyền hạn thành công!", Severity.Success);
|
||||
_permsDirty = false;
|
||||
// EN: Reload roles to sync permissions in role list
|
||||
// VI: Tải lại roles để đồng bộ permissions trong danh sách
|
||||
await LoadRoles();
|
||||
SelectRole(_selectedId.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
Snackbar.Add(err ?? "Lỗi khi lưu quyền hạn", Severity.Error);
|
||||
}
|
||||
}
|
||||
};
|
||||
catch (Exception ex) { Snackbar.Add($"Lỗi: {ex.Message}", Severity.Error); }
|
||||
finally { _savingPerms = false; StateHasChanged(); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,8 @@ public class IamApiService
|
||||
string? Description,
|
||||
[property: JsonPropertyName("isSystemRole")] bool IsSystem,
|
||||
DateTime CreatedAt,
|
||||
int? UserCount);
|
||||
int? UserCount,
|
||||
List<string>? Permissions = null);
|
||||
|
||||
public async Task<List<RoleDto>> GetRolesAsync()
|
||||
{
|
||||
@@ -128,6 +129,30 @@ public class IamApiService
|
||||
catch (Exception ex) { return (false, ex.Message); }
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// ─── ROLE PERMISSIONS ───
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
/// <summary>
|
||||
/// EN: Update permissions for a role (PUT to role_claims).
|
||||
/// VI: Cập nhật permissions cho một role (PUT vào role_claims).
|
||||
/// </summary>
|
||||
public async Task<(bool Success, string? Error)> UpdateRolePermissionsAsync(Guid roleId, List<string> permissions)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _http.PutAsJsonAsync(
|
||||
$"/api/iam/api/v1/roles/{roleId}/permissions",
|
||||
new { permissions }, _jsonOptions);
|
||||
if (response.IsSuccessStatusCode) return (true, null);
|
||||
var json = await response.Content.ReadFromJsonAsync<JsonElement>(_jsonOptions);
|
||||
var msg = json.TryGetProperty("error", out var e) && e.TryGetProperty("message", out var m)
|
||||
? m.GetString() : response.ReasonPhrase;
|
||||
return (false, msg ?? "Cập nhật quyền hạn thất bại");
|
||||
}
|
||||
catch (Exception ex) { return (false, ex.Message); }
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// ─── USERS ───
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using IamService.Domain.AggregatesModel.RoleAggregate;
|
||||
using IamService.Domain.Exceptions;
|
||||
using IamService.Infrastructure;
|
||||
|
||||
namespace IamService.API.Application.Commands.Roles;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Command to update permissions for a role (stored in role_claims table).
|
||||
/// VI: Command để cập nhật permissions cho một role (lưu trong bảng role_claims).
|
||||
/// </summary>
|
||||
public record UpdateRolePermissionsCommand(
|
||||
Guid RoleId,
|
||||
List<string> Permissions) : IRequest<bool>;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Valid permission names matching MerchantService StaffPermissions enum.
|
||||
/// VI: Tên permission hợp lệ khớp với StaffPermissions enum của MerchantService.
|
||||
/// </summary>
|
||||
public static class ValidPermissions
|
||||
{
|
||||
public static readonly HashSet<string> Names = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"ViewSales", "ProcessPayment", "RefundOrder",
|
||||
"ManageInventory", "ViewReports", "ManageStaff", "ManageSettings", "All"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for UpdateRolePermissionsCommand.
|
||||
/// VI: Handler cho UpdateRolePermissionsCommand.
|
||||
/// </summary>
|
||||
public class UpdateRolePermissionsCommandHandler : IRequestHandler<UpdateRolePermissionsCommand, bool>
|
||||
{
|
||||
private readonly RoleManager<ApplicationRole> _roleManager;
|
||||
private readonly IamServiceContext _dbContext;
|
||||
private readonly ILogger<UpdateRolePermissionsCommandHandler> _logger;
|
||||
|
||||
public UpdateRolePermissionsCommandHandler(
|
||||
RoleManager<ApplicationRole> roleManager,
|
||||
IamServiceContext dbContext,
|
||||
ILogger<UpdateRolePermissionsCommandHandler> logger)
|
||||
{
|
||||
_roleManager = roleManager;
|
||||
_dbContext = dbContext;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<bool> Handle(UpdateRolePermissionsCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var role = await _roleManager.FindByIdAsync(request.RoleId.ToString())
|
||||
?? throw new DomainException($"Role with ID {request.RoleId} not found");
|
||||
|
||||
if (role.IsSystemRole)
|
||||
throw new DomainException("Cannot modify permissions of system roles");
|
||||
|
||||
// EN: Validate all permission names
|
||||
// VI: Kiểm tra tất cả tên permission
|
||||
var invalid = request.Permissions.Where(p => !ValidPermissions.Names.Contains(p)).ToList();
|
||||
if (invalid.Any())
|
||||
throw new DomainException($"Invalid permissions: {string.Join(", ", invalid)}");
|
||||
|
||||
// EN: Remove existing permission claims
|
||||
// VI: Xóa permission claims hiện tại
|
||||
var existing = await _dbContext.RoleClaims
|
||||
.Where(rc => rc.RoleId == request.RoleId && rc.ClaimType == "permission")
|
||||
.ToListAsync(cancellationToken);
|
||||
_dbContext.RoleClaims.RemoveRange(existing);
|
||||
|
||||
// EN: Add new permission claims
|
||||
// VI: Thêm permission claims mới
|
||||
foreach (var perm in request.Permissions.Distinct())
|
||||
{
|
||||
_dbContext.RoleClaims.Add(new IdentityRoleClaim<Guid>
|
||||
{
|
||||
RoleId = request.RoleId,
|
||||
ClaimType = "permission",
|
||||
ClaimValue = perm
|
||||
});
|
||||
}
|
||||
|
||||
await _dbContext.SaveChangesAsync(cancellationToken);
|
||||
_logger.LogInformation("Updated permissions for role {RoleName}: [{Permissions}]",
|
||||
role.Name, string.Join(", ", request.Permissions));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using MediatR;
|
||||
|
||||
namespace IamService.API.Application.Queries.Roles;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Query to get permissions for a specific role from role_claims table.
|
||||
/// VI: Query để lấy permissions cho một role cụ thể từ bảng role_claims.
|
||||
/// </summary>
|
||||
public record GetRolePermissionsQuery(Guid RoleId) : IRequest<GetRolePermissionsQueryResult?>;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Result of GetRolePermissionsQuery.
|
||||
/// VI: Kết quả của GetRolePermissionsQuery.
|
||||
/// </summary>
|
||||
public record GetRolePermissionsQueryResult(
|
||||
Guid RoleId,
|
||||
string RoleName,
|
||||
bool IsSystemRole,
|
||||
IReadOnlyList<string> Permissions);
|
||||
@@ -0,0 +1,43 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using IamService.Domain.AggregatesModel.RoleAggregate;
|
||||
using IamService.Infrastructure;
|
||||
|
||||
namespace IamService.API.Application.Queries.Roles;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for GetRolePermissionsQuery — reads permission claims from role_claims table.
|
||||
/// VI: Handler cho GetRolePermissionsQuery — đọc permission claims từ bảng role_claims.
|
||||
/// </summary>
|
||||
public class GetRolePermissionsQueryHandler : IRequestHandler<GetRolePermissionsQuery, GetRolePermissionsQueryResult?>
|
||||
{
|
||||
private readonly RoleManager<ApplicationRole> _roleManager;
|
||||
private readonly IamServiceContext _dbContext;
|
||||
|
||||
public GetRolePermissionsQueryHandler(
|
||||
RoleManager<ApplicationRole> roleManager,
|
||||
IamServiceContext dbContext)
|
||||
{
|
||||
_roleManager = roleManager;
|
||||
_dbContext = dbContext;
|
||||
}
|
||||
|
||||
public async Task<GetRolePermissionsQueryResult?> Handle(
|
||||
GetRolePermissionsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var role = await _roleManager.FindByIdAsync(request.RoleId.ToString());
|
||||
if (role == null) return null;
|
||||
|
||||
var permissions = await _dbContext.RoleClaims
|
||||
.Where(rc => rc.RoleId == request.RoleId && rc.ClaimType == "permission")
|
||||
.Select(rc => rc.ClaimValue!)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return new GetRolePermissionsQueryResult(
|
||||
role.Id,
|
||||
role.Name!,
|
||||
role.IsSystemRole,
|
||||
permissions);
|
||||
}
|
||||
}
|
||||
@@ -32,4 +32,5 @@ public record RoleDto(
|
||||
string? Description,
|
||||
bool IsSystemRole,
|
||||
DateTime CreatedAt,
|
||||
int UserCount = 0);
|
||||
int UserCount = 0,
|
||||
IReadOnlyList<string>? Permissions = null);
|
||||
|
||||
@@ -7,8 +7,8 @@ using IamService.Infrastructure;
|
||||
namespace IamService.API.Application.Queries.Roles;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for GetRolesQuery — includes user count per role via DB join.
|
||||
/// VI: Handler cho GetRolesQuery — bao gồm số user mỗi role qua DB join.
|
||||
/// EN: Handler for GetRolesQuery — includes user count and permissions per role.
|
||||
/// VI: Handler cho GetRolesQuery — bao gồm số user và permissions mỗi role.
|
||||
/// </summary>
|
||||
public class GetRolesQueryHandler : IRequestHandler<GetRolesQuery, GetRolesQueryResult>
|
||||
{
|
||||
@@ -33,9 +33,9 @@ public class GetRolesQueryHandler : IRequestHandler<GetRolesQuery, GetRolesQuery
|
||||
var query = _roleManager.Roles;
|
||||
var totalCount = await query.CountAsync(cancellationToken);
|
||||
|
||||
// EN: Join with UserRoles to get user count per role
|
||||
// VI: Join với UserRoles để lấy số user mỗi role
|
||||
var roles = await query
|
||||
// EN: Load roles with user count
|
||||
// VI: Tải roles với số lượng user
|
||||
var rolesWithCount = await query
|
||||
.OrderBy(r => r.Name)
|
||||
.Skip((request.PageNumber - 1) * request.PageSize)
|
||||
.Take(request.PageSize)
|
||||
@@ -43,19 +43,30 @@ public class GetRolesQueryHandler : IRequestHandler<GetRolesQuery, GetRolesQuery
|
||||
_dbContext.UserRoles,
|
||||
role => role.Id,
|
||||
userRole => userRole.RoleId,
|
||||
(role, userRoles) => new RoleDto(
|
||||
role.Id,
|
||||
role.Name!,
|
||||
role.Description,
|
||||
role.IsSystemRole,
|
||||
role.CreatedAt,
|
||||
userRoles.Count()))
|
||||
(role, userRoles) => new { role, UserCount = userRoles.Count() })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return new GetRolesQueryResult(
|
||||
roles,
|
||||
totalCount,
|
||||
request.PageNumber,
|
||||
request.PageSize);
|
||||
// EN: Batch-fetch permissions from role_claims for all loaded roles
|
||||
// VI: Batch-fetch permissions từ role_claims cho tất cả roles đã tải
|
||||
var roleIds = rolesWithCount.Select(r => r.role.Id).ToList();
|
||||
var permissionsByRole = await _dbContext.RoleClaims
|
||||
.Where(rc => roleIds.Contains(rc.RoleId) && rc.ClaimType == "permission")
|
||||
.GroupBy(rc => rc.RoleId)
|
||||
.ToDictionaryAsync(
|
||||
g => g.Key,
|
||||
g => (IReadOnlyList<string>)g.Select(rc => rc.ClaimValue!).ToList(),
|
||||
cancellationToken);
|
||||
|
||||
var roles = rolesWithCount.Select(r => new RoleDto(
|
||||
r.role.Id,
|
||||
r.role.Name!,
|
||||
r.role.Description,
|
||||
r.role.IsSystemRole,
|
||||
r.role.CreatedAt,
|
||||
r.UserCount,
|
||||
permissionsByRole.GetValueOrDefault(r.role.Id, Array.Empty<string>())
|
||||
)).ToList();
|
||||
|
||||
return new GetRolesQueryResult(roles, totalCount, request.PageNumber, request.PageSize);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ using Swashbuckle.AspNetCore.Annotations;
|
||||
using IamService.API.Application.Common;
|
||||
using IamService.API.Application.Commands.Roles;
|
||||
using IamService.API.Application.Queries.Roles;
|
||||
using RolesQueries = IamService.API.Application.Queries.Roles;
|
||||
using RolesCommands = IamService.API.Application.Commands.Roles;
|
||||
|
||||
namespace IamService.API.Controllers;
|
||||
|
||||
@@ -68,7 +70,8 @@ public class RolesController : ControllerBase
|
||||
Description = r.Description,
|
||||
IsSystemRole = r.IsSystemRole,
|
||||
CreatedAt = r.CreatedAt,
|
||||
UserCount = r.UserCount
|
||||
UserCount = r.UserCount,
|
||||
Permissions = r.Permissions?.ToList() ?? new()
|
||||
}),
|
||||
Pagination = new PaginationInfo
|
||||
{
|
||||
@@ -256,6 +259,54 @@ public class RolesController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
// ═══ PERMISSION ENDPOINTS ═══
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get permissions for a role (from role_claims table).
|
||||
/// VI: Lấy permissions cho một role (từ bảng role_claims).
|
||||
/// </summary>
|
||||
[HttpGet("{id:guid}/permissions")]
|
||||
[SwaggerOperation(Summary = "Get role permissions", OperationId = "GetRolePermissions")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetRolePermissions(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await _mediator.Send(new GetRolePermissionsQuery(id), cancellationToken);
|
||||
if (result == null)
|
||||
return NotFound(ApiResponse<object>.Fail("ROLE_NOT_FOUND", $"Role with ID {id} not found."));
|
||||
return Ok(ApiResponse<object>.Ok(result));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Update permissions for a role (replaces all permission claims).
|
||||
/// VI: Cập nhật permissions cho một role (thay thế toàn bộ permission claims).
|
||||
/// </summary>
|
||||
[HttpPut("{id:guid}/permissions")]
|
||||
[SwaggerOperation(Summary = "Update role permissions", OperationId = "UpdateRolePermissions")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> UpdateRolePermissions(
|
||||
Guid id, [FromBody] UpdateRolePermissionsRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _mediator.Send(new UpdateRolePermissionsCommand(id, request.Permissions), cancellationToken);
|
||||
var updated = await _mediator.Send(new GetRolePermissionsQuery(id), cancellationToken);
|
||||
return Ok(ApiResponse<object>.Ok(updated!));
|
||||
}
|
||||
catch (Exception ex) when (ex.Message.Contains("not found"))
|
||||
{
|
||||
return NotFound(ApiResponse<object>.Fail("ROLE_NOT_FOUND", ex.Message));
|
||||
}
|
||||
catch (Exception ex) when (ex.Message.Contains("system") || ex.Message.Contains("Invalid"))
|
||||
{
|
||||
return BadRequest(ApiResponse<object>.Fail("INVALID_REQUEST", ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
// ═══ USER ROLE ASSIGNMENT ═══
|
||||
|
||||
/// <summary>
|
||||
/// EN: Assign a role to a user.
|
||||
/// VI: Gán role cho user.
|
||||
@@ -442,6 +493,25 @@ public class RoleResponse
|
||||
/// VI: Số lượng người dùng được gán role này.
|
||||
/// </summary>
|
||||
public int UserCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// EN: Permissions assigned to this role (from role_claims).
|
||||
/// VI: Permissions được gán cho role này (từ role_claims).
|
||||
/// </summary>
|
||||
public List<string> Permissions { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Request body for updating role permissions.
|
||||
/// VI: Request body để cập nhật permissions cho role.
|
||||
/// </summary>
|
||||
public class UpdateRolePermissionsRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// EN: List of permission names to assign.
|
||||
/// VI: Danh sách tên permissions cần gán.
|
||||
/// </summary>
|
||||
public List<string> Permissions { get; set; } = new();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
Reference in New Issue
Block a user