83e072bc27
- Bao darf jetzt Status ändern (neben Iris), Sub-Agents weiterhin nicht - CanEditContent für Inhaltsbearbeitung durch alle bekannten Caller - Bao-Content-Änderungen triggern task_content_changed-Notification an Iris - Bao-Status-Änderungen triggern task_status_changed-Notification an Iris - Iris-Status-Änderungen triggern task_status_changed-Notification an Bao - Neue WorkTask-Felder: IsAgentTask (bool), ExpectedFrom (string) - Agent-Workflow-API: CreateAgentTask, WaitingTasks, AgentOverview - Frontend: Agent-Task-Badge, Iris-Overview-Panel, isBao-Getter - Login-Rate-Limiter mit strukturiertem JSON-Fehlermeldungs-Body - Volume-Name: nexus-postgres → postgres-data (Standardisierung)
250 lines
9.8 KiB
C#
250 lines
9.8 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.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;
|
|
|
|
/// <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.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;
|
|
}
|
|
|
|
/// <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.AddHttpContextAccessor();
|
|
services.AddSingleton<LoginAttemptTracker>();
|
|
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<INotificationService, NotificationService>();
|
|
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;
|
|
}
|
|
}
|