Branch Produção

This commit is contained in:
Eduardo 2026-02-11 16:19:30 -03:00
parent dc3351a5f8
commit 2872de9f4b
18 changed files with 421 additions and 33 deletions

View File

@ -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>

1
.gitignore vendored
View File

@ -5,6 +5,7 @@
# dotenv files # dotenv files
.env .env
appsettings.Local.json
# User-specific files # User-specific files
*.rsuser *.rsuser

View File

@ -8,6 +8,7 @@ using line_gestao_api.Services;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
@ -60,7 +61,8 @@ public class AuthController : ControllerBase
UserName = email, UserName = email,
TenantId = tenantId.Value, TenantId = tenantId.Value,
IsActive = true, IsActive = true,
EmailConfirmed = true EmailConfirmed = true,
LockoutEnabled = true
}; };
var createResult = await _userManager.CreateAsync(user, req.Password); var createResult = await _userManager.CreateAsync(user, req.Password);
@ -78,6 +80,7 @@ public class AuthController : ControllerBase
} }
[HttpPost("login")] [HttpPost("login")]
[EnableRateLimiting("auth-login")]
public async Task<IActionResult> Login(LoginRequest req) public async Task<IActionResult> Login(LoginRequest req)
{ {
var email = (req.Email ?? "").Trim().ToLowerInvariant(); var email = (req.Email ?? "").Trim().ToLowerInvariant();
@ -115,9 +118,24 @@ public class AuthController : ControllerBase
if (!user.IsActive) if (!user.IsActive)
return Unauthorized("Usuário desativado."); 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); var valid = await _userManager.CheckPasswordAsync(user, password);
if (!valid) if (!valid)
{
if (user.LockoutEnabled)
{
await _userManager.AccessFailedAsync(user);
}
return Unauthorized("Credenciais inválidas."); return Unauthorized("Credenciais inválidas.");
}
if (user.LockoutEnabled)
{
await _userManager.ResetAccessFailedCountAsync(user);
}
var effectiveTenantId = await EnsureValidTenantIdAsync(user); var effectiveTenantId = await EnsureValidTenantIdAsync(user);
if (!effectiveTenantId.HasValue) if (!effectiveTenantId.HasValue)

View File

@ -93,6 +93,7 @@ namespace line_gestao_api.Controllers
} }
[HttpPost] [HttpPost]
[Authorize(Roles = "admin,gestor")]
public async Task<ActionResult<ChipVirgemDetailDto>> Create([FromBody] CreateChipVirgemDto req) public async Task<ActionResult<ChipVirgemDetailDto>> Create([FromBody] CreateChipVirgemDto req)
{ {
var now = DateTime.UtcNow; var now = DateTime.UtcNow;

View File

@ -138,6 +138,7 @@ namespace line_gestao_api.Controllers
} }
[HttpPost] [HttpPost]
[Authorize(Roles = "admin,gestor")]
public async Task<ActionResult<ControleRecebidoDetailDto>> Create([FromBody] CreateControleRecebidoDto req) public async Task<ActionResult<ControleRecebidoDetailDto>> Create([FromBody] CreateControleRecebidoDto req)
{ {
var now = DateTime.UtcNow; var now = DateTime.UtcNow;

View File

@ -686,6 +686,7 @@ namespace line_gestao_api.Controllers
// ✅ 5. CREATE // ✅ 5. CREATE
// ========================================================== // ==========================================================
[HttpPost] [HttpPost]
[Authorize(Roles = "admin,gestor")]
public async Task<ActionResult<MobileLineDetailDto>> Create([FromBody] CreateMobileLineDto req) public async Task<ActionResult<MobileLineDetailDto>> Create([FromBody] CreateMobileLineDto req)
{ {
if (string.IsNullOrWhiteSpace(req.Cliente)) if (string.IsNullOrWhiteSpace(req.Cliente))
@ -788,6 +789,7 @@ namespace line_gestao_api.Controllers
// ✅ 6. UPDATE // ✅ 6. UPDATE
// ========================================================== // ==========================================================
[HttpPut("{id:guid}")] [HttpPut("{id:guid}")]
[Authorize(Roles = "admin,gestor")]
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateMobileLineRequest req) public async Task<IActionResult> Update(Guid id, [FromBody] UpdateMobileLineRequest req)
{ {
var x = await _db.MobileLines.FirstOrDefaultAsync(a => a.Id == id); 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); 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; return decimal.TryParse(normalized, NumberStyles.Float, CultureInfo.InvariantCulture, out var value)
if (decimal.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out d)) return d; ? value
: null;
}
var s2 = s.Replace(".", "").Replace(",", "."); private static string NormalizeDecimalInput(string raw)
if (decimal.TryParse(s2, NumberStyles.Any, CultureInfo.InvariantCulture, out d)) return d; {
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) private static int TryInt(string s)

View File

@ -187,6 +187,7 @@ namespace line_gestao_api.Controllers
} }
[HttpPost] [HttpPost]
[Authorize(Roles = "admin,gestor")]
public async Task<ActionResult<MuregDetailDto>> Create([FromBody] CreateMuregDto req) public async Task<ActionResult<MuregDetailDto>> Create([FromBody] CreateMuregDto req)
{ {
if (req.MobileLineId == Guid.Empty) if (req.MobileLineId == Guid.Empty)
@ -288,6 +289,7 @@ namespace line_gestao_api.Controllers
} }
[HttpPut("{id:guid}")] [HttpPut("{id:guid}")]
[Authorize(Roles = "admin,gestor")]
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateMuregDto req) public async Task<IActionResult> Update(Guid id, [FromBody] UpdateMuregDto req)
{ {
var entity = await _db.MuregLines.FirstOrDefaultAsync(x => x.Id == id); 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) // ✅ POST: /api/mureg/import-excel (mantido)
// ========================================================== // ==========================================================
[HttpPost("import-excel")] [HttpPost("import-excel")]
[Authorize(Roles = "admin,gestor")]
[Consumes("multipart/form-data")] [Consumes("multipart/form-data")]
[RequestSizeLimit(50_000_000)] [RequestSizeLimit(50_000_000)]
public async Task<IActionResult> ImportExcel([FromForm] ImportExcelForm form) public async Task<IActionResult> ImportExcel([FromForm] ImportExcelForm form)

View File

@ -66,6 +66,12 @@ public class NotificationsController : ControllerBase
}) })
.ToListAsync(); .ToListAsync();
var todayUtc = DateTime.SpecifyKind(DateTime.UtcNow.Date, DateTimeKind.Utc);
foreach (var item in items)
{
ApplyEffectiveType(item, todayUtc);
}
return Ok(items); return Ok(items);
} }
@ -160,6 +166,11 @@ public class NotificationsController : ControllerBase
notification.Tipo)) notification.Tipo))
.ToListAsync(); .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(); using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Notificacoes"); var worksheet = workbook.Worksheets.Add("Notificacoes");
@ -291,6 +302,38 @@ public class NotificationsController : ControllerBase
return filter?.Trim().ToLowerInvariant(); 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( private sealed record NotificationExportRow(
string? Conta, string? Conta,
string? Linha, string? Linha,

View File

@ -165,6 +165,7 @@ public class ParcelamentosController : ControllerBase
} }
[HttpPost] [HttpPost]
[Authorize(Roles = "admin,gestor")]
public async Task<ActionResult<ParcelamentoDetailDto>> Create([FromBody] ParcelamentoUpsertDto req) public async Task<ActionResult<ParcelamentoDetailDto>> Create([FromBody] ParcelamentoUpsertDto req)
{ {
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
@ -201,6 +202,7 @@ public class ParcelamentosController : ControllerBase
} }
[HttpPut("{id:guid}")] [HttpPut("{id:guid}")]
[Authorize(Roles = "admin,gestor")]
public async Task<IActionResult> Update(Guid id, [FromBody] ParcelamentoUpsertDto req) public async Task<IActionResult> Update(Guid id, [FromBody] ParcelamentoUpsertDto req)
{ {
var entity = await _db.ParcelamentoLines var entity = await _db.ParcelamentoLines

View File

@ -112,6 +112,7 @@ namespace line_gestao_api.Controllers
// ✅ CREATE // ✅ CREATE
// ========================================================== // ==========================================================
[HttpPost] [HttpPost]
[Authorize(Roles = "admin,gestor")]
public async Task<ActionResult<TrocaNumeroDetailDto>> Create([FromBody] CreateTrocaNumeroDto req) public async Task<ActionResult<TrocaNumeroDetailDto>> Create([FromBody] CreateTrocaNumeroDto req)
{ {
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
@ -140,6 +141,7 @@ namespace line_gestao_api.Controllers
// ✅ UPDATE // ✅ UPDATE
// ========================================================== // ==========================================================
[HttpPut("{id:guid}")] [HttpPut("{id:guid}")]
[Authorize(Roles = "admin,gestor")]
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateTrocaNumeroRequest req) public async Task<IActionResult> Update(Guid id, [FromBody] UpdateTrocaNumeroRequest req)
{ {
var x = await _db.TrocaNumeroLines.FirstOrDefaultAsync(a => a.Id == id); var x = await _db.TrocaNumeroLines.FirstOrDefaultAsync(a => a.Id == id);

View File

@ -8,10 +8,12 @@ namespace line_gestao_api.Data;
public class SeedOptions public class SeedOptions
{ {
public bool Enabled { get; set; } = true;
public string DefaultTenantName { get; set; } = "Default"; public string DefaultTenantName { get; set; } = "Default";
public string AdminName { get; set; } = "Administrador"; public string AdminName { get; set; } = "Administrador";
public string AdminEmail { get; set; } = "admin@linegestao.local"; 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 public static class SeedData
@ -29,6 +31,11 @@ public static class SeedData
await db.Database.MigrateAsync(); await db.Database.MigrateAsync();
if (!options.Enabled)
{
return;
}
var roles = new[] { "admin", "gestor", "operador", "leitura" }; var roles = new[] { "admin", "gestor", "operador", "leitura" };
foreach (var role in roles) foreach (var role in roles)
{ {
@ -69,7 +76,8 @@ public static class SeedData
Name = options.AdminName, Name = options.AdminName,
TenantId = tenant.Id, TenantId = tenant.Id,
EmailConfirmed = true, EmailConfirmed = true,
IsActive = true IsActive = true,
LockoutEnabled = true
}; };
var createResult = await userManager.CreateAsync(adminUser, options.AdminPassword); var createResult = await userManager.CreateAsync(adminUser, options.AdminPassword);
@ -78,21 +86,28 @@ public static class SeedData
await userManager.AddToRoleAsync(adminUser, "admin"); await userManager.AddToRoleAsync(adminUser, "admin");
} }
} }
else else if (options.ReapplyAdminCredentialsOnStartup)
{ {
existingAdmin.UserName = options.AdminEmail;
existingAdmin.Email = options.AdminEmail;
existingAdmin.Name = options.AdminName; existingAdmin.Name = options.AdminName;
existingAdmin.TenantId = tenant.Id; existingAdmin.Email = options.AdminEmail;
existingAdmin.UserName = options.AdminEmail;
existingAdmin.EmailConfirmed = true; existingAdmin.EmailConfirmed = true;
existingAdmin.IsActive = true; existingAdmin.IsActive = true;
existingAdmin.LockoutEnabled = true;
await userManager.SetLockoutEndDateAsync(existingAdmin, null);
await userManager.ResetAccessFailedCountAsync(existingAdmin);
await userManager.UpdateAsync(existingAdmin); await userManager.UpdateAsync(existingAdmin);
if (!await userManager.CheckPasswordAsync(existingAdmin, options.AdminPassword)) var resetToken = await userManager.GeneratePasswordResetTokenAsync(existingAdmin);
var resetPasswordResult = await userManager.ResetPasswordAsync(existingAdmin, resetToken, options.AdminPassword);
if (!resetPasswordResult.Succeeded)
{ {
var resetToken = await userManager.GeneratePasswordResetTokenAsync(existingAdmin); var removePasswordResult = await userManager.RemovePasswordAsync(existingAdmin);
await userManager.ResetPasswordAsync(existingAdmin, resetToken, options.AdminPassword); if (removePasswordResult.Succeeded)
{
await userManager.AddPasswordAsync(existingAdmin, options.AdminPassword);
}
} }
if (!await userManager.IsInRoleAsync(existingAdmin, "admin")) if (!await userManager.IsInRoleAsync(existingAdmin, "admin"))

View File

@ -16,6 +16,15 @@ namespace line_gestao_api.Dtos
public GeralDashboardVivoKpiDto Vivo { get; set; } = new(); public GeralDashboardVivoKpiDto Vivo { get; set; } = new();
public GeralDashboardTravelKpiDto TravelMundo { get; set; } = new(); public GeralDashboardTravelKpiDto TravelMundo { get; set; } = new();
public GeralDashboardAdditionalKpiDto Adicionais { 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 public class GeralDashboardVivoKpiDto

View File

@ -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.Data;
using line_gestao_api.Models; using line_gestao_api.Models;
using line_gestao_api.Services; using line_gestao_api.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args); 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.AddControllers();
builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(dataProtectionKeyPath))
.SetApplicationName("line-gestao-api");
builder.Services.Configure<ForwardedHeadersOptions>(options => builder.Services.Configure<ForwardedHeadersOptions>(options =>
{ {
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
@ -24,7 +36,7 @@ builder.Services.Configure<FormOptions>(o =>
o.MultipartBodyLengthLimit = 50_000_000; o.MultipartBodyLengthLimit = 50_000_000;
}); });
var corsOrigins = builder.Configuration var configuredCorsOrigins = builder.Configuration
.GetSection("Cors:AllowedOrigins") .GetSection("Cors:AllowedOrigins")
.Get<string[]>()? .Get<string[]>()?
.Where(o => !string.IsNullOrWhiteSpace(o)) .Where(o => !string.IsNullOrWhiteSpace(o))
@ -33,18 +45,43 @@ var corsOrigins = builder.Configuration
.ToArray() .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"]; corsOrigins = ["http://localhost:4200"];
} }
builder.Services.AddCors(options => builder.Services.AddCors(options =>
{ {
options.AddPolicy("Front", p => options.AddPolicy("Front", p =>
{
if (allowAnyCorsOrigin)
{
p.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod();
return;
}
p.WithOrigins(corsOrigins) p.WithOrigins(corsOrigins)
.AllowAnyHeader() .AllowAnyHeader()
.AllowAnyMethod() .AllowAnyMethod();
); });
}); });
builder.Services.AddDbContext<AppDbContext>(options => builder.Services.AddDbContext<AppDbContext>(options =>
@ -63,6 +100,9 @@ builder.Services.AddIdentityCore<ApplicationUser>(options =>
{ {
options.Password.RequiredLength = 6; options.Password.RequiredLength = 6;
options.User.RequireUniqueEmail = false; options.User.RequireUniqueEmail = false;
options.Lockout.AllowedForNewUsers = true;
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
}) })
.AddRoles<IdentityRole<Guid>>() .AddRoles<IdentityRole<Guid>>()
.AddEntityFrameworkStores<AppDbContext>() .AddEntityFrameworkStores<AppDbContext>()
@ -77,6 +117,12 @@ if (string.IsNullOrWhiteSpace(jwtKey))
throw new InvalidOperationException("Configuration 'Jwt:Key' is required."); 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 issuer = builder.Configuration["Jwt:Issuer"];
var audience = builder.Configuration["Jwt:Audience"]; var audience = builder.Configuration["Jwt:Audience"];
@ -97,6 +143,21 @@ builder.Services
}); });
builder.Services.AddAuthorization(); 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.Configure<NotificationOptions>(builder.Configuration.GetSection("Notifications"));
builder.Services.AddHostedService<VigenciaNotificationBackgroundService>(); builder.Services.AddHostedService<VigenciaNotificationBackgroundService>();
@ -119,6 +180,7 @@ if (useHttpsRedirection)
} }
app.UseCors("Front"); app.UseCors("Front");
app.UseRateLimiter();
app.UseAuthentication(); app.UseAuthentication();
app.UseMiddleware<TenantMiddleware>(); app.UseMiddleware<TenantMiddleware>();

View File

@ -35,6 +35,32 @@ namespace line_gestao_api.Services
{ {
TotalLinhas = g.Count(), TotalLinhas = g.Count(),
TotalAtivas = g.Count(x => (x.Status ?? "").ToLower().Contains("ativo")), 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 => TotalBloqueados = g.Count(x =>
(x.Status ?? "").ToLower().Contains("bloque") || (x.Status ?? "").ToLower().Contains("bloque") ||
(x.Status ?? "").ToLower().Contains("perda") || (x.Status ?? "").ToLower().Contains("perda") ||
@ -662,7 +688,8 @@ namespace line_gestao_api.Services
new() { ServiceName = ServiceVivoSync, CountLines = totals.NotPaidSync, TotalValue = 0m }, new() { ServiceName = ServiceVivoSync, CountLines = totals.NotPaidSync, TotalValue = 0m },
new() { ServiceName = ServiceVivoGestaoDispositivo, CountLines = totals.NotPaidGestaoDispositivo, 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) private static List<GeralDashboardClientGroupDto> BuildClientGroups(IEnumerable<ClientGroupProjection> rows)
{ {
var list = new List<GeralDashboardClientGroupDto>(); var list = new List<GeralDashboardClientGroupDto>();
@ -1037,6 +1101,12 @@ namespace line_gestao_api.Services
{ {
public int TotalLinhas { get; set; } public int TotalLinhas { get; set; }
public int TotalAtivas { 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 TotalBloqueados { get; set; }
public int VivoLinhas { get; set; } public int VivoLinhas { get; set; }
public decimal VivoFranquiaTotalGb { get; set; } public decimal VivoFranquiaTotalGb { get; set; }

View File

@ -1,9 +1,9 @@
{ {
"ConnectionStrings": { "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": { "Jwt": {
"Key": "dev-only-please-replace-with-env-variable-in-production", "Key": "linegestao-local-jwt-key-2026-dev-strong-123456789",
"Issuer": "LineGestao", "Issuer": "LineGestao",
"Audience": "LineGestao", "Audience": "LineGestao",
"ExpiresMinutes": 360 "ExpiresMinutes": 360
@ -28,9 +28,11 @@
"ReminderDays": [30, 15, 7] "ReminderDays": [30, 15, 7]
}, },
"Seed": { "Seed": {
"Enabled": true,
"ReapplyAdminCredentialsOnStartup": true,
"DefaultTenantName": "Default", "DefaultTenantName": "Default",
"AdminName": "Administrador", "AdminName": "Administrador",
"AdminEmail": "admin@linegestao.local", "AdminEmail": "admin@linegestao.local",
"AdminPassword": "Admin123!" "AdminPassword": "DevAdmin123!"
} }
} }

View File

@ -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"
}
}

View File

@ -1,28 +1,38 @@
{ {
"ConnectionStrings": { "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": { "Jwt": {
"Key": "", "Key": "linegestao-local-jwt-key-2026-dev-strong-123456789",
"Issuer": "LineGestao", "Issuer": "LineGestao",
"Audience": "LineGestao", "Audience": "LineGestao",
"ExpiresMinutes": 360 "ExpiresMinutes": 360
}, },
"Cors": { "Cors": {
"AllowedOrigins": [ "http://localhost:4200" ] "AllowedOrigins": [
"http://localhost:4200"
]
}, },
"App": { "App": {
"UseHttpsRedirection": true "UseHttpsRedirection": false
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}, },
"Notifications": { "Notifications": {
"CheckIntervalMinutes": 60, "CheckIntervalMinutes": 60,
"NotifyAllFutureDates": true, "NotifyAllFutureDates": true,
"ReminderDays": [30, 15, 7] "ReminderDays": [ 30, 15, 7 ]
}, },
"Seed": { "Seed": {
"Enabled": true,
"ReapplyAdminCredentialsOnStartup": true,
"DefaultTenantName": "Default", "DefaultTenantName": "Default",
"AdminName": "Administrador", "AdminName": "Administrador",
"AdminEmail": "admin@linegestao.local", "AdminEmail": "admin@linegestao.local",
"AdminPassword": "CHANGE_ME" "AdminPassword": "DevAdmin123!"
} }
} }

View File

@ -17,6 +17,13 @@
<None Remove="line-gestao-api.Tests\**" /> <None Remove="line-gestao-api.Tests\**" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Compile Remove="publish\**" />
<Content Remove="publish\**" />
<EmbeddedResource Remove="publish\**" />
<None Remove="publish\**" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ClosedXML" Version="0.105.0" /> <PackageReference Include="ClosedXML" Version="0.105.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.1" />