using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.RateLimiting; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.IdentityModel.Tokens; using Nexus.Api.Data; using Nexus.Api.Integrations; using Nexus.Api.Repositories; using Nexus.Api.Routing; using Nexus.Api.Services; using System.IdentityModel.Tokens.Jwt; using System.Text; using System.Text.Json.Serialization; using System.Threading.RateLimiting; namespace Nexus.Api.Extensions; /// /// Extension methods for registering Nexus application services in the DI container. /// public static class ServiceCollectionExtensions { /// /// Configures JWT authentication, authorization, and antiforgery. /// public static IServiceCollection AddNexusAuth(this IServiceCollection services, IConfiguration configuration) { var jwtKey = configuration["Jwt:Key"]; var jwtIssuer = configuration["Jwt:Issuer"] ?? "nexus"; var jwtAudience = configuration["Jwt:Audience"] ?? "nexus-web"; if (string.IsNullOrWhiteSpace(jwtKey) || Encoding.UTF8.GetByteCount(jwtKey) < 32) throw new InvalidOperationException("Jwt:Key must be configured with at least 32 bytes."); services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.MapInboundClaims = false; options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = jwtIssuer, ValidAudience = jwtAudience, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey)), NameClaimType = JwtRegisteredClaimNames.Sub, RoleClaimType = System.Security.Claims.ClaimTypes.Role, ClockSkew = TimeSpan.FromSeconds(30) }; }); services.AddAuthorization(); services.AddAntiforgery(options => { options.HeaderName = "X-CSRF-TOKEN"; options.Cookie.Name = "nexus-csrf"; options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; options.Cookie.HttpOnly = false; }); return services; } /// /// Configures rate limiting policies (auth and agents). /// public static IServiceCollection AddNexusRateLimiting(this IServiceCollection services) { services.AddRateLimiter(options => { options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; options.AddPolicy("auth", context => RateLimitPartition.GetFixedWindowLimiter( context.Connection.RemoteIpAddress?.ToString() ?? "unknown", _ => new FixedWindowRateLimiterOptions { PermitLimit = 5, Window = TimeSpan.FromMinutes(1), QueueLimit = 0, AutoReplenishment = true })); options.AddPolicy("agents", context => RateLimitPartition.GetFixedWindowLimiter( context.Connection.RemoteIpAddress?.ToString() ?? "unknown", _ => new FixedWindowRateLimiterOptions { PermitLimit = 30, Window = TimeSpan.FromMinutes(1), QueueLimit = 0, AutoReplenishment = true })); }); return services; } /// /// Configures forwarded headers for reverse proxy scenarios. /// public static IServiceCollection AddNexusForwardedHeaders(this IServiceCollection services) { services.Configure(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; options.KnownIPNetworks.Clear(); options.KnownProxies.Clear(); }); return services; } /// /// Configures Swagger and JSON serialization options. /// public static IServiceCollection AddNexusSwagger(this IServiceCollection services) { services.AddEndpointsApiExplorer(); services.AddSwaggerGen(); services.ConfigureHttpJsonOptions(options => options.SerializerOptions.Converters.Add(new JsonStringEnumConverter())); return services; } /// /// Registers the Entity Framework Core DbContext with Npgsql. /// public static IServiceCollection AddNexusDatabase(this IServiceCollection services, IConfiguration configuration) { services.AddDbContext(options => options.UseNpgsql(configuration.GetConnectionString("Nexus")) .ConfigureWarnings(w => w.Ignore( Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning))); return services; } /// /// Registers typed and named HTTP clients for OpenClaw integration. /// public static IServiceCollection AddNexusHttpClients(this IServiceCollection services, IConfiguration configuration) { services.AddHttpClient(client => { client.BaseAddress = new(configuration["Integrations:OpenClaw:BaseUrl"] ?? "http://127.0.0.1:18789"); client.Timeout = TimeSpan.FromSeconds(120); }); services.AddHttpClient("gateway", client => { client.BaseAddress = new(configuration["Integrations:OpenClaw:BaseUrl"] ?? "http://127.0.0.1:18789"); client.Timeout = TimeSpan.FromSeconds(120); }); services.AddHttpClient(client => { client.BaseAddress = new(configuration["Integrations:OpenClaw:BaseUrl"] ?? "http://127.0.0.1:18789"); client.Timeout = TimeSpan.FromSeconds(120); }); return services; } /// /// Registers application domain services (transient, scoped, singleton). /// public static IServiceCollection AddNexusApplicationServices(this IServiceCollection services) { services.AddTransient(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddScoped(); return services; } /// /// Registers data repositories. /// public static IServiceCollection AddNexusRepositories(this IServiceCollection services) { services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); return services; } /// /// Configures health checks (PostgreSQL connectivity and runtime status). /// public static IServiceCollection AddNexusHealthChecks(this IServiceCollection services, IConfiguration configuration) { services.AddHealthChecks() .AddNpgSql(configuration.GetConnectionString("Nexus")!, name: "postgresql", tags: ["database"]) .AddCheck("runtime", () => HealthCheckResult.Healthy("Runtime configured"), tags: ["runtime"]); return services; } }