Add admin password reset API
This commit is contained in:
@@ -74,6 +74,9 @@ builder.Services.AddHttpClient<ApiClient>();
|
|||||||
builder.Services.AddScoped<ApiClient>();
|
builder.Services.AddScoped<ApiClient>();
|
||||||
|
|
||||||
var app = builder.Build();
|
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)
|
// Initialize database tables (PostgreSQL-backed repositories)
|
||||||
using (var scope = app.Services.CreateScope())
|
using (var scope = app.Services.CreateScope())
|
||||||
@@ -125,20 +128,36 @@ app.MapGet("/", () => Results.Redirect("/login"));
|
|||||||
app.MapCollectionEndpoints();
|
app.MapCollectionEndpoints();
|
||||||
|
|
||||||
// Login API (API-First for Blazor WASM client authentication)
|
// 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" });
|
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))
|
if (account is null || !string.Equals(account.IsActive, "true", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return Results.Json(new { success = false, error = "invalid_credentials" }, statusCode: 401);
|
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))
|
if (!string.Equals(account.PasswordHash, passwordHash, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return Results.Json(new { success = false, error = "invalid_credentials" }, statusCode: 401);
|
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 });
|
return Results.Ok(new { success = true });
|
||||||
}).DisableAntiforgery();
|
}).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)
|
// Operational Report serving API (WASM safe file loading substitute)
|
||||||
app.MapGet("/api/operational-report", async (IWebHostEnvironment env) =>
|
app.MapGet("/api/operational-report", async (IWebHostEnvironment env) =>
|
||||||
{
|
{
|
||||||
@@ -274,12 +346,6 @@ app.MapRazorComponents<App>()
|
|||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
||||||
public class LoginRequest
|
|
||||||
{
|
|
||||||
public string Username { get; set; } = "";
|
|
||||||
public string Password { get; set; } = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class QuantAdminAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
|
internal sealed class QuantAdminAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
|
||||||
{
|
{
|
||||||
public QuantAdminAuthHandler(
|
public QuantAdminAuthHandler(
|
||||||
|
|||||||
Reference in New Issue
Block a user