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) => { var username = request.Username?.Trim(); if (string.IsNullOrWhiteSpace(username) || username.Length > 24) return Results.BadRequest(new { error = "Username is required and must be <= 24 characters." }); if (string.IsNullOrWhiteSpace(request.Password)) return Results.BadRequest(new { error = "Password is required." }); var displayName = EndpointHelpers.TrimTo(request.DisplayName, 16); 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 wantsAdmin = !string.IsNullOrWhiteSpace(adminKey); if (wantsAdmin) { if (string.IsNullOrWhiteSpace(expectedAdminKey) || adminKey != expectedAdminKey) return Results.BadRequest(new { error = "Invalid admin key." }); } var isAdmin = wantsAdmin; 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(); 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) => { var username = request.Username?.Trim(); if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(request.Password)) return Results.BadRequest(new { error = "Username and password are required." }); if (username.Length > 24) return Results.BadRequest(new { error = "Username must be <= 24 characters." }); 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 = EndpointHelpers.TrimTo(player.Username, 16); } 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(); }); } }