line-gestao-api/Controllers/SystemTenantUsersController.cs

425 lines
13 KiB
C#

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<ApplicationUser> _userManager;
private readonly RoleManager<IdentityRole<Guid>> _roleManager;
private readonly ISystemAuditService _systemAuditService;
public SystemTenantUsersController(
AppDbContext db,
UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole<Guid>> roleManager,
ISystemAuditService systemAuditService)
{
_db = db;
_userManager = userManager;
_roleManager = roleManager;
_systemAuditService = systemAuditService;
}
[HttpPost]
public async Task<ActionResult<SystemTenantUserCreatedDto>> 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<ActionResult<SystemTenantUserCreatedDto>> 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<int> 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<string>(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<MobileLine> candidates,
IReadOnlyCollection<string> 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<string> 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();
}
}