using line_gestao_api.Data; using line_gestao_api.Dtos; using line_gestao_api.Models; using line_gestao_api.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System.Globalization; using System.Text; namespace line_gestao_api.Controllers; [ApiController] [Route("api/system/tenants/{tenantId:guid}/users")] [Authorize(Policy = "SystemAdmin")] public class SystemTenantUsersController : ControllerBase { private readonly AppDbContext _db; private readonly UserManager _userManager; private readonly RoleManager> _roleManager; private readonly ISystemAuditService _systemAuditService; public SystemTenantUsersController( AppDbContext db, UserManager userManager, RoleManager> roleManager, ISystemAuditService systemAuditService) { _db = db; _userManager = userManager; _roleManager = roleManager; _systemAuditService = systemAuditService; } [HttpPost] public async Task> CreateUserForTenant( [FromRoute] Guid tenantId, [FromBody] CreateSystemTenantUserRequest request) { if (tenantId == Guid.Empty) { return await RejectAsync(tenantId, StatusCodes.Status400BadRequest, "TenantId inválido.", "invalid_tenant_id"); } var tenant = await _db.Tenants.AsNoTracking().FirstOrDefaultAsync(t => t.Id == tenantId); if (tenant == null) { return await RejectAsync(tenantId, StatusCodes.Status404NotFound, "Tenant não encontrado.", "tenant_not_found"); } if (string.IsNullOrWhiteSpace(request.Email)) { return await RejectAsync(tenantId, StatusCodes.Status400BadRequest, "Email é obrigatório.", "missing_email"); } if (string.IsNullOrWhiteSpace(request.Password)) { return await RejectAsync(tenantId, StatusCodes.Status400BadRequest, "Password é obrigatória.", "missing_password"); } if (request.Roles == null || request.Roles.Count == 0) { return await RejectAsync(tenantId, StatusCodes.Status400BadRequest, "Informe ao menos uma role.", "missing_roles"); } var normalizedRoles = request.Roles .Where(r => !string.IsNullOrWhiteSpace(r)) .Select(r => r.Trim().ToLowerInvariant()) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); if (normalizedRoles.Count == 0) { return await RejectAsync(tenantId, StatusCodes.Status400BadRequest, "Roles inválidas.", "invalid_roles"); } var unsupportedRoles = normalizedRoles .Where(role => !AppRoles.All.Contains(role, StringComparer.OrdinalIgnoreCase)) .ToList(); if (unsupportedRoles.Count > 0) { return await RejectAsync( tenantId, StatusCodes.Status400BadRequest, $"Roles não suportadas: {string.Join(", ", unsupportedRoles)}. Use apenas: {string.Join(", ", AppRoles.All)}.", "unsupported_roles"); } if (request.ClientCredentialsOnly) { if (tenant.IsSystem) { return await RejectAsync( tenantId, StatusCodes.Status400BadRequest, "Credenciais de cliente não podem ser criadas no SystemTenant.", "invalid_client_credentials_on_system_tenant"); } if (normalizedRoles.Count != 1 || !normalizedRoles.Contains(AppRoles.Cliente, StringComparer.OrdinalIgnoreCase)) { return await RejectAsync( tenantId, StatusCodes.Status400BadRequest, "Neste fluxo, somente a role cliente é permitida.", "invalid_roles_for_client_credentials_flow"); } } if (!tenant.IsSystem && normalizedRoles.Contains(SystemTenantConstants.SystemRole)) { return await RejectAsync( tenantId, StatusCodes.Status400BadRequest, "A role sysadmin só pode ser usada no SystemTenant.", "invalid_sysadmin_outside_system_tenant"); } if (tenant.IsSystem && normalizedRoles.Any(r => r != SystemTenantConstants.SystemRole)) { return await RejectAsync( tenantId, StatusCodes.Status400BadRequest, "No SystemTenant é permitido apenas a role sysadmin.", "invalid_non_system_role_for_system_tenant"); } foreach (var role in normalizedRoles) { if (!await _roleManager.RoleExistsAsync(role)) { return await RejectAsync( tenantId, StatusCodes.Status400BadRequest, $"Role inexistente: {role}", "role_not_found"); } } var email = request.Email.Trim().ToLowerInvariant(); var normalizedEmail = _userManager.NormalizeEmail(email); var alreadyExists = await _userManager.Users .IgnoreQueryFilters() .AnyAsync(u => u.TenantId == tenantId && u.NormalizedEmail == normalizedEmail); if (alreadyExists) { return await RejectAsync(tenantId, StatusCodes.Status409Conflict, "Já existe usuário com este email neste tenant.", "email_exists"); } var name = string.IsNullOrWhiteSpace(request.Name) ? email : request.Name.Trim(); var user = new ApplicationUser { Name = name, Email = email, UserName = email, TenantId = tenantId, EmailConfirmed = true, IsActive = true, LockoutEnabled = true }; IdentityResult createResult; try { createResult = await _userManager.CreateAsync(user, request.Password); } catch (DbUpdateException) { return await RejectAsync( tenantId, StatusCodes.Status409Conflict, "Não foi possível criar usuário. Email/username já em uso.", "db_conflict"); } if (!createResult.Succeeded) { await _systemAuditService.LogAsync( action: SystemAuditActions.CreateTenantUserRejected, targetTenantId: tenantId, metadata: new { reason = "identity_create_failed", email, errors = createResult.Errors.Select(e => e.Description).ToList() }); return BadRequest(createResult.Errors.Select(e => e.Description).ToList()); } var addRolesResult = await _userManager.AddToRolesAsync(user, normalizedRoles); if (!addRolesResult.Succeeded) { await _userManager.DeleteAsync(user); await _systemAuditService.LogAsync( action: SystemAuditActions.CreateTenantUserRejected, targetTenantId: tenantId, metadata: new { reason = "identity_add_roles_failed", email, roles = normalizedRoles, errors = addRolesResult.Errors.Select(e => e.Description).ToList() }); return BadRequest(addRolesResult.Errors.Select(e => e.Description).ToList()); } var linesReassigned = 0; if (request.ClientCredentialsOnly) { linesReassigned = await RebindMobileLinesToTenantBySourceKeyAsync(tenant); } await _systemAuditService.LogAsync( action: SystemAuditActions.CreateTenantUser, targetTenantId: tenantId, metadata: new { createdUserId = user.Id, email, roles = normalizedRoles, linesReassigned }); var response = new SystemTenantUserCreatedDto { UserId = user.Id, TenantId = tenantId, Email = email, Roles = normalizedRoles }; return StatusCode(StatusCodes.Status201Created, response); } private async Task> RejectAsync( Guid targetTenantId, int statusCode, string message, string reason) { await _systemAuditService.LogAsync( action: SystemAuditActions.CreateTenantUserRejected, targetTenantId: targetTenantId, metadata: new { reason, message }); return StatusCode(statusCode, message); } private async Task RebindMobileLinesToTenantBySourceKeyAsync(Tenant tenant) { if (tenant.Id == Guid.Empty) { return 0; } if (!string.Equals( tenant.SourceType, SystemTenantConstants.MobileLinesClienteSourceType, StringComparison.OrdinalIgnoreCase)) { return 0; } var normalizedKeys = new HashSet(StringComparer.Ordinal); AddNormalizedTenantKey(normalizedKeys, tenant.SourceKey); AddNormalizedTenantKey(normalizedKeys, tenant.NomeOficial); if (normalizedKeys.Count == 0) { return 0; } var candidates = await _db.MobileLines .IgnoreQueryFilters() .Where(x => x.TenantId != tenant.Id) .Where(x => x.Cliente != null && x.Cliente != string.Empty) .ToListAsync(); if (candidates.Count == 0) { return 0; } var hasAnyTenantLine = await _db.MobileLines .IgnoreQueryFilters() .AsNoTracking() .AnyAsync(x => x.TenantId == tenant.Id); var now = DateTime.UtcNow; var reassigned = ReassignByMatcher( candidates, normalizedKeys, tenant.Id, now, isRelaxedMatch: false); if (reassigned == 0 && !hasAnyTenantLine) { reassigned = ReassignByMatcher( candidates, normalizedKeys, tenant.Id, now, isRelaxedMatch: true); } if (reassigned > 0) { await _db.SaveChangesAsync(); } return reassigned; } private static int ReassignByMatcher( IReadOnlyList candidates, IReadOnlyCollection normalizedKeys, Guid tenantId, DateTime now, bool isRelaxedMatch) { if (normalizedKeys.Count == 0) { return 0; } var keys = isRelaxedMatch ? normalizedKeys.Where(k => k.Length >= 6).ToList() : normalizedKeys.ToList(); if (keys.Count == 0) { return 0; } var reassigned = 0; foreach (var line in candidates) { var normalizedClient = NormalizeTenantKey(line.Cliente ?? string.Empty); if (string.IsNullOrWhiteSpace(normalizedClient) || string.Equals(normalizedClient, "RESERVA", StringComparison.Ordinal)) { continue; } var matches = !isRelaxedMatch ? keys.Contains(normalizedClient, StringComparer.Ordinal) : keys.Any(k => normalizedClient.Contains(k, StringComparison.Ordinal) || k.Contains(normalizedClient, StringComparison.Ordinal)); if (!matches) { continue; } line.TenantId = tenantId; line.UpdatedAt = now; reassigned++; } return reassigned; } private static void AddNormalizedTenantKey(ISet keys, string? rawKey) { var normalized = NormalizeTenantKey(rawKey ?? string.Empty); if (string.IsNullOrWhiteSpace(normalized)) { return; } if (string.Equals(normalized, "RESERVA", StringComparison.Ordinal)) { return; } keys.Add(normalized); } private static string NormalizeTenantKey(string value) { if (string.IsNullOrWhiteSpace(value)) { return string.Empty; } var normalized = value.Trim().Normalize(NormalizationForm.FormD); var sb = new StringBuilder(normalized.Length); var previousWasSpace = false; foreach (var ch in normalized) { var category = CharUnicodeInfo.GetUnicodeCategory(ch); if (category == UnicodeCategory.NonSpacingMark) { continue; } if (char.IsWhiteSpace(ch)) { if (!previousWasSpace) { sb.Append(' '); previousWasSpace = true; } continue; } sb.Append(char.ToUpperInvariant(ch)); previousWasSpace = false; } return sb.ToString().Trim(); } }