425 lines
13 KiB
C#
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();
|
|
}
|
|
}
|