using GameList.Contracts; using GameList.Data; using GameList.Domain; using GameList.Infrastructure; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Http; 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) => { var username = request.Username?.Trim(); if (string.IsNullOrWhiteSpace(username) || username.Length > 64) return Results.BadRequest(new { error = "Username is required and must be <= 64 characters." }); if (string.IsNullOrWhiteSpace(request.Password)) return Results.BadRequest(new { error = "Password is required." }); var displayName = EndpointHelpers.TrimTo(request.DisplayName, 64); if (string.IsNullOrWhiteSpace(displayName)) return Results.BadRequest(new { error = "Display name is required." }); var normalized = username.ToLowerInvariant(); var exists = await db.Players.AnyAsync(p => p.NormalizedUsername == normalized); if (exists) return Results.Conflict(new { error = "Username already taken." }); var (hash, salt) = PasswordHasher.HashPassword(request.Password); var adminKey = EndpointHelpers.TrimTo(request.AdminKey, 128); var expectedAdminKey = config["ADMIN_PASSWORD"]; var isAdmin = !string.IsNullOrWhiteSpace(expectedAdminKey) && adminKey == expectedAdminKey; var player = new Player { Id = Guid.NewGuid(), Username = username, NormalizedUsername = normalized, PasswordHash = hash, PasswordSalt = salt, DisplayName = displayName, IsAdmin = isAdmin, CreatedAt = DateTimeOffset.UtcNow, LastLoginAt = DateTimeOffset.UtcNow }; db.Players.Add(player); await db.SaveChangesAsync(); PlayerIdentityExtensions.IssuePlayerCookie(ctx, player.Id); return Results.Ok(new { player.Id, player.Username, player.DisplayName, player.IsAdmin }); }); group.MapPost("/login", async ([FromBody] LoginRequest request, HttpContext ctx, AppDbContext db) => { var username = request.Username?.Trim(); if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(request.Password)) return Results.BadRequest(new { error = "Username and password are required." }); var normalized = username.ToLowerInvariant(); var player = await db.Players.FirstOrDefaultAsync(p => p.NormalizedUsername == normalized); 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 = player.Username; } player.LastLoginAt = DateTimeOffset.UtcNow; await db.SaveChangesAsync(); PlayerIdentityExtensions.IssuePlayerCookie(ctx, player.Id); return Results.Ok(new { player.Id, player.Username, player.DisplayName, player.IsAdmin }); }); group.MapPost("/logout", (HttpContext ctx) => { PlayerIdentityExtensions.ClearPlayerCookie(ctx); return Results.NoContent(); }); } }