91 lines
3.8 KiB
C#
91 lines
3.8 KiB
C#
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();
|
|
});
|
|
}
|
|
}
|