215 lines
8.3 KiB
C#
215 lines
8.3 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Extension methods for registering Nexus application services in the DI container.
|
|
/// </summary>
|
|
public static class ServiceCollectionExtensions
|
|
{
|
|
/// <summary>
|
|
/// Configures JWT authentication, authorization, and antiforgery.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Configures rate limiting policies (auth and agents).
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Configures forwarded headers for reverse proxy scenarios.
|
|
/// </summary>
|
|
public static IServiceCollection AddNexusForwardedHeaders(this IServiceCollection services)
|
|
{
|
|
services.Configure<ForwardedHeadersOptions>(options =>
|
|
{
|
|
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
|
options.KnownIPNetworks.Clear();
|
|
options.KnownProxies.Clear();
|
|
});
|
|
|
|
return services;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Configures Swagger and JSON serialization options.
|
|
/// </summary>
|
|
public static IServiceCollection AddNexusSwagger(this IServiceCollection services)
|
|
{
|
|
services.AddEndpointsApiExplorer();
|
|
services.AddSwaggerGen();
|
|
services.ConfigureHttpJsonOptions(options =>
|
|
options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()));
|
|
|
|
return services;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers the Entity Framework Core DbContext with Npgsql.
|
|
/// </summary>
|
|
public static IServiceCollection AddNexusDatabase(this IServiceCollection services, IConfiguration configuration)
|
|
{
|
|
services.AddDbContext<NexusDbContext>(options =>
|
|
options.UseNpgsql(configuration.GetConnectionString("Nexus"))
|
|
.ConfigureWarnings(w => w.Ignore(
|
|
Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning)));
|
|
|
|
return services;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers typed and named HTTP clients for OpenClaw integration.
|
|
/// </summary>
|
|
public static IServiceCollection AddNexusHttpClients(this IServiceCollection services, IConfiguration configuration)
|
|
{
|
|
services.AddHttpClient<IAgentRuntime, OpenClawRuntime>(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<IOpenClawGatewayClient, OpenClawGatewayClient>(client =>
|
|
{
|
|
client.BaseAddress = new(configuration["Integrations:OpenClaw:BaseUrl"]
|
|
?? "http://127.0.0.1:18789");
|
|
client.Timeout = TimeSpan.FromSeconds(120);
|
|
});
|
|
|
|
return services;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers application domain services (transient, scoped, singleton).
|
|
/// </summary>
|
|
public static IServiceCollection AddNexusApplicationServices(this IServiceCollection services)
|
|
{
|
|
services.AddTransient<ModelRoutingService>();
|
|
services.AddScoped<IAuthService, AuthService>();
|
|
services.AddScoped<IAgentService, AgentService>();
|
|
services.AddScoped<IDashboardService, DashboardService>();
|
|
services.AddScoped<IProjectService, ProjectService>();
|
|
services.AddScoped<ITaskService, TaskService>();
|
|
services.AddScoped<IOperationsService, OperationsService>();
|
|
services.AddScoped<ITeamService, TeamService>();
|
|
services.AddSingleton<IAgentConfigService, AgentConfigService>();
|
|
services.AddSingleton<IMemoryService, MemoryService>();
|
|
services.AddSingleton<IIncidentService, IncidentService>();
|
|
services.AddSingleton<IDocService, DocService>();
|
|
services.AddScoped<ICalendarService, CalendarService>();
|
|
|
|
return services;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers data repositories.
|
|
/// </summary>
|
|
public static IServiceCollection AddNexusRepositories(this IServiceCollection services)
|
|
{
|
|
services.AddScoped<IUserRepository, UserRepository>();
|
|
services.AddScoped<IProjectRepository, ProjectRepository>();
|
|
services.AddScoped<ITaskRepository, TaskRepository>();
|
|
services.AddScoped<IActivityRepository, ActivityRepository>();
|
|
|
|
return services;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Configures health checks (PostgreSQL connectivity and runtime status).
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|