line-gestao-api/Program.cs

195 lines
6.2 KiB
C#

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;
options.KnownIPNetworks.Clear();
options.KnownProxies.Clear();
});
builder.Services.Configure<FormOptions>(o =>
{
o.MultipartBodyLengthLimit = 50_000_000;
});
var configuredCorsOrigins = builder.Configuration
.GetSection("Cors:AllowedOrigins")
.Get<string[]>()?
.Where(o => !string.IsNullOrWhiteSpace(o))
.Select(o => o.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray()
?? [];
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();
});
});
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default"))
);
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ITenantProvider, TenantProvider>();
builder.Services.AddScoped<IAuditLogBuilder, AuditLogBuilder>();
builder.Services.AddScoped<IVigenciaNotificationSyncService, VigenciaNotificationSyncService>();
builder.Services.AddScoped<ParcelamentosImportService>();
builder.Services.AddScoped<GeralDashboardInsightsService>();
builder.Services.AddScoped<SpreadsheetImportAuditService>();
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>()
.AddDefaultTokenProviders();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var jwtKey = builder.Configuration["Jwt:Key"];
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"];
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = issuer,
ValidAudience = audience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey))
};
});
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>();
builder.Services.Configure<SeedOptions>(builder.Configuration.GetSection("Seed"));
var app = builder.Build();
app.UseForwardedHeaders();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
var useHttpsRedirection = builder.Configuration.GetValue("App:UseHttpsRedirection", !app.Environment.IsDevelopment());
if (useHttpsRedirection)
{
app.UseHttpsRedirection();
}
app.UseCors("Front");
app.UseRateLimiter();
app.UseAuthentication();
app.UseMiddleware<TenantMiddleware>();
app.UseAuthorization();
await SeedData.EnsureSeedDataAsync(app.Services);
app.MapControllers();
app.MapGet("/health", () => Results.Ok(new { status = "ok" }));
app.Run();