Files
GameList/Endpoints/AuthEndpoints.cs
2026-02-05 20:39:12 +01:00

116 lines
4.5 KiB
C#

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