f707dceb98
- DashboardController: /api/dashboard/status, agents, operations, chat/send, chat/messages, queue - OpenClawGatewayClient: typed HttpClient mit Gateway tools/invoke - Dashboard DTOs: DashboardAgentInfo, ChatRequest, ChatResponse, FeedEntry, QueueItem - Gateway auth: Bearer-Password via Integrations:OpenClaw:Password - Gateway-Down → graceful degradation (HTTP 200, leere Daten) - Build: 0 errors, Tests: 3/3 passed
225 lines
7.9 KiB
C#
225 lines
7.9 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.Middleware;
|
|
using Nexus.Api.Repositories;
|
|
using Nexus.Api.Routing;
|
|
using Nexus.Api.Services;
|
|
using System.IdentityModel.Tokens.Jwt;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.Json.Serialization;
|
|
using System.Threading.RateLimiting;
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
// --- JWT Configuration ---
|
|
var jwtKey = builder.Configuration["Jwt:Key"];
|
|
var jwtIssuer = builder.Configuration["Jwt:Issuer"] ?? "nexus";
|
|
var jwtAudience = builder.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.");
|
|
|
|
builder.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)
|
|
};
|
|
});
|
|
|
|
builder.Services.AddAuthorization();
|
|
builder.Services.AddAntiforgery(options =>
|
|
{
|
|
options.HeaderName = "X-CSRF-TOKEN";
|
|
options.Cookie.Name = "nexus-csrf";
|
|
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
|
|
options.Cookie.HttpOnly = false;
|
|
});
|
|
|
|
// --- Rate Limiting ---
|
|
builder.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
|
|
}));
|
|
});
|
|
|
|
// --- Forwarded Headers ---
|
|
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
|
{
|
|
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
|
options.KnownIPNetworks.Clear();
|
|
options.KnownProxies.Clear();
|
|
});
|
|
|
|
// --- Swagger & JSON ---
|
|
builder.Services.AddEndpointsApiExplorer();
|
|
builder.Services.AddSwaggerGen();
|
|
builder.Services.ConfigureHttpJsonOptions(options =>
|
|
options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()));
|
|
|
|
// --- Database ---
|
|
builder.Services.AddDbContext<NexusDbContext>(options =>
|
|
options.UseNpgsql(builder.Configuration.GetConnectionString("Nexus"))
|
|
.ConfigureWarnings(w => w.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning)));
|
|
|
|
// --- HTTP Clients ---
|
|
builder.Services.AddHttpClient<IAgentRuntime, OpenClawRuntime>(client =>
|
|
{
|
|
client.BaseAddress = new(builder.Configuration["Integrations:OpenClaw:BaseUrl"]
|
|
?? "http://127.0.0.1:18789");
|
|
client.Timeout = TimeSpan.FromSeconds(5);
|
|
});
|
|
|
|
builder.Services.AddHttpClient("gateway", client =>
|
|
{
|
|
client.BaseAddress = new(builder.Configuration["Integrations:OpenClaw:BaseUrl"]
|
|
?? "http://127.0.0.1:18789");
|
|
client.Timeout = TimeSpan.FromSeconds(5);
|
|
});
|
|
|
|
builder.Services.AddHttpClient<OpenClawGatewayClient>(client =>
|
|
{
|
|
client.BaseAddress = new(builder.Configuration["Integrations:OpenClaw:BaseUrl"]
|
|
?? "http://127.0.0.1:18789");
|
|
client.Timeout = TimeSpan.FromSeconds(5);
|
|
});
|
|
|
|
// --- Application Services ---
|
|
builder.Services.AddTransient<ModelRoutingService>();
|
|
builder.Services.AddScoped<IAuthService, AuthService>();
|
|
builder.Services.AddScoped<IAgentService, AgentService>();
|
|
|
|
// --- Repositories ---
|
|
builder.Services.AddScoped<IUserRepository, UserRepository>();
|
|
builder.Services.AddScoped<IProjectRepository, ProjectRepository>();
|
|
builder.Services.AddScoped<ITaskRepository, TaskRepository>();
|
|
builder.Services.AddScoped<IActivityRepository, ActivityRepository>();
|
|
|
|
// --- Health Checks ---
|
|
builder.Services.AddHealthChecks()
|
|
.AddNpgSql(builder.Configuration.GetConnectionString("Nexus")!, name: "postgresql", tags: ["database"])
|
|
.AddCheck("runtime", () => HealthCheckResult.Healthy("Runtime configured"), tags: ["runtime"]);
|
|
|
|
// --- Controllers ---
|
|
builder.Services.AddControllers();
|
|
|
|
var app = builder.Build();
|
|
|
|
// --- Database Migration & Owner Seeding ---
|
|
await using (var scope = app.Services.CreateAsyncScope())
|
|
{
|
|
var db = scope.ServiceProvider.GetRequiredService<NexusDbContext>();
|
|
await db.Database.MigrateAsync();
|
|
|
|
var ownerEmail = builder.Configuration["Owner:Email"]?.Trim().ToLowerInvariant();
|
|
var ownerPassword = builder.Configuration["Owner:Password"];
|
|
var ownerDisplayName = builder.Configuration["Owner:DisplayName"]?.Trim();
|
|
var hasUsers = await db.Users.AnyAsync();
|
|
|
|
if (!hasUsers)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(ownerEmail))
|
|
throw new InvalidOperationException("Owner:Email is required for initial setup.");
|
|
|
|
var initialDisplayName = string.IsNullOrWhiteSpace(ownerDisplayName)
|
|
? BuildOwnerDisplayName(ownerEmail)
|
|
: ownerDisplayName;
|
|
var initialPassword = string.IsNullOrWhiteSpace(ownerPassword)
|
|
? GenerateTemporaryPassword()
|
|
: ownerPassword;
|
|
|
|
if (!string.IsNullOrWhiteSpace(ownerPassword) && ownerPassword.Length < 10)
|
|
throw new InvalidOperationException("Owner:Password must be at least 10 characters when provided explicitly.");
|
|
|
|
db.Users.Add(new NexusUser
|
|
{
|
|
Email = ownerEmail,
|
|
NormalizedEmail = AuthService.NormalizeEmail(ownerEmail),
|
|
DisplayName = initialDisplayName,
|
|
PasswordHash = PasswordSecurity.Hash(initialPassword),
|
|
Role = "owner"
|
|
});
|
|
await db.SaveChangesAsync();
|
|
|
|
if (string.IsNullOrWhiteSpace(ownerPassword))
|
|
{
|
|
Console.Error.WriteLine($"[nexus] Initial owner credentials generated: displayName={initialDisplayName}, password={initialPassword}");
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Middleware Pipeline ---
|
|
app.UseForwardedHeaders();
|
|
app.UseRateLimiter();
|
|
app.UseAuthentication();
|
|
app.UseAuthorization();
|
|
app.UseSecurityHeaders();
|
|
|
|
if (app.Environment.IsDevelopment())
|
|
{
|
|
app.UseSwagger();
|
|
app.UseSwaggerUI();
|
|
}
|
|
|
|
app.MapControllers();
|
|
app.Run();
|
|
|
|
// --- Helpers ---
|
|
|
|
static string GenerateTemporaryPassword()
|
|
=> Convert.ToBase64String(RandomNumberGenerator.GetBytes(18))
|
|
.TrimEnd('=')
|
|
.Replace('+', '-')
|
|
.Replace('/', '_');
|
|
|
|
static string BuildOwnerDisplayName(string email)
|
|
{
|
|
var localPart = email.Split('@', 2)[0].Trim();
|
|
if (string.IsNullOrWhiteSpace(localPart)) return "Owner";
|
|
|
|
var words = localPart
|
|
.Replace('.', ' ')
|
|
.Replace('_', ' ')
|
|
.Replace('-', ' ')
|
|
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
|
.Select(word => char.ToUpperInvariant(word[0]) + word[1..].ToLowerInvariant());
|
|
|
|
var displayName = string.Join(' ', words);
|
|
return string.IsNullOrWhiteSpace(displayName) ? "Owner" : displayName;
|
|
}
|