ReAdd Webscokets and ready all
This commit is contained in:
@@ -13,7 +13,8 @@ public class AuthenticationMiddleware
|
|||||||
{
|
{
|
||||||
"/auth/create",
|
"/auth/create",
|
||||||
"/health",
|
"/health",
|
||||||
"/metrics"
|
"/metrics",
|
||||||
|
"/ws/"
|
||||||
};
|
};
|
||||||
|
|
||||||
public AuthenticationMiddleware(RequestDelegate next, IAuthenticationService authService)
|
public AuthenticationMiddleware(RequestDelegate next, IAuthenticationService authService)
|
||||||
|
|||||||
@@ -266,6 +266,20 @@ static bool TryGetGameIdFromCookie(HttpRequest request, out Guid gameId)
|
|||||||
return Guid.TryParse(v, out 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
|
// create lobby
|
||||||
@@ -500,6 +514,32 @@ app.MapPost("/lobbies/{lobbyId}/ready", async (HttpContext http, ILobbyService s
|
|||||||
.Produces(StatusCodes.Status400BadRequest)
|
.Produces(StatusCodes.Status400BadRequest)
|
||||||
.ProducesProblem(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
|
// start lobby
|
||||||
app.MapPost("/lobbies/{lobbyId}/start", async (HttpContext http, ILobbyService service, string lobbyId, CancellationToken ct) =>
|
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);
|
.Produces(StatusCodes.Status400BadRequest);
|
||||||
|
|
||||||
// lobby websocket
|
// 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)
|
if (!http.WebSockets.IsWebSocketRequest)
|
||||||
return Results.BadRequest("Expected WebSocket");
|
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))
|
if (!TryGetGameIdFromCookie(http.Request, out var gameId))
|
||||||
return Results.BadRequest("Missing or invalid gameId cookie");
|
return Results.BadRequest("Missing or invalid gameId cookie");
|
||||||
|
|
||||||
var user = http.Items["User"] as AuthenticatedUser;
|
// Extract token from Authorization header or query parameter for WebSocket
|
||||||
if (user == null)
|
var token = ExtractTokenForWebSocket(http);
|
||||||
|
if (string.IsNullOrEmpty(token))
|
||||||
|
return Results.Unauthorized();
|
||||||
|
|
||||||
|
var validation = await authService.ValidateTokenAsync(token, ct);
|
||||||
|
if (!validation.IsValid)
|
||||||
return Results.Unauthorized();
|
return Results.Unauthorized();
|
||||||
|
|
||||||
using var socket = await http.WebSockets.AcceptWebSocketAsync();
|
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;
|
return Results.Empty;
|
||||||
}).WithTags("Lobbies").WithOpenApi(op =>
|
}).WithTags("Lobbies").WithOpenApi(op =>
|
||||||
{
|
{
|
||||||
op.Summary = "Lobby Websocket";
|
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;
|
return op;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ public interface ILobbyService
|
|||||||
Task<bool> LeaveLobbyAsync(Guid gameId, string sessionToken, CancellationToken ct = default);
|
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<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> 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<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<string?> GetLobbyDataAsync(Guid gameId, string lobbyId, string key, CancellationToken ct = default);
|
||||||
Task<List<LobbyUser>> GetLobbyMembersAsync(Guid gameId, string lobbyId, CancellationToken ct = default);
|
Task<List<LobbyUser>> GetLobbyMembersAsync(Guid gameId, string lobbyId, CancellationToken ct = default);
|
||||||
@@ -321,6 +322,42 @@ public class LobbyService : ILobbyService
|
|||||||
return true;
|
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)
|
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))
|
if (gameId == Guid.Empty || IsInvalidId(lobbyId) || IsInvalidId(sessionToken))
|
||||||
|
|||||||
Reference in New Issue
Block a user