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:
Ho Ngoc Hai
2026-03-25 19:50:06 +07:00
parent 52f77c0878
commit 4849b7b6fc
8 changed files with 403 additions and 113 deletions

View File

@@ -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(); }
}
}

View File

@@ -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 ───
// ═══════════════════════════════════════════════

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -32,4 +32,5 @@ public record RoleDto(
string? Description,
bool IsSystemRole,
DateTime CreatedAt,
int UserCount = 0);
int UserCount = 0,
IReadOnlyList<string>? Permissions = null);

View File

@@ -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);
}
}

View File

@@ -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