f95463ef50
Root cause: Dual-source architecture for owner password (Gitea secret ENV_OWNER_PASSWORD vs host .env OWNER_PASSWORD) caused drift when the DB was ever re-seeded or the volume recreated. Changes: - Add SeedAudit entity + migration to track one-time seed operations - EnsureDatabaseAsync checks SeedAudit BEFORE seeding — owner is never re-created even if the Users table is wiped - Deploy and rollback workflows now read OWNER_PASSWORD from the host's persistent .env (single source of truth) instead of Gitea secrets - compose.yaml documented: OWNER_PASSWORD only used during initial seed - Cleanup: .gitignore extended for core dumps, changelog/deployment.md updated with 2026-06-20 session notes After this fix the DB is the single source of truth for the owner password after initial seed. The host .env is the single reference for the initial value.
97 lines
3.8 KiB
C#
97 lines
3.8 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.
|
|
/// </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["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}");
|
|
}
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
}
|
|
|
|
/// <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;
|
|
}
|
|
}
|