104 lines
4.2 KiB
C#
104 lines
4.2 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 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.");
|
|
|
|
var initialDisplayName = PasswordHelper.BuildOwnerDisplayName(ownerEmail);
|
|
var initialPassword = PasswordHelper.GenerateTemporaryPassword();
|
|
|
|
db.Users.Add(new NexusUser
|
|
{
|
|
Email = ownerEmail,
|
|
NormalizedEmail = AuthService.NormalizeEmail(ownerEmail),
|
|
DisplayName = initialDisplayName,
|
|
PasswordHash = PasswordSecurity.Hash(initialPassword),
|
|
Role = "owner"
|
|
});
|
|
|
|
Console.Error.WriteLine($"[nexus] Initial owner credentials generated: displayName={initialDisplayName}, password={initialPassword}");
|
|
}
|
|
|
|
// 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.UseRateLimiter();
|
|
app.UseApiKeyAuthentication();
|
|
app.UseAuthentication();
|
|
app.UseAuthorization();
|
|
app.UseSecurityHeaders();
|
|
|
|
if (env.IsDevelopment())
|
|
{
|
|
app.UseSwagger();
|
|
app.UseSwaggerUI();
|
|
}
|
|
|
|
return app;
|
|
}
|
|
}
|