feat(backend): Implement complete RBAC system (Day 5 Phase 2)
Implemented Role-Based Access Control (RBAC) with 5 tenant-level roles following Clean Architecture principles. Changes: - Created TenantRole enum (TenantOwner, TenantAdmin, TenantMember, TenantGuest, AIAgent) - Created UserTenantRole entity with repository pattern - Updated JWT service to include role claims (tenant_role, role) - Updated RegisterTenant to auto-assign TenantOwner role - Updated Login to query and include user role in JWT - Updated RefreshToken to preserve role claims - Added authorization policies in Program.cs (RequireTenantOwner, RequireTenantAdmin, etc.) - Updated /api/auth/me endpoint to return role information - Created EF Core migration for user_tenant_roles table - Applied database migration successfully Database: - New table: identity.user_tenant_roles - Columns: id, user_id, tenant_id, role, assigned_at, assigned_by_user_id - Indexes: user_id, tenant_id, role, unique(user_id, tenant_id) - Foreign keys: CASCADE on user and tenant deletion Testing: - Created test-rbac.ps1 PowerShell script - All RBAC tests passing - JWT tokens contain role claims - Role persists across login and token refresh Documentation: - DAY5-PHASE2-RBAC-IMPLEMENTATION-SUMMARY.md with complete implementation details 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -14,19 +14,22 @@ public class LoginCommandHandler : IRequestHandler<LoginCommand, LoginResponseDt
|
||||
private readonly IJwtService _jwtService;
|
||||
private readonly IPasswordHasher _passwordHasher;
|
||||
private readonly IRefreshTokenService _refreshTokenService;
|
||||
private readonly IUserTenantRoleRepository _userTenantRoleRepository;
|
||||
|
||||
public LoginCommandHandler(
|
||||
ITenantRepository tenantRepository,
|
||||
IUserRepository userRepository,
|
||||
IJwtService jwtService,
|
||||
IPasswordHasher passwordHasher,
|
||||
IRefreshTokenService refreshTokenService)
|
||||
IRefreshTokenService refreshTokenService,
|
||||
IUserTenantRoleRepository userTenantRoleRepository)
|
||||
{
|
||||
_tenantRepository = tenantRepository;
|
||||
_userRepository = userRepository;
|
||||
_jwtService = jwtService;
|
||||
_passwordHasher = passwordHasher;
|
||||
_refreshTokenService = refreshTokenService;
|
||||
_userTenantRoleRepository = userTenantRoleRepository;
|
||||
}
|
||||
|
||||
public async Task<LoginResponseDto> Handle(LoginCommand request, CancellationToken cancellationToken)
|
||||
@@ -53,21 +56,32 @@ public class LoginCommandHandler : IRequestHandler<LoginCommand, LoginResponseDt
|
||||
throw new UnauthorizedAccessException("Invalid credentials");
|
||||
}
|
||||
|
||||
// 4. Generate JWT token
|
||||
var accessToken = _jwtService.GenerateToken(user, tenant);
|
||||
// 4. Get user's tenant role
|
||||
var userTenantRole = await _userTenantRoleRepository.GetByUserAndTenantAsync(
|
||||
user.Id,
|
||||
tenant.Id,
|
||||
cancellationToken);
|
||||
|
||||
// 5. Generate refresh token
|
||||
if (userTenantRole == null)
|
||||
{
|
||||
throw new InvalidOperationException($"User {user.Id} has no role assigned for tenant {tenant.Id}");
|
||||
}
|
||||
|
||||
// 5. Generate JWT token with role
|
||||
var accessToken = _jwtService.GenerateToken(user, tenant, userTenantRole.Role);
|
||||
|
||||
// 6. Generate refresh token
|
||||
var refreshToken = await _refreshTokenService.GenerateRefreshTokenAsync(
|
||||
user,
|
||||
ipAddress: null,
|
||||
userAgent: null,
|
||||
cancellationToken);
|
||||
|
||||
// 6. Update last login time
|
||||
// 7. Update last login time
|
||||
user.RecordLogin();
|
||||
await _userRepository.UpdateAsync(user, cancellationToken);
|
||||
|
||||
// 7. Return result
|
||||
// 8. Return result
|
||||
return new LoginResponseDto
|
||||
{
|
||||
User = new UserDto
|
||||
|
||||
@@ -13,19 +13,22 @@ public class RegisterTenantCommandHandler : IRequestHandler<RegisterTenantComman
|
||||
private readonly IJwtService _jwtService;
|
||||
private readonly IPasswordHasher _passwordHasher;
|
||||
private readonly IRefreshTokenService _refreshTokenService;
|
||||
private readonly IUserTenantRoleRepository _userTenantRoleRepository;
|
||||
|
||||
public RegisterTenantCommandHandler(
|
||||
ITenantRepository tenantRepository,
|
||||
IUserRepository userRepository,
|
||||
IJwtService jwtService,
|
||||
IPasswordHasher passwordHasher,
|
||||
IRefreshTokenService refreshTokenService)
|
||||
IRefreshTokenService refreshTokenService,
|
||||
IUserTenantRoleRepository userTenantRoleRepository)
|
||||
{
|
||||
_tenantRepository = tenantRepository;
|
||||
_userRepository = userRepository;
|
||||
_jwtService = jwtService;
|
||||
_passwordHasher = passwordHasher;
|
||||
_refreshTokenService = refreshTokenService;
|
||||
_userTenantRoleRepository = userTenantRoleRepository;
|
||||
}
|
||||
|
||||
public async Task<RegisterTenantResult> Handle(
|
||||
@@ -59,10 +62,18 @@ public class RegisterTenantCommandHandler : IRequestHandler<RegisterTenantComman
|
||||
|
||||
await _userRepository.AddAsync(adminUser, cancellationToken);
|
||||
|
||||
// 4. Generate JWT token
|
||||
var accessToken = _jwtService.GenerateToken(adminUser, tenant);
|
||||
// 4. Assign TenantOwner role to admin user
|
||||
var tenantOwnerRole = UserTenantRole.Create(
|
||||
UserId.Create(adminUser.Id),
|
||||
TenantId.Create(tenant.Id),
|
||||
TenantRole.TenantOwner);
|
||||
|
||||
// 5. Generate refresh token
|
||||
await _userTenantRoleRepository.AddAsync(tenantOwnerRole, cancellationToken);
|
||||
|
||||
// 5. Generate JWT token with role
|
||||
var accessToken = _jwtService.GenerateToken(adminUser, tenant, TenantRole.TenantOwner);
|
||||
|
||||
// 6. Generate refresh token
|
||||
var refreshToken = await _refreshTokenService.GenerateRefreshTokenAsync(
|
||||
adminUser,
|
||||
ipAddress: null,
|
||||
|
||||
@@ -5,6 +5,6 @@ namespace ColaFlow.Modules.Identity.Application.Services;
|
||||
|
||||
public interface IJwtService
|
||||
{
|
||||
string GenerateToken(User user, Tenant tenant);
|
||||
string GenerateToken(User user, Tenant tenant, TenantRole tenantRole);
|
||||
Task<string> GenerateRefreshTokenAsync(User user, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user