diff --git a/services/merchant-service-net/src/MerchantService.API/Application/Behaviors/IRequirePermission.cs b/services/merchant-service-net/src/MerchantService.API/Application/Behaviors/IRequirePermission.cs
new file mode 100644
index 00000000..f0a09878
--- /dev/null
+++ b/services/merchant-service-net/src/MerchantService.API/Application/Behaviors/IRequirePermission.cs
@@ -0,0 +1,18 @@
+using MerchantService.Domain.AggregatesModel.MerchantStaffAggregate;
+
+namespace MerchantService.API.Application.Behaviors;
+
+///
+/// EN: Marker interface for commands that require a specific StaffPermission.
+/// Commands implementing this will be checked by PermissionAuthorizationBehavior.
+/// VI: Marker interface cho commands yêu cầu StaffPermission cụ thể.
+/// Commands implement interface này sẽ được kiểm tra bởi PermissionAuthorizationBehavior.
+///
+public interface IRequirePermission
+{
+ ///
+ /// EN: The permission required to execute this command.
+ /// VI: Permission cần thiết để thực thi command này.
+ ///
+ StaffPermissions RequiredPermission { get; }
+}
diff --git a/services/merchant-service-net/src/MerchantService.API/Application/Behaviors/PermissionAuthorizationBehavior.cs b/services/merchant-service-net/src/MerchantService.API/Application/Behaviors/PermissionAuthorizationBehavior.cs
new file mode 100644
index 00000000..ea911b87
--- /dev/null
+++ b/services/merchant-service-net/src/MerchantService.API/Application/Behaviors/PermissionAuthorizationBehavior.cs
@@ -0,0 +1,72 @@
+using MediatR;
+using MerchantService.Domain.AggregatesModel.MerchantStaffAggregate;
+
+namespace MerchantService.API.Application.Behaviors;
+
+///
+/// EN: MediatR pipeline behavior that checks permission claims from JWT token.
+/// Only applies to commands implementing IRequirePermission.
+/// Reads "permission" claims from HttpContext.User and converts to StaffPermissions bitmask.
+/// VI: MediatR pipeline behavior kiểm tra permission claims từ JWT token.
+/// Chỉ áp dụng cho commands implement IRequirePermission.
+/// Đọc "permission" claims từ HttpContext.User và chuyển sang StaffPermissions bitmask.
+///
+public class PermissionAuthorizationBehavior : IPipelineBehavior
+ where TRequest : IRequest
+{
+ private readonly IHttpContextAccessor _httpContextAccessor;
+ private readonly ILogger> _logger;
+
+ public PermissionAuthorizationBehavior(
+ IHttpContextAccessor httpContextAccessor,
+ ILogger> logger)
+ {
+ _httpContextAccessor = httpContextAccessor;
+ _logger = logger;
+ }
+
+ public async Task Handle(
+ TRequest request,
+ RequestHandlerDelegate next,
+ CancellationToken cancellationToken)
+ {
+ // EN: Skip if command doesn't require permission check
+ // VI: Bỏ qua nếu command không yêu cầu kiểm tra permission
+ if (request is not IRequirePermission permissionRequest)
+ return await next();
+
+ var required = permissionRequest.RequiredPermission;
+ if (required == StaffPermissions.None)
+ return await next();
+
+ var user = _httpContextAccessor.HttpContext?.User;
+ if (user?.Identity?.IsAuthenticated != true)
+ {
+ _logger.LogWarning("EN: Permission check failed — user not authenticated for {Command}",
+ typeof(TRequest).Name);
+ throw new UnauthorizedAccessException("User not authenticated / Người dùng chưa xác thực");
+ }
+
+ // EN: Extract permission claims from JWT and convert to bitmask
+ // VI: Trích xuất permission claims từ JWT và chuyển sang bitmask
+ var permClaims = user.Claims
+ .Where(c => c.Type == PermissionConstants.ClaimType)
+ .Select(c => c.Value)
+ .ToList();
+
+ var granted = PermissionConstants.FromClaims(permClaims);
+
+ // EN: Check if user has "All" permission or the specific required permission
+ // VI: Kiểm tra user có "All" permission hoặc permission cụ thể cần thiết
+ if (granted == StaffPermissions.All || (granted & required) == required)
+ return await next();
+
+ _logger.LogWarning(
+ "EN: Permission denied — {Command} requires {Required}, user has {Granted}. " +
+ "VI: Từ chối quyền — {Command} yêu cầu {Required}, user có {Granted}.",
+ typeof(TRequest).Name, required, granted);
+
+ throw new UnauthorizedAccessException(
+ $"Permission denied: requires {required} / Từ chối quyền: yêu cầu {required}");
+ }
+}
diff --git a/services/merchant-service-net/src/MerchantService.API/Application/Commands/Staff/StaffCommands.cs b/services/merchant-service-net/src/MerchantService.API/Application/Commands/Staff/StaffCommands.cs
index b8de8b08..b490c8e0 100644
--- a/services/merchant-service-net/src/MerchantService.API/Application/Commands/Staff/StaffCommands.cs
+++ b/services/merchant-service-net/src/MerchantService.API/Application/Commands/Staff/StaffCommands.cs
@@ -2,6 +2,7 @@
// VI: Commands quản lý nhân viên.
using MediatR;
+using MerchantService.API.Application.Behaviors;
using MerchantService.Domain.AggregatesModel.MerchantAggregate;
using MerchantService.Domain.AggregatesModel.MerchantStaffAggregate;
using MerchantService.Domain.Exceptions;
@@ -11,11 +12,12 @@ namespace MerchantService.API.Application.Commands.Staff;
#region Invite Staff Command
///
-/// EN: Command to invite a new staff member.
-/// VI: Command để mời nhân viên mới.
+/// EN: Command to invite a new staff member. Requires ManageStaff permission.
+/// VI: Command để mời nhân viên mới. Yêu cầu quyền ManageStaff.
///
-public record InviteStaffCommand : IRequest
+public record InviteStaffCommand : IRequest, IRequirePermission
{
+ public StaffPermissions RequiredPermission => StaffPermissions.ManageStaff;
///
/// EN: Owner's user ID extracted from JWT claims by the controller.
/// VI: User ID của chủ merchant được lấy từ JWT claims bởi controller.
@@ -96,8 +98,9 @@ public class InviteStaffCommandHandler : IRequestHandler
-public record CreateActiveStaffCommand : IRequest
+public record CreateActiveStaffCommand : IRequest, IRequirePermission
{
+ public StaffPermissions RequiredPermission => StaffPermissions.ManageStaff;
///
/// EN: Owner's user ID extracted from JWT claims by the controller.
/// VI: User ID của chủ merchant được lấy từ JWT claims bởi controller.
@@ -190,8 +193,9 @@ public class CreateActiveStaffCommandHandler : IRequestHandler
-public record UpdateStaffCommand : IRequest
+public record UpdateStaffCommand : IRequest, IRequirePermission
{
+ public StaffPermissions RequiredPermission => StaffPermissions.ManageStaff;
///
/// EN: Owner's user ID extracted from JWT claims by the controller.
/// VI: User ID của chủ merchant được lấy từ JWT claims bởi controller.
diff --git a/services/merchant-service-net/src/MerchantService.API/Program.cs b/services/merchant-service-net/src/MerchantService.API/Program.cs
index 8e15e61a..0bcd0b59 100644
--- a/services/merchant-service-net/src/MerchantService.API/Program.cs
+++ b/services/merchant-service-net/src/MerchantService.API/Program.cs
@@ -32,12 +32,17 @@ try
cfg.RegisterServicesFromAssemblyContaining();
cfg.AddOpenBehavior(typeof(LoggingBehavior<,>));
cfg.AddOpenBehavior(typeof(ValidatorBehavior<,>));
+ cfg.AddOpenBehavior(typeof(PermissionAuthorizationBehavior<,>));
cfg.AddOpenBehavior(typeof(TransactionBehavior<,>));
});
// EN: Add FluentValidation / VI: Thêm FluentValidation
builder.Services.AddValidatorsFromAssemblyContaining();
+ // EN: Required for PermissionAuthorizationBehavior to read JWT claims
+ // VI: Cần thiết cho PermissionAuthorizationBehavior đọc JWT claims
+ builder.Services.AddHttpContextAccessor();
+
// EN: Add controllers / VI: Thêm controllers
builder.Services.AddControllers();
diff --git a/services/merchant-service-net/src/MerchantService.Domain/AggregatesModel/MerchantStaffAggregate/PermissionConstants.cs b/services/merchant-service-net/src/MerchantService.Domain/AggregatesModel/MerchantStaffAggregate/PermissionConstants.cs
new file mode 100644
index 00000000..d9946e0a
--- /dev/null
+++ b/services/merchant-service-net/src/MerchantService.Domain/AggregatesModel/MerchantStaffAggregate/PermissionConstants.cs
@@ -0,0 +1,37 @@
+namespace MerchantService.Domain.AggregatesModel.MerchantStaffAggregate;
+
+///
+/// EN: Maps permission claim strings (from IAM role_claims) to StaffPermissions bitmask.
+/// VI: Map chuỗi permission claim (từ IAM role_claims) sang StaffPermissions bitmask.
+///
+public static class PermissionConstants
+{
+ public const string ClaimType = "permission";
+
+ private static readonly Dictionary Map = new(StringComparer.OrdinalIgnoreCase)
+ {
+ ["ViewSales"] = StaffPermissions.ViewSales,
+ ["ProcessPayment"] = StaffPermissions.ProcessPayment,
+ ["RefundOrder"] = StaffPermissions.RefundOrder,
+ ["ManageInventory"] = StaffPermissions.ManageInventory,
+ ["ViewReports"] = StaffPermissions.ViewReports,
+ ["ManageStaff"] = StaffPermissions.ManageStaff,
+ ["ManageSettings"] = StaffPermissions.ManageSettings,
+ ["All"] = StaffPermissions.All,
+ };
+
+ ///
+ /// EN: Convert JWT permission claim values to StaffPermissions bitmask.
+ /// VI: Chuyển đổi giá trị permission claim từ JWT sang StaffPermissions bitmask.
+ ///
+ public static StaffPermissions FromClaims(IEnumerable claimValues)
+ {
+ var result = StaffPermissions.None;
+ foreach (var val in claimValues)
+ {
+ if (Map.TryGetValue(val, out var perm))
+ result |= perm;
+ }
+ return result;
+ }
+}