refactor: extract DI, helpers from Program.cs into extension classes
This commit is contained in:
@@ -0,0 +1,83 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Nexus.Api.Data;
|
||||||
|
using Nexus.Api.Helpers;
|
||||||
|
using Nexus.Api.Middleware;
|
||||||
|
using Nexus.Api.Services;
|
||||||
|
|
||||||
|
namespace Nexus.Api.Extensions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extension methods for configuring the Nexus application pipeline and startup.
|
||||||
|
/// </summary>
|
||||||
|
public static class ApplicationBuilderExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Applies pending EF Core migrations and seeds the initial owner account if none exist.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task EnsureDatabaseAsync(this WebApplication app)
|
||||||
|
{
|
||||||
|
var configuration = app.Configuration;
|
||||||
|
|
||||||
|
await using (var scope = app.Services.CreateAsyncScope())
|
||||||
|
{
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<NexusDbContext>();
|
||||||
|
await db.Database.MigrateAsync();
|
||||||
|
|
||||||
|
var ownerEmail = configuration["Owner:Email"]?.Trim().ToLowerInvariant();
|
||||||
|
var ownerPassword = configuration["Owner:Password"];
|
||||||
|
var ownerDisplayName = 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)
|
||||||
|
? PasswordHelper.BuildOwnerDisplayName(ownerEmail)
|
||||||
|
: ownerDisplayName;
|
||||||
|
var initialPassword = string.IsNullOrWhiteSpace(ownerPassword)
|
||||||
|
? PasswordHelper.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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures the HTTP middleware pipeline: forwarded headers, rate limiting, auth, security headers, and Swagger in development.
|
||||||
|
/// </summary>
|
||||||
|
public static IApplicationBuilder UseNexusPipeline(this IApplicationBuilder app, IWebHostEnvironment env)
|
||||||
|
{
|
||||||
|
app.UseForwardedHeaders();
|
||||||
|
app.UseRateLimiter();
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
app.UseSecurityHeaders();
|
||||||
|
|
||||||
|
if (env.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
|
namespace Nexus.Api.Helpers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper methods for password generation and name construction.
|
||||||
|
/// </summary>
|
||||||
|
public static class PasswordHelper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Generates a cryptographically random temporary password (30 chars, URL-safe base64).
|
||||||
|
/// </summary>
|
||||||
|
public static string GenerateTemporaryPassword()
|
||||||
|
=> Convert.ToBase64String(RandomNumberGenerator.GetBytes(18))
|
||||||
|
.TrimEnd('=')
|
||||||
|
.Replace('+', '-')
|
||||||
|
.Replace('/', '_');
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a human-readable display name from an email address.
|
||||||
|
/// </summary>
|
||||||
|
public 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
-222
@@ -1,234 +1,26 @@
|
|||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Nexus.Api.Extensions;
|
||||||
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);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
// --- JWT Configuration ---
|
// --- Service Registration ---
|
||||||
var jwtKey = builder.Configuration["Jwt:Key"];
|
builder.Services.AddNexusAuth(builder.Configuration);
|
||||||
var jwtIssuer = builder.Configuration["Jwt:Issuer"] ?? "nexus";
|
builder.Services.AddNexusRateLimiting();
|
||||||
var jwtAudience = builder.Configuration["Jwt:Audience"] ?? "nexus-web";
|
builder.Services.AddNexusForwardedHeaders();
|
||||||
if (string.IsNullOrWhiteSpace(jwtKey) || Encoding.UTF8.GetByteCount(jwtKey) < 32)
|
builder.Services.AddNexusSwagger();
|
||||||
throw new InvalidOperationException("Jwt:Key must be configured with at least 32 bytes.");
|
builder.Services.AddNexusDatabase(builder.Configuration);
|
||||||
|
builder.Services.AddNexusHttpClients(builder.Configuration);
|
||||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
builder.Services.AddNexusApplicationServices();
|
||||||
.AddJwtBearer(options =>
|
builder.Services.AddNexusRepositories();
|
||||||
{
|
builder.Services.AddNexusHealthChecks(builder.Configuration);
|
||||||
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(120);
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.Services.AddHttpClient("gateway", client =>
|
|
||||||
{
|
|
||||||
client.BaseAddress = new(builder.Configuration["Integrations:OpenClaw:BaseUrl"]
|
|
||||||
?? "http://127.0.0.1:18789");
|
|
||||||
client.Timeout = TimeSpan.FromSeconds(120);
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.Services.AddHttpClient<IOpenClawGatewayClient, OpenClawGatewayClient>(client =>
|
|
||||||
{
|
|
||||||
client.BaseAddress = new(builder.Configuration["Integrations:OpenClaw:BaseUrl"]
|
|
||||||
?? "http://127.0.0.1:18789");
|
|
||||||
client.Timeout = TimeSpan.FromSeconds(120);
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- Application Services ---
|
|
||||||
builder.Services.AddTransient<ModelRoutingService>();
|
|
||||||
builder.Services.AddScoped<IAuthService, AuthService>();
|
|
||||||
builder.Services.AddScoped<IAgentService, AgentService>();
|
|
||||||
builder.Services.AddScoped<IDashboardService, DashboardService>();
|
|
||||||
builder.Services.AddScoped<IProjectService, ProjectService>();
|
|
||||||
builder.Services.AddScoped<ITaskService, TaskService>();
|
|
||||||
builder.Services.AddScoped<IOperationsService, OperationsService>();
|
|
||||||
builder.Services.AddScoped<ITeamService, TeamService>();
|
|
||||||
builder.Services.AddSingleton<IAgentConfigService, AgentConfigService>();
|
|
||||||
builder.Services.AddSingleton<IMemoryService, MemoryService>();
|
|
||||||
builder.Services.AddSingleton<IIncidentService, IncidentService>();
|
|
||||||
builder.Services.AddSingleton<IDocService, DocService>();
|
|
||||||
builder.Services.AddScoped<ICalendarService, CalendarService>();
|
|
||||||
|
|
||||||
// --- 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();
|
builder.Services.AddControllers();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// --- Database Migration & Owner Seeding ---
|
// --- Database Migration & Seeding ---
|
||||||
await using (var scope = app.Services.CreateAsyncScope())
|
await app.EnsureDatabaseAsync();
|
||||||
{
|
|
||||||
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 ---
|
// --- Middleware Pipeline ---
|
||||||
app.UseForwardedHeaders();
|
app.UseNexusPipeline(app.Environment);
|
||||||
app.UseRateLimiter();
|
|
||||||
app.UseAuthentication();
|
|
||||||
app.UseAuthorization();
|
|
||||||
app.UseSecurityHeaders();
|
|
||||||
|
|
||||||
if (app.Environment.IsDevelopment())
|
|
||||||
{
|
|
||||||
app.UseSwagger();
|
|
||||||
app.UseSwaggerUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
app.Run();
|
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;
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user