using System.Globalization; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; 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.AspNetCore.RateLimiting; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; namespace line_gestao_api.Controllers; [ApiController] [Route("auth")] public class AuthController : ControllerBase { private readonly UserManager _userManager; private readonly AppDbContext _db; private readonly ITenantProvider _tenantProvider; private readonly IConfiguration _config; private readonly ILogger _logger; public AuthController( UserManager userManager, AppDbContext db, ITenantProvider tenantProvider, IConfiguration config, ILogger logger) { _userManager = userManager; _db = db; _tenantProvider = tenantProvider; _config = config; _logger = logger; } [HttpPost("register")] [Authorize(Roles = "sysadmin")] public async Task Register(RegisterRequest req) { if (req.Password != req.ConfirmPassword) return BadRequest("As senhas não conferem."); var tenantId = _tenantProvider.TenantId; if (tenantId == null || tenantId == Guid.Empty) return Unauthorized("Tenant inválido."); var email = req.Email.Trim().ToLowerInvariant(); var normalizedEmail = _userManager.NormalizeEmail(email); var exists = await _userManager.Users .AnyAsync(u => u.NormalizedEmail == normalizedEmail && u.TenantId == tenantId); if (exists) return BadRequest("E-mail já cadastrado."); var user = new ApplicationUser { Name = req.Name.Trim(), Email = email, UserName = email, TenantId = tenantId.Value, IsActive = true, EmailConfirmed = true, LockoutEnabled = true }; var createResult = await _userManager.CreateAsync(user, req.Password); if (!createResult.Succeeded) return BadRequest(createResult.Errors.Select(e => e.Description).ToList()); await _userManager.AddToRoleAsync(user, AppRoles.Cliente); var effectiveTenantId = await EnsureValidTenantIdAsync(user); if (!effectiveTenantId.HasValue) return Unauthorized("Tenant inválido."); await EnsureClientTenantDataBoundAsync(user, effectiveTenantId.Value); var token = await GenerateJwtAsync(user, effectiveTenantId.Value); return Ok(new AuthResponse(token)); } [HttpPost("login")] [EnableRateLimiting("auth-login")] public async Task Login(LoginRequest req) { var email = (req.Email ?? "").Trim().ToLowerInvariant(); var password = req.Password ?? ""; var normalizedEmail = _userManager.NormalizeEmail(email); var tenantId = ResolveTenantId(req); ApplicationUser? user; if (tenantId == null) { var users = await _userManager.Users .IgnoreQueryFilters() .Where(u => u.NormalizedEmail == normalizedEmail) .ToListAsync(); if (users.Count == 0) return Unauthorized("Credenciais inválidas."); if (users.Count > 1) return BadRequest("Informe o tenant para realizar o login."); user = users[0]; } else { user = await _userManager.Users .IgnoreQueryFilters() .FirstOrDefaultAsync(u => u.NormalizedEmail == normalizedEmail && u.TenantId == tenantId); } if (user == null) return Unauthorized("Credenciais inválidas."); if (!user.IsActive) return Unauthorized("Usuário desativado."); if (await _userManager.IsLockedOutAsync(user)) return Unauthorized("Usuário temporariamente bloqueado por tentativas inválidas. Tente novamente em alguns minutos."); var valid = await _userManager.CheckPasswordAsync(user, password); if (!valid) { if (user.LockoutEnabled) { await _userManager.AccessFailedAsync(user); } return Unauthorized("Credenciais inválidas."); } if (user.LockoutEnabled) { await _userManager.ResetAccessFailedCountAsync(user); } var effectiveTenantId = await EnsureValidTenantIdAsync(user); if (!effectiveTenantId.HasValue) return Unauthorized("Tenant inválido."); await EnsureClientTenantDataBoundAsync(user, effectiveTenantId.Value); var token = await GenerateJwtAsync(user, effectiveTenantId.Value); return Ok(new AuthResponse(token)); } private Guid? ResolveTenantId(LoginRequest req) { if (req.TenantId.HasValue) return req.TenantId.Value; var headerValue = Request.Headers["X-Tenant-Id"].FirstOrDefault(); return Guid.TryParse(headerValue, out var parsed) ? parsed : null; } private async Task GenerateJwtAsync(ApplicationUser user, Guid tenantId) { var key = _config["Jwt:Key"]!; var issuer = _config["Jwt:Issuer"]!; var audience = _config["Jwt:Audience"]!; var expiresMinutes = int.Parse(_config["Jwt:ExpiresMinutes"]!); var roles = await _userManager.GetRolesAsync(user); var displayName = user.Name ?? user.Email ?? string.Empty; var userEmail = user.Email ?? string.Empty; var claims = new List { new(JwtRegisteredClaimNames.Sub, user.Id.ToString()), new(JwtRegisteredClaimNames.Email, userEmail), new("name", displayName), new("tenantId", tenantId.ToString()) }; claims.AddRange(roles.Select(r => new Claim(ClaimTypes.Role, r))); var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)); var creds = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( issuer: issuer, audience: audience, claims: claims, expires: DateTime.UtcNow.AddMinutes(expiresMinutes), signingCredentials: creds ); return new JwtSecurityTokenHandler().WriteToken(token); } private async Task EnsureValidTenantIdAsync(ApplicationUser user) { if (user.TenantId == Guid.Empty) { return null; } var existsAndActive = await _db.Tenants .AsNoTracking() .AnyAsync(t => t.Id == user.TenantId && t.Ativo); if (!existsAndActive) { return null; } return user.TenantId; } private async Task EnsureClientTenantDataBoundAsync(ApplicationUser user, Guid tenantId) { var roles = await _userManager.GetRolesAsync(user); if (!roles.Any(r => string.Equals(r, AppRoles.Cliente, StringComparison.OrdinalIgnoreCase))) { return; } try { await RebindMobileLinesToTenantBySourceKeyAsync(tenantId); } catch (Exception ex) { _logger.LogError(ex, "Falha ao sincronizar linhas para tenant {TenantId} durante login de cliente.", tenantId); } } private async Task RebindMobileLinesToTenantBySourceKeyAsync(Guid tenantId) { if (tenantId == Guid.Empty) { return 0; } var tenant = await _db.Tenants .AsNoTracking() .FirstOrDefaultAsync(t => t.Id == tenantId && t.Ativo && !t.IsSystem); if (tenant == null) { 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(); } }