From 4849b7b6fc86b331e895b87ee9422d2c7807a010 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Wed, 25 Mar 2026 19:50:06 +0700 Subject: [PATCH] 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) --- .../Pages/Admin/Staff/RolePermissions.razor | 217 ++++++++++-------- .../Services/IamApiService.cs | 27 ++- .../Roles/UpdateRolePermissionsCommand.cs | 90 ++++++++ .../Queries/Roles/GetRolePermissionsQuery.cs | 19 ++ .../Roles/GetRolePermissionsQueryHandler.cs | 43 ++++ .../Queries/Roles/GetRolesQuery.cs | 3 +- .../Queries/Roles/GetRolesQueryHandler.cs | 45 ++-- .../Controllers/RolesController.cs | 72 +++++- 8 files changed, 403 insertions(+), 113 deletions(-) create mode 100644 services/iam-service-net/src/IamService.API/Application/Commands/Roles/UpdateRolePermissionsCommand.cs create mode 100644 services/iam-service-net/src/IamService.API/Application/Queries/Roles/GetRolePermissionsQuery.cs create mode 100644 services/iam-service-net/src/IamService.API/Application/Queries/Roles/GetRolePermissionsQueryHandler.cs diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Staff/RolePermissions.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Staff/RolePermissions.razor index cd63c63f..fc85f9b4 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Staff/RolePermissions.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Staff/RolePermissions.razor @@ -51,7 +51,7 @@ else @foreach (var role in _roles) { + } @@ -205,6 +223,25 @@ else private Guid? _selectedId; private List _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 _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; } + } /// - /// 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). /// - 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); + } + + /// + /// EN: Build permission toggles from API data (role.Permissions list). + /// VI: Tạo permission toggles từ dữ liệu API (role.Permissions list). + /// + private void BuildPermToggles(List? granted) + { + var grantedSet = new HashSet(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(); } + } } diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/IamApiService.cs b/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/IamApiService.cs index 0f0edd3b..7bf0de71 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/IamApiService.cs +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/IamApiService.cs @@ -40,7 +40,8 @@ public class IamApiService string? Description, [property: JsonPropertyName("isSystemRole")] bool IsSystem, DateTime CreatedAt, - int? UserCount); + int? UserCount, + List? Permissions = null); public async Task> GetRolesAsync() { @@ -128,6 +129,30 @@ public class IamApiService catch (Exception ex) { return (false, ex.Message); } } + // ═══════════════════════════════════════════════ + // ─── ROLE PERMISSIONS ─── + // ═══════════════════════════════════════════════ + + /// + /// 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). + /// + public async Task<(bool Success, string? Error)> UpdateRolePermissionsAsync(Guid roleId, List 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(_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 ─── // ═══════════════════════════════════════════════ diff --git a/services/iam-service-net/src/IamService.API/Application/Commands/Roles/UpdateRolePermissionsCommand.cs b/services/iam-service-net/src/IamService.API/Application/Commands/Roles/UpdateRolePermissionsCommand.cs new file mode 100644 index 00000000..33960b48 --- /dev/null +++ b/services/iam-service-net/src/IamService.API/Application/Commands/Roles/UpdateRolePermissionsCommand.cs @@ -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; + +/// +/// 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). +/// +public record UpdateRolePermissionsCommand( + Guid RoleId, + List Permissions) : IRequest; + +/// +/// EN: Valid permission names matching MerchantService StaffPermissions enum. +/// VI: Tên permission hợp lệ khớp với StaffPermissions enum của MerchantService. +/// +public static class ValidPermissions +{ + public static readonly HashSet Names = new(StringComparer.OrdinalIgnoreCase) + { + "ViewSales", "ProcessPayment", "RefundOrder", + "ManageInventory", "ViewReports", "ManageStaff", "ManageSettings", "All" + }; +} + +/// +/// EN: Handler for UpdateRolePermissionsCommand. +/// VI: Handler cho UpdateRolePermissionsCommand. +/// +public class UpdateRolePermissionsCommandHandler : IRequestHandler +{ + private readonly RoleManager _roleManager; + private readonly IamServiceContext _dbContext; + private readonly ILogger _logger; + + public UpdateRolePermissionsCommandHandler( + RoleManager roleManager, + IamServiceContext dbContext, + ILogger logger) + { + _roleManager = roleManager; + _dbContext = dbContext; + _logger = logger; + } + + public async Task 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 + { + 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; + } +} diff --git a/services/iam-service-net/src/IamService.API/Application/Queries/Roles/GetRolePermissionsQuery.cs b/services/iam-service-net/src/IamService.API/Application/Queries/Roles/GetRolePermissionsQuery.cs new file mode 100644 index 00000000..92ff01b8 --- /dev/null +++ b/services/iam-service-net/src/IamService.API/Application/Queries/Roles/GetRolePermissionsQuery.cs @@ -0,0 +1,19 @@ +using MediatR; + +namespace IamService.API.Application.Queries.Roles; + +/// +/// 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. +/// +public record GetRolePermissionsQuery(Guid RoleId) : IRequest; + +/// +/// EN: Result of GetRolePermissionsQuery. +/// VI: Kết quả của GetRolePermissionsQuery. +/// +public record GetRolePermissionsQueryResult( + Guid RoleId, + string RoleName, + bool IsSystemRole, + IReadOnlyList Permissions); diff --git a/services/iam-service-net/src/IamService.API/Application/Queries/Roles/GetRolePermissionsQueryHandler.cs b/services/iam-service-net/src/IamService.API/Application/Queries/Roles/GetRolePermissionsQueryHandler.cs new file mode 100644 index 00000000..f06d5177 --- /dev/null +++ b/services/iam-service-net/src/IamService.API/Application/Queries/Roles/GetRolePermissionsQueryHandler.cs @@ -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; + +/// +/// EN: Handler for GetRolePermissionsQuery — reads permission claims from role_claims table. +/// VI: Handler cho GetRolePermissionsQuery — đọc permission claims từ bảng role_claims. +/// +public class GetRolePermissionsQueryHandler : IRequestHandler +{ + private readonly RoleManager _roleManager; + private readonly IamServiceContext _dbContext; + + public GetRolePermissionsQueryHandler( + RoleManager roleManager, + IamServiceContext dbContext) + { + _roleManager = roleManager; + _dbContext = dbContext; + } + + public async Task 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); + } +} diff --git a/services/iam-service-net/src/IamService.API/Application/Queries/Roles/GetRolesQuery.cs b/services/iam-service-net/src/IamService.API/Application/Queries/Roles/GetRolesQuery.cs index 4accd2e4..af298163 100644 --- a/services/iam-service-net/src/IamService.API/Application/Queries/Roles/GetRolesQuery.cs +++ b/services/iam-service-net/src/IamService.API/Application/Queries/Roles/GetRolesQuery.cs @@ -32,4 +32,5 @@ public record RoleDto( string? Description, bool IsSystemRole, DateTime CreatedAt, - int UserCount = 0); + int UserCount = 0, + IReadOnlyList? Permissions = null); diff --git a/services/iam-service-net/src/IamService.API/Application/Queries/Roles/GetRolesQueryHandler.cs b/services/iam-service-net/src/IamService.API/Application/Queries/Roles/GetRolesQueryHandler.cs index 492be883..bcd6474f 100644 --- a/services/iam-service-net/src/IamService.API/Application/Queries/Roles/GetRolesQueryHandler.cs +++ b/services/iam-service-net/src/IamService.API/Application/Queries/Roles/GetRolesQueryHandler.cs @@ -7,8 +7,8 @@ using IamService.Infrastructure; namespace IamService.API.Application.Queries.Roles; /// -/// 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. /// public class GetRolesQueryHandler : IRequestHandler { @@ -33,9 +33,9 @@ public class GetRolesQueryHandler : IRequestHandler r.Name) .Skip((request.PageNumber - 1) * request.PageSize) .Take(request.PageSize) @@ -43,19 +43,30 @@ public class GetRolesQueryHandler : IRequestHandler 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)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()) + )).ToList(); + + return new GetRolesQueryResult(roles, totalCount, request.PageNumber, request.PageSize); } } diff --git a/services/iam-service-net/src/IamService.API/Controllers/RolesController.cs b/services/iam-service-net/src/IamService.API/Controllers/RolesController.cs index bda7cd73..750500d7 100644 --- a/services/iam-service-net/src/IamService.API/Controllers/RolesController.cs +++ b/services/iam-service-net/src/IamService.API/Controllers/RolesController.cs @@ -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 ═══ + + /// + /// EN: Get permissions for a role (from role_claims table). + /// VI: Lấy permissions cho một role (từ bảng role_claims). + /// + [HttpGet("{id:guid}/permissions")] + [SwaggerOperation(Summary = "Get role permissions", OperationId = "GetRolePermissions")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetRolePermissions(Guid id, CancellationToken cancellationToken = default) + { + var result = await _mediator.Send(new GetRolePermissionsQuery(id), cancellationToken); + if (result == null) + return NotFound(ApiResponse.Fail("ROLE_NOT_FOUND", $"Role with ID {id} not found.")); + return Ok(ApiResponse.Ok(result)); + } + + /// + /// 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). + /// + [HttpPut("{id:guid}/permissions")] + [SwaggerOperation(Summary = "Update role permissions", OperationId = "UpdateRolePermissions")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task 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.Ok(updated!)); + } + catch (Exception ex) when (ex.Message.Contains("not found")) + { + return NotFound(ApiResponse.Fail("ROLE_NOT_FOUND", ex.Message)); + } + catch (Exception ex) when (ex.Message.Contains("system") || ex.Message.Contains("Invalid")) + { + return BadRequest(ApiResponse.Fail("INVALID_REQUEST", ex.Message)); + } + } + + // ═══ USER ROLE ASSIGNMENT ═══ + /// /// 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. /// public int UserCount { get; set; } + + /// + /// EN: Permissions assigned to this role (from role_claims). + /// VI: Permissions được gán cho role này (từ role_claims). + /// + public List Permissions { get; set; } = new(); +} + +/// +/// EN: Request body for updating role permissions. +/// VI: Request body để cập nhật permissions cho role. +/// +public class UpdateRolePermissionsRequest +{ + /// + /// EN: List of permission names to assign. + /// VI: Danh sách tên permissions cần gán. + /// + public List Permissions { get; set; } = new(); } #endregion