ReAdd Webscokets and ready all

This commit is contained in:
Exil Productions
2025-12-10 20:49:58 +01:00
parent 27bc55fd9d
commit be7004f633
3 changed files with 110 additions and 6 deletions

View File

@@ -13,7 +13,8 @@ public class AuthenticationMiddleware
{
"/auth/create",
"/health",
"/metrics"
"/metrics",
"/ws/"
};
public AuthenticationMiddleware(RequestDelegate next, IAuthenticationService authService)

View File

@@ -266,6 +266,20 @@ static bool TryGetGameIdFromCookie(HttpRequest request, out Guid gameId)
return Guid.TryParse(v, out gameId);
}
// WebSocket token extraction helper
static string? ExtractTokenForWebSocket(HttpContext context)
{
// Try Authorization header first
var authHeader = context.Request.Headers.Authorization.FirstOrDefault();
if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer "))
{
return authHeader["Bearer ".Length..];
}
// Fall back to query parameter
return context.Request.Query["token"].FirstOrDefault();
}
// create lobby
@@ -500,6 +514,32 @@ app.MapPost("/lobbies/{lobbyId}/ready", async (HttpContext http, ILobbyService s
.Produces(StatusCodes.Status400BadRequest)
.ProducesProblem(StatusCodes.Status400BadRequest);
// set everyone ready
app.MapPost("/lobbies/{lobbyId}/ready-all", async (HttpContext http, ILobbyService service, string lobbyId, CancellationToken ct) =>
{
if (!TryGetGameIdFromCookie(http.Request, out var gameId))
return Results.BadRequest("Missing or invalid gameId cookie");
var user = http.Items["User"] as AuthenticatedUser;
if (user == null)
return Results.Unauthorized();
var ok = await service.SetEveryoneReadyAsync(gameId, lobbyId, user.SessionToken, ct);
return ok ? Results.Ok() : Results.NotFound();
})
.WithTags("Lobbies")
.WithOpenApi(op =>
{
op.Summary = "Set all members ready";
op.Description = "Owner only. Sets all lobby members as ready. Broadcastes an everyone_ready event with affected members list.";
return op;
})
.Produces(StatusCodes.Status200OK)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status400BadRequest)
.ProducesProblem(StatusCodes.Status400BadRequest);
// start lobby
app.MapPost("/lobbies/{lobbyId}/start", async (HttpContext http, ILobbyService service, string lobbyId, CancellationToken ct) =>
{
@@ -526,7 +566,7 @@ app.MapPost("/lobbies/{lobbyId}/start", async (HttpContext http, ILobbyService s
.Produces(StatusCodes.Status400BadRequest);
// lobby websocket
app.Map("/ws/lobbies/{lobbyId}", async (HttpContext http, string lobbyId, ILobbyEventHub hub, CancellationToken ct) =>
app.Map("/ws/lobbies/{lobbyId}", async (HttpContext http, string lobbyId, ILobbyEventHub hub, IAuthenticationService authService, CancellationToken ct) =>
{
if (!http.WebSockets.IsWebSocketRequest)
return Results.BadRequest("Expected WebSocket");
@@ -534,17 +574,43 @@ app.Map("/ws/lobbies/{lobbyId}", async (HttpContext http, string lobbyId, ILobby
if (!TryGetGameIdFromCookie(http.Request, out var gameId))
return Results.BadRequest("Missing or invalid gameId cookie");
var user = http.Items["User"] as AuthenticatedUser;
if (user == null)
// Extract token from Authorization header or query parameter for WebSocket
var token = ExtractTokenForWebSocket(http);
if (string.IsNullOrEmpty(token))
return Results.Unauthorized();
var validation = await authService.ValidateTokenAsync(token, ct);
if (!validation.IsValid)
return Results.Unauthorized();
using var socket = await http.WebSockets.AcceptWebSocketAsync();
await hub.HandleConnectionAsync(gameId, lobbyId, user.SessionToken, socket, ct);
await hub.HandleConnectionAsync(gameId, lobbyId, token, socket, ct);
return Results.Empty;
}).WithTags("Lobbies").WithOpenApi(op =>
{
op.Summary = "Lobby Websocket";
op.Description = "WebSocket for lobby-specific updates. Requires valid session token.";
op.Description = @"WebSocket for real-time lobby-specific updates. Requires valid session token provided via Authorization header or 'token' query parameter.
Authentication:
- Bearer token in Authorization header: 'Authorization: Bearer <token>'
- Or token as query parameter: '?token=<token>'
WebSocket Events:
- lobby_created: New lobby created
- member_joined: User joined the lobby
- member_left: User left the lobby
- member_ready: User ready state changed
- everyone_ready: Owner set all members as ready (includes affectedMembers array)
- lobby_data: Lobby property updated
- lobby_started: Lobby started by owner
- lobby_empty: Lobby closed due to no members
- lobby_deleted: Lobby forcefully closed
- ping: Server heartbeat (respond with 'pong')
Connection Requirements:
- Valid gameId cookie must be set
- Valid session token required
- User must be member of the lobby";
return op;
});

View File

@@ -12,6 +12,7 @@ public interface ILobbyService
Task<bool> LeaveLobbyAsync(Guid gameId, string sessionToken, CancellationToken ct = default);
Task<List<Lobby>> SearchLobbiesAsync(Guid gameId, int maxRoomsToFind, Dictionary<string, string>? filters, CancellationToken ct = default);
Task<bool> SetIsReadyAsync(Guid gameId, string lobbyId, string sessionToken, bool isReady, CancellationToken ct = default);
Task<bool> SetEveryoneReadyAsync(Guid gameId, string lobbyId, string sessionToken, CancellationToken ct = default);
Task<bool> SetLobbyDataAsync(Guid gameId, string lobbyId, string sessionToken, string key, string value, CancellationToken ct = default);
Task<string?> GetLobbyDataAsync(Guid gameId, string lobbyId, string key, CancellationToken ct = default);
Task<List<LobbyUser>> GetLobbyMembersAsync(Guid gameId, string lobbyId, CancellationToken ct = default);
@@ -321,6 +322,42 @@ public class LobbyService : ILobbyService
return true;
}
public async Task<bool> SetEveryoneReadyAsync(Guid gameId, string lobbyId, string sessionToken, CancellationToken ct = default)
{
if (gameId == Guid.Empty || IsInvalidId(lobbyId) || IsInvalidId(sessionToken))
return false;
var validation = await _authService.ValidateTokenAsync(sessionToken, ct);
if (!validation.IsValid)
return false;
if (!_lobbies.TryGetValue(lobbyId, out var state))
return false;
if (state.GameId != gameId)
return false;
// Only owner can set everyone ready
if (state.OwnerUserId != validation.UserId)
return false;
List<LobbyUser> membersToUpdate;
lock (state)
{
if (state.Started) return false;
membersToUpdate = state.Members.Where(m => !m.IsReady).ToList();
foreach (var m in membersToUpdate)
{
m.IsReady = true;
}
}
if (membersToUpdate.Count > 0)
{
_ = _events.BroadcastAsync(gameId, lobbyId, new { type = "everyone_ready", userId = validation.UserId, affectedMembers = membersToUpdate.Select(m => new { m.UserId, m.DisplayName }) }, ct);
}
return true;
}
public async Task<bool> SetLobbyDataAsync(Guid gameId, string lobbyId, string sessionToken, string key, string value, CancellationToken ct = default)
{
if (gameId == Guid.Empty || IsInvalidId(lobbyId) || IsInvalidId(sessionToken))