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(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; options.KnownIPNetworks.Clear(); options.KnownProxies.Clear(); }); builder.Services.Configure(o => { o.MultipartBodyLengthLimit = 50_000_000; }); var configuredCorsOrigins = builder.Configuration .GetSection("Cors:AllowedOrigins") .Get()? .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(options => options.UseNpgsql(builder.Configuration.GetConnectionString("Default")) ); builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddIdentityCore(options => { options.Password.RequiredLength = 6; options.User.RequireUniqueEmail = false; options.Lockout.AllowedForNewUsers = true; options.Lockout.MaxFailedAccessAttempts = 5; options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15); }) .AddRoles>() .AddEntityFrameworkStores() .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(builder.Configuration.GetSection("Notifications")); builder.Services.AddHostedService(); builder.Services.Configure(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(); app.UseAuthorization(); await SeedData.EnsureSeedDataAsync(app.Services); app.MapControllers(); app.MapGet("/health", () => Results.Ok(new { status = "ok" })); app.Run();