feat(backend): Activate domain events for user login, role assignment, and tenant removal
Implemented domain event raising in command handlers to enable audit logging and event-driven architecture for key Identity module operations. Changes: - Updated LoginCommand to include IpAddress and UserAgent fields for audit trail - Updated AuthController to extract and pass IP address and user agent from HTTP context - Modified LoginCommandHandler to raise UserLoggedInEvent on successful login - Updated AssignUserRoleCommand to include AssignedBy field for audit purposes - Modified AssignUserRoleCommandHandler to raise UserRoleAssignedEvent with previous role tracking - Updated RemoveUserFromTenantCommand to include RemovedBy and Reason fields - Modified RemoveUserFromTenantCommandHandler to raise UserRemovedFromTenantEvent before deletion - Added domain methods to User aggregate: RecordLoginWithEvent, RaiseRoleAssignedEvent, RaiseRemovedFromTenantEvent - Updated TenantUsersController to extract current user ID from JWT claims and pass to commands Technical Details: - All event raising follows aggregate root encapsulation pattern - Domain events are persisted through repository UpdateAsync calls - Event handlers will automatically log these events for audit trail - Maintains backward compatibility with existing login flow 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -31,8 +31,19 @@ public class AuthController : ControllerBase
|
||||
/// Login with email and password
|
||||
/// </summary>
|
||||
[HttpPost("login")]
|
||||
public async Task<IActionResult> Login([FromBody] LoginCommand command)
|
||||
public async Task<IActionResult> Login([FromBody] LoginRequest request)
|
||||
{
|
||||
var ipAddress = HttpContext.Connection.RemoteIpAddress?.ToString();
|
||||
var userAgent = HttpContext.Request.Headers["User-Agent"].ToString();
|
||||
|
||||
var command = new LoginCommand(
|
||||
request.TenantSlug,
|
||||
request.Email,
|
||||
request.Password,
|
||||
ipAddress,
|
||||
userAgent
|
||||
);
|
||||
|
||||
var result = await _mediator.Send(command);
|
||||
return Ok(result);
|
||||
}
|
||||
@@ -148,3 +159,9 @@ public class AuthController : ControllerBase
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public record LoginRequest(
|
||||
string TenantSlug,
|
||||
string Email,
|
||||
string Password
|
||||
);
|
||||
|
||||
@@ -64,7 +64,14 @@ public class TenantUsersController : ControllerBase
|
||||
if (userTenantId != tenantId)
|
||||
return StatusCode(403, new { error = "Access denied: You can only manage users in your own tenant" });
|
||||
|
||||
var command = new AssignUserRoleCommand(tenantId, userId, request.Role);
|
||||
// Extract current user ID from claims
|
||||
var currentUserIdClaim = User.FindFirst("user_id")?.Value;
|
||||
if (currentUserIdClaim == null)
|
||||
return Unauthorized(new { error = "User ID not found in token" });
|
||||
|
||||
var currentUserId = Guid.Parse(currentUserIdClaim);
|
||||
|
||||
var command = new AssignUserRoleCommand(tenantId, userId, request.Role, currentUserId);
|
||||
await _mediator.Send(command);
|
||||
return Ok(new { Message = "Role assigned successfully" });
|
||||
}
|
||||
@@ -87,7 +94,14 @@ public class TenantUsersController : ControllerBase
|
||||
if (userTenantId != tenantId)
|
||||
return StatusCode(403, new { error = "Access denied: You can only manage users in your own tenant" });
|
||||
|
||||
var command = new RemoveUserFromTenantCommand(tenantId, userId);
|
||||
// Extract current user ID from claims
|
||||
var currentUserIdClaim = User.FindFirst("user_id")?.Value;
|
||||
if (currentUserIdClaim == null)
|
||||
return Unauthorized(new { error = "User ID not found in token" });
|
||||
|
||||
var currentUserId = Guid.Parse(currentUserIdClaim);
|
||||
|
||||
var command = new RemoveUserFromTenantCommand(tenantId, userId, currentUserId, null);
|
||||
await _mediator.Send(command);
|
||||
return Ok(new { Message = "User removed from tenant successfully" });
|
||||
}
|
||||
|
||||
@@ -5,4 +5,5 @@ namespace ColaFlow.Modules.Identity.Application.Commands.AssignUserRole;
|
||||
public record AssignUserRoleCommand(
|
||||
Guid TenantId,
|
||||
Guid UserId,
|
||||
string Role) : IRequest<Unit>;
|
||||
string Role,
|
||||
Guid AssignedBy) : IRequest<Unit>;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using ColaFlow.Modules.Identity.Domain.Aggregates.Users;
|
||||
using ColaFlow.Modules.Identity.Domain.Aggregates.Users.Events;
|
||||
using ColaFlow.Modules.Identity.Domain.Aggregates.Tenants;
|
||||
using ColaFlow.Modules.Identity.Domain.Repositories;
|
||||
using MediatR;
|
||||
@@ -47,10 +48,12 @@ public class AssignUserRoleCommandHandler : IRequestHandler<AssignUserRoleComman
|
||||
request.TenantId,
|
||||
cancellationToken);
|
||||
|
||||
TenantRole? previousRole = existingRole?.Role;
|
||||
|
||||
if (existingRole != null)
|
||||
{
|
||||
// Update existing role
|
||||
existingRole.UpdateRole(role, Guid.Empty); // OperatorUserId can be set from HttpContext in controller
|
||||
existingRole.UpdateRole(role, request.AssignedBy);
|
||||
await _userTenantRoleRepository.UpdateAsync(existingRole, cancellationToken);
|
||||
}
|
||||
else
|
||||
@@ -60,11 +63,22 @@ public class AssignUserRoleCommandHandler : IRequestHandler<AssignUserRoleComman
|
||||
UserId.Create(request.UserId),
|
||||
TenantId.Create(request.TenantId),
|
||||
role,
|
||||
null); // AssignedByUserId can be set from HttpContext in controller
|
||||
UserId.Create(request.AssignedBy));
|
||||
|
||||
await _userTenantRoleRepository.AddAsync(userTenantRole, cancellationToken);
|
||||
}
|
||||
|
||||
// Raise domain event for role assignment
|
||||
user.RaiseRoleAssignedEvent(
|
||||
TenantId.Create(request.TenantId),
|
||||
role,
|
||||
previousRole,
|
||||
request.AssignedBy
|
||||
);
|
||||
|
||||
// Save changes to persist event
|
||||
await _userRepository.UpdateAsync(user, cancellationToken);
|
||||
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,7 @@ namespace ColaFlow.Modules.Identity.Application.Commands.Login;
|
||||
public record LoginCommand(
|
||||
string TenantSlug,
|
||||
string Email,
|
||||
string Password
|
||||
string Password,
|
||||
string? IpAddress,
|
||||
string? UserAgent
|
||||
) : IRequest<LoginResponseDto>;
|
||||
|
||||
@@ -2,6 +2,7 @@ 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.Aggregates.Users.Events;
|
||||
using ColaFlow.Modules.Identity.Domain.Repositories;
|
||||
using MediatR;
|
||||
|
||||
@@ -67,18 +68,24 @@ public class LoginCommandHandler : IRequestHandler<LoginCommand, LoginResponseDt
|
||||
throw new InvalidOperationException($"User {user.Id} has no role assigned for tenant {tenant.Id}");
|
||||
}
|
||||
|
||||
// 4.5. Record login and raise domain event
|
||||
user.RecordLoginWithEvent(
|
||||
TenantId.Create(tenant.Id),
|
||||
request.IpAddress ?? "unknown",
|
||||
request.UserAgent ?? "unknown"
|
||||
);
|
||||
|
||||
// 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,
|
||||
ipAddress: request.IpAddress,
|
||||
userAgent: request.UserAgent,
|
||||
cancellationToken);
|
||||
|
||||
// 7. Update last login time
|
||||
user.RecordLogin();
|
||||
// 7. Save user changes (including domain event)
|
||||
await _userRepository.UpdateAsync(user, cancellationToken);
|
||||
|
||||
// 8. Return result
|
||||
|
||||
@@ -4,4 +4,6 @@ namespace ColaFlow.Modules.Identity.Application.Commands.RemoveUserFromTenant;
|
||||
|
||||
public record RemoveUserFromTenantCommand(
|
||||
Guid TenantId,
|
||||
Guid UserId) : IRequest<Unit>;
|
||||
Guid UserId,
|
||||
Guid RemovedBy,
|
||||
string? Reason) : IRequest<Unit>;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using ColaFlow.Modules.Identity.Domain.Aggregates.Users;
|
||||
using ColaFlow.Modules.Identity.Domain.Aggregates.Users.Events;
|
||||
using ColaFlow.Modules.Identity.Domain.Aggregates.Tenants;
|
||||
using ColaFlow.Modules.Identity.Domain.Repositories;
|
||||
using MediatR;
|
||||
|
||||
@@ -8,13 +10,16 @@ public class RemoveUserFromTenantCommandHandler : IRequestHandler<RemoveUserFrom
|
||||
{
|
||||
private readonly IUserTenantRoleRepository _userTenantRoleRepository;
|
||||
private readonly IRefreshTokenRepository _refreshTokenRepository;
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public RemoveUserFromTenantCommandHandler(
|
||||
IUserTenantRoleRepository userTenantRoleRepository,
|
||||
IRefreshTokenRepository refreshTokenRepository)
|
||||
IRefreshTokenRepository refreshTokenRepository,
|
||||
IUserRepository userRepository)
|
||||
{
|
||||
_userTenantRoleRepository = userTenantRoleRepository;
|
||||
_refreshTokenRepository = refreshTokenRepository;
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
public async Task<Unit> Handle(RemoveUserFromTenantCommand request, CancellationToken cancellationToken)
|
||||
@@ -42,7 +47,7 @@ public class RemoveUserFromTenantCommandHandler : IRequestHandler<RemoveUserFrom
|
||||
|
||||
foreach (var token in userTokens.Where(t => !t.RevokedAt.HasValue))
|
||||
{
|
||||
token.Revoke("User removed from tenant");
|
||||
token.Revoke(request.Reason ?? "User removed from tenant");
|
||||
}
|
||||
|
||||
if (userTokens.Any())
|
||||
@@ -50,6 +55,19 @@ public class RemoveUserFromTenantCommandHandler : IRequestHandler<RemoveUserFrom
|
||||
await _refreshTokenRepository.UpdateRangeAsync(userTokens, cancellationToken);
|
||||
}
|
||||
|
||||
// Raise domain event before deletion
|
||||
var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken);
|
||||
if (user != null)
|
||||
{
|
||||
user.RaiseRemovedFromTenantEvent(
|
||||
TenantId.Create(request.TenantId),
|
||||
request.RemovedBy,
|
||||
request.Reason ?? "User removed from tenant"
|
||||
);
|
||||
|
||||
await _userRepository.UpdateAsync(user, cancellationToken);
|
||||
}
|
||||
|
||||
// Remove user's role
|
||||
await _userTenantRoleRepository.DeleteAsync(userTenantRole, cancellationToken);
|
||||
|
||||
|
||||
@@ -139,6 +139,24 @@ public sealed class User : AggregateRoot
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void RecordLoginWithEvent(TenantId tenantId, string ipAddress, string userAgent)
|
||||
{
|
||||
LastLoginAt = DateTime.UtcNow;
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
AddDomainEvent(new UserLoggedInEvent(Id, tenantId, ipAddress, userAgent));
|
||||
}
|
||||
|
||||
public void RaiseRoleAssignedEvent(TenantId tenantId, TenantRole role, TenantRole? previousRole, Guid assignedBy)
|
||||
{
|
||||
AddDomainEvent(new UserRoleAssignedEvent(Id, tenantId, role, previousRole, assignedBy));
|
||||
}
|
||||
|
||||
public void RaiseRemovedFromTenantEvent(TenantId tenantId, Guid removedBy, string reason)
|
||||
{
|
||||
AddDomainEvent(new UserRemovedFromTenantEvent(Id, tenantId, removedBy, reason));
|
||||
}
|
||||
|
||||
public void VerifyEmail()
|
||||
{
|
||||
EmailVerifiedAt = DateTime.UtcNow;
|
||||
|
||||
Reference in New Issue
Block a user