Branch Produção
This commit is contained in:
parent
dc3351a5f8
commit
2872de9f4b
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<key id="df4b6848-d6f8-4258-b66f-efc9c9908378" version="1">
|
||||
<creationDate>2026-02-11T18:29:16.4250416Z</creationDate>
|
||||
<activationDate>2026-02-11T18:29:16.4250416Z</activationDate>
|
||||
<expirationDate>2026-05-12T18:29:16.4250416Z</expirationDate>
|
||||
<descriptor deserializerType="Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AuthenticatedEncryptorDescriptorDeserializer, Microsoft.AspNetCore.DataProtection, Version=10.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60">
|
||||
<descriptor>
|
||||
<encryption algorithm="AES_256_CBC" />
|
||||
<validation algorithm="HMACSHA256" />
|
||||
<masterKey p4:requiresEncryption="true" xmlns:p4="http://schemas.asp.net/2015/03/dataProtection">
|
||||
<!-- Warning: the key below is in an unencrypted form. -->
|
||||
<value>ZPHp/1oEBziSImYed0VIGR6ghUDI9+xXPXqb8t5Hs91BPBBvLM2NCReZtsmfKfmoqP0uFJ0jK+t/7tr6XxBAaA==</value>
|
||||
</masterKey>
|
||||
</descriptor>
|
||||
</descriptor>
|
||||
</key>
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
# dotenv files
|
||||
.env
|
||||
appsettings.Local.json
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using line_gestao_api.Services;
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
|
|
@ -60,7 +61,8 @@ public class AuthController : ControllerBase
|
|||
UserName = email,
|
||||
TenantId = tenantId.Value,
|
||||
IsActive = true,
|
||||
EmailConfirmed = true
|
||||
EmailConfirmed = true,
|
||||
LockoutEnabled = true
|
||||
};
|
||||
|
||||
var createResult = await _userManager.CreateAsync(user, req.Password);
|
||||
|
|
@ -78,6 +80,7 @@ public class AuthController : ControllerBase
|
|||
}
|
||||
|
||||
[HttpPost("login")]
|
||||
[EnableRateLimiting("auth-login")]
|
||||
public async Task<IActionResult> Login(LoginRequest req)
|
||||
{
|
||||
var email = (req.Email ?? "").Trim().ToLowerInvariant();
|
||||
|
|
@ -115,9 +118,24 @@ public class AuthController : ControllerBase
|
|||
if (!user.IsActive)
|
||||
return Unauthorized("Usuário desativado.");
|
||||
|
||||
if (await _userManager.IsLockedOutAsync(user))
|
||||
return Unauthorized("Usuário temporariamente bloqueado por tentativas inválidas. Tente novamente em alguns minutos.");
|
||||
|
||||
var valid = await _userManager.CheckPasswordAsync(user, password);
|
||||
if (!valid)
|
||||
{
|
||||
if (user.LockoutEnabled)
|
||||
{
|
||||
await _userManager.AccessFailedAsync(user);
|
||||
}
|
||||
|
||||
return Unauthorized("Credenciais inválidas.");
|
||||
}
|
||||
|
||||
if (user.LockoutEnabled)
|
||||
{
|
||||
await _userManager.ResetAccessFailedCountAsync(user);
|
||||
}
|
||||
|
||||
var effectiveTenantId = await EnsureValidTenantIdAsync(user);
|
||||
if (!effectiveTenantId.HasValue)
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ namespace line_gestao_api.Controllers
|
|||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(Roles = "admin,gestor")]
|
||||
public async Task<ActionResult<ChipVirgemDetailDto>> Create([FromBody] CreateChipVirgemDto req)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
|
|
|||
|
|
@ -138,6 +138,7 @@ namespace line_gestao_api.Controllers
|
|||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(Roles = "admin,gestor")]
|
||||
public async Task<ActionResult<ControleRecebidoDetailDto>> Create([FromBody] CreateControleRecebidoDto req)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
|
|
|||
|
|
@ -686,6 +686,7 @@ namespace line_gestao_api.Controllers
|
|||
// ✅ 5. CREATE
|
||||
// ==========================================================
|
||||
[HttpPost]
|
||||
[Authorize(Roles = "admin,gestor")]
|
||||
public async Task<ActionResult<MobileLineDetailDto>> Create([FromBody] CreateMobileLineDto req)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(req.Cliente))
|
||||
|
|
@ -788,6 +789,7 @@ namespace line_gestao_api.Controllers
|
|||
// ✅ 6. UPDATE
|
||||
// ==========================================================
|
||||
[HttpPut("{id:guid}")]
|
||||
[Authorize(Roles = "admin,gestor")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateMobileLineRequest req)
|
||||
{
|
||||
var x = await _db.MobileLines.FirstOrDefaultAsync(a => a.Id == id);
|
||||
|
|
@ -3649,19 +3651,124 @@ namespace line_gestao_api.Controllers
|
|||
return new DateTime(dt.Year, dt.Month, dt.Day, 12, 0, 0, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
private static decimal? TryDecimal(string? s)
|
||||
private static decimal? TryDecimal(string? input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(s)) return null;
|
||||
if (string.IsNullOrWhiteSpace(input)) return null;
|
||||
|
||||
s = s.Replace("R$", "", StringComparison.OrdinalIgnoreCase).Trim();
|
||||
var normalized = NormalizeDecimalInput(input);
|
||||
if (string.IsNullOrWhiteSpace(normalized)) return null;
|
||||
|
||||
if (decimal.TryParse(s, NumberStyles.Any, new CultureInfo("pt-BR"), out var d)) return d;
|
||||
if (decimal.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out d)) return d;
|
||||
return decimal.TryParse(normalized, NumberStyles.Float, CultureInfo.InvariantCulture, out var value)
|
||||
? value
|
||||
: null;
|
||||
}
|
||||
|
||||
var s2 = s.Replace(".", "").Replace(",", ".");
|
||||
if (decimal.TryParse(s2, NumberStyles.Any, CultureInfo.InvariantCulture, out d)) return d;
|
||||
private static string NormalizeDecimalInput(string raw)
|
||||
{
|
||||
var s = raw
|
||||
.Replace("R$", "", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("%", "", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace(" ", "")
|
||||
.Replace("\u00A0", "")
|
||||
.Trim();
|
||||
|
||||
return null;
|
||||
if (string.IsNullOrWhiteSpace(s)) return string.Empty;
|
||||
|
||||
var negativeByParentheses = s.StartsWith("(") && s.EndsWith(")");
|
||||
if (negativeByParentheses && s.Length >= 2)
|
||||
{
|
||||
s = s[1..^1];
|
||||
}
|
||||
|
||||
var allowed = new StringBuilder(s.Length);
|
||||
foreach (var ch in s)
|
||||
{
|
||||
if (char.IsDigit(ch) || ch == '.' || ch == ',' || ch == '-' || ch == '+' || ch == 'e' || ch == 'E')
|
||||
{
|
||||
allowed.Append(ch);
|
||||
}
|
||||
}
|
||||
|
||||
s = allowed.ToString();
|
||||
if (string.IsNullOrWhiteSpace(s)) return string.Empty;
|
||||
|
||||
string normalized;
|
||||
var commaCount = s.Count(c => c == ',');
|
||||
var dotCount = s.Count(c => c == '.');
|
||||
|
||||
if (s.IndexOf('e') >= 0 || s.IndexOf('E') >= 0)
|
||||
{
|
||||
normalized = s.Replace(",", ".");
|
||||
}
|
||||
else if (commaCount > 0 && dotCount > 0)
|
||||
{
|
||||
var lastComma = s.LastIndexOf(',');
|
||||
var lastDot = s.LastIndexOf('.');
|
||||
|
||||
if (lastComma > lastDot)
|
||||
{
|
||||
// Ex.: 1.234,56 -> 1234.56
|
||||
normalized = s.Replace(".", "").Replace(",", ".");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Ex.: 1,234.56 -> 1234.56
|
||||
normalized = s.Replace(",", "");
|
||||
}
|
||||
}
|
||||
else if (commaCount > 0)
|
||||
{
|
||||
normalized = NormalizeSingleSeparatorNumber(s, ',');
|
||||
}
|
||||
else if (dotCount > 0)
|
||||
{
|
||||
normalized = NormalizeSingleSeparatorNumber(s, '.');
|
||||
}
|
||||
else
|
||||
{
|
||||
normalized = s;
|
||||
}
|
||||
|
||||
if (negativeByParentheses && !normalized.StartsWith("-"))
|
||||
{
|
||||
normalized = "-" + normalized;
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static string NormalizeSingleSeparatorNumber(string value, char separator)
|
||||
{
|
||||
var separatorCount = value.Count(c => c == separator);
|
||||
if (separatorCount <= 0) return value;
|
||||
|
||||
if (separatorCount == 1)
|
||||
{
|
||||
var idx = value.IndexOf(separator);
|
||||
var leftDigits = value[..idx].Count(char.IsDigit);
|
||||
var rightDigits = value[(idx + 1)..].Count(char.IsDigit);
|
||||
|
||||
// "1.234"/"1,234" normalmente representa milhar.
|
||||
var looksLikeThousandsSeparator = rightDigits == 3 && leftDigits > 0 && leftDigits <= 3;
|
||||
if (looksLikeThousandsSeparator)
|
||||
{
|
||||
return value.Replace(separator.ToString(), "");
|
||||
}
|
||||
|
||||
return separator == ',' ? value.Replace(",", ".") : value;
|
||||
}
|
||||
|
||||
var parts = value.Split(separator);
|
||||
var allGroupsAreThousands = parts.Skip(1).All(p => p.Length == 3 && p.All(char.IsDigit));
|
||||
if (allGroupsAreThousands)
|
||||
{
|
||||
return value.Replace(separator.ToString(), "");
|
||||
}
|
||||
|
||||
var last = value.LastIndexOf(separator);
|
||||
var whole = value[..last].Replace(separator.ToString(), "");
|
||||
var fraction = value[(last + 1)..];
|
||||
return $"{whole}.{fraction}";
|
||||
}
|
||||
|
||||
private static int TryInt(string s)
|
||||
|
|
|
|||
|
|
@ -187,6 +187,7 @@ namespace line_gestao_api.Controllers
|
|||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(Roles = "admin,gestor")]
|
||||
public async Task<ActionResult<MuregDetailDto>> Create([FromBody] CreateMuregDto req)
|
||||
{
|
||||
if (req.MobileLineId == Guid.Empty)
|
||||
|
|
@ -288,6 +289,7 @@ namespace line_gestao_api.Controllers
|
|||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[Authorize(Roles = "admin,gestor")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateMuregDto req)
|
||||
{
|
||||
var entity = await _db.MuregLines.FirstOrDefaultAsync(x => x.Id == id);
|
||||
|
|
@ -375,6 +377,7 @@ namespace line_gestao_api.Controllers
|
|||
// ✅ POST: /api/mureg/import-excel (mantido)
|
||||
// ==========================================================
|
||||
[HttpPost("import-excel")]
|
||||
[Authorize(Roles = "admin,gestor")]
|
||||
[Consumes("multipart/form-data")]
|
||||
[RequestSizeLimit(50_000_000)]
|
||||
public async Task<IActionResult> ImportExcel([FromForm] ImportExcelForm form)
|
||||
|
|
|
|||
|
|
@ -66,6 +66,12 @@ public class NotificationsController : ControllerBase
|
|||
})
|
||||
.ToListAsync();
|
||||
|
||||
var todayUtc = DateTime.SpecifyKind(DateTime.UtcNow.Date, DateTimeKind.Utc);
|
||||
foreach (var item in items)
|
||||
{
|
||||
ApplyEffectiveType(item, todayUtc);
|
||||
}
|
||||
|
||||
return Ok(items);
|
||||
}
|
||||
|
||||
|
|
@ -160,6 +166,11 @@ public class NotificationsController : ControllerBase
|
|||
notification.Tipo))
|
||||
.ToListAsync();
|
||||
|
||||
var todayUtc = DateTime.SpecifyKind(DateTime.UtcNow.Date, DateTimeKind.Utc);
|
||||
rows = rows
|
||||
.Select(r => r with { Tipo = ResolveEffectiveType(r.Tipo, r.DataReferencia, todayUtc) })
|
||||
.ToList();
|
||||
|
||||
using var workbook = new XLWorkbook();
|
||||
var worksheet = workbook.Worksheets.Add("Notificacoes");
|
||||
|
||||
|
|
@ -291,6 +302,38 @@ public class NotificationsController : ControllerBase
|
|||
return filter?.Trim().ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static void ApplyEffectiveType(NotificationDto item, DateTime todayUtc)
|
||||
{
|
||||
item.Tipo = ResolveEffectiveType(item.Tipo, item.DtTerminoFidelizacao ?? item.ReferenciaData, todayUtc);
|
||||
var effectiveDate = item.DtTerminoFidelizacao ?? item.ReferenciaData;
|
||||
if (!effectiveDate.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var endDateUtc = DateTime.SpecifyKind(effectiveDate.Value.Date, DateTimeKind.Utc);
|
||||
var daysUntil = (endDateUtc - todayUtc).Days;
|
||||
item.DiasParaVencer = daysUntil < 0 ? 0 : daysUntil;
|
||||
}
|
||||
|
||||
private static string ResolveEffectiveType(string currentType, DateTime? referenceDate, DateTime todayUtc)
|
||||
{
|
||||
if (!referenceDate.HasValue)
|
||||
{
|
||||
return currentType;
|
||||
}
|
||||
|
||||
var isKnownType = currentType.Equals("AVencer", StringComparison.OrdinalIgnoreCase)
|
||||
|| currentType.Equals("Vencido", StringComparison.OrdinalIgnoreCase);
|
||||
if (!isKnownType)
|
||||
{
|
||||
return currentType;
|
||||
}
|
||||
|
||||
var endDateUtc = DateTime.SpecifyKind(referenceDate.Value.Date, DateTimeKind.Utc);
|
||||
return endDateUtc < todayUtc ? "Vencido" : "AVencer";
|
||||
}
|
||||
|
||||
private sealed record NotificationExportRow(
|
||||
string? Conta,
|
||||
string? Linha,
|
||||
|
|
|
|||
|
|
@ -165,6 +165,7 @@ public class ParcelamentosController : ControllerBase
|
|||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(Roles = "admin,gestor")]
|
||||
public async Task<ActionResult<ParcelamentoDetailDto>> Create([FromBody] ParcelamentoUpsertDto req)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
|
@ -201,6 +202,7 @@ public class ParcelamentosController : ControllerBase
|
|||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[Authorize(Roles = "admin,gestor")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] ParcelamentoUpsertDto req)
|
||||
{
|
||||
var entity = await _db.ParcelamentoLines
|
||||
|
|
|
|||
|
|
@ -112,6 +112,7 @@ namespace line_gestao_api.Controllers
|
|||
// ✅ CREATE
|
||||
// ==========================================================
|
||||
[HttpPost]
|
||||
[Authorize(Roles = "admin,gestor")]
|
||||
public async Task<ActionResult<TrocaNumeroDetailDto>> Create([FromBody] CreateTrocaNumeroDto req)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
|
@ -140,6 +141,7 @@ namespace line_gestao_api.Controllers
|
|||
// ✅ UPDATE
|
||||
// ==========================================================
|
||||
[HttpPut("{id:guid}")]
|
||||
[Authorize(Roles = "admin,gestor")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateTrocaNumeroRequest req)
|
||||
{
|
||||
var x = await _db.TrocaNumeroLines.FirstOrDefaultAsync(a => a.Id == id);
|
||||
|
|
|
|||
|
|
@ -8,10 +8,12 @@ namespace line_gestao_api.Data;
|
|||
|
||||
public class SeedOptions
|
||||
{
|
||||
public bool Enabled { get; set; } = true;
|
||||
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; } = "Admin123!";
|
||||
public string AdminPassword { get; set; } = "DevAdmin123!";
|
||||
public bool ReapplyAdminCredentialsOnStartup { get; set; } = false;
|
||||
}
|
||||
|
||||
public static class SeedData
|
||||
|
|
@ -29,6 +31,11 @@ public static class SeedData
|
|||
|
||||
await db.Database.MigrateAsync();
|
||||
|
||||
if (!options.Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var roles = new[] { "admin", "gestor", "operador", "leitura" };
|
||||
foreach (var role in roles)
|
||||
{
|
||||
|
|
@ -69,7 +76,8 @@ public static class SeedData
|
|||
Name = options.AdminName,
|
||||
TenantId = tenant.Id,
|
||||
EmailConfirmed = true,
|
||||
IsActive = true
|
||||
IsActive = true,
|
||||
LockoutEnabled = true
|
||||
};
|
||||
|
||||
var createResult = await userManager.CreateAsync(adminUser, options.AdminPassword);
|
||||
|
|
@ -78,21 +86,28 @@ public static class SeedData
|
|||
await userManager.AddToRoleAsync(adminUser, "admin");
|
||||
}
|
||||
}
|
||||
else
|
||||
else if (options.ReapplyAdminCredentialsOnStartup)
|
||||
{
|
||||
existingAdmin.UserName = options.AdminEmail;
|
||||
existingAdmin.Email = options.AdminEmail;
|
||||
existingAdmin.Name = options.AdminName;
|
||||
existingAdmin.TenantId = tenant.Id;
|
||||
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);
|
||||
|
||||
if (!await userManager.CheckPasswordAsync(existingAdmin, options.AdminPassword))
|
||||
{
|
||||
var resetToken = await userManager.GeneratePasswordResetTokenAsync(existingAdmin);
|
||||
await userManager.ResetPasswordAsync(existingAdmin, resetToken, options.AdminPassword);
|
||||
var resetPasswordResult = await userManager.ResetPasswordAsync(existingAdmin, resetToken, options.AdminPassword);
|
||||
if (!resetPasswordResult.Succeeded)
|
||||
{
|
||||
var removePasswordResult = await userManager.RemovePasswordAsync(existingAdmin);
|
||||
if (removePasswordResult.Succeeded)
|
||||
{
|
||||
await userManager.AddPasswordAsync(existingAdmin, options.AdminPassword);
|
||||
}
|
||||
}
|
||||
|
||||
if (!await userManager.IsInRoleAsync(existingAdmin, "admin"))
|
||||
|
|
|
|||
|
|
@ -16,6 +16,15 @@ namespace line_gestao_api.Dtos
|
|||
public GeralDashboardVivoKpiDto Vivo { get; set; } = new();
|
||||
public GeralDashboardTravelKpiDto TravelMundo { get; set; } = new();
|
||||
public GeralDashboardAdditionalKpiDto Adicionais { get; set; } = new();
|
||||
public List<GeralDashboardLineTotalDto> TotaisLine { get; set; } = new();
|
||||
}
|
||||
|
||||
public class GeralDashboardLineTotalDto
|
||||
{
|
||||
public string Tipo { get; set; } = string.Empty;
|
||||
public int QtdLinhas { get; set; }
|
||||
public decimal ValorTotalLine { get; set; }
|
||||
public decimal LucroTotalLine { get; set; }
|
||||
}
|
||||
|
||||
public class GeralDashboardVivoKpiDto
|
||||
|
|
|
|||
72
Program.cs
72
Program.cs
|
|
@ -1,17 +1,29 @@
|
|||
using System.Text;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.RateLimiting;
|
||||
using line_gestao_api.Data;
|
||||
using line_gestao_api.Models;
|
||||
using line_gestao_api.Services;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Configuration.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: true);
|
||||
|
||||
var dataProtectionKeyPath = builder.Environment.IsProduction()
|
||||
? "/var/www/html/line-gestao-api/publish/.aspnet-keys"
|
||||
: Path.Combine(builder.Environment.ContentRootPath, ".aspnet-keys");
|
||||
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddDataProtection()
|
||||
.PersistKeysToFileSystem(new DirectoryInfo(dataProtectionKeyPath))
|
||||
.SetApplicationName("line-gestao-api");
|
||||
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||||
{
|
||||
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
||||
|
|
@ -24,7 +36,7 @@ builder.Services.Configure<FormOptions>(o =>
|
|||
o.MultipartBodyLengthLimit = 50_000_000;
|
||||
});
|
||||
|
||||
var corsOrigins = builder.Configuration
|
||||
var configuredCorsOrigins = builder.Configuration
|
||||
.GetSection("Cors:AllowedOrigins")
|
||||
.Get<string[]>()?
|
||||
.Where(o => !string.IsNullOrWhiteSpace(o))
|
||||
|
|
@ -33,18 +45,43 @@ var corsOrigins = builder.Configuration
|
|||
.ToArray()
|
||||
?? [];
|
||||
|
||||
if (corsOrigins.Length == 0)
|
||||
var allowAnyCorsOrigin = configuredCorsOrigins.Any(o => o == "*");
|
||||
var corsOrigins = configuredCorsOrigins
|
||||
.Where(o => o != "*")
|
||||
.ToArray();
|
||||
|
||||
var isProduction = builder.Environment.IsProduction();
|
||||
if (isProduction && allowAnyCorsOrigin)
|
||||
{
|
||||
throw new InvalidOperationException("CORS with wildcard '*' is not allowed in production. Configure explicit origins in 'Cors:AllowedOrigins'.");
|
||||
}
|
||||
|
||||
if (!allowAnyCorsOrigin && corsOrigins.Length == 0)
|
||||
{
|
||||
if (isProduction)
|
||||
{
|
||||
throw new InvalidOperationException("No CORS origins configured for production. Set 'Cors:AllowedOrigins' with explicit trusted origins.");
|
||||
}
|
||||
|
||||
corsOrigins = ["http://localhost:4200"];
|
||||
}
|
||||
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("Front", p =>
|
||||
{
|
||||
if (allowAnyCorsOrigin)
|
||||
{
|
||||
p.AllowAnyOrigin()
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod();
|
||||
return;
|
||||
}
|
||||
|
||||
p.WithOrigins(corsOrigins)
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod()
|
||||
);
|
||||
.AllowAnyMethod();
|
||||
});
|
||||
});
|
||||
|
||||
builder.Services.AddDbContext<AppDbContext>(options =>
|
||||
|
|
@ -63,6 +100,9 @@ builder.Services.AddIdentityCore<ApplicationUser>(options =>
|
|||
{
|
||||
options.Password.RequiredLength = 6;
|
||||
options.User.RequireUniqueEmail = false;
|
||||
options.Lockout.AllowedForNewUsers = true;
|
||||
options.Lockout.MaxFailedAccessAttempts = 5;
|
||||
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
|
||||
})
|
||||
.AddRoles<IdentityRole<Guid>>()
|
||||
.AddEntityFrameworkStores<AppDbContext>()
|
||||
|
|
@ -77,6 +117,12 @@ if (string.IsNullOrWhiteSpace(jwtKey))
|
|||
throw new InvalidOperationException("Configuration 'Jwt:Key' is required.");
|
||||
}
|
||||
|
||||
var jwtKeyBytes = Encoding.UTF8.GetByteCount(jwtKey);
|
||||
if (jwtKeyBytes < 16)
|
||||
{
|
||||
throw new InvalidOperationException($"Configuration 'Jwt:Key' must be at least 16 bytes (128 bits). Current length: {jwtKeyBytes} bytes.");
|
||||
}
|
||||
|
||||
var issuer = builder.Configuration["Jwt:Issuer"];
|
||||
var audience = builder.Configuration["Jwt:Audience"];
|
||||
|
||||
|
|
@ -97,6 +143,21 @@ builder.Services
|
|||
});
|
||||
|
||||
builder.Services.AddAuthorization();
|
||||
builder.Services.AddRateLimiter(options =>
|
||||
{
|
||||
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
|
||||
options.AddPolicy("auth-login", httpContext =>
|
||||
RateLimitPartition.GetFixedWindowLimiter(
|
||||
partitionKey: httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown",
|
||||
factory: _ => new FixedWindowRateLimiterOptions
|
||||
{
|
||||
PermitLimit = 8,
|
||||
Window = TimeSpan.FromMinutes(1),
|
||||
QueueLimit = 0,
|
||||
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
|
||||
AutoReplenishment = true
|
||||
}));
|
||||
});
|
||||
|
||||
builder.Services.Configure<NotificationOptions>(builder.Configuration.GetSection("Notifications"));
|
||||
builder.Services.AddHostedService<VigenciaNotificationBackgroundService>();
|
||||
|
|
@ -119,6 +180,7 @@ if (useHttpsRedirection)
|
|||
}
|
||||
|
||||
app.UseCors("Front");
|
||||
app.UseRateLimiter();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseMiddleware<TenantMiddleware>();
|
||||
|
|
|
|||
|
|
@ -35,6 +35,32 @@ namespace line_gestao_api.Services
|
|||
{
|
||||
TotalLinhas = g.Count(),
|
||||
TotalAtivas = g.Count(x => (x.Status ?? "").ToLower().Contains("ativo")),
|
||||
PfAtivas = g.Count(x =>
|
||||
(x.Status ?? "").ToLower().Contains("ativo") &&
|
||||
((x.Skil ?? "").ToLower().Contains("fís") || (x.Skil ?? "").ToLower().Contains("fis") || (x.Skil ?? "").ToLower().Contains("pf"))),
|
||||
PjAtivas = g.Count(x =>
|
||||
(x.Status ?? "").ToLower().Contains("ativo") &&
|
||||
((x.Skil ?? "").ToLower().Contains("jur") || (x.Skil ?? "").ToLower().Contains("pj"))),
|
||||
PfValorTotalLineAtivas = g.Sum(x =>
|
||||
(x.Status ?? "").ToLower().Contains("ativo") &&
|
||||
((x.Skil ?? "").ToLower().Contains("fís") || (x.Skil ?? "").ToLower().Contains("fis") || (x.Skil ?? "").ToLower().Contains("pf"))
|
||||
? (x.ValorContratoLine ?? 0m)
|
||||
: 0m),
|
||||
PjValorTotalLineAtivas = g.Sum(x =>
|
||||
(x.Status ?? "").ToLower().Contains("ativo") &&
|
||||
((x.Skil ?? "").ToLower().Contains("jur") || (x.Skil ?? "").ToLower().Contains("pj"))
|
||||
? (x.ValorContratoLine ?? 0m)
|
||||
: 0m),
|
||||
PfLucroTotalLineAtivas = g.Sum(x =>
|
||||
(x.Status ?? "").ToLower().Contains("ativo") &&
|
||||
((x.Skil ?? "").ToLower().Contains("fís") || (x.Skil ?? "").ToLower().Contains("fis") || (x.Skil ?? "").ToLower().Contains("pf"))
|
||||
? (x.Lucro ?? 0m)
|
||||
: 0m),
|
||||
PjLucroTotalLineAtivas = g.Sum(x =>
|
||||
(x.Status ?? "").ToLower().Contains("ativo") &&
|
||||
((x.Skil ?? "").ToLower().Contains("jur") || (x.Skil ?? "").ToLower().Contains("pj"))
|
||||
? (x.Lucro ?? 0m)
|
||||
: 0m),
|
||||
TotalBloqueados = g.Count(x =>
|
||||
(x.Status ?? "").ToLower().Contains("bloque") ||
|
||||
(x.Status ?? "").ToLower().Contains("perda") ||
|
||||
|
|
@ -662,7 +688,8 @@ namespace line_gestao_api.Services
|
|||
new() { ServiceName = ServiceVivoSync, CountLines = totals.NotPaidSync, TotalValue = 0m },
|
||||
new() { ServiceName = ServiceVivoGestaoDispositivo, CountLines = totals.NotPaidGestaoDispositivo, TotalValue = 0m }
|
||||
}
|
||||
}
|
||||
},
|
||||
TotaisLine = BuildTotaisLineRows(totals)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -730,6 +757,43 @@ namespace line_gestao_api.Services
|
|||
};
|
||||
}
|
||||
|
||||
private static List<GeralDashboardLineTotalDto> BuildTotaisLineRows(TotalsProjection? totals)
|
||||
{
|
||||
if (totals == null)
|
||||
{
|
||||
return new List<GeralDashboardLineTotalDto>();
|
||||
}
|
||||
|
||||
var diffQtd = totals.PjAtivas - totals.PfAtivas;
|
||||
var diffValor = totals.PjValorTotalLineAtivas - totals.PfValorTotalLineAtivas;
|
||||
var diffLucro = totals.PjLucroTotalLineAtivas - totals.PfLucroTotalLineAtivas;
|
||||
|
||||
return new List<GeralDashboardLineTotalDto>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Tipo = "PESSOA FISICA",
|
||||
QtdLinhas = totals.PfAtivas,
|
||||
ValorTotalLine = totals.PfValorTotalLineAtivas,
|
||||
LucroTotalLine = totals.PfLucroTotalLineAtivas
|
||||
},
|
||||
new()
|
||||
{
|
||||
Tipo = "PESSOA JURIDICA",
|
||||
QtdLinhas = totals.PjAtivas,
|
||||
ValorTotalLine = totals.PjValorTotalLineAtivas,
|
||||
LucroTotalLine = totals.PjLucroTotalLineAtivas
|
||||
},
|
||||
new()
|
||||
{
|
||||
Tipo = "DIFERENCA PJ X PF",
|
||||
QtdLinhas = diffQtd,
|
||||
ValorTotalLine = diffValor,
|
||||
LucroTotalLine = diffLucro
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static List<GeralDashboardClientGroupDto> BuildClientGroups(IEnumerable<ClientGroupProjection> rows)
|
||||
{
|
||||
var list = new List<GeralDashboardClientGroupDto>();
|
||||
|
|
@ -1037,6 +1101,12 @@ namespace line_gestao_api.Services
|
|||
{
|
||||
public int TotalLinhas { get; set; }
|
||||
public int TotalAtivas { get; set; }
|
||||
public int PfAtivas { get; set; }
|
||||
public int PjAtivas { get; set; }
|
||||
public decimal PfValorTotalLineAtivas { get; set; }
|
||||
public decimal PjValorTotalLineAtivas { get; set; }
|
||||
public decimal PfLucroTotalLineAtivas { get; set; }
|
||||
public decimal PjLucroTotalLineAtivas { get; set; }
|
||||
public int TotalBloqueados { get; set; }
|
||||
public int VivoLinhas { get; set; }
|
||||
public decimal VivoFranquiaTotalGb { get; set; }
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"ConnectionStrings": {
|
||||
"Default": "Host=localhost;Port=5432;Database=linegestao;Username=linegestao_app;Password=255851Ed@"
|
||||
"Default": "Host=localhost;Port=5432;Database=linegestao;Username=linegestao_app;Password=1231234512Ed@"
|
||||
},
|
||||
"Jwt": {
|
||||
"Key": "dev-only-please-replace-with-env-variable-in-production",
|
||||
"Key": "linegestao-local-jwt-key-2026-dev-strong-123456789",
|
||||
"Issuer": "LineGestao",
|
||||
"Audience": "LineGestao",
|
||||
"ExpiresMinutes": 360
|
||||
|
|
@ -28,9 +28,11 @@
|
|||
"ReminderDays": [30, 15, 7]
|
||||
},
|
||||
"Seed": {
|
||||
"Enabled": true,
|
||||
"ReapplyAdminCredentialsOnStartup": true,
|
||||
"DefaultTenantName": "Default",
|
||||
"AdminName": "Administrador",
|
||||
"AdminEmail": "admin@linegestao.local",
|
||||
"AdminPassword": "Admin123!"
|
||||
"AdminPassword": "DevAdmin123!"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"ConnectionStrings": {
|
||||
"Default": "Host=localhost;Port=5432;Database=linegestao;Username=linegestao_app;Password=1231234512Ed@"
|
||||
},
|
||||
"Jwt": {
|
||||
"Key": "YOUR_LOCAL_STRONG_JWT_KEY_MIN_32_CHARS",
|
||||
"Issuer": "LineGestao",
|
||||
"Audience": "LineGestao",
|
||||
"ExpiresMinutes": 360
|
||||
},
|
||||
"Seed": {
|
||||
"Enabled": true,
|
||||
"ReapplyAdminCredentialsOnStartup": false,
|
||||
"DefaultTenantName": "Default",
|
||||
"AdminName": "Administrador",
|
||||
"AdminEmail": "admin@linegestao.local",
|
||||
"AdminPassword": "YOUR_LOCAL_ADMIN_SEED_PASSWORD"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +1,38 @@
|
|||
{
|
||||
"ConnectionStrings": {
|
||||
"Default": "Host=localhost;Port=5432;Database=linegestao;Username=linegestao_app;Password=CHANGE_ME"
|
||||
"Default": "Host=localhost;Port=5432;Database=linegestao;Username=linegestao_app;Password=1231234512Ed@"
|
||||
},
|
||||
"Jwt": {
|
||||
"Key": "",
|
||||
"Key": "linegestao-local-jwt-key-2026-dev-strong-123456789",
|
||||
"Issuer": "LineGestao",
|
||||
"Audience": "LineGestao",
|
||||
"ExpiresMinutes": 360
|
||||
},
|
||||
"Cors": {
|
||||
"AllowedOrigins": [ "http://localhost:4200" ]
|
||||
"AllowedOrigins": [
|
||||
"http://localhost:4200"
|
||||
]
|
||||
},
|
||||
"App": {
|
||||
"UseHttpsRedirection": true
|
||||
"UseHttpsRedirection": false
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"Notifications": {
|
||||
"CheckIntervalMinutes": 60,
|
||||
"NotifyAllFutureDates": true,
|
||||
"ReminderDays": [30, 15, 7]
|
||||
"ReminderDays": [ 30, 15, 7 ]
|
||||
},
|
||||
"Seed": {
|
||||
"Enabled": true,
|
||||
"ReapplyAdminCredentialsOnStartup": true,
|
||||
"DefaultTenantName": "Default",
|
||||
"AdminName": "Administrador",
|
||||
"AdminEmail": "admin@linegestao.local",
|
||||
"AdminPassword": "CHANGE_ME"
|
||||
"AdminPassword": "DevAdmin123!"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,13 @@
|
|||
<None Remove="line-gestao-api.Tests\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="publish\**" />
|
||||
<Content Remove="publish\**" />
|
||||
<EmbeddedResource Remove="publish\**" />
|
||||
<None Remove="publish\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ClosedXML" Version="0.105.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.1" />
|
||||
|
|
|
|||
Loading…
Reference in New Issue