using System.Text; using System.Threading.RateLimiting; using api.Data; using api.Features.Auth.Settings; using api.Features.Events.Services; using api.Features.Assets.Services; using api.Features.Email.Services; using api.Features.Billing.Services; using api.Features.Billing.Settings; using api.Features.Plans.Services; using api.Features.QRCodes.Services; using api.Middleware; using FastEndpoints; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.RateLimiting; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using Serilog; // Configure Serilog Log.Logger = new LoggerConfiguration() .MinimumLevel.Information() .MinimumLevel.Override("Microsoft", Serilog.Events.LogEventLevel.Warning) .MinimumLevel.Override("Microsoft.EntityFrameworkCore", Serilog.Events.LogEventLevel.Warning) .Enrich.FromLogContext() .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}") .WriteTo.File("logs/api-.log", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 7) .CreateLogger(); try { Log.Information("Starting TrakQR API"); var builder = WebApplication.CreateBuilder(args); // Use Serilog builder.Host.UseSerilog(); // Configure CORS builder.Services.AddCors(options => { options.AddDefaultPolicy(policy => { if (builder.Environment.IsDevelopment()) { policy.SetIsOriginAllowed(origin => new Uri(origin).IsLoopback) .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); } else { // Production: configure allowed origins from config var allowedOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get() ?? ["https://trakqr.com"]; policy.WithOrigins(allowedOrigins) .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); } }); }); // Configure Rate Limiting (skip in Testing environment) var isTestingEnvironment = builder.Environment.EnvironmentName == "Testing"; builder.Services.AddRateLimiter(options => { options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; // Use very high limits in testing environment var authLimit = isTestingEnvironment ? 100000 : 10; var globalLimit = isTestingEnvironment ? 100000 : 100; var redirectLimit = isTestingEnvironment ? 100000 : 1000; var apiLimit = isTestingEnvironment ? 100000 : 200; // Global rate limit for all endpoints options.AddPolicy("global", context => RateLimitPartition.GetFixedWindowLimiter( partitionKey: context.Connection.RemoteIpAddress?.ToString() ?? "unknown", factory: _ => new FixedWindowRateLimiterOptions { PermitLimit = globalLimit, Window = TimeSpan.FromMinutes(1), QueueLimit = 0 })); // Strict rate limit for authentication endpoints options.AddPolicy("auth", context => RateLimitPartition.GetFixedWindowLimiter( partitionKey: context.Connection.RemoteIpAddress?.ToString() ?? "unknown", factory: _ => new FixedWindowRateLimiterOptions { PermitLimit = authLimit, Window = TimeSpan.FromMinutes(1), QueueLimit = 0 })); // Higher limit for redirect endpoint (public, needs to be fast) options.AddPolicy("redirect", context => RateLimitPartition.GetSlidingWindowLimiter( partitionKey: context.Connection.RemoteIpAddress?.ToString() ?? "unknown", factory: _ => new SlidingWindowRateLimiterOptions { PermitLimit = redirectLimit, Window = TimeSpan.FromMinutes(1), SegmentsPerWindow = 4, QueueLimit = 0 })); // API rate limit for authenticated endpoints options.AddPolicy("api", context => RateLimitPartition.GetTokenBucketLimiter( partitionKey: context.User?.Identity?.Name ?? context.Connection.RemoteIpAddress?.ToString() ?? "unknown", factory: _ => new TokenBucketRateLimiterOptions { TokenLimit = apiLimit, ReplenishmentPeriod = TimeSpan.FromMinutes(1), TokensPerPeriod = apiLimit, QueueLimit = 0 })); }); // Add services to the container builder.Services.AddDbContext(options => options.UseNpgsql(builder.Configuration.GetConnectionString("PostgresConnection"))); // Register application services builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); // Configure email service builder.Services.Configure(builder.Configuration.GetSection("Email")); var emailProvider = builder.Configuration.GetValue("Email:Provider") ?? "console"; if (emailProvider == "smtp") { builder.Services.AddSingleton(); } else { // Use console email service for development builder.Services.AddSingleton(); } // Configure Stripe builder.Services.Configure(builder.Configuration.GetSection("Stripe")); builder.Services.AddSingleton(); // Configure JWT settings builder.Services.Configure(builder.Configuration.GetSection("Jwt")); var jwtSettings = builder.Configuration.GetSection("Jwt").Get()!; // Configure authentication builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = jwtSettings.Issuer, ValidAudience = jwtSettings.Audience, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Secret)) }; }); builder.Services.AddAuthorization(); builder.Services.AddFastEndpoints(); builder.Services.AddOpenApi(); var app = builder.Build(); // Global error handling middleware (must be first) app.UseMiddleware(); // Request logging middleware app.UseSerilogRequestLogging(options => { options.MessageTemplate = "{RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms"; options.EnrichDiagnosticContext = (diagnosticContext, httpContext) => { diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value); diagnosticContext.Set("UserAgent", httpContext.Request.Headers.UserAgent.ToString()); diagnosticContext.Set("ClientIP", httpContext.Connection.RemoteIpAddress?.ToString()); }; }); app.UseCors(); app.UseRateLimiter(); // Configure the HTTP request pipeline if (app.Environment.IsDevelopment()) { app.MapOpenApi().CacheOutput(); app.UseSwaggerUI(options => { options.SwaggerEndpoint("/openapi/v1.json", "v1"); }); } app.UseHttpsRedirection(); app.UseAuthentication(); app.UseAuthorization(); app.UseFastEndpoints(); app.Run(); } catch (Exception ex) { Log.Fatal(ex, "Application terminated unexpectedly"); } finally { Log.CloseAndFlush(); }