Compare commits

..

1 Commits

Author SHA1 Message Date
Alexa 05099cce91 Whitelist api.{apex} as a shared public host
api.{apex} is the client's primary API host, served by path
dispatch across every service. It belonged to no route group's
HostSubdomains, so it was absent from PublicSubdomains and thus
from the monolith's HostFiltering AllowedHosts — every request to
api.{apex} was rejected with 400 "Bad Request - Invalid Hostname".

Append "api" alongside admin/feed so it is whitelisted while
remaining path-routed (it owns no service by host).
2026-06-27 23:59:06 +03:00
2 changed files with 13 additions and 37 deletions
@@ -20,6 +20,7 @@ namespace DorkNet.Server.Controllers.Match;
[ApiController] [ApiController]
public class MatchPlayerController( public class MatchPlayerController(
PlayerPresenceService presence, PlayerPresenceService presence,
NotificationService notifications,
RoomService rooms, RoomService rooms,
PrivateInstanceService privateInstances, PrivateInstanceService privateInstances,
IConfiguration config, IConfiguration config,
@@ -36,7 +37,7 @@ public class MatchPlayerController(
/// </summary> /// </summary>
[HttpPost("/player/login")] [HttpPost("/player/login")]
[Consumes("application/x-www-form-urlencoded", "multipart/form-data")] [Consumes("application/x-www-form-urlencoded", "multipart/form-data")]
public IActionResult PlayerLogin([FromForm(Name = "LoginLock")] string? loginLock) public async Task<IActionResult> PlayerLogin([FromForm(Name = "LoginLock")] string? loginLock)
{ {
var playerId = TryGetCurrentPlayerId(); var playerId = TryGetCurrentPlayerId();
if (playerId != 0 && !string.IsNullOrWhiteSpace(loginLock)) if (playerId != 0 && !string.IsNullOrWhiteSpace(loginLock))
@@ -45,15 +46,17 @@ public class MatchPlayerController(
if (replaced) if (replaced)
{ {
log.LogWarning( log.LogWarning(
"[player-login] concurrent session marker replaced for player={PlayerId}; stale SignalR connections will be evicted on hub connect", "[player-login] concurrent session for player={PlayerId}; kicking old session(s) via ModerationKick push",
playerId); playerId);
// Do not broadcast ModerationKick to the whole player group // The new session hasn't opened its SignalR socket yet
// here. During reconnects the active watch can already have a // (negotiate fires AFTER /player/login on the watch's
// SignalR connection in that group, and the 2020 client crashes // boot sequence — verified in the live trace), so the
// hard if it receives a malformed/stale moderation payload. // ModerationKick push hits only the OLDER session's
// NotifyHub.OnConnectedAsync snapshots existing connection ids // existing connections. They'll show the "you've been
// before adding the new connection and sends a targeted kick to // kicked" dialog and gracefully disconnect via the
// those ids only. // watch's LocalModerationKickSelf path.
await notifications.KickStaleSession(playerId,
"Your account was signed in on another device.");
} }
} }
return Ok(Array.Empty<object>()); return Ok(Array.Empty<object>());
+1 -28
View File
@@ -64,12 +64,7 @@ public class GameSessionService(DorkNetDbContext db)
// in, so we save once with a placeholder, then patch and save // in, so we save once with a placeholder, then patch and save
// again. Two SaveChangesAsync calls is cheap at our QPS. // again. Two SaveChangesAsync calls is cheap at our QPS.
PhotonRoomName = "pending", PhotonRoomName = "pending",
// Track the room's "Max Player Count in This Subroom" slider MaxCapacity = 8,
// instead of a hardcoded 8, so matchmade joiners fill the room to
// the host's chosen limit rather than fragmenting into a new
// instance every 8 players. (Existing sessions keep whatever cap
// they were created with.)
MaxCapacity = await ResolveRoomMaxCapacityAsync(effectiveRoom),
PlayerCount = 1, PlayerCount = 1,
CreatedAt = DateTime.UtcNow, CreatedAt = DateTime.UtcNow,
}; };
@@ -80,28 +75,6 @@ public class GameSessionService(DorkNetDbContext db)
return ToDto(session); return ToDto(session);
} }
// Default session cap when we can't resolve a room-specific value (system
// rooms like the dorm use a GUID roomId that doesn't map to a RoomEntity).
private const int DefaultMaxCapacity = 8;
/// <summary>Resolve a new session's capacity from the room's entry
/// subroom (lowest <c>OrderIndex</c>) <see cref="RoomSceneEntity.MaxPlayers"/>
/// — the value the in-game "Max Player Count in This Subroom" slider
/// writes. Matchmade sessions land in the entry subroom, so its cap is the
/// right ceiling. GameSession roomIds are the room's numeric Id as a
/// string for user rooms; non-numeric (dorm GUID, etc.) → default. Clamped
/// to a sane Photon ceiling.</summary>
private async Task<int> ResolveRoomMaxCapacityAsync(string roomId)
{
if (!long.TryParse(roomId, out var roomLongId)) return DefaultMaxCapacity;
var max = await db.RoomScenes
.Where(s => s.RoomId == roomLongId)
.OrderBy(s => s.OrderIndex)
.Select(s => (int?)s.MaxPlayers)
.FirstOrDefaultAsync();
return max is int v && v > 0 ? Math.Clamp(v, 1, 80) : DefaultMaxCapacity;
}
public async Task<GameSession?> GetByIdAsync(long id) public async Task<GameSession?> GetByIdAsync(long id)
{ {
var s = await db.GameSessions.AsNoTracking() var s = await db.GameSessions.AsNoTracking()