Compare commits
No commits in common. "8f0fa83b7882395ff244fd0f0538cf9fb57a79e4" and "0ab7fa955f3dc96faeaf20fbd3ab504ee5c782f2" have entirely different histories.
8f0fa83b78
...
0ab7fa955f
|
|
@ -36,7 +36,7 @@ public class AuthController : ControllerBase
|
|||
}
|
||||
|
||||
[HttpPost("register")]
|
||||
[Authorize(Roles = "sysadmin")]
|
||||
[Authorize(Roles = "admin")]
|
||||
public async Task<IActionResult> Register(RegisterRequest req)
|
||||
{
|
||||
if (req.Password != req.ConfirmPassword)
|
||||
|
|
@ -69,7 +69,7 @@ public class AuthController : ControllerBase
|
|||
if (!createResult.Succeeded)
|
||||
return BadRequest(createResult.Errors.Select(e => e.Description).ToList());
|
||||
|
||||
await _userManager.AddToRoleAsync(user, AppRoles.Cliente);
|
||||
await _userManager.AddToRoleAsync(user, "leitura");
|
||||
|
||||
var effectiveTenantId = await EnsureValidTenantIdAsync(user);
|
||||
if (!effectiveTenantId.HasValue)
|
||||
|
|
@ -192,19 +192,22 @@ public class AuthController : ControllerBase
|
|||
|
||||
private async Task<Guid?> EnsureValidTenantIdAsync(ApplicationUser user)
|
||||
{
|
||||
if (user.TenantId == Guid.Empty)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (user.TenantId != Guid.Empty)
|
||||
return user.TenantId;
|
||||
|
||||
var existsAndActive = await _db.Tenants
|
||||
var fallbackTenantId = await _db.Tenants
|
||||
.AsNoTracking()
|
||||
.AnyAsync(t => t.Id == user.TenantId && t.Ativo);
|
||||
.OrderBy(t => t.CreatedAt)
|
||||
.Select(t => (Guid?)t.Id)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (!existsAndActive)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ namespace line_gestao_api.Controllers
|
|||
{
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[Authorize(Roles = "admin")]
|
||||
public class BillingController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
|
@ -188,7 +188,7 @@ namespace line_gestao_api.Controllers
|
|||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[Authorize(Roles = "admin")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateBillingClientRequest req)
|
||||
{
|
||||
var x = await _db.BillingClients.FirstOrDefaultAsync(a => a.Id == id);
|
||||
|
|
@ -221,7 +221,7 @@ namespace line_gestao_api.Controllers
|
|||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[Authorize(Roles = "admin")]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
var x = await _db.BillingClients.FirstOrDefaultAsync(a => a.Id == id);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ namespace line_gestao_api.Controllers
|
|||
{
|
||||
[ApiController]
|
||||
[Route("api/chips-virgens")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[Authorize(Roles = "admin")]
|
||||
public class ChipsVirgensController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
|
@ -93,7 +93,7 @@ namespace line_gestao_api.Controllers
|
|||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[Authorize(Roles = "admin,gestor")]
|
||||
public async Task<ActionResult<ChipVirgemDetailDto>> Create([FromBody] CreateChipVirgemDto req)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
|
@ -122,7 +122,7 @@ namespace line_gestao_api.Controllers
|
|||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[Authorize(Roles = "admin")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateChipVirgemRequest req)
|
||||
{
|
||||
var x = await _db.ChipVirgemLines.FirstOrDefaultAsync(a => a.Id == id);
|
||||
|
|
@ -139,7 +139,7 @@ namespace line_gestao_api.Controllers
|
|||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[Authorize(Roles = "admin")]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
var x = await _db.ChipVirgemLines.FirstOrDefaultAsync(a => a.Id == id);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ namespace line_gestao_api.Controllers
|
|||
{
|
||||
[ApiController]
|
||||
[Route("api/controle-recebidos")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[Authorize(Roles = "admin")]
|
||||
public class ControleRecebidosController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
|
@ -138,7 +138,7 @@ namespace line_gestao_api.Controllers
|
|||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[Authorize(Roles = "admin,gestor")]
|
||||
public async Task<ActionResult<ControleRecebidoDetailDto>> Create([FromBody] CreateControleRecebidoDto req)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
|
@ -195,7 +195,7 @@ namespace line_gestao_api.Controllers
|
|||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[Authorize(Roles = "admin")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateControleRecebidoRequest req)
|
||||
{
|
||||
var x = await _db.ControleRecebidoLines.FirstOrDefaultAsync(a => a.Id == id);
|
||||
|
|
@ -223,7 +223,7 @@ namespace line_gestao_api.Controllers
|
|||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[Authorize(Roles = "admin")]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
var x = await _db.ControleRecebidoLines.FirstOrDefaultAsync(a => a.Id == id);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ namespace line_gestao_api.Controllers;
|
|||
|
||||
[ApiController]
|
||||
[Route("api/historico")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[Authorize(Roles = "admin")]
|
||||
public class HistoricoController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
|
|
|||
|
|
@ -686,7 +686,7 @@ namespace line_gestao_api.Controllers
|
|||
// ✅ 5. CREATE
|
||||
// ==========================================================
|
||||
[HttpPost]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[Authorize(Roles = "admin,gestor")]
|
||||
public async Task<ActionResult<MobileLineDetailDto>> Create([FromBody] CreateMobileLineDto req)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(req.Cliente))
|
||||
|
|
@ -789,7 +789,7 @@ namespace line_gestao_api.Controllers
|
|||
// ✅ 5.1. CREATE BATCH
|
||||
// ==========================================================
|
||||
[HttpPost("batch")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[Authorize(Roles = "admin,gestor")]
|
||||
public async Task<ActionResult<CreateMobileLinesBatchResultDto>> CreateBatch([FromBody] CreateMobileLinesBatchRequestDto req)
|
||||
{
|
||||
var requests = req?.Lines ?? new List<CreateMobileLineDto>();
|
||||
|
|
@ -945,7 +945,7 @@ namespace line_gestao_api.Controllers
|
|||
// ✅ 6. UPDATE
|
||||
// ==========================================================
|
||||
[HttpPut("{id:guid}")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[Authorize(Roles = "admin,gestor")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateMobileLineRequest req)
|
||||
{
|
||||
var x = await _db.MobileLines.FirstOrDefaultAsync(a => a.Id == id);
|
||||
|
|
@ -1018,7 +1018,7 @@ namespace line_gestao_api.Controllers
|
|||
// ✅ 7. DELETE
|
||||
// ==========================================================
|
||||
[HttpDelete("{id:guid}")]
|
||||
[Authorize(Roles = "sysadmin")]
|
||||
[Authorize(Roles = "admin")]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
var x = await _db.MobileLines.FirstOrDefaultAsync(a => a.Id == id);
|
||||
|
|
@ -1033,7 +1033,7 @@ namespace line_gestao_api.Controllers
|
|||
// ✅ 8. IMPORT EXCEL
|
||||
// ==========================================================
|
||||
[HttpPost("import-excel")]
|
||||
[Authorize(Roles = "sysadmin")]
|
||||
[Authorize(Roles = "admin")]
|
||||
[Consumes("multipart/form-data")]
|
||||
[RequestSizeLimit(50_000_000)]
|
||||
public async Task<ActionResult<ImportResultDto>> ImportExcel([FromForm] ImportExcelForm form)
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ namespace line_gestao_api.Controllers
|
|||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[Authorize(Roles = "admin,gestor")]
|
||||
public async Task<ActionResult<MuregDetailDto>> Create([FromBody] CreateMuregDto req)
|
||||
{
|
||||
if (req.MobileLineId == Guid.Empty)
|
||||
|
|
@ -289,7 +289,7 @@ namespace line_gestao_api.Controllers
|
|||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[Authorize(Roles = "admin,gestor")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateMuregDto req)
|
||||
{
|
||||
var entity = await _db.MuregLines.FirstOrDefaultAsync(x => x.Id == id);
|
||||
|
|
@ -361,7 +361,7 @@ namespace line_gestao_api.Controllers
|
|||
// Exclui registro MUREG
|
||||
// ==========================================================
|
||||
[HttpDelete("{id:guid}")]
|
||||
[Authorize(Roles = "sysadmin")]
|
||||
[Authorize(Roles = "admin")]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
var entity = await _db.MuregLines.FirstOrDefaultAsync(x => x.Id == id);
|
||||
|
|
@ -377,7 +377,7 @@ namespace line_gestao_api.Controllers
|
|||
// ✅ POST: /api/mureg/import-excel (mantido)
|
||||
// ==========================================================
|
||||
[HttpPost("import-excel")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[Authorize(Roles = "admin,gestor")]
|
||||
[Consumes("multipart/form-data")]
|
||||
[RequestSizeLimit(50_000_000)]
|
||||
public async Task<IActionResult> ImportExcel([FromForm] ImportExcelForm form)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ namespace line_gestao_api.Controllers;
|
|||
|
||||
[ApiController]
|
||||
[Route("api/parcelamentos")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[Authorize(Roles = "admin")]
|
||||
public class ParcelamentosController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
|
@ -165,7 +165,7 @@ public class ParcelamentosController : ControllerBase
|
|||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[Authorize(Roles = "admin,gestor")]
|
||||
public async Task<ActionResult<ParcelamentoDetailDto>> Create([FromBody] ParcelamentoUpsertDto req)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
|
@ -202,7 +202,7 @@ public class ParcelamentosController : ControllerBase
|
|||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[Authorize(Roles = "admin,gestor")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] ParcelamentoUpsertDto req)
|
||||
{
|
||||
var entity = await _db.ParcelamentoLines
|
||||
|
|
@ -239,7 +239,7 @@ public class ParcelamentosController : ControllerBase
|
|||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[Authorize(Roles = "admin")]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
var entity = await _db.ParcelamentoLines.FirstOrDefaultAsync(x => x.Id == id);
|
||||
|
|
|
|||
|
|
@ -1,226 +0,0 @@
|
|||
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;
|
||||
|
||||
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 (!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());
|
||||
}
|
||||
|
||||
await _systemAuditService.LogAsync(
|
||||
action: SystemAuditActions.CreateTenantUser,
|
||||
targetTenantId: tenantId,
|
||||
metadata: new
|
||||
{
|
||||
createdUserId = user.Id,
|
||||
email,
|
||||
roles = normalizedRoles
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
using line_gestao_api.Data;
|
||||
using line_gestao_api.Dtos;
|
||||
using line_gestao_api.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace line_gestao_api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/system/tenants")]
|
||||
[Authorize(Policy = "SystemAdmin")]
|
||||
public class SystemTenantsController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly ISystemAuditService _systemAuditService;
|
||||
|
||||
public SystemTenantsController(AppDbContext db, ISystemAuditService systemAuditService)
|
||||
{
|
||||
_db = db;
|
||||
_systemAuditService = systemAuditService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IReadOnlyList<SystemTenantListItemDto>>> GetTenants(
|
||||
[FromQuery] string source = SystemTenantConstants.MobileLinesClienteSourceType,
|
||||
[FromQuery] bool active = true)
|
||||
{
|
||||
var query = _db.Tenants
|
||||
.AsNoTracking()
|
||||
.Where(t => !t.IsSystem);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(source))
|
||||
{
|
||||
query = query.Where(t => t.SourceType == source);
|
||||
}
|
||||
|
||||
query = query.Where(t => t.Ativo == active);
|
||||
|
||||
var tenants = await query
|
||||
.OrderBy(t => t.NomeOficial)
|
||||
.Select(t => new SystemTenantListItemDto
|
||||
{
|
||||
TenantId = t.Id,
|
||||
NomeOficial = t.NomeOficial
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
await _systemAuditService.LogAsync(
|
||||
action: SystemAuditActions.ListTenants,
|
||||
targetTenantId: SystemTenantConstants.SystemTenantId,
|
||||
metadata: new { source, active, returnedCount = tenants.Count });
|
||||
|
||||
return Ok(tenants);
|
||||
}
|
||||
}
|
||||
|
|
@ -112,7 +112,7 @@ namespace line_gestao_api.Controllers
|
|||
// ✅ CREATE
|
||||
// ==========================================================
|
||||
[HttpPost]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[Authorize(Roles = "admin,gestor")]
|
||||
public async Task<ActionResult<TrocaNumeroDetailDto>> Create([FromBody] CreateTrocaNumeroDto req)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
|
@ -141,7 +141,7 @@ namespace line_gestao_api.Controllers
|
|||
// ✅ UPDATE
|
||||
// ==========================================================
|
||||
[HttpPut("{id:guid}")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[Authorize(Roles = "admin,gestor")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateTrocaNumeroRequest req)
|
||||
{
|
||||
var x = await _db.TrocaNumeroLines.FirstOrDefaultAsync(a => a.Id == id);
|
||||
|
|
@ -167,7 +167,7 @@ namespace line_gestao_api.Controllers
|
|||
// ✅ DELETE
|
||||
// ==========================================================
|
||||
[HttpDelete("{id:guid}")]
|
||||
[Authorize(Roles = "sysadmin")]
|
||||
[Authorize(Roles = "admin")]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
var x = await _db.TrocaNumeroLines.FirstOrDefaultAsync(a => a.Id == id);
|
||||
|
|
|
|||
|
|
@ -261,7 +261,7 @@ namespace line_gestao_api.Controllers
|
|||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(Roles = "sysadmin")]
|
||||
[Authorize(Roles = "admin")]
|
||||
public async Task<ActionResult<UserDataDetailDto>> Create([FromBody] CreateUserDataRequest req)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
|
@ -363,7 +363,7 @@ namespace line_gestao_api.Controllers
|
|||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[Authorize(Roles = "sysadmin")]
|
||||
[Authorize(Roles = "admin")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateUserDataRequest req)
|
||||
{
|
||||
var x = await _db.UserDatas.FirstOrDefaultAsync(a => a.Id == id);
|
||||
|
|
@ -395,7 +395,7 @@ namespace line_gestao_api.Controllers
|
|||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
[Authorize(Roles = "sysadmin")]
|
||||
[Authorize(Roles = "admin")]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
var x = await _db.UserDatas.FirstOrDefaultAsync(a => a.Id == id);
|
||||
|
|
|
|||
|
|
@ -17,9 +17,8 @@ public class UsersController : ControllerBase
|
|||
{
|
||||
private static readonly HashSet<string> AllowedRoles = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
AppRoles.SysAdmin,
|
||||
AppRoles.Gestor,
|
||||
AppRoles.Cliente
|
||||
"admin",
|
||||
"gestor"
|
||||
};
|
||||
|
||||
private readonly AppDbContext _db;
|
||||
|
|
@ -40,7 +39,7 @@ public class UsersController : ControllerBase
|
|||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(Roles = "sysadmin")]
|
||||
[Authorize(Roles = "admin")]
|
||||
public async Task<ActionResult<UserListItemDto>> Create([FromBody] UserCreateRequest req)
|
||||
{
|
||||
var errors = ValidateCreate(req);
|
||||
|
|
@ -123,7 +122,7 @@ public class UsersController : ControllerBase
|
|||
}
|
||||
|
||||
[HttpGet]
|
||||
[Authorize(Roles = "sysadmin")]
|
||||
[Authorize(Roles = "admin")]
|
||||
public async Task<ActionResult<PagedResult<UserListItemDto>>> GetAll(
|
||||
[FromQuery] string? search,
|
||||
[FromQuery] string? permissao,
|
||||
|
|
@ -192,7 +191,7 @@ public class UsersController : ControllerBase
|
|||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
[Authorize(Roles = "sysadmin")]
|
||||
[Authorize(Roles = "admin")]
|
||||
public async Task<ActionResult<UserListItemDto>> GetById(Guid id)
|
||||
{
|
||||
var user = await _userManager.Users.AsNoTracking().FirstOrDefaultAsync(u => u.Id == id);
|
||||
|
|
@ -216,7 +215,7 @@ public class UsersController : ControllerBase
|
|||
}
|
||||
|
||||
[HttpPatch("{id:guid}")]
|
||||
[Authorize(Roles = "sysadmin")]
|
||||
[Authorize(Roles = "admin")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] UserUpdateRequest req)
|
||||
{
|
||||
var errors = await ValidateUpdateAsync(id, req);
|
||||
|
|
@ -296,7 +295,7 @@ public class UsersController : ControllerBase
|
|||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
[Authorize(Roles = "sysadmin")]
|
||||
[Authorize(Roles = "admin")]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
if (_tenantProvider.TenantId == null)
|
||||
|
|
@ -335,12 +334,12 @@ public class UsersController : ControllerBase
|
|||
}
|
||||
|
||||
var targetRoles = await _userManager.GetRolesAsync(user);
|
||||
var isAdmin = targetRoles.Any(r => string.Equals(r, AppRoles.SysAdmin, StringComparison.OrdinalIgnoreCase));
|
||||
var isAdmin = targetRoles.Any(r => string.Equals(r, "admin", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (isAdmin)
|
||||
{
|
||||
var adminRoleId = await _roleManager.Roles
|
||||
.Where(r => r.Name == AppRoles.SysAdmin)
|
||||
.Where(r => r.Name == "admin")
|
||||
.Select(r => (Guid?)r.Id)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
|
|
@ -361,7 +360,7 @@ public class UsersController : ControllerBase
|
|||
{
|
||||
Errors = new List<ValidationErrorDto>
|
||||
{
|
||||
new() { Field = "usuario", Message = "Não é permitido excluir o último sysadmin." }
|
||||
new() { Field = "usuario", Message = "Não é permitido excluir o último administrador." }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ namespace line_gestao_api.Controllers
|
|||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(Roles = "sysadmin")]
|
||||
[Authorize(Roles = "admin")]
|
||||
public async Task<ActionResult<VigenciaLineDetailDto>> Create([FromBody] CreateVigenciaRequest req)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
|
@ -340,7 +340,7 @@ namespace line_gestao_api.Controllers
|
|||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[Authorize(Roles = "sysadmin")]
|
||||
[Authorize(Roles = "admin")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateVigenciaRequest req)
|
||||
{
|
||||
var x = await _db.VigenciaLines.FirstOrDefaultAsync(a => a.Id == id);
|
||||
|
|
@ -366,7 +366,7 @@ namespace line_gestao_api.Controllers
|
|||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
[Authorize(Roles = "sysadmin")]
|
||||
[Authorize(Roles = "admin")]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
var x = await _db.VigenciaLines.FirstOrDefaultAsync(a => a.Id == id);
|
||||
|
|
|
|||
|
|
@ -78,15 +78,6 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
|
|||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.Entity<Tenant>(e =>
|
||||
{
|
||||
e.Property(x => x.NomeOficial).HasMaxLength(200);
|
||||
e.Property(x => x.SourceType).HasMaxLength(80);
|
||||
e.Property(x => x.SourceKey).HasMaxLength(300);
|
||||
e.HasIndex(x => new { x.SourceType, x.SourceKey }).IsUnique();
|
||||
e.HasIndex(x => new { x.IsSystem, x.Ativo });
|
||||
});
|
||||
|
||||
// =========================
|
||||
// ✅ USER (Identity)
|
||||
// =========================
|
||||
|
|
@ -279,7 +270,6 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
|
|||
// =========================
|
||||
modelBuilder.Entity<AuditLog>(e =>
|
||||
{
|
||||
e.Property(x => x.MetadataJson).HasColumnType("jsonb");
|
||||
e.Property(x => x.Action).HasMaxLength(20);
|
||||
e.Property(x => x.Page).HasMaxLength(80);
|
||||
e.Property(x => x.EntityName).HasMaxLength(120);
|
||||
|
|
@ -291,13 +281,8 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
|
|||
e.Property(x => x.RequestMethod).HasMaxLength(10);
|
||||
e.Property(x => x.IpAddress).HasMaxLength(80);
|
||||
e.Property(x => x.ChangesJson).HasColumnType("jsonb");
|
||||
e.Property(x => x.ActorTenantId);
|
||||
e.Property(x => x.TargetTenantId);
|
||||
|
||||
e.HasIndex(x => x.TenantId);
|
||||
e.HasIndex(x => x.ActorTenantId);
|
||||
e.HasIndex(x => x.TargetTenantId);
|
||||
e.HasIndex(x => x.ActorUserId);
|
||||
e.HasIndex(x => x.OccurredAtUtc);
|
||||
e.HasIndex(x => x.Page);
|
||||
e.HasIndex(x => x.UserId);
|
||||
|
|
@ -333,33 +318,33 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
|
|||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<MobileLine>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||
modelBuilder.Entity<MuregLine>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||
modelBuilder.Entity<BillingClient>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||
modelBuilder.Entity<UserData>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||
modelBuilder.Entity<VigenciaLine>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||
modelBuilder.Entity<TrocaNumeroLine>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||
modelBuilder.Entity<ChipVirgemLine>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||
modelBuilder.Entity<ControleRecebidoLine>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||
modelBuilder.Entity<Notification>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||
modelBuilder.Entity<ResumoMacrophonyPlan>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||
modelBuilder.Entity<ResumoMacrophonyTotal>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||
modelBuilder.Entity<ResumoVivoLineResumo>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||
modelBuilder.Entity<ResumoVivoLineTotal>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||
modelBuilder.Entity<ResumoClienteEspecial>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||
modelBuilder.Entity<ResumoPlanoContratoResumo>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||
modelBuilder.Entity<ResumoPlanoContratoTotal>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||
modelBuilder.Entity<ResumoLineTotais>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||
modelBuilder.Entity<ResumoGbDistribuicao>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||
modelBuilder.Entity<ResumoGbDistribuicaoTotal>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||
modelBuilder.Entity<ResumoReservaLine>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||
modelBuilder.Entity<ResumoReservaTotal>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||
modelBuilder.Entity<ParcelamentoLine>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||
modelBuilder.Entity<ParcelamentoMonthValue>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||
modelBuilder.Entity<AuditLog>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||
modelBuilder.Entity<ImportAuditRun>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||
modelBuilder.Entity<ImportAuditIssue>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||
modelBuilder.Entity<ApplicationUser>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||
modelBuilder.Entity<MobileLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
modelBuilder.Entity<MuregLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
modelBuilder.Entity<BillingClient>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
modelBuilder.Entity<UserData>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
modelBuilder.Entity<VigenciaLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
modelBuilder.Entity<TrocaNumeroLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
modelBuilder.Entity<ChipVirgemLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
modelBuilder.Entity<ControleRecebidoLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
modelBuilder.Entity<Notification>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
modelBuilder.Entity<ResumoMacrophonyPlan>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
modelBuilder.Entity<ResumoMacrophonyTotal>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
modelBuilder.Entity<ResumoVivoLineResumo>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
modelBuilder.Entity<ResumoVivoLineTotal>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
modelBuilder.Entity<ResumoClienteEspecial>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
modelBuilder.Entity<ResumoPlanoContratoResumo>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
modelBuilder.Entity<ResumoPlanoContratoTotal>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
modelBuilder.Entity<ResumoLineTotais>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
modelBuilder.Entity<ResumoGbDistribuicao>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
modelBuilder.Entity<ResumoGbDistribuicaoTotal>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
modelBuilder.Entity<ResumoReservaLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
modelBuilder.Entity<ResumoReservaTotal>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
modelBuilder.Entity<ParcelamentoLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
modelBuilder.Entity<ParcelamentoMonthValue>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
modelBuilder.Entity<AuditLog>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
modelBuilder.Entity<ImportAuditRun>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
modelBuilder.Entity<ImportAuditIssue>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
modelBuilder.Entity<ApplicationUser>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
}
|
||||
|
||||
public override int SaveChanges()
|
||||
|
|
@ -378,12 +363,12 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
|
|||
|
||||
private void ApplyTenantIds()
|
||||
{
|
||||
if (_tenantProvider.ActorTenantId == null)
|
||||
if (_tenantProvider.TenantId == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var tenantId = _tenantProvider.ActorTenantId.Value;
|
||||
var tenantId = _tenantProvider.TenantId.Value;
|
||||
foreach (var entry in ChangeTracker.Entries<ITenantEntity>().Where(e => e.State == EntityState.Added))
|
||||
{
|
||||
if (entry.Entity.TenantId == Guid.Empty)
|
||||
|
|
|
|||
385
Data/SeedData.cs
385
Data/SeedData.cs
|
|
@ -9,14 +9,17 @@ namespace line_gestao_api.Data;
|
|||
public class SeedOptions
|
||||
{
|
||||
public bool Enabled { get; set; } = true;
|
||||
public string AdminMasterName { get; set; } = "System Admin";
|
||||
public string? AdminMasterEmail { get; set; } = "sysadmin@linegestao.local";
|
||||
public string? AdminMasterPassword { get; set; } = "DevSysAdmin123!";
|
||||
public string DefaultTenantName { get; set; } = "Default";
|
||||
public string AdminName { get; set; } = "Administrador";
|
||||
public string AdminEmail { get; set; } = "admin@linegestao.local";
|
||||
public string AdminPassword { get; set; } = "DevAdmin123!";
|
||||
public bool ReapplyAdminCredentialsOnStartup { get; set; } = false;
|
||||
}
|
||||
|
||||
public static class SeedData
|
||||
{
|
||||
public static readonly Guid DefaultTenantId = Guid.Parse("11111111-1111-1111-1111-111111111111");
|
||||
|
||||
public static async Task EnsureSeedDataAsync(IServiceProvider services)
|
||||
{
|
||||
using var scope = services.CreateScope();
|
||||
|
|
@ -26,236 +29,224 @@ public static class SeedData
|
|||
var tenantProvider = scope.ServiceProvider.GetRequiredService<ITenantProvider>();
|
||||
var options = scope.ServiceProvider.GetRequiredService<IOptions<SeedOptions>>().Value;
|
||||
|
||||
if (db.Database.IsRelational())
|
||||
{
|
||||
await db.Database.MigrateAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
await db.Database.EnsureCreatedAsync();
|
||||
}
|
||||
await db.Database.MigrateAsync();
|
||||
|
||||
if (!options.Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var systemTenantId = SystemTenantConstants.SystemTenantId;
|
||||
var roles = AppRoles.All;
|
||||
var roles = new[] { "admin", "gestor", "operador", "leitura" };
|
||||
foreach (var role in roles)
|
||||
{
|
||||
if (!await roleManager.RoleExistsAsync(role))
|
||||
{
|
||||
var roleResult = await roleManager.CreateAsync(new IdentityRole<Guid>(role));
|
||||
EnsureIdentitySucceeded(roleResult, $"Falha ao criar role '{role}'.");
|
||||
await roleManager.CreateAsync(new IdentityRole<Guid>(role));
|
||||
}
|
||||
}
|
||||
|
||||
await MigrateLegacyRolesAsync(db, roleManager);
|
||||
|
||||
var systemTenant = await db.Tenants.FirstOrDefaultAsync(t => t.Id == systemTenantId);
|
||||
if (systemTenant == null)
|
||||
var tenant = await db.Tenants.FirstOrDefaultAsync(t => t.Id == DefaultTenantId);
|
||||
if (tenant == null)
|
||||
{
|
||||
systemTenant = new Tenant
|
||||
tenant = new Tenant
|
||||
{
|
||||
Id = systemTenantId,
|
||||
NomeOficial = SystemTenantConstants.SystemTenantNomeOficial,
|
||||
IsSystem = true,
|
||||
Ativo = true,
|
||||
Id = DefaultTenantId,
|
||||
Name = options.DefaultTenantName,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
db.Tenants.Add(systemTenant);
|
||||
}
|
||||
else
|
||||
{
|
||||
systemTenant.NomeOficial = SystemTenantConstants.SystemTenantNomeOficial;
|
||||
systemTenant.IsSystem = true;
|
||||
systemTenant.Ativo = true;
|
||||
|
||||
db.Tenants.Add(tenant);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
await NormalizeLegacyTenantDataAsync(db, tenant.Id);
|
||||
|
||||
var emailFromEnv = Environment.GetEnvironmentVariable("SYSADMIN_EMAIL")
|
||||
?? Environment.GetEnvironmentVariable("ADMIN_MASTER_EMAIL");
|
||||
var passwordFromEnv = Environment.GetEnvironmentVariable("SYSADMIN_PASSWORD")
|
||||
?? Environment.GetEnvironmentVariable("ADMIN_MASTER_PASSWORD");
|
||||
tenantProvider.SetTenantId(tenant.Id);
|
||||
|
||||
var adminMasterEmail = (emailFromEnv ?? options.AdminMasterEmail ?? string.Empty).Trim().ToLowerInvariant();
|
||||
var adminMasterPassword = passwordFromEnv ?? options.AdminMasterPassword ?? string.Empty;
|
||||
var normalizedEmail = userManager.NormalizeEmail(options.AdminEmail);
|
||||
var existingAdmin = await userManager.Users
|
||||
.FirstOrDefaultAsync(u => u.NormalizedEmail == normalizedEmail && u.TenantId == tenant.Id);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(adminMasterEmail) || string.IsNullOrWhiteSpace(adminMasterPassword))
|
||||
if (existingAdmin == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Credenciais do sysadmin ausentes. Defina SYSADMIN_EMAIL e SYSADMIN_PASSWORD (ou Seed:AdminMasterEmail/Seed:AdminMasterPassword).");
|
||||
}
|
||||
|
||||
var normalizedEmail = userManager.NormalizeEmail(adminMasterEmail);
|
||||
|
||||
var previousTenant = tenantProvider.TenantId;
|
||||
tenantProvider.SetTenantId(systemTenantId);
|
||||
|
||||
try
|
||||
{
|
||||
var existingAdminMaster = await userManager.Users
|
||||
.IgnoreQueryFilters()
|
||||
.FirstOrDefaultAsync(u => u.TenantId == systemTenantId && u.NormalizedEmail == normalizedEmail);
|
||||
|
||||
if (existingAdminMaster == null)
|
||||
var adminUser = new ApplicationUser
|
||||
{
|
||||
var adminMaster = new ApplicationUser
|
||||
{
|
||||
Name = options.AdminMasterName,
|
||||
Email = adminMasterEmail,
|
||||
UserName = adminMasterEmail,
|
||||
TenantId = systemTenantId,
|
||||
EmailConfirmed = true,
|
||||
IsActive = true,
|
||||
LockoutEnabled = true
|
||||
};
|
||||
UserName = options.AdminEmail,
|
||||
Email = options.AdminEmail,
|
||||
Name = options.AdminName,
|
||||
TenantId = tenant.Id,
|
||||
EmailConfirmed = true,
|
||||
IsActive = true,
|
||||
LockoutEnabled = true
|
||||
};
|
||||
|
||||
var createResult = await userManager.CreateAsync(adminMaster, adminMasterPassword);
|
||||
EnsureIdentitySucceeded(createResult, "Falha ao criar usuário sysadmin.");
|
||||
|
||||
var addRoleResult = await userManager.AddToRoleAsync(adminMaster, SystemTenantConstants.SystemRole);
|
||||
EnsureIdentitySucceeded(addRoleResult, "Falha ao associar role sysadmin ao usuário inicial.");
|
||||
var createResult = await userManager.CreateAsync(adminUser, options.AdminPassword);
|
||||
if (createResult.Succeeded)
|
||||
{
|
||||
await userManager.AddToRoleAsync(adminUser, "admin");
|
||||
}
|
||||
else
|
||||
}
|
||||
else if (options.ReapplyAdminCredentialsOnStartup)
|
||||
{
|
||||
existingAdmin.Name = options.AdminName;
|
||||
existingAdmin.Email = options.AdminEmail;
|
||||
existingAdmin.UserName = options.AdminEmail;
|
||||
existingAdmin.EmailConfirmed = true;
|
||||
existingAdmin.IsActive = true;
|
||||
existingAdmin.LockoutEnabled = true;
|
||||
|
||||
await userManager.SetLockoutEndDateAsync(existingAdmin, null);
|
||||
await userManager.ResetAccessFailedCountAsync(existingAdmin);
|
||||
await userManager.UpdateAsync(existingAdmin);
|
||||
|
||||
var resetToken = await userManager.GeneratePasswordResetTokenAsync(existingAdmin);
|
||||
var resetPasswordResult = await userManager.ResetPasswordAsync(existingAdmin, resetToken, options.AdminPassword);
|
||||
if (!resetPasswordResult.Succeeded)
|
||||
{
|
||||
existingAdminMaster.Name = options.AdminMasterName;
|
||||
existingAdminMaster.Email = adminMasterEmail;
|
||||
existingAdminMaster.UserName = adminMasterEmail;
|
||||
existingAdminMaster.EmailConfirmed = true;
|
||||
existingAdminMaster.IsActive = true;
|
||||
existingAdminMaster.LockoutEnabled = true;
|
||||
|
||||
var updateResult = await userManager.UpdateAsync(existingAdminMaster);
|
||||
EnsureIdentitySucceeded(updateResult, "Falha ao atualizar usuário sysadmin.");
|
||||
|
||||
if (options.ReapplyAdminCredentialsOnStartup)
|
||||
var removePasswordResult = await userManager.RemovePasswordAsync(existingAdmin);
|
||||
if (removePasswordResult.Succeeded)
|
||||
{
|
||||
await userManager.SetLockoutEndDateAsync(existingAdminMaster, null);
|
||||
await userManager.ResetAccessFailedCountAsync(existingAdminMaster);
|
||||
|
||||
var resetToken = await userManager.GeneratePasswordResetTokenAsync(existingAdminMaster);
|
||||
var resetPasswordResult = await userManager.ResetPasswordAsync(existingAdminMaster, resetToken, adminMasterPassword);
|
||||
if (!resetPasswordResult.Succeeded)
|
||||
{
|
||||
var removePasswordResult = await userManager.RemovePasswordAsync(existingAdminMaster);
|
||||
if (removePasswordResult.Succeeded)
|
||||
{
|
||||
var addPasswordResult = await userManager.AddPasswordAsync(existingAdminMaster, adminMasterPassword);
|
||||
EnsureIdentitySucceeded(addPasswordResult, "Falha ao definir senha do sysadmin.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var addPasswordResult = await userManager.AddPasswordAsync(existingAdminMaster, adminMasterPassword);
|
||||
EnsureIdentitySucceeded(addPasswordResult, "Falha ao definir senha do sysadmin.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!await userManager.IsInRoleAsync(existingAdminMaster, SystemTenantConstants.SystemRole))
|
||||
{
|
||||
var addRoleResult = await userManager.AddToRoleAsync(existingAdminMaster, SystemTenantConstants.SystemRole);
|
||||
EnsureIdentitySucceeded(addRoleResult, "Falha ao associar role sysadmin ao usuário inicial.");
|
||||
await userManager.AddPasswordAsync(existingAdmin, options.AdminPassword);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
tenantProvider.SetTenantId(previousTenant);
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureIdentitySucceeded(IdentityResult result, string message)
|
||||
{
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var details = string.Join("; ", result.Errors.Select(e => e.Description));
|
||||
throw new InvalidOperationException($"{message} Detalhes: {details}");
|
||||
}
|
||||
|
||||
private static async Task MigrateLegacyRolesAsync(AppDbContext db, RoleManager<IdentityRole<Guid>> roleManager)
|
||||
{
|
||||
await MigrateLegacyRoleAsync(db, roleManager, "admin_master", AppRoles.SysAdmin);
|
||||
await MigrateLegacyRoleAsync(db, roleManager, "admin", AppRoles.SysAdmin);
|
||||
await MigrateLegacyRoleAsync(db, roleManager, "leitura", AppRoles.Cliente);
|
||||
await MigrateLegacyRoleAsync(db, roleManager, "operador", AppRoles.Cliente);
|
||||
}
|
||||
|
||||
private static async Task MigrateLegacyRoleAsync(
|
||||
AppDbContext db,
|
||||
RoleManager<IdentityRole<Guid>> roleManager,
|
||||
string legacyRole,
|
||||
string newRole)
|
||||
{
|
||||
var legacyRoleId = await roleManager.Roles
|
||||
.Where(r => r.Name == legacyRole)
|
||||
.Select(r => (Guid?)r.Id)
|
||||
.FirstOrDefaultAsync();
|
||||
if (!legacyRoleId.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var newRoleId = await roleManager.Roles
|
||||
.Where(r => r.Name == newRole)
|
||||
.Select(r => (Guid?)r.Id)
|
||||
.FirstOrDefaultAsync();
|
||||
if (!newRoleId.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var legacyUserIds = await db.UserRoles
|
||||
.Where(ur => ur.RoleId == legacyRoleId.Value)
|
||||
.Select(ur => ur.UserId)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
if (legacyUserIds.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var alreadyInNewRole = await db.UserRoles
|
||||
.Where(ur => ur.RoleId == newRoleId.Value && legacyUserIds.Contains(ur.UserId))
|
||||
.Select(ur => ur.UserId)
|
||||
.ToListAsync();
|
||||
var existingSet = alreadyInNewRole.ToHashSet();
|
||||
|
||||
foreach (var userId in legacyUserIds)
|
||||
{
|
||||
if (!existingSet.Contains(userId))
|
||||
if (!await userManager.IsInRoleAsync(existingAdmin, "admin"))
|
||||
{
|
||||
db.UserRoles.Add(new IdentityUserRole<Guid>
|
||||
{
|
||||
UserId = userId,
|
||||
RoleId = newRoleId.Value
|
||||
});
|
||||
await userManager.AddToRoleAsync(existingAdmin, "admin");
|
||||
}
|
||||
}
|
||||
|
||||
var legacyAssignments = await db.UserRoles
|
||||
.Where(ur => ur.RoleId == legacyRoleId.Value)
|
||||
.ToListAsync();
|
||||
if (legacyAssignments.Count > 0)
|
||||
{
|
||||
db.UserRoles.RemoveRange(legacyAssignments);
|
||||
}
|
||||
tenantProvider.SetTenantId(null);
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
private static async Task NormalizeLegacyTenantDataAsync(AppDbContext db, Guid defaultTenantId)
|
||||
{
|
||||
if (defaultTenantId == Guid.Empty)
|
||||
return;
|
||||
|
||||
var legacyRoleStillUsed = await db.UserRoles.AnyAsync(ur => ur.RoleId == legacyRoleId.Value);
|
||||
if (!legacyRoleStillUsed)
|
||||
{
|
||||
var legacyRoleEntity = await roleManager.Roles.FirstOrDefaultAsync(r => r.Id == legacyRoleId.Value);
|
||||
if (legacyRoleEntity != null)
|
||||
{
|
||||
await roleManager.DeleteAsync(legacyRoleEntity);
|
||||
}
|
||||
}
|
||||
await db.Users
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.MobileLines
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.MuregLines
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.BillingClients
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.UserDatas
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.VigenciaLines
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.TrocaNumeroLines
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.ChipVirgemLines
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.ControleRecebidoLines
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.Notifications
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.ResumoMacrophonyPlans
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.ResumoMacrophonyTotals
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.ResumoVivoLineResumos
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.ResumoVivoLineTotals
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.ResumoClienteEspeciais
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.ResumoPlanoContratoResumos
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.ResumoPlanoContratoTotals
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.ResumoLineTotais
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.ResumoReservaLines
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.ResumoReservaTotals
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.ParcelamentoLines
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.ParcelamentoMonthValues
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.AuditLogs
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.ImportAuditRuns
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
|
||||
await db.ImportAuditIssues
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == Guid.Empty)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(x => x.TenantId, defaultTenantId));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
namespace line_gestao_api.Dtos;
|
||||
|
||||
public class SystemTenantListItemDto
|
||||
{
|
||||
public Guid TenantId { get; set; }
|
||||
public string NomeOficial { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class CreateSystemTenantUserRequest
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public string Password { get; set; } = string.Empty;
|
||||
public List<string> Roles { get; set; } = new();
|
||||
}
|
||||
|
||||
public class SystemTenantUserCreatedDto
|
||||
{
|
||||
public Guid UserId { get; set; }
|
||||
public Guid TenantId { get; set; }
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public IReadOnlyList<string> Roles { get; set; } = Array.Empty<string>();
|
||||
}
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using line_gestao_api.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace line_gestao_api.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260226130000_CreateTenantsAndAuditLogsSystemContracts")]
|
||||
public partial class CreateTenantsAndAuditLogsSystemContracts : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'Tenants'
|
||||
AND column_name = 'Name'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'Tenants'
|
||||
AND column_name = 'NomeOficial'
|
||||
) THEN
|
||||
ALTER TABLE "Tenants" RENAME COLUMN "Name" TO "NomeOficial";
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
""");
|
||||
|
||||
migrationBuilder.Sql("""ALTER TABLE "Tenants" ADD COLUMN IF NOT EXISTS "IsSystem" boolean NOT NULL DEFAULT FALSE;""");
|
||||
migrationBuilder.Sql("""ALTER TABLE "Tenants" ADD COLUMN IF NOT EXISTS "Ativo" boolean NOT NULL DEFAULT TRUE;""");
|
||||
migrationBuilder.Sql("""ALTER TABLE "Tenants" ADD COLUMN IF NOT EXISTS "SourceType" character varying(80) NULL;""");
|
||||
migrationBuilder.Sql("""ALTER TABLE "Tenants" ADD COLUMN IF NOT EXISTS "SourceKey" character varying(300) NULL;""");
|
||||
migrationBuilder.Sql("""
|
||||
UPDATE "Tenants"
|
||||
SET "NomeOficial" = COALESCE(NULLIF("NomeOficial", ''), 'TENANT_SEM_NOME')
|
||||
WHERE "NomeOficial" IS NULL OR "NomeOficial" = '';
|
||||
""");
|
||||
migrationBuilder.Sql("""ALTER TABLE "Tenants" ALTER COLUMN "NomeOficial" SET NOT NULL;""");
|
||||
migrationBuilder.Sql("""CREATE UNIQUE INDEX IF NOT EXISTS "IX_Tenants_SourceType_SourceKey" ON "Tenants" ("SourceType", "SourceKey");""");
|
||||
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_Tenants_IsSystem_Ativo" ON "Tenants" ("IsSystem", "Ativo");""");
|
||||
|
||||
migrationBuilder.Sql("""ALTER TABLE "AuditLogs" ADD COLUMN IF NOT EXISTS "ActorUserId" uuid NULL;""");
|
||||
migrationBuilder.Sql("""ALTER TABLE "AuditLogs" ADD COLUMN IF NOT EXISTS "ActorTenantId" uuid NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000';""");
|
||||
migrationBuilder.Sql("""ALTER TABLE "AuditLogs" ADD COLUMN IF NOT EXISTS "TargetTenantId" uuid NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000';""");
|
||||
migrationBuilder.Sql("""ALTER TABLE "AuditLogs" ADD COLUMN IF NOT EXISTS "MetadataJson" jsonb NOT NULL DEFAULT '{}'::jsonb;""");
|
||||
|
||||
migrationBuilder.Sql("""
|
||||
UPDATE "AuditLogs"
|
||||
SET "ActorUserId" = COALESCE("ActorUserId", "UserId"),
|
||||
"ActorTenantId" = CASE
|
||||
WHEN "ActorTenantId" = '00000000-0000-0000-0000-000000000000' THEN "TenantId"
|
||||
ELSE "ActorTenantId"
|
||||
END,
|
||||
"TargetTenantId" = CASE
|
||||
WHEN "TargetTenantId" = '00000000-0000-0000-0000-000000000000' THEN "TenantId"
|
||||
ELSE "TargetTenantId"
|
||||
END,
|
||||
"MetadataJson" = COALESCE("MetadataJson", '{}'::jsonb);
|
||||
""");
|
||||
|
||||
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_AuditLogs_ActorTenantId" ON "AuditLogs" ("ActorTenantId");""");
|
||||
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_AuditLogs_TargetTenantId" ON "AuditLogs" ("TargetTenantId");""");
|
||||
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_AuditLogs_ActorUserId" ON "AuditLogs" ("ActorUserId");""");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_AuditLogs_ActorUserId";""");
|
||||
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_AuditLogs_TargetTenantId";""");
|
||||
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_AuditLogs_ActorTenantId";""");
|
||||
migrationBuilder.Sql("""ALTER TABLE "AuditLogs" DROP COLUMN IF EXISTS "MetadataJson";""");
|
||||
migrationBuilder.Sql("""ALTER TABLE "AuditLogs" DROP COLUMN IF EXISTS "TargetTenantId";""");
|
||||
migrationBuilder.Sql("""ALTER TABLE "AuditLogs" DROP COLUMN IF EXISTS "ActorTenantId";""");
|
||||
migrationBuilder.Sql("""ALTER TABLE "AuditLogs" DROP COLUMN IF EXISTS "ActorUserId";""");
|
||||
|
||||
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_Tenants_IsSystem_Ativo";""");
|
||||
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_Tenants_SourceType_SourceKey";""");
|
||||
migrationBuilder.Sql("""ALTER TABLE "Tenants" DROP COLUMN IF EXISTS "SourceKey";""");
|
||||
migrationBuilder.Sql("""ALTER TABLE "Tenants" DROP COLUMN IF EXISTS "SourceType";""");
|
||||
migrationBuilder.Sql("""ALTER TABLE "Tenants" DROP COLUMN IF EXISTS "Ativo";""");
|
||||
migrationBuilder.Sql("""ALTER TABLE "Tenants" DROP COLUMN IF EXISTS "IsSystem";""");
|
||||
|
||||
migrationBuilder.Sql("""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'Tenants'
|
||||
AND column_name = 'NomeOficial'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'Tenants'
|
||||
AND column_name = 'Name'
|
||||
) THEN
|
||||
ALTER TABLE "Tenants" RENAME COLUMN "NomeOficial" TO "Name";
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
""");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using line_gestao_api.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace line_gestao_api.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260226130100_AddTenantIdToMobileLinesIfNeeded")]
|
||||
public partial class AddTenantIdToMobileLinesIfNeeded : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'MobileLines'
|
||||
AND column_name = 'TenantId'
|
||||
) THEN
|
||||
ALTER TABLE "MobileLines" ADD COLUMN "TenantId" uuid NULL;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
""");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// No-op intencional para evitar perda de dados em bancos legados.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using line_gestao_api.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace line_gestao_api.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260226130200_BackfillTenantsFromDistinctMobileLinesCliente")]
|
||||
public partial class BackfillTenantsFromDistinctMobileLinesCliente : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM "MobileLines"
|
||||
WHERE "Cliente" IS NULL OR btrim("Cliente") = ''
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Backfill abortado: MobileLines.Cliente possui valores NULL/vazios. Corrija os dados antes de migrar.';
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
""");
|
||||
|
||||
migrationBuilder.Sql("""CREATE EXTENSION IF NOT EXISTS pgcrypto;""");
|
||||
|
||||
migrationBuilder.Sql("""
|
||||
INSERT INTO "Tenants" (
|
||||
"Id",
|
||||
"NomeOficial",
|
||||
"IsSystem",
|
||||
"Ativo",
|
||||
"SourceType",
|
||||
"SourceKey",
|
||||
"CreatedAt"
|
||||
)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
src."Cliente",
|
||||
FALSE,
|
||||
TRUE,
|
||||
'MobileLines.Cliente',
|
||||
src."Cliente",
|
||||
NOW()
|
||||
FROM (
|
||||
SELECT DISTINCT "Cliente"
|
||||
FROM "MobileLines"
|
||||
) src
|
||||
LEFT JOIN "Tenants" t
|
||||
ON t."SourceType" = 'MobileLines.Cliente'
|
||||
AND t."SourceKey" = src."Cliente"
|
||||
WHERE t."Id" IS NULL;
|
||||
""");
|
||||
|
||||
migrationBuilder.Sql("""
|
||||
UPDATE "Tenants"
|
||||
SET "NomeOficial" = "SourceKey",
|
||||
"IsSystem" = FALSE,
|
||||
"Ativo" = TRUE
|
||||
WHERE "SourceType" = 'MobileLines.Cliente'
|
||||
AND "SourceKey" IS NOT NULL;
|
||||
""");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// No-op intencional. Evita remover tenants já em uso.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using line_gestao_api.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace line_gestao_api.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260226130300_BackfillMobileLinesTenantIdFromTenantSourceKey")]
|
||||
public partial class BackfillMobileLinesTenantIdFromTenantSourceKey : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("""
|
||||
UPDATE "MobileLines" m
|
||||
SET "TenantId" = t."Id"
|
||||
FROM "Tenants" t
|
||||
WHERE t."SourceType" = 'MobileLines.Cliente'
|
||||
AND t."SourceKey" = m."Cliente"
|
||||
AND (m."TenantId" IS NULL OR m."TenantId" <> t."Id");
|
||||
""");
|
||||
|
||||
migrationBuilder.Sql("""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM "MobileLines" m
|
||||
LEFT JOIN "Tenants" t
|
||||
ON t."SourceType" = 'MobileLines.Cliente'
|
||||
AND t."SourceKey" = m."Cliente"
|
||||
WHERE t."Id" IS NULL
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Backfill abortado: existem MobileLines sem tenant correspondente por SourceKey exato.';
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
""");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// No-op intencional.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using line_gestao_api.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace line_gestao_api.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260226130400_MakeMobileLinesTenantIdNotNullAndIndexes")]
|
||||
public partial class MakeMobileLinesTenantIdNotNullAndIndexes : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("""ALTER TABLE "MobileLines" ALTER COLUMN "TenantId" DROP DEFAULT;""");
|
||||
migrationBuilder.Sql("""ALTER TABLE "MobileLines" ALTER COLUMN "TenantId" SET NOT NULL;""");
|
||||
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_MobileLines_TenantId" ON "MobileLines" ("TenantId");""");
|
||||
migrationBuilder.Sql("""CREATE UNIQUE INDEX IF NOT EXISTS "IX_MobileLines_TenantId_Linha" ON "MobileLines" ("TenantId", "Linha");""");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_MobileLines_TenantId";""");
|
||||
migrationBuilder.Sql("""ALTER TABLE "MobileLines" ALTER COLUMN "TenantId" DROP NOT NULL;""");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -245,12 +245,6 @@ namespace line_gestao_api.Migrations
|
|||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<Guid?>("ActorUserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("ActorTenantId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("ChangesJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
|
@ -280,10 +274,6 @@ namespace line_gestao_api.Migrations
|
|||
.HasMaxLength(80)
|
||||
.HasColumnType("character varying(80)");
|
||||
|
||||
b.Property<string>("MetadataJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("RequestMethod")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)");
|
||||
|
|
@ -295,9 +285,6 @@ namespace line_gestao_api.Migrations
|
|||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("TargetTenantId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("UserEmail")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
|
@ -311,10 +298,6 @@ namespace line_gestao_api.Migrations
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ActorUserId");
|
||||
|
||||
b.HasIndex("ActorTenantId");
|
||||
|
||||
b.HasIndex("EntityName");
|
||||
|
||||
b.HasIndex("OccurredAtUtc");
|
||||
|
|
@ -323,8 +306,6 @@ namespace line_gestao_api.Migrations
|
|||
|
||||
b.HasIndex("TenantId");
|
||||
|
||||
b.HasIndex("TargetTenantId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AuditLogs");
|
||||
|
|
@ -1376,35 +1357,15 @@ namespace line_gestao_api.Migrations
|
|||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("Ativo")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsSystem")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("NomeOficial")
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("SourceKey")
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("character varying(300)");
|
||||
|
||||
b.Property<string>("SourceType")
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("character varying(80)");
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("IsSystem", "Ativo");
|
||||
|
||||
b.HasIndex("SourceType", "SourceKey")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Tenants");
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -6,16 +6,10 @@ public class AuditLog : ITenantEntity
|
|||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
// Compatibilidade com histórico atual + filtro global.
|
||||
public Guid TenantId { get; set; }
|
||||
|
||||
public Guid? ActorUserId { get; set; }
|
||||
public Guid ActorTenantId { get; set; }
|
||||
public Guid TargetTenantId { get; set; }
|
||||
|
||||
public DateTime OccurredAtUtc { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Campos legados usados pela tela de histórico.
|
||||
public Guid? UserId { get; set; }
|
||||
public string? UserName { get; set; }
|
||||
public string? UserEmail { get; set; }
|
||||
|
|
@ -27,7 +21,6 @@ public class AuditLog : ITenantEntity
|
|||
public string? EntityLabel { get; set; }
|
||||
|
||||
public string ChangesJson { get; set; } = "[]";
|
||||
public string MetadataJson { get; set; } = "{}";
|
||||
|
||||
public string? RequestPath { get; set; }
|
||||
public string? RequestMethod { get; set; }
|
||||
|
|
|
|||
|
|
@ -3,10 +3,6 @@ namespace line_gestao_api.Models;
|
|||
public class Tenant
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public string NomeOficial { get; set; } = string.Empty;
|
||||
public bool IsSystem { get; set; }
|
||||
public bool Ativo { get; set; } = true;
|
||||
public string? SourceType { get; set; }
|
||||
public string? SourceKey { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
|
|
|||
14
Program.cs
14
Program.cs
|
|
@ -1,5 +1,4 @@
|
|||
using System.IO;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Threading.RateLimiting;
|
||||
using line_gestao_api.Data;
|
||||
|
|
@ -92,7 +91,6 @@ builder.Services.AddDbContext<AppDbContext>(options =>
|
|||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddScoped<ITenantProvider, TenantProvider>();
|
||||
builder.Services.AddScoped<IAuditLogBuilder, AuditLogBuilder>();
|
||||
builder.Services.AddScoped<ISystemAuditService, SystemAuditService>();
|
||||
builder.Services.AddScoped<IVigenciaNotificationSyncService, VigenciaNotificationSyncService>();
|
||||
builder.Services.AddScoped<ParcelamentosImportService>();
|
||||
builder.Services.AddScoped<GeralDashboardInsightsService>();
|
||||
|
|
@ -145,13 +143,7 @@ builder.Services
|
|||
};
|
||||
});
|
||||
|
||||
builder.Services.AddAuthorization(options =>
|
||||
{
|
||||
options.AddPolicy("SystemAdmin", policy =>
|
||||
{
|
||||
policy.RequireRole(SystemTenantConstants.SystemRole);
|
||||
});
|
||||
});
|
||||
builder.Services.AddAuthorization();
|
||||
builder.Services.AddRateLimiter(options =>
|
||||
{
|
||||
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
|
||||
|
|
@ -201,7 +193,3 @@ app.MapControllers();
|
|||
app.MapGet("/health", () => Results.Ok(new { status = "ok" }));
|
||||
|
||||
app.Run();
|
||||
|
||||
public partial class Program
|
||||
{
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
namespace line_gestao_api.Services;
|
||||
|
||||
public static class AppRoles
|
||||
{
|
||||
public const string SysAdmin = "sysadmin";
|
||||
public const string Gestor = "gestor";
|
||||
public const string Cliente = "cliente";
|
||||
|
||||
public static readonly string[] All = [SysAdmin, Gestor, Cliente];
|
||||
}
|
||||
|
|
@ -67,7 +67,7 @@ public class AuditLogBuilder : IAuditLogBuilder
|
|||
|
||||
public List<AuditLog> BuildAuditLogs(ChangeTracker changeTracker)
|
||||
{
|
||||
var tenantId = _tenantProvider.ActorTenantId;
|
||||
var tenantId = _tenantProvider.TenantId;
|
||||
if (tenantId == null)
|
||||
{
|
||||
return new List<AuditLog>();
|
||||
|
|
@ -88,12 +88,6 @@ public class AuditLogBuilder : IAuditLogBuilder
|
|||
return new List<AuditLog>();
|
||||
}
|
||||
|
||||
if (IsSystemRequest(requestPath))
|
||||
{
|
||||
// Endpoints system usam auditoria explicita com actor/target.
|
||||
return new List<AuditLog>();
|
||||
}
|
||||
|
||||
var logs = new List<AuditLog>();
|
||||
|
||||
foreach (var entry in changeTracker.Entries())
|
||||
|
|
@ -115,9 +109,6 @@ public class AuditLogBuilder : IAuditLogBuilder
|
|||
logs.Add(new AuditLog
|
||||
{
|
||||
TenantId = tenantId.Value,
|
||||
ActorUserId = userInfo.UserId,
|
||||
ActorTenantId = tenantId.Value,
|
||||
TargetTenantId = tenantId.Value,
|
||||
OccurredAtUtc = DateTime.UtcNow,
|
||||
UserId = userInfo.UserId,
|
||||
UserName = userInfo.UserName,
|
||||
|
|
@ -128,7 +119,6 @@ public class AuditLogBuilder : IAuditLogBuilder
|
|||
EntityId = BuildEntityId(entry),
|
||||
EntityLabel = BuildEntityLabel(entry),
|
||||
ChangesJson = JsonSerializer.Serialize(changes, JsonOptions),
|
||||
MetadataJson = "{}",
|
||||
RequestPath = requestPath,
|
||||
RequestMethod = requestMethod,
|
||||
IpAddress = ipAddress
|
||||
|
|
@ -148,16 +138,6 @@ public class AuditLogBuilder : IAuditLogBuilder
|
|||
return requestPath.Contains("/import-excel", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool IsSystemRequest(string? requestPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(requestPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return requestPath.StartsWith("/api/system", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string ResolveAction(EntityState state)
|
||||
=> state switch
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace line_gestao_api.Services;
|
||||
|
||||
public static class DeterministicGuid
|
||||
{
|
||||
public static Guid FromString(string input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
throw new ArgumentException("Valor obrigatório para gerar Guid determinístico.", nameof(input));
|
||||
}
|
||||
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
|
||||
Span<byte> bytes = stackalloc byte[16];
|
||||
hash.AsSpan(0, 16).CopyTo(bytes);
|
||||
return new Guid(bytes);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
namespace line_gestao_api.Services;
|
||||
|
||||
public interface ISystemAuditService
|
||||
{
|
||||
Task LogAsync(string action, Guid targetTenantId, object? metadata = null, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
@ -2,8 +2,6 @@ namespace line_gestao_api.Services;
|
|||
|
||||
public interface ITenantProvider
|
||||
{
|
||||
Guid? ActorTenantId { get; }
|
||||
Guid? TenantId { get; }
|
||||
bool HasGlobalViewAccess { get; }
|
||||
void SetTenantId(Guid? tenantId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
namespace line_gestao_api.Services;
|
||||
|
||||
public static class SystemAuditActions
|
||||
{
|
||||
public const string ListTenants = "SYSTEM_LIST_TENANTS";
|
||||
public const string CreateTenantUser = "SYS_CREATE_USER";
|
||||
public const string CreateTenantUserRejected = "SYS_CREATE_USER_ERR";
|
||||
}
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using line_gestao_api.Data;
|
||||
using line_gestao_api.Models;
|
||||
|
||||
namespace line_gestao_api.Services;
|
||||
|
||||
public class SystemAuditService : ISystemAuditService
|
||||
{
|
||||
private const int ActionMaxLength = 20;
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
private readonly AppDbContext _db;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly ITenantProvider _tenantProvider;
|
||||
|
||||
public SystemAuditService(
|
||||
AppDbContext db,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ITenantProvider tenantProvider)
|
||||
{
|
||||
_db = db;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_tenantProvider = tenantProvider;
|
||||
}
|
||||
|
||||
public async Task LogAsync(string action, Guid targetTenantId, object? metadata = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var actorTenantId = _tenantProvider.ActorTenantId;
|
||||
if (!actorTenantId.HasValue || actorTenantId.Value == Guid.Empty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var user = _httpContextAccessor.HttpContext?.User;
|
||||
var userId = ResolveUserId(user);
|
||||
var userName = ResolveUserName(user);
|
||||
var userEmail = ResolveUserEmail(user);
|
||||
|
||||
var request = _httpContextAccessor.HttpContext?.Request;
|
||||
var requestPath = request?.Path.Value;
|
||||
var requestMethod = request?.Method;
|
||||
var ipAddress = _httpContextAccessor.HttpContext?.Connection?.RemoteIpAddress?.ToString();
|
||||
var safeMetadataJson = JsonSerializer.Serialize(metadata ?? new { }, JsonOptions);
|
||||
var normalizedAction = NormalizeAction(action);
|
||||
|
||||
_db.AuditLogs.Add(new AuditLog
|
||||
{
|
||||
TenantId = actorTenantId.Value,
|
||||
ActorUserId = userId,
|
||||
ActorTenantId = actorTenantId.Value,
|
||||
TargetTenantId = targetTenantId,
|
||||
OccurredAtUtc = DateTime.UtcNow,
|
||||
Action = normalizedAction,
|
||||
Page = "System",
|
||||
EntityName = "System",
|
||||
EntityId = targetTenantId.ToString(),
|
||||
EntityLabel = null,
|
||||
ChangesJson = "[]",
|
||||
MetadataJson = safeMetadataJson,
|
||||
UserId = userId,
|
||||
UserName = userName,
|
||||
UserEmail = userEmail,
|
||||
RequestPath = requestPath,
|
||||
RequestMethod = requestMethod,
|
||||
IpAddress = ipAddress
|
||||
});
|
||||
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private static string NormalizeAction(string? action)
|
||||
{
|
||||
var normalized = (action ?? string.Empty).Trim().ToUpperInvariant();
|
||||
if (string.IsNullOrEmpty(normalized))
|
||||
{
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
if (normalized.Length <= ActionMaxLength)
|
||||
{
|
||||
return normalized;
|
||||
}
|
||||
|
||||
return normalized[..ActionMaxLength];
|
||||
}
|
||||
|
||||
private static Guid? ResolveUserId(ClaimsPrincipal? user)
|
||||
{
|
||||
var raw = user?.FindFirstValue(ClaimTypes.NameIdentifier)
|
||||
?? user?.FindFirstValue(JwtRegisteredClaimNames.Sub)
|
||||
?? user?.FindFirstValue("sub");
|
||||
|
||||
return Guid.TryParse(raw, out var parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
private static string? ResolveUserName(ClaimsPrincipal? user)
|
||||
{
|
||||
return user?.FindFirstValue("name")
|
||||
?? user?.FindFirstValue(ClaimTypes.Name)
|
||||
?? user?.Identity?.Name;
|
||||
}
|
||||
|
||||
private static string? ResolveUserEmail(ClaimsPrincipal? user)
|
||||
{
|
||||
return user?.FindFirstValue(ClaimTypes.Email)
|
||||
?? user?.FindFirstValue(JwtRegisteredClaimNames.Email)
|
||||
?? user?.FindFirstValue("email");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
namespace line_gestao_api.Services;
|
||||
|
||||
public static class SystemTenantConstants
|
||||
{
|
||||
public const string SystemTenantSeed = "SYSTEM_TENANT";
|
||||
public const string SystemTenantNomeOficial = "SystemTenant";
|
||||
public const string SystemRole = AppRoles.SysAdmin;
|
||||
public const string MobileLinesClienteSourceType = "MobileLines.Cliente";
|
||||
|
||||
public static readonly Guid SystemTenantId = DeterministicGuid.FromString(SystemTenantSeed);
|
||||
}
|
||||
|
|
@ -13,14 +13,8 @@ public class TenantProvider : ITenantProvider
|
|||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
public Guid? ActorTenantId => TenantId;
|
||||
|
||||
public Guid? TenantId => CurrentTenant.Value ?? ResolveFromClaims();
|
||||
|
||||
public bool HasGlobalViewAccess =>
|
||||
HasRole(AppRoles.SysAdmin) ||
|
||||
HasRole(AppRoles.Gestor);
|
||||
|
||||
public void SetTenantId(Guid? tenantId)
|
||||
{
|
||||
CurrentTenant.Value = tenantId;
|
||||
|
|
@ -33,21 +27,4 @@ public class TenantProvider : ITenantProvider
|
|||
|
||||
return Guid.TryParse(claim, out var tenantId) ? tenantId : null;
|
||||
}
|
||||
|
||||
private bool HasRole(string role)
|
||||
{
|
||||
var principal = _httpContextAccessor.HttpContext?.User;
|
||||
if (principal?.Identity?.IsAuthenticated != true)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var roleClaims = principal.FindAll(ClaimTypes.Role)
|
||||
.Select(c => c.Value)
|
||||
.Concat(principal.FindAll("role").Select(c => c.Value))
|
||||
.Concat(principal.FindAll("roles").Select(c => c.Value))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
return roleClaims.Any(r => string.Equals(r, role, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,10 +49,7 @@ public class VigenciaNotificationBackgroundService : BackgroundService
|
|||
return;
|
||||
}
|
||||
|
||||
var tenants = await db.Tenants
|
||||
.AsNoTracking()
|
||||
.Where(t => !t.IsSystem && t.Ativo)
|
||||
.ToListAsync(stoppingToken);
|
||||
var tenants = await db.Tenants.AsNoTracking().ToListAsync(stoppingToken);
|
||||
if (tenants.Count == 0)
|
||||
{
|
||||
_logger.LogWarning("Nenhum tenant encontrado para gerar notificações.");
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"ConnectionStrings": {
|
||||
"Default": "Host=localhost;Port=5432;Database=linegestao;Username=postgres;Password=postgres"
|
||||
"Default": "Host=localhost;Port=5432;Database=linegestao;Username=linegestao_app;Password=1231234512Ed@"
|
||||
},
|
||||
"Jwt": {
|
||||
"Key": "linegestao-local-jwt-key-2026-dev-strong-123456789",
|
||||
|
|
@ -31,8 +31,9 @@
|
|||
"Seed": {
|
||||
"Enabled": true,
|
||||
"ReapplyAdminCredentialsOnStartup": true,
|
||||
"AdminMasterName": "Admin Master",
|
||||
"AdminMasterEmail": "admin.master@linegestao.local",
|
||||
"AdminMasterPassword": "DevAdminMaster123!"
|
||||
"DefaultTenantName": "Default",
|
||||
"AdminName": "Administrador",
|
||||
"AdminEmail": "admin@linegestao.local",
|
||||
"AdminPassword": "DevAdmin123!"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"ConnectionStrings": {
|
||||
"Default": "Host=localhost;Port=5432;Database=linegestao;Username=postgres;Password=postgres"
|
||||
"Default": "Host=localhost;Port=5432;Database=linegestao;Username=linegestao_app;Password=1231234512Ed@"
|
||||
},
|
||||
"Jwt": {
|
||||
"Key": "YOUR_LOCAL_STRONG_JWT_KEY_MIN_32_CHARS",
|
||||
|
|
@ -11,8 +11,9 @@
|
|||
"Seed": {
|
||||
"Enabled": true,
|
||||
"ReapplyAdminCredentialsOnStartup": false,
|
||||
"AdminMasterName": "Admin Master",
|
||||
"AdminMasterEmail": "admin.master@linegestao.local",
|
||||
"AdminMasterPassword": "YOUR_LOCAL_ADMIN_MASTER_SEED_PASSWORD"
|
||||
"DefaultTenantName": "Default",
|
||||
"AdminName": "Administrador",
|
||||
"AdminEmail": "admin@linegestao.local",
|
||||
"AdminPassword": "YOUR_LOCAL_ADMIN_SEED_PASSWORD"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"ConnectionStrings": {
|
||||
"Default": "Host=localhost;Port=5432;Database=linegestao;Username=postgres;Password=postgres"
|
||||
"Default": "Host=localhost;Port=5432;Database=linegestao;Username=linegestao_app;Password=1231234512Ed@"
|
||||
},
|
||||
"Jwt": {
|
||||
"Key": "linegestao-local-jwt-key-2026-dev-strong-123456789",
|
||||
|
|
@ -31,8 +31,9 @@
|
|||
"Seed": {
|
||||
"Enabled": true,
|
||||
"ReapplyAdminCredentialsOnStartup": true,
|
||||
"AdminMasterName": "Admin Master",
|
||||
"AdminMasterEmail": "admin.master@linegestao.local",
|
||||
"AdminMasterPassword": "DevAdminMaster123!"
|
||||
"DefaultTenantName": "Default",
|
||||
"AdminName": "Administrador",
|
||||
"AdminEmail": "admin@linegestao.local",
|
||||
"AdminPassword": "DevAdmin123!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -148,12 +148,8 @@ namespace line_gestao_api.Tests
|
|||
TenantId = tenantId;
|
||||
}
|
||||
|
||||
public Guid? ActorTenantId => TenantId;
|
||||
|
||||
public Guid? TenantId { get; private set; }
|
||||
|
||||
public bool HasGlobalViewAccess => false;
|
||||
|
||||
public void SetTenantId(Guid? tenantId)
|
||||
{
|
||||
TenantId = tenantId;
|
||||
|
|
|
|||
|
|
@ -1,335 +0,0 @@
|
|||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using line_gestao_api.Data;
|
||||
using line_gestao_api.Dtos;
|
||||
using line_gestao_api.Models;
|
||||
using line_gestao_api.Services;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Xunit;
|
||||
|
||||
namespace line_gestao_api.Tests;
|
||||
|
||||
public class SystemTenantIntegrationTests
|
||||
{
|
||||
private static readonly Guid TenantAId = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
|
||||
private static readonly Guid TenantBId = Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb");
|
||||
|
||||
private const string TenantAClientName = "CLIENTE-ALFA LTDA";
|
||||
private const string TenantBClientName = "CLIENTE-BETA S/A";
|
||||
|
||||
[Fact]
|
||||
public async Task CommonUser_OnlySeesOwnTenantData()
|
||||
{
|
||||
using var factory = new ApiFactory();
|
||||
var client = factory.CreateClient();
|
||||
|
||||
await SeedTenantsAndLinesAsync(factory.Services);
|
||||
await UpsertUserAsync(factory.Services, TenantAId, "tenanta.user@test.local", "TenantA123!", "cliente");
|
||||
|
||||
var token = await LoginAndGetTokenAsync(client, "tenanta.user@test.local", "TenantA123!", TenantAId);
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
|
||||
var response = await client.GetAsync("/api/lines/clients");
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var clients = await response.Content.ReadFromJsonAsync<List<string>>();
|
||||
Assert.NotNull(clients);
|
||||
Assert.Contains(TenantAClientName, clients!);
|
||||
Assert.DoesNotContain(TenantBClientName, clients);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CommonUser_CannotAccessSystemEndpoints()
|
||||
{
|
||||
using var factory = new ApiFactory();
|
||||
var client = factory.CreateClient();
|
||||
|
||||
await SeedTenantsAndLinesAsync(factory.Services);
|
||||
await UpsertUserAsync(factory.Services, TenantAId, "tenanta.block@test.local", "TenantA123!", "cliente");
|
||||
|
||||
var token = await LoginAndGetTokenAsync(client, "tenanta.block@test.local", "TenantA123!", TenantAId);
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
|
||||
var response = await client.GetAsync("/api/system/tenants?source=MobileLines.Cliente&active=true");
|
||||
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SysAdmin_CanListClientTenants()
|
||||
{
|
||||
using var factory = new ApiFactory();
|
||||
var client = factory.CreateClient();
|
||||
|
||||
await SeedTenantsAndLinesAsync(factory.Services);
|
||||
|
||||
var token = await LoginAndGetTokenAsync(
|
||||
client,
|
||||
"admin.master@test.local",
|
||||
"AdminMaster123!",
|
||||
SystemTenantConstants.SystemTenantId);
|
||||
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
var response = await client.GetAsync("/api/system/tenants?source=MobileLines.Cliente&active=true");
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var tenants = await response.Content.ReadFromJsonAsync<List<SystemTenantListItemDto>>();
|
||||
Assert.NotNull(tenants);
|
||||
Assert.Contains(tenants!, t => t.TenantId == TenantAId && t.NomeOficial == TenantAClientName);
|
||||
Assert.Contains(tenants, t => t.TenantId == TenantBId && t.NomeOficial == TenantBClientName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SysAdmin_CreatesTenantUser_AndNewUserSeesOnlyOwnTenant()
|
||||
{
|
||||
using var factory = new ApiFactory();
|
||||
var client = factory.CreateClient();
|
||||
|
||||
await SeedTenantsAndLinesAsync(factory.Services);
|
||||
|
||||
var adminToken = await LoginAndGetTokenAsync(
|
||||
client,
|
||||
"admin.master@test.local",
|
||||
"AdminMaster123!",
|
||||
SystemTenantConstants.SystemTenantId);
|
||||
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", adminToken);
|
||||
|
||||
var request = new CreateSystemTenantUserRequest
|
||||
{
|
||||
Name = "Usuário Cliente A",
|
||||
Email = "novo.clientea@test.local",
|
||||
Password = "ClienteA123!",
|
||||
Roles = new List<string> { "cliente" }
|
||||
};
|
||||
|
||||
var createResponse = await client.PostAsJsonAsync($"/api/system/tenants/{TenantAId}/users", request);
|
||||
createResponse.EnsureSuccessStatusCode();
|
||||
|
||||
var created = await createResponse.Content.ReadFromJsonAsync<SystemTenantUserCreatedDto>();
|
||||
Assert.NotNull(created);
|
||||
Assert.Equal(TenantAId, created!.TenantId);
|
||||
Assert.Equal("novo.clientea@test.local", created.Email);
|
||||
Assert.Contains("cliente", created.Roles);
|
||||
|
||||
var userToken = await LoginAndGetTokenAsync(client, "novo.clientea@test.local", "ClienteA123!", TenantAId);
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", userToken);
|
||||
|
||||
var visibleClientsResponse = await client.GetAsync("/api/lines/clients");
|
||||
visibleClientsResponse.EnsureSuccessStatusCode();
|
||||
var clients = await visibleClientsResponse.Content.ReadFromJsonAsync<List<string>>();
|
||||
|
||||
Assert.NotNull(clients);
|
||||
Assert.Contains(TenantAClientName, clients!);
|
||||
Assert.DoesNotContain(TenantBClientName, clients);
|
||||
|
||||
await using var scope = factory.Services.CreateAsyncScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
var systemAudit = await db.AuditLogs
|
||||
.IgnoreQueryFilters()
|
||||
.OrderByDescending(x => x.OccurredAtUtc)
|
||||
.FirstOrDefaultAsync(x => x.Action == SystemAuditActions.CreateTenantUser);
|
||||
|
||||
Assert.NotNull(systemAudit);
|
||||
Assert.Equal(SystemTenantConstants.SystemTenantId, systemAudit!.ActorTenantId);
|
||||
Assert.Equal(TenantAId, systemAudit.TargetTenantId);
|
||||
Assert.DoesNotContain("ClienteA123!", systemAudit.MetadataJson);
|
||||
}
|
||||
|
||||
private static async Task SeedTenantsAndLinesAsync(IServiceProvider services)
|
||||
{
|
||||
await using var scope = services.CreateAsyncScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
||||
var tenantA = await db.Tenants.FirstOrDefaultAsync(t => t.Id == TenantAId);
|
||||
if (tenantA == null)
|
||||
{
|
||||
db.Tenants.Add(new Tenant
|
||||
{
|
||||
Id = TenantAId,
|
||||
NomeOficial = TenantAClientName,
|
||||
IsSystem = false,
|
||||
Ativo = true,
|
||||
SourceType = SystemTenantConstants.MobileLinesClienteSourceType,
|
||||
SourceKey = TenantAClientName,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
|
||||
var tenantB = await db.Tenants.FirstOrDefaultAsync(t => t.Id == TenantBId);
|
||||
if (tenantB == null)
|
||||
{
|
||||
db.Tenants.Add(new Tenant
|
||||
{
|
||||
Id = TenantBId,
|
||||
NomeOficial = TenantBClientName,
|
||||
IsSystem = false,
|
||||
Ativo = true,
|
||||
SourceType = SystemTenantConstants.MobileLinesClienteSourceType,
|
||||
SourceKey = TenantBClientName,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
|
||||
var currentLines = await db.MobileLines.IgnoreQueryFilters().ToListAsync();
|
||||
if (currentLines.Count > 0)
|
||||
{
|
||||
db.MobileLines.RemoveRange(currentLines);
|
||||
}
|
||||
|
||||
db.MobileLines.AddRange(
|
||||
new MobileLine
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Item = 1,
|
||||
Linha = "5511999990001",
|
||||
Cliente = TenantAClientName,
|
||||
TenantId = TenantAId
|
||||
},
|
||||
new MobileLine
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Item = 2,
|
||||
Linha = "5511888880002",
|
||||
Cliente = TenantBClientName,
|
||||
TenantId = TenantBId
|
||||
});
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private static async Task UpsertUserAsync(
|
||||
IServiceProvider services,
|
||||
Guid tenantId,
|
||||
string email,
|
||||
string password,
|
||||
params string[] roles)
|
||||
{
|
||||
await using var scope = services.CreateAsyncScope();
|
||||
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
|
||||
var tenantProvider = scope.ServiceProvider.GetRequiredService<ITenantProvider>();
|
||||
|
||||
var previousTenant = tenantProvider.ActorTenantId;
|
||||
tenantProvider.SetTenantId(tenantId);
|
||||
|
||||
try
|
||||
{
|
||||
var normalizedEmail = userManager.NormalizeEmail(email);
|
||||
var user = await userManager.Users
|
||||
.IgnoreQueryFilters()
|
||||
.FirstOrDefaultAsync(u => u.TenantId == tenantId && u.NormalizedEmail == normalizedEmail);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
user = new ApplicationUser
|
||||
{
|
||||
Name = email,
|
||||
Email = email,
|
||||
UserName = email,
|
||||
TenantId = tenantId,
|
||||
EmailConfirmed = true,
|
||||
IsActive = true,
|
||||
LockoutEnabled = true
|
||||
};
|
||||
|
||||
var createResult = await userManager.CreateAsync(user, password);
|
||||
Assert.True(createResult.Succeeded, string.Join("; ", createResult.Errors.Select(e => e.Description)));
|
||||
}
|
||||
else
|
||||
{
|
||||
var resetToken = await userManager.GeneratePasswordResetTokenAsync(user);
|
||||
var reset = await userManager.ResetPasswordAsync(user, resetToken, password);
|
||||
Assert.True(reset.Succeeded, string.Join("; ", reset.Errors.Select(e => e.Description)));
|
||||
}
|
||||
|
||||
var existingRoles = await userManager.GetRolesAsync(user);
|
||||
if (existingRoles.Count > 0)
|
||||
{
|
||||
var removeRolesResult = await userManager.RemoveFromRolesAsync(user, existingRoles);
|
||||
Assert.True(removeRolesResult.Succeeded, string.Join("; ", removeRolesResult.Errors.Select(e => e.Description)));
|
||||
}
|
||||
|
||||
var addRolesResult = await userManager.AddToRolesAsync(user, roles);
|
||||
Assert.True(addRolesResult.Succeeded, string.Join("; ", addRolesResult.Errors.Select(e => e.Description)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
tenantProvider.SetTenantId(previousTenant);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<string> LoginAndGetTokenAsync(HttpClient client, string email, string password, Guid tenantId)
|
||||
{
|
||||
var previousAuth = client.DefaultRequestHeaders.Authorization;
|
||||
client.DefaultRequestHeaders.Authorization = null;
|
||||
|
||||
try
|
||||
{
|
||||
var response = await client.PostAsJsonAsync("/auth/login", new
|
||||
{
|
||||
email,
|
||||
password,
|
||||
tenantId
|
||||
});
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
var auth = await response.Content.ReadFromJsonAsync<AuthResponse>();
|
||||
Assert.NotNull(auth);
|
||||
Assert.False(string.IsNullOrWhiteSpace(auth!.Token));
|
||||
return auth.Token;
|
||||
}
|
||||
finally
|
||||
{
|
||||
client.DefaultRequestHeaders.Authorization = previousAuth;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ApiFactory : WebApplicationFactory<Program>
|
||||
{
|
||||
private readonly string _databaseName = $"line-gestao-tests-{Guid.NewGuid()}";
|
||||
|
||||
protected override void ConfigureWebHost(Microsoft.AspNetCore.Hosting.IWebHostBuilder builder)
|
||||
{
|
||||
builder.UseEnvironment("Testing");
|
||||
|
||||
builder.ConfigureAppConfiguration((_, config) =>
|
||||
{
|
||||
config.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["App:UseHttpsRedirection"] = "false",
|
||||
["Seed:Enabled"] = "true",
|
||||
["Seed:AdminMasterName"] = "Admin Master",
|
||||
["Seed:AdminMasterEmail"] = "admin.master@test.local",
|
||||
["Seed:AdminMasterPassword"] = "AdminMaster123!",
|
||||
["Seed:ReapplyAdminCredentialsOnStartup"] = "true"
|
||||
});
|
||||
});
|
||||
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
var notificationHostedService = services
|
||||
.Where(d => d.ServiceType == typeof(IHostedService) &&
|
||||
d.ImplementationType == typeof(VigenciaNotificationBackgroundService))
|
||||
.ToList();
|
||||
foreach (var descriptor in notificationHostedService)
|
||||
{
|
||||
services.Remove(descriptor);
|
||||
}
|
||||
|
||||
services.RemoveAll<AppDbContext>();
|
||||
services.RemoveAll<DbContextOptions<AppDbContext>>();
|
||||
|
||||
services.AddDbContext<AppDbContext>(options =>
|
||||
{
|
||||
options.UseInMemoryDatabase(_databaseName);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,6 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
|
|
|
|||
|
|
@ -1,395 +0,0 @@
|
|||
{
|
||||
"info": {
|
||||
"name": "Line Gestao - SystemTenant Multi-tenant Tests",
|
||||
"_postman_id": "c4c0b7d9-7f11-4a0c-b8ca-332633f12601",
|
||||
"description": "Fluxo de testes para sysadmin, endpoints /api/system/* e isolamento por tenant.",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
"variable": [
|
||||
{ "key": "adminMasterToken", "value": "" },
|
||||
{ "key": "tenantAUserToken", "value": "" },
|
||||
{ "key": "newTenantAUserToken", "value": "" },
|
||||
{ "key": "tenantAUserId", "value": "" },
|
||||
{ "key": "newTenantAUserId", "value": "" },
|
||||
{ "key": "newTenantAUserEmail", "value": "" },
|
||||
{ "key": "newTenantAUserPassword", "value": "" },
|
||||
{ "key": "newTenantAUserName", "value": "" }
|
||||
],
|
||||
"item": [
|
||||
{
|
||||
"name": "1) Login sysadmin",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{ "key": "Content-Type", "value": "application/json" }
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"email\": \"{{adminMasterEmail}}\",\n \"password\": \"{{adminMasterPassword}}\",\n \"tenantId\": \"{{systemTenantId}}\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/auth/login",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["auth", "login"]
|
||||
}
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"pm.test('Status 200', function () {",
|
||||
" pm.response.to.have.status(200);",
|
||||
"});",
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('Retorna token JWT', function () {",
|
||||
" pm.expect(json.token).to.be.a('string').and.not.empty;",
|
||||
"});",
|
||||
"pm.collectionVariables.set('adminMasterToken', json.token);"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "2) GET /api/system/tenants (sysadmin)",
|
||||
"request": {
|
||||
"auth": {
|
||||
"type": "bearer",
|
||||
"bearer": [
|
||||
{ "key": "token", "value": "{{adminMasterToken}}", "type": "string" }
|
||||
]
|
||||
},
|
||||
"method": "GET",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/system/tenants?source=MobileLines.Cliente&active=true",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["api", "system", "tenants"],
|
||||
"query": [
|
||||
{ "key": "source", "value": "MobileLines.Cliente" },
|
||||
{ "key": "active", "value": "true" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"pm.test('Status 200', function () {",
|
||||
" pm.response.to.have.status(200);",
|
||||
"});",
|
||||
"const tenants = pm.response.json();",
|
||||
"pm.test('Retorna array de tenants', function () {",
|
||||
" pm.expect(Array.isArray(tenants)).to.eql(true);",
|
||||
"});",
|
||||
"const tenantAClientName = pm.environment.get('tenantAClientName');",
|
||||
"const tenantBClientName = pm.environment.get('tenantBClientName');",
|
||||
"if (tenantAClientName) {",
|
||||
" const tenantA = tenants.find(t => t.nomeOficial === tenantAClientName || t.NomeOficial === tenantAClientName);",
|
||||
" pm.test('Tenant A encontrado por nomeOficial', function () {",
|
||||
" pm.expect(tenantA).to.exist;",
|
||||
" });",
|
||||
" if (tenantA && (tenantA.tenantId || tenantA.TenantId)) {",
|
||||
" pm.environment.set('tenantAId', tenantA.tenantId || tenantA.TenantId);",
|
||||
" }",
|
||||
"}",
|
||||
"if (tenantBClientName) {",
|
||||
" const tenantB = tenants.find(t => t.nomeOficial === tenantBClientName || t.NomeOficial === tenantBClientName);",
|
||||
" pm.test('Tenant B encontrado por nomeOficial', function () {",
|
||||
" pm.expect(tenantB).to.exist;",
|
||||
" });",
|
||||
" if (tenantB && (tenantB.tenantId || tenantB.TenantId)) {",
|
||||
" pm.environment.set('tenantBId', tenantB.tenantId || tenantB.TenantId);",
|
||||
" }",
|
||||
"}"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "3) POST /api/system/tenants/{tenantId}/users (criar usuário comum tenant A)",
|
||||
"request": {
|
||||
"auth": {
|
||||
"type": "bearer",
|
||||
"bearer": [
|
||||
{ "key": "token", "value": "{{adminMasterToken}}", "type": "string" }
|
||||
]
|
||||
},
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{ "key": "Content-Type", "value": "application/json" }
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"name\": \"{{tenantAUserName}}\",\n \"email\": \"{{tenantAUserEmail}}\",\n \"password\": \"{{tenantAUserPassword}}\",\n \"roles\": [\"cliente\"]\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/system/tenants/{{tenantAId}}/users",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["api", "system", "tenants", "{{tenantAId}}", "users"]
|
||||
}
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"pm.test('Status 201 (criado) ou 409 (já existe)', function () {",
|
||||
" pm.expect([201, 409]).to.include(pm.response.code);",
|
||||
"});",
|
||||
"if (pm.response.code === 201) {",
|
||||
" const json = pm.response.json();",
|
||||
" pm.collectionVariables.set('tenantAUserId', json.userId || json.UserId || '');",
|
||||
"}"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "4) Login usuário comum tenant A",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{ "key": "Content-Type", "value": "application/json" }
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"email\": \"{{tenantAUserEmail}}\",\n \"password\": \"{{tenantAUserPassword}}\",\n \"tenantId\": \"{{tenantAId}}\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/auth/login",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["auth", "login"]
|
||||
}
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"pm.test('Status 200', function () {",
|
||||
" pm.response.to.have.status(200);",
|
||||
"});",
|
||||
"const json = pm.response.json();",
|
||||
"pm.collectionVariables.set('tenantAUserToken', json.token);"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "5) Usuário comum NÃO acessa /api/system/*",
|
||||
"request": {
|
||||
"auth": {
|
||||
"type": "bearer",
|
||||
"bearer": [
|
||||
{ "key": "token", "value": "{{tenantAUserToken}}", "type": "string" }
|
||||
]
|
||||
},
|
||||
"method": "GET",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/system/tenants?source=MobileLines.Cliente&active=true",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["api", "system", "tenants"],
|
||||
"query": [
|
||||
{ "key": "source", "value": "MobileLines.Cliente" },
|
||||
{ "key": "active", "value": "true" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"pm.test('Status 403 Forbidden', function () {",
|
||||
" pm.response.to.have.status(403);",
|
||||
"});"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "6) Usuário comum tenant A vê apenas seu tenant",
|
||||
"request": {
|
||||
"auth": {
|
||||
"type": "bearer",
|
||||
"bearer": [
|
||||
{ "key": "token", "value": "{{tenantAUserToken}}", "type": "string" }
|
||||
]
|
||||
},
|
||||
"method": "GET",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/lines/clients",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["api", "lines", "clients"]
|
||||
}
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"pm.test('Status 200', function () {",
|
||||
" pm.response.to.have.status(200);",
|
||||
"});",
|
||||
"const clients = pm.response.json();",
|
||||
"pm.test('Retorna lista de clientes', function () {",
|
||||
" pm.expect(Array.isArray(clients)).to.eql(true);",
|
||||
"});",
|
||||
"const tenantAClientName = pm.environment.get('tenantAClientName');",
|
||||
"const tenantBClientName = pm.environment.get('tenantBClientName');",
|
||||
"if (tenantAClientName) {",
|
||||
" pm.test('Contém cliente do tenant A', function () {",
|
||||
" pm.expect(clients).to.include(tenantAClientName);",
|
||||
" });",
|
||||
"}",
|
||||
"if (tenantBClientName) {",
|
||||
" pm.test('Não contém cliente do tenant B', function () {",
|
||||
" pm.expect(clients).to.not.include(tenantBClientName);",
|
||||
" });",
|
||||
"}"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "7) POST /api/system/tenants/{tenantId}/users (novo usuário tenant A)",
|
||||
"event": [
|
||||
{
|
||||
"listen": "prerequest",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const suffix = Date.now().toString().slice(-8);",
|
||||
"pm.collectionVariables.set('newTenantAUserEmail', `novo.tenant.a.${suffix}@test.local`);",
|
||||
"pm.collectionVariables.set('newTenantAUserPassword', 'ClienteA123!');",
|
||||
"pm.collectionVariables.set('newTenantAUserName', `Novo Tenant A ${suffix}`);"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"pm.test('Status 201', function () {",
|
||||
" pm.response.to.have.status(201);",
|
||||
"});",
|
||||
"const json = pm.response.json();",
|
||||
"pm.collectionVariables.set('newTenantAUserId', json.userId || json.UserId || '');"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"request": {
|
||||
"auth": {
|
||||
"type": "bearer",
|
||||
"bearer": [
|
||||
{ "key": "token", "value": "{{adminMasterToken}}", "type": "string" }
|
||||
]
|
||||
},
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{ "key": "Content-Type", "value": "application/json" }
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"name\": \"{{newTenantAUserName}}\",\n \"email\": \"{{newTenantAUserEmail}}\",\n \"password\": \"{{newTenantAUserPassword}}\",\n \"roles\": [\"cliente\"]\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/system/tenants/{{tenantAId}}/users",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["api", "system", "tenants", "{{tenantAId}}", "users"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "8) Login novo usuário tenant A",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{ "key": "Content-Type", "value": "application/json" }
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"email\": \"{{newTenantAUserEmail}}\",\n \"password\": \"{{newTenantAUserPassword}}\",\n \"tenantId\": \"{{tenantAId}}\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/auth/login",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["auth", "login"]
|
||||
}
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"pm.test('Status 200', function () {",
|
||||
" pm.response.to.have.status(200);",
|
||||
"});",
|
||||
"const json = pm.response.json();",
|
||||
"pm.collectionVariables.set('newTenantAUserToken', json.token);"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "9) Novo usuário tenant A vê apenas seu tenant",
|
||||
"request": {
|
||||
"auth": {
|
||||
"type": "bearer",
|
||||
"bearer": [
|
||||
{ "key": "token", "value": "{{newTenantAUserToken}}", "type": "string" }
|
||||
]
|
||||
},
|
||||
"method": "GET",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/lines/clients",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["api", "lines", "clients"]
|
||||
}
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"pm.test('Status 200', function () {",
|
||||
" pm.response.to.have.status(200);",
|
||||
"});",
|
||||
"const clients = pm.response.json();",
|
||||
"const tenantAClientName = pm.environment.get('tenantAClientName');",
|
||||
"const tenantBClientName = pm.environment.get('tenantBClientName');",
|
||||
"if (tenantAClientName) {",
|
||||
" pm.test('Contém cliente do tenant A', function () {",
|
||||
" pm.expect(clients).to.include(tenantAClientName);",
|
||||
" });",
|
||||
"}",
|
||||
"if (tenantBClientName) {",
|
||||
" pm.test('Não contém cliente do tenant B', function () {",
|
||||
" pm.expect(clients).to.not.include(tenantBClientName);",
|
||||
" });",
|
||||
"}"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
{
|
||||
"id": "d1d8e905-e4b8-40c5-a62e-afb27c59b685",
|
||||
"name": "Line Gestao - Local",
|
||||
"values": [
|
||||
{
|
||||
"key": "baseUrl",
|
||||
"value": "http://localhost:5000",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "systemTenantId",
|
||||
"value": "562617c4-90dc-cfce-ddf4-64b6284dc4f2",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "adminMasterEmail",
|
||||
"value": "sysadmin@linegestao.local",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "adminMasterPassword",
|
||||
"value": "",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "tenantAId",
|
||||
"value": "",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "tenantBId",
|
||||
"value": "",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "tenantAClientName",
|
||||
"value": "",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "tenantBClientName",
|
||||
"value": "",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "tenantAUserName",
|
||||
"value": "Usuario Tenant A",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "tenantAUserEmail",
|
||||
"value": "tenanta.user@test.local",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "tenantAUserPassword",
|
||||
"value": "TenantA123!",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"_postman_variable_scope": "environment",
|
||||
"_postman_exported_at": "2026-02-26T12:00:00.000Z",
|
||||
"_postman_exported_using": "Codex GPT-5"
|
||||
}
|
||||
Loading…
Reference in New Issue