From ce3505cd33dd21e583c992d327cce2ab54ce18d6 Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Wed, 1 Jul 2026 14:30:33 +0900 Subject: [PATCH] Add admin password reset API --- src/dotnet/QuantEngine.Web/Program.cs | 86 +++++++++++++++++++++++---- 1 file changed, 76 insertions(+), 10 deletions(-) diff --git a/src/dotnet/QuantEngine.Web/Program.cs b/src/dotnet/QuantEngine.Web/Program.cs index 29edf7a..d0ca719 100644 --- a/src/dotnet/QuantEngine.Web/Program.cs +++ b/src/dotnet/QuantEngine.Web/Program.cs @@ -74,6 +74,9 @@ builder.Services.AddHttpClient(); builder.Services.AddScoped(); var app = builder.Build(); +var adminSettings = app.Configuration.GetSection("AdminSettings"); +var adminUsername = adminSettings["Username"] ?? "admin"; +var adminPassword = adminSettings["Password"] ?? string.Empty; // Initialize database tables (PostgreSQL-backed repositories) using (var scope = app.Services.CreateScope()) @@ -125,20 +128,36 @@ app.MapGet("/", () => Results.Redirect("/login")); app.MapCollectionEndpoints(); // Login API (API-First for Blazor WASM client authentication) -app.MapPost("/api/auth/login", async (LoginRequest request, IWorkspaceRepository workspaceRepo) => +app.MapPost("/api/auth/login", async (JsonElement payload, IWorkspaceRepository workspaceRepo) => { - if (string.IsNullOrWhiteSpace(request.Username) || string.IsNullOrWhiteSpace(request.Password)) + static string? ReadString(JsonElement root, params string[] names) + { + foreach (var name in names) + { + if (root.ValueKind == JsonValueKind.Object && root.TryGetProperty(name, out var property) && property.ValueKind == JsonValueKind.String) + { + return property.GetString(); + } + } + + return null; + } + + var username = ReadString(payload, "Username", "username"); + var password = ReadString(payload, "Password", "password"); + + if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) { return Results.BadRequest(new { success = false, error = "missing_credentials" }); } - var account = await workspaceRepo.GetAccountByUsernameAsync(request.Username.Trim()); + var account = await workspaceRepo.GetAccountByUsernameAsync(username.Trim()); if (account is null || !string.Equals(account.IsActive, "true", StringComparison.OrdinalIgnoreCase)) { return Results.Json(new { success = false, error = "invalid_credentials" }, statusCode: 401); } - var passwordHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(request.Password))); + var passwordHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(password))); if (!string.Equals(account.PasswordHash, passwordHash, StringComparison.OrdinalIgnoreCase)) { return Results.Json(new { success = false, error = "invalid_credentials" }, statusCode: 401); @@ -212,6 +231,59 @@ app.MapPost("/api/auth/logout", async (HttpContext context, IWorkspaceRepository return Results.Ok(new { success = true }); }).DisableAntiforgery(); +app.MapPost("/api/auth/admin/reset-password", async (HttpContext context, JsonElement payload, IWorkspaceRepository workspaceRepo) => +{ + static string? ReadString(JsonElement root, params string[] names) + { + foreach (var name in names) + { + if (root.ValueKind == JsonValueKind.Object && root.TryGetProperty(name, out var property) && property.ValueKind == JsonValueKind.String) + { + return property.GetString(); + } + } + + return null; + } + + var username = ReadString(payload, "adminUsername", "AdminUsername", "username", "Username"); + var password = ReadString(payload, "adminPassword", "AdminPassword", "password", "Password"); + var targetUsername = ReadString(payload, "targetUsername", "TargetUsername", "usernameToReset", "UsernameToReset"); + var newPassword = ReadString(payload, "newPassword", "NewPassword"); + + if (!string.Equals(username, adminUsername, StringComparison.Ordinal) || !string.Equals(password, adminPassword, StringComparison.Ordinal)) + { + return Results.Unauthorized(); + } + + if (string.IsNullOrWhiteSpace(targetUsername) || string.IsNullOrWhiteSpace(newPassword)) + { + return Results.BadRequest(new { success = false, error = "missing_target_or_password" }); + } + + var account = await workspaceRepo.GetAccountByUsernameAsync(targetUsername.Trim()); + if (account is null) + { + return Results.NotFound(new { success = false, error = "account_not_found" }); + } + + var passwordHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(newPassword))); + account.PasswordHash = passwordHash; + account.UpdatedAt = DateTimeOffset.UtcNow.ToString("O"); + var updated = await workspaceRepo.UpsertAccountAsync(account); + if (!updated) + { + return Results.StatusCode(500); + } + + return Results.Ok(new + { + success = true, + username = account.Username, + updatedAt = account.UpdatedAt + }); +}).DisableAntiforgery(); + // Operational Report serving API (WASM safe file loading substitute) app.MapGet("/api/operational-report", async (IWebHostEnvironment env) => { @@ -274,12 +346,6 @@ app.MapRazorComponents() app.Run(); -public class LoginRequest -{ - public string Username { get; set; } = ""; - public string Password { get; set; } = ""; -} - internal sealed class QuantAdminAuthHandler : AuthenticationHandler { public QuantAdminAuthHandler(