diff --git a/Controllers/AuthController.cs b/Controllers/AuthController.cs index a4d2dc9..c5b3269 100644 --- a/Controllers/AuthController.cs +++ b/Controllers/AuthController.cs @@ -72,16 +72,34 @@ public class AuthController : ControllerBase [HttpPost("login")] public async Task Login(LoginRequest req) { - // ✅ normaliza e evita null var email = (req.Email ?? "").Trim().ToLowerInvariant(); var password = req.Password ?? ""; var normalizedEmail = _userManager.NormalizeEmail(email); - // ✅ SOLUÇÃO A: ignora filtros globais (multi-tenant / HasQueryFilter) - // e pega 1 usuário (pra você logar logo). - var user = await _userManager.Users - .IgnoreQueryFilters() - .FirstOrDefaultAsync(u => u.NormalizedEmail == normalizedEmail); + 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."); @@ -97,6 +115,15 @@ public class AuthController : ControllerBase 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) { var key = _config["Jwt:Key"]!; diff --git a/Controllers/UsersController.cs b/Controllers/UsersController.cs index ee908e2..3af3ce0 100644 --- a/Controllers/UsersController.cs +++ b/Controllers/UsersController.cs @@ -14,6 +14,12 @@ namespace line_gestao_api.Controllers; [Authorize] public class UsersController : ControllerBase { + private static readonly HashSet AllowedRoles = new(StringComparer.OrdinalIgnoreCase) + { + "admin", + "gestor" + }; + private readonly AppDbContext _db; private readonly UserManager _userManager; private readonly RoleManager> _roleManager; @@ -65,7 +71,7 @@ public class UsersController : ControllerBase } var role = req.Permissao.Trim().ToLowerInvariant(); - if (!await _roleManager.RoleExistsAsync(role)) + if (!AllowedRoles.Contains(role) || !await _roleManager.RoleExistsAsync(role)) { return BadRequest(new ValidationErrorResponse { @@ -211,12 +217,33 @@ public class UsersController : ControllerBase [Authorize(Roles = "admin")] public async Task Update(Guid id, [FromBody] UserUpdateRequest req) { + var errors = await ValidateUpdateAsync(id, req); + if (errors.Count > 0) + { + return BadRequest(new ValidationErrorResponse { Errors = errors }); + } + var user = await _userManager.Users.FirstOrDefaultAsync(u => u.Id == id); if (user == null) { return NotFound(); } + if (!string.IsNullOrWhiteSpace(req.Nome)) + { + user.Name = req.Nome.Trim(); + } + + if (!string.IsNullOrWhiteSpace(req.Email)) + { + var email = req.Email.Trim().ToLowerInvariant(); + if (!string.Equals(user.Email, email, StringComparison.OrdinalIgnoreCase)) + { + await _userManager.SetEmailAsync(user, email); + await _userManager.SetUserNameAsync(user, email); + } + } + if (req.Ativo.HasValue) { user.IsActive = req.Ativo.Value; @@ -225,7 +252,7 @@ public class UsersController : ControllerBase if (!string.IsNullOrWhiteSpace(req.Permissao)) { var roleName = req.Permissao.Trim().ToLowerInvariant(); - if (!await _roleManager.RoleExistsAsync(roleName)) + if (!AllowedRoles.Contains(roleName) || !await _roleManager.RoleExistsAsync(roleName)) { return BadRequest(new ValidationErrorResponse { @@ -245,6 +272,23 @@ public class UsersController : ControllerBase await _userManager.AddToRoleAsync(user, roleName); } + if (!string.IsNullOrWhiteSpace(req.Senha)) + { + var token = await _userManager.GeneratePasswordResetTokenAsync(user); + var resetResult = await _userManager.ResetPasswordAsync(user, token, req.Senha); + if (!resetResult.Succeeded) + { + return BadRequest(new ValidationErrorResponse + { + Errors = resetResult.Errors.Select(e => new ValidationErrorDto + { + Field = "senha", + Message = e.Description + }).ToList() + }); + } + } + await _userManager.UpdateAsync(user); return NoContent(); } @@ -277,6 +321,64 @@ public class UsersController : ControllerBase { errors.Add(new ValidationErrorDto { Field = "permissao", Message = "Permissão é obrigatória." }); } + else if (!AllowedRoles.Contains(req.Permissao.Trim())) + { + errors.Add(new ValidationErrorDto { Field = "permissao", Message = "Permissão inválida." }); + } + + return errors; + } + + private async Task> ValidateUpdateAsync(Guid userId, UserUpdateRequest req) + { + var errors = new List(); + + if (!string.IsNullOrWhiteSpace(req.Nome) && req.Nome.Trim().Length < 2) + { + errors.Add(new ValidationErrorDto { Field = "nome", Message = "Nome inválido." }); + } + + if (!string.IsNullOrWhiteSpace(req.Email)) + { + var email = req.Email.Trim().ToLowerInvariant(); + var normalized = _userManager.NormalizeEmail(email); + + var tenantId = _tenantProvider.TenantId; + if (tenantId == null) + { + errors.Add(new ValidationErrorDto { Field = "email", Message = "Tenant inválido." }); + } + else + { + var exists = await _userManager.Users.AnyAsync(u => + u.Id != userId && + u.TenantId == tenantId && + u.NormalizedEmail == normalized); + + if (exists) + { + errors.Add(new ValidationErrorDto { Field = "email", Message = "E-mail já cadastrado." }); + } + } + } + + if (!string.IsNullOrWhiteSpace(req.Senha)) + { + if (req.Senha.Length < 6) + { + errors.Add(new ValidationErrorDto { Field = "senha", Message = "Senha deve ter pelo menos 6 caracteres." }); + } + + if (req.Senha != req.ConfirmarSenha) + { + errors.Add(new ValidationErrorDto { Field = "confirmarSenha", Message = "Confirmação de senha inválida." }); + } + } + + if (!string.IsNullOrWhiteSpace(req.Permissao) && !AllowedRoles.Contains(req.Permissao.Trim())) + { + errors.Add(new ValidationErrorDto { Field = "permissao", Message = "Permissão inválida." }); + } return errors; } diff --git a/Dtos/LoginRequest.cs b/Dtos/LoginRequest.cs index cd9cb75..1c48a61 100644 --- a/Dtos/LoginRequest.cs +++ b/Dtos/LoginRequest.cs @@ -1,3 +1,3 @@ namespace line_gestao_api.Dtos; -public record LoginRequest(string Email, string Password); +public record LoginRequest(string Email, string Password, Guid? TenantId = null); diff --git a/Dtos/UserDtos.cs b/Dtos/UserDtos.cs index 97edb4d..697fadc 100644 --- a/Dtos/UserDtos.cs +++ b/Dtos/UserDtos.cs @@ -11,6 +11,10 @@ public class UserCreateRequest public class UserUpdateRequest { + public string? Nome { get; set; } + public string? Email { get; set; } + public string? Senha { get; set; } + public string? ConfirmarSenha { get; set; } public string? Permissao { get; set; } public bool? Ativo { get; set; } } diff --git a/Services/TenantMiddleware.cs b/Services/TenantMiddleware.cs index 1806992..f978eed 100644 --- a/Services/TenantMiddleware.cs +++ b/Services/TenantMiddleware.cs @@ -16,11 +16,16 @@ public class TenantMiddleware Guid? tenantId = null; var claim = context.User.FindFirst("tenantId")?.Value ?? context.User.FindFirst("tenant")?.Value; + var headerValue = context.Request.Headers["X-Tenant-Id"].FirstOrDefault(); if (Guid.TryParse(claim, out var parsed)) { tenantId = parsed; } + else if (Guid.TryParse(headerValue, out var headerTenant)) + { + tenantId = headerTenant; + } tenantProvider.SetTenantId(tenantId);