line-gestao-api/Controllers/AuthController.cs

215 lines
6.9 KiB
C#

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<ApplicationUser> _userManager;
private readonly AppDbContext _db;
private readonly ITenantProvider _tenantProvider;
private readonly IConfiguration _config;
public AuthController(
UserManager<ApplicationUser> userManager,
AppDbContext db,
ITenantProvider tenantProvider,
IConfiguration config)
{
_userManager = userManager;
_db = db;
_tenantProvider = tenantProvider;
_config = config;
}
[HttpPost("register")]
[Authorize(Roles = "admin")]
public async Task<IActionResult> 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, "leitura");
var effectiveTenantId = await EnsureValidTenantIdAsync(user);
if (!effectiveTenantId.HasValue)
return Unauthorized("Tenant inválido.");
var token = await GenerateJwtAsync(user, effectiveTenantId.Value);
return Ok(new AuthResponse(token));
}
[HttpPost("login")]
[EnableRateLimiting("auth-login")]
public async Task<IActionResult> 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.");
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<string> 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<Claim>
{
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<Guid?> EnsureValidTenantIdAsync(ApplicationUser user)
{
if (user.TenantId != Guid.Empty)
return user.TenantId;
var fallbackTenantId = await _db.Tenants
.AsNoTracking()
.OrderBy(t => t.CreatedAt)
.Select(t => (Guid?)t.Id)
.FirstOrDefaultAsync();
if (!fallbackTenantId.HasValue || fallbackTenantId.Value == Guid.Empty)
return null;
user.TenantId = fallbackTenantId.Value;
var updateResult = await _userManager.UpdateAsync(user);
if (!updateResult.Succeeded)
return null;
return user.TenantId;
}
}