Files
ColaFlow/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Commands/CancelInvitation/CancelInvitationCommandHandler.cs
Yaojia Wang 4594ebef84 feat(backend): Implement User Invitation System (Phase 4)
Add complete user invitation system to enable multi-user tenants.

Changes:
- Created Invitation domain entity with 7-day expiration
- Implemented InviteUserCommand with security validation
- Implemented AcceptInvitationCommand (creates user + assigns role)
- Implemented GetPendingInvitationsQuery
- Implemented CancelInvitationCommand
- Added TenantInvitationsController with tenant-scoped endpoints
- Added public invitation acceptance endpoint to AuthController
- Created database migration for invitations table
- Registered InvitationRepository in DI container
- Created domain event handlers for audit trail

Security Features:
- Cannot invite as TenantOwner or AIAgent roles
- Cross-tenant validation on all endpoints
- Secure token generation and hashing
- RequireTenantAdmin policy for invite/list
- RequireTenantOwner policy for cancel

This UNBLOCKS 3 skipped Day 6 tests (RemoveUserFromTenant).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 22:02:56 +01:00

49 lines
1.8 KiB
C#

using ColaFlow.Modules.Identity.Domain.Aggregates.Invitations;
using ColaFlow.Modules.Identity.Domain.Aggregates.Tenants;
using ColaFlow.Modules.Identity.Domain.Repositories;
using MediatR;
using Microsoft.Extensions.Logging;
namespace ColaFlow.Modules.Identity.Application.Commands.CancelInvitation;
public class CancelInvitationCommandHandler : IRequestHandler<CancelInvitationCommand, Unit>
{
private readonly IInvitationRepository _invitationRepository;
private readonly ILogger<CancelInvitationCommandHandler> _logger;
public CancelInvitationCommandHandler(
IInvitationRepository invitationRepository,
ILogger<CancelInvitationCommandHandler> logger)
{
_invitationRepository = invitationRepository;
_logger = logger;
}
public async Task<Unit> Handle(CancelInvitationCommand request, CancellationToken cancellationToken)
{
var invitationId = InvitationId.Create(request.InvitationId);
var tenantId = TenantId.Create(request.TenantId);
// Get the invitation
var invitation = await _invitationRepository.GetByIdAsync(invitationId, cancellationToken);
if (invitation == null)
throw new InvalidOperationException($"Invitation {request.InvitationId} not found");
// Verify invitation belongs to the tenant (security check)
if (invitation.TenantId != tenantId)
throw new InvalidOperationException("Invitation does not belong to this tenant");
// Cancel the invitation
invitation.Cancel();
await _invitationRepository.UpdateAsync(invitation, cancellationToken);
_logger.LogInformation(
"Invitation {InvitationId} cancelled by user {CancelledBy} in tenant {TenantId}",
request.InvitationId,
request.CancelledBy,
request.TenantId);
return Unit.Value;
}
}