Files
ColaFlow/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Commands/Login/LoginCommandHandler.cs
Yaojia Wang aaab26ba6c 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>
2025-11-03 15:00:39 +01:00

116 lines
4.3 KiB
C#

using ColaFlow.Modules.Identity.Application.Dtos;
using ColaFlow.Modules.Identity.Application.Services;
using ColaFlow.Modules.Identity.Domain.Aggregates.Tenants;
using ColaFlow.Modules.Identity.Domain.Aggregates.Users;
using ColaFlow.Modules.Identity.Domain.Repositories;
using MediatR;
namespace ColaFlow.Modules.Identity.Application.Commands.Login;
public class LoginCommandHandler : IRequestHandler<LoginCommand, LoginResponseDto>
{
private readonly ITenantRepository _tenantRepository;
private readonly IUserRepository _userRepository;
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,
IUserTenantRoleRepository userTenantRoleRepository)
{
_tenantRepository = tenantRepository;
_userRepository = userRepository;
_jwtService = jwtService;
_passwordHasher = passwordHasher;
_refreshTokenService = refreshTokenService;
_userTenantRoleRepository = userTenantRoleRepository;
}
public async Task<LoginResponseDto> Handle(LoginCommand request, CancellationToken cancellationToken)
{
// 1. Find tenant
var slug = TenantSlug.Create(request.TenantSlug);
var tenant = await _tenantRepository.GetBySlugAsync(slug, cancellationToken);
if (tenant == null)
{
throw new UnauthorizedAccessException("Invalid credentials");
}
// 2. Find user
var email = Email.Create(request.Email);
var user = await _userRepository.GetByEmailAsync(TenantId.Create(tenant.Id), email, cancellationToken);
if (user == null)
{
throw new UnauthorizedAccessException("Invalid credentials");
}
// 3. Verify password
if (user.PasswordHash == null || !_passwordHasher.VerifyPassword(request.Password, user.PasswordHash))
{
throw new UnauthorizedAccessException("Invalid credentials");
}
// 4. Get user's tenant role
var userTenantRole = await _userTenantRoleRepository.GetByUserAndTenantAsync(
user.Id,
tenant.Id,
cancellationToken);
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);
// 7. Update last login time
user.RecordLogin();
await _userRepository.UpdateAsync(user, cancellationToken);
// 8. Return result
return new LoginResponseDto
{
User = new UserDto
{
Id = user.Id,
TenantId = tenant.Id,
Email = user.Email.Value,
FullName = user.FullName.Value,
Status = user.Status.ToString(),
AuthProvider = user.AuthProvider.ToString(),
IsEmailVerified = user.EmailVerifiedAt.HasValue,
LastLoginAt = user.LastLoginAt,
CreatedAt = user.CreatedAt
},
Tenant = new TenantDto
{
Id = tenant.Id,
Name = tenant.Name.Value,
Slug = tenant.Slug.Value,
Status = tenant.Status.ToString(),
Plan = tenant.Plan.ToString(),
SsoEnabled = tenant.SsoConfig != null,
SsoProvider = tenant.SsoConfig?.Provider.ToString(),
CreatedAt = tenant.CreatedAt,
UpdatedAt = tenant.UpdatedAt ?? tenant.CreatedAt
},
AccessToken = accessToken,
RefreshToken = refreshToken
};
}
}