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(); } }