using GameList.Contracts; using GameList.Data; using GameList.Domain; using GameList.Infrastructure; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace GameList.Endpoints; public static class AuthEndpoints { public static void MapAuthEndpoints(this IEndpointRouteBuilder app) { var group = app.MapGroup("/api/auth"); group.MapPost("/register", async ([FromBody] RegisterRequest request, HttpContext ctx, AppDbContext db, IConfiguration config) => { if (!AuthValidator.TryValidateRegistration(request, out var validated, out var registrationError)) return Results.BadRequest(new { error = registrationError }); var exists = await db.Players.AnyAsync(p => p.NormalizedUsername == validated.NormalizedUsername); if (exists) return Results.Conflict(new { error = "Username already taken." }); var (hash, salt) = PasswordHasher.HashPassword(request.Password); var expectedAdminKey = config["ADMIN_PASSWORD"]; var wantsAdmin = !string.IsNullOrWhiteSpace(validated.AdminKey); if (wantsAdmin) { if (string.IsNullOrWhiteSpace(expectedAdminKey) || validated.AdminKey != expectedAdminKey) return Results.BadRequest(new { error = "Invalid admin key." }); } var isAdmin = wantsAdmin; var player = new Player { Id = Guid.NewGuid(), Username = validated.Username, NormalizedUsername = validated.NormalizedUsername, PasswordHash = hash, PasswordSalt = salt, DisplayName = validated.DisplayName, IsAdmin = isAdmin, CreatedAt = DateTimeOffset.UtcNow, LastLoginAt = DateTimeOffset.UtcNow }; db.Players.Add(player); await db.SaveChangesAsync(); await PlayerIdentityExtensions.SignInPlayerAsync(ctx, player); return Results.Ok(new { player.Id, player.Username, player.DisplayName, player.IsAdmin }); }); group.MapPost("/login", async ([FromBody] LoginRequest request, HttpContext ctx, AppDbContext db) => { if (!AuthValidator.TryValidateLogin(request, out _, out var normalizedUsername, out var loginError)) return Results.BadRequest(new { error = loginError }); var player = await db.Players.FirstOrDefaultAsync(p => p.NormalizedUsername == normalizedUsername); if (player == null || !PasswordHasher.Verify(request.Password, player.PasswordHash, player.PasswordSalt)) return Results.Json(new { error = "Invalid username or password." }, statusCode: StatusCodes.Status401Unauthorized); if (string.IsNullOrWhiteSpace(player.DisplayName)) { player.DisplayName = EndpointHelpers.TrimTo(player.Username, AuthValidator.MaxDisplayNameLength); } player.LastLoginAt = DateTimeOffset.UtcNow; await db.SaveChangesAsync(); await PlayerIdentityExtensions.SignInPlayerAsync(ctx, player); return Results.Ok(new { player.Id, player.Username, player.DisplayName, player.IsAdmin }); }); group.MapPost("/logout", async (HttpContext ctx) => { await PlayerIdentityExtensions.SignOutPlayerAsync(ctx); return Results.NoContent(); }); } }