diff --git a/Endpoints/AuthEndpoints.cs b/Endpoints/AuthEndpoints.cs index eb9ecfc..63ec427 100644 --- a/Endpoints/AuthEndpoints.cs +++ b/Endpoints/AuthEndpoints.cs @@ -15,33 +15,19 @@ public static class AuthEndpoints 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 (!AuthValidator.TryValidateRegistration(request, out var validated, out var registrationError)) + return Results.BadRequest(new { error = registrationError }); - if (string.IsNullOrWhiteSpace(request.Password)) - return Results.BadRequest(new { error = "Password is required." }); - - if (request.DisplayName?.Trim().Length > 16) - return Results.BadRequest(new { error = "Display name must be <= 16 characters." }); - - 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); + 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 adminKey = EndpointHelpers.TrimTo(request.AdminKey, 128); var expectedAdminKey = config["ADMIN_PASSWORD"]; - var wantsAdmin = !string.IsNullOrWhiteSpace(adminKey); + var wantsAdmin = !string.IsNullOrWhiteSpace(validated.AdminKey); if (wantsAdmin) { - if (string.IsNullOrWhiteSpace(expectedAdminKey) || adminKey != expectedAdminKey) + if (string.IsNullOrWhiteSpace(expectedAdminKey) || validated.AdminKey != expectedAdminKey) return Results.BadRequest(new { error = "Invalid admin key." }); } @@ -50,11 +36,11 @@ public static class AuthEndpoints var player = new Player { Id = Guid.NewGuid(), - Username = username, - NormalizedUsername = normalized, + Username = validated.Username, + NormalizedUsername = validated.NormalizedUsername, PasswordHash = hash, PasswordSalt = salt, - DisplayName = displayName, + DisplayName = validated.DisplayName, IsAdmin = isAdmin, CreatedAt = DateTimeOffset.UtcNow, LastLoginAt = DateTimeOffset.UtcNow @@ -76,20 +62,16 @@ public static class AuthEndpoints 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." }); + if (!AuthValidator.TryValidateLogin(request, out _, out var normalizedUsername, out var loginError)) + return Results.BadRequest(new { error = loginError }); - var normalized = username.ToLowerInvariant(); - var player = await db.Players.FirstOrDefaultAsync(p => p.NormalizedUsername == normalized); + 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, 16); + player.DisplayName = EndpointHelpers.TrimTo(player.Username, AuthValidator.MaxDisplayNameLength); } player.LastLoginAt = DateTimeOffset.UtcNow; diff --git a/Endpoints/AuthValidator.cs b/Endpoints/AuthValidator.cs new file mode 100644 index 0000000..854614f --- /dev/null +++ b/Endpoints/AuthValidator.cs @@ -0,0 +1,77 @@ +using GameList.Contracts; + +namespace GameList.Endpoints; + +internal static class AuthValidator +{ + public const int MaxUsernameLength = 24; + public const int MaxDisplayNameLength = 16; + public const int MaxAdminKeyLength = 128; + + public static bool TryValidateRegistration(RegisterRequest request, out ValidatedRegistration validated, out string error) + { + var username = (request.Username ?? string.Empty).Trim(); + if (string.IsNullOrWhiteSpace(username) || username.Length > MaxUsernameLength) + { + validated = default; + error = $"Username is required and must be <= {MaxUsernameLength} characters."; + return false; + } + + if (string.IsNullOrWhiteSpace(request.Password)) + { + validated = default; + error = "Password is required."; + return false; + } + + if ((request.DisplayName ?? string.Empty).Trim().Length > MaxDisplayNameLength) + { + validated = default; + error = $"Display name must be <= {MaxDisplayNameLength} characters."; + return false; + } + + var displayName = EndpointHelpers.TrimTo(request.DisplayName, MaxDisplayNameLength); + if (string.IsNullOrWhiteSpace(displayName)) + { + validated = default; + error = "Display name is required."; + return false; + } + + var adminKey = EndpointHelpers.TrimTo(request.AdminKey, MaxAdminKeyLength); + validated = new ValidatedRegistration(username, username.ToLowerInvariant(), displayName, adminKey); + error = string.Empty; + return true; + } + + public static bool TryValidateLogin(LoginRequest request, out string username, out string normalizedUsername, out string error) + { + username = (request.Username ?? string.Empty).Trim(); + normalizedUsername = string.Empty; + + if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(request.Password)) + { + error = "Username and password are required."; + return false; + } + + if (username.Length > MaxUsernameLength) + { + error = $"Username must be <= {MaxUsernameLength} characters."; + return false; + } + + normalizedUsername = username.ToLowerInvariant(); + error = string.Empty; + return true; + } + + public readonly record struct ValidatedRegistration( + string Username, + string NormalizedUsername, + string DisplayName, + string? AdminKey + ); +}