215 lines
6.9 KiB
C#
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;
|
|
}
|
|
}
|