Multi-agent AI laboratory with ASP.NET Core 8.0 backend and Flutter frontend. Implements CQRS architecture, OpenAPI contract-first API design. BACKEND: Agent management, conversations, executions with PostgreSQL + Ollama FRONTEND: Cross-platform UI with strict typing and Result-based error handling Co-Authored-By: Jean-Philippe Brule <jp@svrnty.io>
222 lines
6.8 KiB
C#
222 lines
6.8 KiB
C#
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);
|
|
|
|
// XML documentation files for Swagger
|
|
string[] xmlFiles = { "Codex.Api.xml", "Codex.CQRS.xml", "Codex.Dal.xml" };
|
|
|
|
builder.Services.Configure<ForwardedHeadersOptions>(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<string[]>() ?? Array.Empty<string>();
|
|
|
|
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<HttpContext, string>(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<AppModule>();
|
|
|
|
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<CodexDbContext>(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
|
|
|
|
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<string>()
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
var app = builder.Build();
|
|
|
|
// Global exception handler (must be first)
|
|
app.UseMiddleware<GlobalExceptionHandler>();
|
|
|
|
// 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(); |