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(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(options => options.UseNpgsql(builder.Configuration.GetConnectionString("Nexus")) .ConfigureWarnings(w => w.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning))); // --- HTTP Clients --- builder.Services.AddHttpClient(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(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(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddScoped(); // --- Repositories --- builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // --- 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(); 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; }