Pular para conteúdo

👥 Módulo Users - Gestão de Usuários

Este documento detalha a implementação completa do módulo Users, responsável pela gestão de usuários e integração com autenticação na plataforma MeAjudaAi.

🎯 Visão Geral

O módulo Users implementa um Bounded Context dedicado para gestão de identidade e perfis de usuários, seguindo os princípios de Domain-Driven Design (DDD) e Clean Architecture.

Responsabilidades Principais

  • Registro e gestão de usuários
  • Integração com Keycloak para autenticação externa
  • Perfis de usuário detalhados
  • Preferências personalizadas
  • Soft delete e gestão de lifecycle
  • Module API para comunicação entre módulos

🏗️ Arquitetura do Módulo

Estrutura de Pastas

src/Modules/Users/
├── API/                           # Camada de apresentação
│   ├── Endpoints/                 # Minimal APIs
│   ├── Extensions.cs              # Registro de serviços
│   └── Mappers/                   # Mapeamento entre camadas
├── Application/                   # Camada de aplicação
│   ├── Commands/                  # Commands (CQRS)
│   ├── Queries/                   # Queries (CQRS)
│   ├── Handlers/                  # Command/Query handlers
│   ├── DTOs/                      # Data Transfer Objects
│   ├── Services/                  # Module API e serviços
│   └── Mappers/                   # Mapeadores
├── Domain/                        # Camada de domínio
│   ├── Entities/                  # Agregados e entidades
│   ├── ValueObjects/              # Value Objects
│   ├── Events/                    # Domain Events
│   ├── Exceptions/                # Exceções de domínio
│   ├── Services/                  # Domain Services
│   └── Repositories/              # Interfaces de repositório
├── Infrastructure/                # Camada de infraestrutura
│   ├── Persistence/               # Entity Framework
│   │   ├── Configurations/        # Configurações EF
│   │   └── Migrations/            # Migrações
│   ├── Identity/                  # Integração Keycloak
│   ├── Services/                  # Implementações de serviços
│   └── Repositories/              # Implementações de repositório
└── Tests/                         # Testes unitários
    └── Unit/                      # Testes por camada

🎭 Domain Model

Agregado Principal: User

/// <summary>
/// Agregado raiz para gestão de usuários do sistema
/// </summary>
public sealed class User : AggregateRoot<UserId>
{
    public string KeycloakId { get; private set; }      // ID externo do Keycloak
    public string Username { get; private set; }        // Nome de usuário único
    public string Email { get; private set; }           // Email único
    public string FirstName { get; private set; }       // Nome
    public string LastName { get; private set; }        // Sobrenome

    // Soft delete
    public bool IsDeleted { get; private set; }
    public DateTime? DeletedAt { get; private set; }

    // Métodos de negócio
    public string GetFullName() => $"{FirstName} {LastName}".Trim();
    public void UpdateProfile(string firstName, string lastName);
    public void MarkAsDeleted();
}

Value Objects

UserId

public sealed record UserId(Guid Value) : EntityId(Value)
{
    public static UserId New() => new(Guid.NewGuid());
    public static UserId From(Guid value) => new(value);
    public static UserId From(string value) => new(Guid.Parse(value));
}

UserProfile

public class UserProfile : ValueObject
{
    public string FirstName { get; }
    public string LastName { get; }
    public PhoneNumber? PhoneNumber { get; }
    public string FullName => $"{FirstName} {LastName}";

    public UserProfile(string firstName, string lastName, PhoneNumber? phoneNumber = null)
    {
        // Validações e inicialização
    }
}

PhoneNumber

public class PhoneNumber : ValueObject
{
    public string Value { get; }

    public PhoneNumber(string value)
    {
        // Validação de formato de telefone
    }
}

🔄 Domain Events

Eventos Implementados

/// <summary>
/// Evento disparado quando um novo usuário é registrado
/// </summary>
public record UserRegisteredDomainEvent(
    Guid AggregateId,
    int Version,
    string KeycloakId,
    string Username,
    string Email
) : DomainEvent(AggregateId, Version);

/// <summary>
/// Evento disparado quando perfil é atualizado
/// </summary>
public record UserProfileUpdatedDomainEvent(
    Guid AggregateId,
    int Version,
    string FirstName,
    string LastName
) : DomainEvent(AggregateId, Version);

/// <summary>
/// Evento disparado quando usuário é suspenso
/// </summary>
public record UserSuspendedDomainEvent(
    Guid AggregateId,
    int Version,
    string Reason,
    DateTime SuspendedAt
) : DomainEvent(AggregateId, Version);

/// <summary>
/// Evento disparado quando usuário é reativado
/// </summary>
public record UserReactivatedDomainEvent(
    Guid AggregateId,
    int Version,
    DateTime ReactivatedAt
) : DomainEvent(AggregateId, Version);

Padrões Distribuídos e Comunicação Inter-Módulos

Event-Driven Architecture

O módulo Users é o ponto de entrada para muitos workflows distribuídos:

// Handler para coordenar criação de usuário
public class UserRegistrationSagaOrchestrator : ISagaOrchestrator
{
    public async Task HandleAsync(UserRegisteredDomainEvent evt)
    {
        var sagaId = Guid.NewGuid();

        try
        {
            // 1. Criar perfil de notificações
            await _notificationService.CreateUserProfileAsync(evt.AggregateId, evt.Email);

            // 2. Inicializar preferências do usuário
            await _preferencesService.InitializeDefaultPreferencesAsync(evt.AggregateId);

            // 3. Verificar se é um Provider baseado no contexto
            var userType = await DetermineUserTypeAsync(evt.Username, evt.Email);
            if (userType == UserType.Provider)
            {
                await _providerModule.InitializeProviderProfileAsync(evt.AggregateId);
            }

            // 4. Enviar email de boas-vindas
            await _emailService.SendWelcomeEmailAsync(evt.Email, $"{evt.Username}");

            await _sagaRepository.CompleteAsync(sagaId);
        }
        catch (Exception ex)
        {
            await CompensateUserRegistration(sagaId, evt.AggregateId, ex);
        }
    }
}

Module API para Consultas Síncronas

Interface para outros módulos consultarem dados de usuários:

public interface IUsersModuleApi
{
    Task<UserDto> GetByIdAsync(Guid userId);
    Task<UserDto> GetByEmailAsync(string email);
    Task<bool> IsUserActiveAsync(Guid userId);
    Task<UserContactInfoDto> GetUserContactAsync(Guid userId);
    Task<IEnumerable<UserDto>> GetUsersByIdsAsync(IEnumerable<Guid> userIds);
    Task<bool> ValidateUserExistsAsync(Guid userId);
}

// Implementação com cache distribuído para performance
public class CachedUsersModuleApi : IUsersModuleApi
{
    private readonly IUsersModuleApi _inner;
    private readonly IDistributedCache _cache;

    public async Task<UserDto> GetByIdAsync(Guid userId)
    {
        var cacheKey = $"user:{userId}";
        var cached = await _cache.GetStringAsync(cacheKey);

        if (cached != null)
            return JsonSerializer.Deserialize<UserDto>(cached);

        var user = await _inner.GetByIdAsync(userId);

        if (user != null)
        {
            await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(user),
                new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30),
                    SlidingExpiration = TimeSpan.FromMinutes(10)
                });
        }

        return user;
    }

    public async Task<bool> IsUserActiveAsync(Guid userId)
    {
        // Cache mais agressivo para verificações de status
        var cacheKey = $"user:active:{userId}";
        var cached = await _cache.GetStringAsync(cacheKey);

        if (cached != null)
            return bool.Parse(cached);

        var isActive = await _inner.IsUserActiveAsync(userId);

        await _cache.SetStringAsync(cacheKey, isActive.ToString(),
            new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
            });

        return isActive;
    }
}

Invalidação de Cache Distribuído

Coordenação de cache quando dados de usuário mudam:

public class UserCacheInvalidationHandler : 
    IEventHandler<UserProfileUpdatedDomainEvent>,
    IEventHandler<UserSuspendedDomainEvent>,
    IEventHandler<UserReactivatedDomainEvent>
{
    private readonly IDistributedCache _cache;
    private readonly IEventBus _eventBus;

    public async Task HandleAsync(UserProfileUpdatedDomainEvent evt)
    {
        await InvalidateUserCacheAsync(evt.AggregateId);

        // Notificar outros módulos sobre atualização
        await _eventBus.PublishAsync(new UserProfileChangedIntegrationEvent
        {
            UserId = evt.AggregateId,
            FirstName = evt.FirstName,
            LastName = evt.LastName,
            ChangedAt = DateTime.UtcNow
        });
    }

    public async Task HandleAsync(UserSuspendedDomainEvent evt)
    {
        await InvalidateUserCacheAsync(evt.AggregateId);

        // Coordenar suspensão em outros módulos
        await _eventBus.PublishAsync(new UserStatusChangedIntegrationEvent
        {
            UserId = evt.AggregateId,
            Status = "Suspended",
            Reason = evt.Reason,
            ChangedAt = evt.SuspendedAt
        });
    }

    private async Task InvalidateUserCacheAsync(Guid userId)
    {
        var tasks = new[]
        {
            _cache.RemoveAsync($"user:{userId}"),
            _cache.RemoveAsync($"user:active:{userId}"),
            _cache.RemoveAsync($"user:contact:{userId}")
        };

        await Task.WhenAll(tasks);
    }
}

⚡ CQRS Implementation

Commands

  • RegisterUserCommand: Registro de novo usuário
  • UpdateUserProfileCommand: Atualização de perfil
  • DeleteUserCommand: Exclusão lógica

Queries

  • GetUserByIdQuery: Busca por ID
  • GetUserByKeycloakIdQuery: Busca por ID do Keycloak
  • GetUserByEmailQuery: Busca por email
  • GetUserByUsernameQuery: Busca por username
  • GetUsersByIdsQuery: Busca em lote

🌐 API Endpoints

Endpoints Principais

  • POST /api/v1/users/register - Registrar usuário
  • GET /api/v1/users - Listar usuários (paginado)
  • GET /api/v1/users/{id} - Obter usuário por ID
  • PUT /api/v1/users/{id} - Atualizar perfil
  • DELETE /api/v1/users/{id} - Excluir usuário
  • GET /api/v1/users/check-email - Verificar disponibilidade de email

🔌 Module API

Interface IUsersModuleApi

public interface IUsersModuleApi : IModuleApi
{
    Task<Result<ModuleUserDto?>> GetUserByIdAsync(Guid userId, CancellationToken cancellationToken = default);
    Task<Result<ModuleUserDto?>> GetUserByEmailAsync(string email, CancellationToken cancellationToken = default);
    Task<Result<IReadOnlyList<ModuleUserBasicDto>>> GetUsersBatchAsync(IReadOnlyList<Guid> userIds, CancellationToken cancellationToken = default);
    Task<Result<bool>> UserExistsAsync(Guid userId, CancellationToken cancellationToken = default);
    Task<Result<bool>> EmailExistsAsync(string email, CancellationToken cancellationToken = default);
}

DTOs para Module API

public sealed record ModuleUserDto(
    Guid Id,
    string Username,
    string Email,
    string FirstName,
    string LastName,
    string FullName
);

public sealed record ModuleUserBasicDto(
    Guid Id,
    string Username,
    string Email,
    bool IsActive
);

🔐 Integração com Keycloak

Fluxo de Autenticação

  1. Usuário autentica no Keycloak
  2. JWT token é validado pela aplicação
  3. Usuário é sincronizado automaticamente se não existir
  4. Contexto de usuário é estabelecido para a sessão

Domain Service: KeycloakUserDomainService

public interface IKeycloakUserDomainService
{
    Task<Result<TokenValidationResult>> ValidateTokenAsync(string token, CancellationToken cancellationToken = default);
    Task<Result<bool>> UserExistsInKeycloakAsync(string keycloakId, CancellationToken cancellationToken = default);
}

🗄️ Persistência

Schema de Banco

-- Tabela principal de usuários
CREATE TABLE users.Users (
    Id uuid PRIMARY KEY,
    KeycloakId varchar(255) NOT NULL UNIQUE,
    Username varchar(100) NOT NULL UNIQUE,
    Email varchar(255) NOT NULL UNIQUE,
    FirstName varchar(100) NOT NULL,
    LastName varchar(100) NOT NULL,
    IsDeleted boolean NOT NULL DEFAULT false,
    DeletedAt timestamp NULL,
    CreatedAt timestamp NOT NULL DEFAULT NOW(),
    UpdatedAt timestamp NULL
);

-- Índices para performance
CREATE INDEX idx_users_keycloak_id ON users.Users(KeycloakId);
CREATE INDEX idx_users_email ON users.Users(Email);
CREATE INDEX idx_users_username ON users.Users(Username);
CREATE INDEX idx_users_deleted ON users.Users(IsDeleted, DeletedAt);

Configuração Entity Framework

public class UserConfiguration : IEntityTypeConfiguration<User>
{
    public void Configure(EntityTypeBuilder<User> builder)
    {
        builder.ToTable("Users", "users");

        builder.HasKey(u => u.Id);
        builder.Property(u => u.Id)
            .HasConversion(id => id.Value, value => new UserId(value));

        builder.Property(u => u.KeycloakId)
            .HasMaxLength(255)
            .IsRequired();

        builder.Property(u => u.Username)
            .HasMaxLength(100)
            .IsRequired();

        builder.Property(u => u.Email)
            .HasMaxLength(255)
            .IsRequired();

        // Índices únicos
        builder.HasIndex(u => u.KeycloakId).IsUnique();
        builder.HasIndex(u => u.Username).IsUnique();
        builder.HasIndex(u => u.Email).IsUnique();

        // Soft delete
        builder.HasIndex(u => new { u.IsDeleted, u.DeletedAt });
    }
}

🧪 Estratégia de Testes

Cobertura Completa

  • Testes Unitários: Domain, Application, Infrastructure
  • Testes de Integração: API endpoints completos
  • Testes de Module API: Comunicação entre módulos
  • Testes Arquiteturais: Validação de dependências

Padrões de Teste

[Trait("Category", "Unit")]
[Trait("Module", "Users")]
[Trait("Layer", "Domain")]
public class UserTests
{
    [Fact]
    public void Constructor_WithValidParameters_ShouldCreateUser()
    {
        // Arrange
        var keycloakId = "keycloak-123";
        var username = "testuser";
        var email = "test@example.com";
        var firstName = "Test";
        var lastName = "User";

        // Act
        var user = new User(keycloakId, username, email, firstName, lastName);

        // Assert
        user.KeycloakId.Should().Be(keycloakId);
        user.Username.Should().Be(username);
        user.Email.Should().Be(email);
        user.FirstName.Should().Be(firstName);
        user.LastName.Should().Be(lastName);
        user.IsDeleted.Should().BeFalse();

        // Verifica evento de domínio
        user.DomainEvents.Should().ContainSingle()
            .Which.Should().BeOfType<UserRegisteredDomainEvent>();
    }
}

📊 Métricas e Observabilidade

Logs Estruturados

[LoggerMessage(
    EventId = 1001,
    Level = LogLevel.Information,
    Message = "User {UserId} registered successfully (Email: {Email}, Username: {Username})")]
public static partial void UserRegistered(
    this ILogger logger, Guid userId, string email, string username);

[LoggerMessage(
    EventId = 1002,
    Level = LogLevel.Warning,
    Message = "Duplicate user registration attempt (KeycloakId: {KeycloakId})")]
public static partial void DuplicateUserRegistration(
    this ILogger logger, string keycloakId);

Métricas Customizadas

  • user_registrations_total: Total de registros
  • user_authentication_duration_ms: Tempo de autenticação
  • active_users_total: Usuários ativos
  • keycloak_sync_operations_total: Operações de sincronização

🔧 Configuração

Registro no DI Container

public static class UsersModuleServiceCollectionExtensions
{
    public static IServiceCollection AddUsersModule(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        // DbContext
        services.AddDbContext<UsersDbContext>(options =>
            options.UseNpgsql(configuration.GetConnectionString("Users")));

        // Repositórios
        services.AddScoped<IUsersRepository, UsersRepository>();

        // Domain Services
        services.AddScoped<IKeycloakUserDomainService, KeycloakUserDomainService>();

        // Handlers CQRS
        services.AddMediatR(cfg => 
            cfg.RegisterServicesFromAssembly(typeof(RegisterUserCommandHandler).Assembly));

        // Validators
        services.AddValidatorsFromAssembly(typeof(RegisterUserCommandValidator).Assembly);

        // Module API
        services.AddScoped<IUsersModuleApi, UsersModuleApi>();

        // Keycloak integration
        services.Configure<KeycloakOptions>(configuration.GetSection("Keycloak"));
        services.AddHttpClient<IKeycloakService, KeycloakService>();

        return services;
    }
}

Configuração do Keycloak

{
  "Keycloak": {
    "Authority": "https://keycloak.exemplo.com/realms/meajudaai",
    "ClientId": "meajudaai-api",
    "ClientSecret": "client-secret",
    "RequireHttpsMetadata": true,
    "ValidateAudience": true,
    "ValidateIssuer": true,
    "ClockSkew": "00:05:00"
  }
}

🔗 Integração com Outros Módulos

Módulo Providers

O módulo Users fornece a base de identidade para prestadores de serviços:

// Providers referencia Users via UserId
public class Provider : AggregateRoot<ProviderId>
{
    public Guid UserId { get; private set; }  // Referência ao User
    // ... outros dados específicos do provider
}

Comunicação via Module API

// Exemplo de uso em outro módulo
public class SomeOtherModuleService
{
    private readonly IUsersModuleApi _usersApi;

    public async Task<Result> ProcessUserData(Guid userId)
    {
        var userResult = await _usersApi.GetUserByIdAsync(userId);
        if (userResult.IsFailure)
            return Result.Failure("User not found");

        var user = userResult.Value;
        // Processar dados do usuário...
    }
}

🚀 Próximos Passos

Funcionalidades Futuras: Consulte o Roadmap do Projeto para ver as funcionalidades planejadas para versões futuras do módulo Users.

Melhorias Técnicas em Desenvolvimento

  • 🔄 Cache distribuído para consultas frequentes
  • 🔄 Event Sourcing para auditoria completa
  • 🔄 Background sync com Keycloak
  • 🔄 Bulk operations para gestão em massa

📚 Referências


📅 Última atualização: Novembro 2025
✨ Documentação mantida pela equipe de desenvolvimento MeAjudaAi