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>
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
using MediatR;
|
||||
|
||||
namespace ColaFlow.Modules.Identity.Application.Queries.GetPendingInvitations;
|
||||
|
||||
/// <summary>
|
||||
/// Query to get all pending invitations for a tenant
|
||||
/// </summary>
|
||||
public sealed record GetPendingInvitationsQuery(
|
||||
Guid TenantId
|
||||
) : IRequest<List<InvitationDto>>;
|
||||
|
||||
/// <summary>
|
||||
/// DTO for invitation data
|
||||
/// </summary>
|
||||
public record InvitationDto(
|
||||
Guid Id,
|
||||
string Email,
|
||||
string Role,
|
||||
string InvitedByName,
|
||||
Guid InvitedById,
|
||||
DateTime CreatedAt,
|
||||
DateTime ExpiresAt);
|
||||
@@ -0,0 +1,57 @@
|
||||
using ColaFlow.Modules.Identity.Domain.Aggregates.Tenants;
|
||||
using ColaFlow.Modules.Identity.Domain.Aggregates.Users;
|
||||
using ColaFlow.Modules.Identity.Domain.Repositories;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ColaFlow.Modules.Identity.Application.Queries.GetPendingInvitations;
|
||||
|
||||
public class GetPendingInvitationsQueryHandler : IRequestHandler<GetPendingInvitationsQuery, List<InvitationDto>>
|
||||
{
|
||||
private readonly IInvitationRepository _invitationRepository;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly ILogger<GetPendingInvitationsQueryHandler> _logger;
|
||||
|
||||
public GetPendingInvitationsQueryHandler(
|
||||
IInvitationRepository invitationRepository,
|
||||
IUserRepository userRepository,
|
||||
ILogger<GetPendingInvitationsQueryHandler> logger)
|
||||
{
|
||||
_invitationRepository = invitationRepository;
|
||||
_userRepository = userRepository;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<List<InvitationDto>> Handle(GetPendingInvitationsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = TenantId.Create(request.TenantId);
|
||||
|
||||
// Get all pending invitations for the tenant
|
||||
var invitations = await _invitationRepository.GetPendingByTenantAsync(tenantId, cancellationToken);
|
||||
|
||||
// Get all unique inviter user IDs
|
||||
var inviterIds = invitations.Select(i => (Guid)i.InvitedBy).Distinct().ToList();
|
||||
|
||||
// Fetch all inviters in one query
|
||||
var inviters = await _userRepository.GetByIdsAsync(inviterIds, cancellationToken);
|
||||
var inviterDict = inviters.ToDictionary(u => u.Id, u => u.FullName.Value);
|
||||
|
||||
// Map to DTOs
|
||||
var dtos = invitations.Select(i => new InvitationDto(
|
||||
Id: i.Id,
|
||||
Email: i.Email,
|
||||
Role: i.Role.ToString(),
|
||||
InvitedByName: inviterDict.TryGetValue(i.InvitedBy, out var name) ? name : "Unknown",
|
||||
InvitedById: i.InvitedBy,
|
||||
CreatedAt: i.CreatedAt,
|
||||
ExpiresAt: i.ExpiresAt
|
||||
)).ToList();
|
||||
|
||||
_logger.LogInformation(
|
||||
"Retrieved {Count} pending invitations for tenant {TenantId}",
|
||||
dtos.Count,
|
||||
request.TenantId);
|
||||
|
||||
return dtos;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user