Files
nexus/backend/Extensions/ApplicationBuilderExtensions.cs
T
AzuTear f5552218bc
CI - Build & Test / Backend (.NET) (push) Successful in 42s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 2m46s
CI - Build & Test / Security Check (push) Successful in 3s
CI - Build & Test / Deploy Nexus (push) Successful in 56s
feat: ship agent-first mission control v0.2.57
2026-07-31 22:39:47 +02:00

107 lines
4.4 KiB
C#

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.
/// Uses a <see cref="SeedAudit"/> guard so the owner is never re-created even if all users
/// are deleted — the DB is the single source of truth for the owner password after first seed.
///
/// Single-transaction guarantee: if the seed block is entered at all (user creation needed
/// or just the audit-log write), the SeedAudit row is written inside the same transaction
/// so that a crash mid-way can never leave the DB in a re-seedable state.
/// </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();
const string seedKey = "owner_created";
var alreadySeeded = await db.SeedAudits.AnyAsync(s => s.Key == seedKey);
if (alreadySeeded)
return;
var ownerEmail = configuration["Bootstrap:OwnerEmail"]?.Trim().ToLowerInvariant();
var ownerPassword = configuration["Bootstrap:OwnerPassword"];
var hasUsers = await db.Users.AnyAsync();
// ── Double-check SeedAudit after the migration — if another pod wrote it
// while we were reading, bail out early. ──
alreadySeeded = await db.SeedAudits.AnyAsync(s => s.Key == seedKey);
if (alreadySeeded)
return;
// ── Use a strategy-based transaction so the user + audit row are
// persisted atomically. If the DB crashes after SaveChanges the
// entire transaction is rolled back, preventing partial-seed states.
var strategy = db.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
await using var tx = await db.Database.BeginTransactionAsync();
if (!hasUsers)
{
if (string.IsNullOrWhiteSpace(ownerEmail))
throw new InvalidOperationException("Bootstrap:OwnerEmail is required for initial setup.");
if (string.IsNullOrWhiteSpace(ownerPassword) || ownerPassword.Length < 10)
throw new InvalidOperationException(
"Bootstrap:OwnerPassword is required for initial setup and must contain at least 10 characters.");
var initialDisplayName = PasswordHelper.BuildOwnerDisplayName(ownerEmail);
db.Users.Add(new NexusUser
{
Email = ownerEmail,
NormalizedEmail = AuthService.NormalizeEmail(ownerEmail),
DisplayName = initialDisplayName,
PasswordHash = PasswordSecurity.Hash(ownerPassword),
Role = "owner"
});
}
// Record the seed attempt regardless of whether users already existed.
// This prevents re-seeding even if the Users table is wiped.
db.SeedAudits.Add(new SeedAudit { Key = seedKey });
await db.SaveChangesAsync();
await tx.CommitAsync();
});
}
}
/// <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.UseExceptionHandler();
app.UseStatusCodePages();
app.UseRateLimiter();
app.UseApiKeyAuthentication();
app.UseAuthentication();
app.UseAuthorization();
app.UseSecurityHeaders();
if (env.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
return app;
}
}