using Microsoft.EntityFrameworkCore; using Nexus.Api.Data; using Nexus.Api.Helpers; using Nexus.Api.Middleware; using Nexus.Api.Services; namespace Nexus.Api.Extensions; /// /// Extension methods for configuring the Nexus application pipeline and startup. /// public static class ApplicationBuilderExtensions { /// /// Applies pending EF Core migrations and seeds the initial owner account if none exist. /// Uses a 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. /// public static async Task EnsureDatabaseAsync(this WebApplication app) { var configuration = app.Configuration; await using (var scope = app.Services.CreateAsyncScope()) { var db = scope.ServiceProvider.GetRequiredService(); 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(); }); } } /// /// Configures the HTTP middleware pipeline: forwarded headers, rate limiting, auth, security headers, and Swagger in development. /// 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; } }