using Halo.Core.Users;
using Halo.Data;
using Microsoft.EntityFrameworkCore;
namespace Halo.Api.Endpoints;
public static class SystemEndpoints
{
/// Verhindert, dass zwei gleichzeitige Anfragen zwei Administratoren anlegen.
///
///
/// Ein Prozess, ein Container — ein Semaphor reicht. Ohne ihn liegt
/// zwischen „kein Benutzer vorhanden" und dem Anlegen ein Fenster, und
/// genau in dieses Fenster passt eine zweite Anfrage.
///
private static readonly SemaphoreSlim EinrichtungsSchloss = new(1, 1);
public static void MapSystem(this IEndpointRouteBuilder app)
{
app.MapGet("/api/v1/system/info", async (HaloContext db) =>
Results.Ok(new SystemInfo(
Version: typeof(SystemEndpoints).Assembly.GetName().Version?.ToString(3) ?? "0.0.0",
NeedsSetup: !await db.Users.AnyAsync())))
.AllowAnonymous();
app.MapPost("/api/v1/system/setup", async (SetupRequest anfrage, HaloContext db) =>
{
if (string.IsNullOrWhiteSpace(anfrage.UserName) || anfrage.Password.Length < 8)
return Results.ValidationProblem(new Dictionary
{
["password"] = ["Benutzername darf nicht leer sein, das Passwort braucht mindestens 8 Zeichen."],
});
await EinrichtungsSchloss.WaitAsync();
try
{
// Noch einmal prüfen, innerhalb des Schlosses. Draussen wäre die
// Antwort schon veraltet, wenn sie ankommt.
if (await db.Users.AnyAsync())
return Results.Problem(
"Dieser Server ist bereits eingerichtet.", statusCode: StatusCodes.Status409Conflict);
var administrator = new User
{
UserName = anfrage.UserName.Trim(),
PasswordHash = PasswordHasher.Hash(anfrage.Password),
IsAdministrator = true,
};
db.Users.Add(administrator);
await db.SaveChangesAsync();
return Results.Ok(new AuthenticatedUser(administrator.Id, administrator.UserName, true));
}
finally { EinrichtungsSchloss.Release(); }
}).AllowAnonymous();
// Ohne Anmeldung, damit Docker und Reverse Proxies danach fragen können.
// Verrät nichts ausser der Tatsache, dass etwas antwortet.
app.MapGet("/health", () => Results.Ok(new { status = "ok" })).AllowAnonymous();
}
}