using System.Text.Json.Serialization; using System.Threading.RateLimiting; using Codex.Api; using Codex.Api.Endpoints; using Codex.Api.Middleware; using Codex.Dal; using FluentValidation.AspNetCore; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.RateLimiting; using Microsoft.EntityFrameworkCore; using OpenHarbor.CQRS; using OpenHarbor.CQRS.DynamicQuery.Abstractions; using PoweredSoft.Data; using PoweredSoft.Data.EntityFrameworkCore; using PoweredSoft.DynamicQuery; using PoweredSoft.Module.Abstractions; using OpenHarbor.CQRS.AspNetCore.Mvc; using OpenHarbor.CQRS.DynamicQuery.AspNetCore; var builder = WebApplication.CreateBuilder(args); builder.Services.Configure(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost; options.KnownNetworks.Clear(); options.KnownProxies.Clear(); options.ForwardLimit = 2; }); builder.Services.AddHttpContextAccessor(); // Configure CORS for Flutter and web clients builder.Services.AddCors(options => { options.AddDefaultPolicy(policy => { if (builder.Environment.IsDevelopment()) { // Development: Allow any localhost port + Capacitor/Ionic policy.SetIsOriginAllowed(origin => { var uri = new Uri(origin); return uri.Host == "localhost" || origin.StartsWith("capacitor://", StringComparison.OrdinalIgnoreCase) || origin.StartsWith("ionic://", StringComparison.OrdinalIgnoreCase); }) .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials(); } else { // Production: Use configured origins only var allowedOrigins = builder.Configuration .GetSection("Cors:AllowedOrigins") .Get() ?? Array.Empty(); if (allowedOrigins.Length > 0) { policy.WithOrigins(allowedOrigins) .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials(); } } }); }); // Add rate limiting (MVP: generous limits to prevent runaway loops) builder.Services.AddRateLimiter(options => { options.GlobalLimiter = PartitionedRateLimiter.Create(context => { var clientId = context.User?.Identity?.Name ?? context.Connection.RemoteIpAddress?.ToString() ?? "anonymous"; return RateLimitPartition.GetFixedWindowLimiter( partitionKey: clientId, factory: _ => new FixedWindowRateLimiterOptions { PermitLimit = 1000, Window = TimeSpan.FromMinutes(1), QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 0 }); }); options.OnRejected = async (context, cancellationToken) => { context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests; await context.HttpContext.Response.WriteAsJsonAsync(new { error = "Rate limit exceeded", message = "Too many requests. Please wait before trying again.", retryAfter = "60 seconds" }, cancellationToken: cancellationToken); }; }); builder.Services.AddPoweredSoftDataServices(); builder.Services.AddPoweredSoftEntityFrameworkCoreDataServices(); builder.Services.AddPoweredSoftDynamicQuery(); builder.Services.AddDefaultCommandDiscovery(); builder.Services.AddDefaultQueryDiscovery(); builder.Logging.ClearProviders(); builder.Logging.AddConsole(); builder.Services.AddHttpClient(); builder.Services.AddMemoryCache(); builder.Services .AddFluentValidationAutoValidation() .AddFluentValidationClientsideAdapters(); builder.Services.AddModule(); var mvcBuilder = builder.Services .AddControllers() .AddJsonOptions(jsonOptions => { jsonOptions.JsonSerializerOptions.Converters.Insert(0, new JsonStringEnumConverter()); }); mvcBuilder .AddOpenHarborCommands(); mvcBuilder .AddOpenHarborQueries() .AddOpenHarborDynamicQueries(); // Register PostgreSQL DbContext builder.Services.AddDbContext(options => options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))); if (builder.Environment.IsDevelopment()) { builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(options => { options.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo { Title = "Codex API", Version = "v1", Description = "CQRS-based API using OpenHarbor.CQRS framework" }); // Include XML comments from all projects var xmlFiles = new[] { "Codex.Api.xml", "Codex.CQRS.xml", "Codex.Dal.xml" }; foreach (var xmlFile in xmlFiles) { var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); if (File.Exists(xmlPath)) { options.IncludeXmlComments(xmlPath); } } // Add authentication scheme documentation (for future use) options.AddSecurityDefinition("Bearer", new Microsoft.OpenApi.Models.OpenApiSecurityScheme { Description = "JWT Authorization header using the Bearer scheme. Example: \"Bearer {token}\"", Name = "Authorization", In = Microsoft.OpenApi.Models.ParameterLocation.Header, Type = Microsoft.OpenApi.Models.SecuritySchemeType.ApiKey, Scheme = "Bearer" }); options.AddSecurityRequirement(new Microsoft.OpenApi.Models.OpenApiSecurityRequirement { { new Microsoft.OpenApi.Models.OpenApiSecurityScheme { Reference = new Microsoft.OpenApi.Models.OpenApiReference { Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme, Id = "Bearer" } }, Array.Empty() } }); }); } var app = builder.Build(); // Global exception handler (must be first) app.UseMiddleware(); // Rate limiting (before CORS and routing) app.UseRateLimiter(); // Use CORS policy configured from appsettings app.UseCors(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseForwardedHeaders(); if (builder.Environment.IsDevelopment() == false) { app.UseHttpsRedirection(); } // Map OpenHarbor auto-generated endpoints (CreateAgent, UpdateAgent, DeleteAgent, GetAgent, Health) app.MapControllers(); // Map manually registered endpoints (commands with return values, queries with return types) app.MapCodexEndpoints(); // Map simple GET endpoints for lists (pragmatic MVP approach) app.MapSimpleEndpoints(); app.Run();