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.RateLimiting;
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.OnRejected = async (context, ct) =>
{
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
context.HttpContext.Response.Headers.ContentType = "application/json";
var retryAfterSeconds = 60;
// Try to read retry-after info from the metadata
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
{
retryAfterSeconds = (int)retryAfter.TotalSeconds;
}
// Set standard headers
context.HttpContext.Response.Headers.RetryAfter = retryAfterSeconds.ToString();
context.HttpContext.Response.Headers["X-RateLimit-Remaining"] = "0";
context.HttpContext.Response.Headers["X-RateLimit-Reset"] =
DateTimeOffset.UtcNow.AddSeconds(retryAfterSeconds).ToUnixTimeSeconds().ToString();
var body = new
{
error = "rate_limit_exceeded",
message = $"Too many attempts. Try again in {retryAfterSeconds} second(s).",
remaining = 0,
retryAfterSeconds
};
await context.HttpContext.Response.WriteAsJsonAsync(body, ct);
};
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.AddHttpContextAccessor();
services.AddSingleton();
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();
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;
}
}