From 4763d766919dce0abe2cd6cb84301d0789d667fb Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Wed, 22 Jul 2026 09:54:43 -0400 Subject: [PATCH] bug: fix sanctions --- src/rcon/rcon.gateway.ts | 4 +-- src/rcon/rcon.service.ts | 53 ++++++++++++++++++++++++++++++ src/sanctions/sanctions.service.ts | 35 +++++++++++++++++++- 3 files changed, 88 insertions(+), 4 deletions(-) diff --git a/src/rcon/rcon.gateway.ts b/src/rcon/rcon.gateway.ts index 8a7ec3c8..aeb3a0de 100644 --- a/src/rcon/rcon.gateway.ts +++ b/src/rcon/rcon.gateway.ts @@ -25,9 +25,7 @@ export class RconGateway { ) { if ( !client.user || - client.user.role === "user" || - client.user.role === "verified_user" || - client.user.role === "streamer" + !(await this.rconService.canAccessServer(data.serverId, client.user)) ) { return; } diff --git a/src/rcon/rcon.service.ts b/src/rcon/rcon.service.ts index ddce75be..d3689b04 100644 --- a/src/rcon/rcon.service.ts +++ b/src/rcon/rcon.service.ts @@ -7,6 +7,8 @@ import { DISCORD_COLORS } from "../notifications/utilities/constants"; import { TypeSenseService } from "../type-sense/type-sense.service"; import { RedisManagerService } from "../redis/redis-manager/redis-manager.service"; import { CacheService } from "../cache/cache.service"; +import { User } from "../auth/types/User"; +import { isRoleAbove } from "../utilities/isRoleAbove"; @Injectable() export class RconService { @@ -26,6 +28,57 @@ export class RconService { private connections: Record = {}; private connectTimeouts: Record = {}; + // An elevated role is not by itself a licence to drive every server: match + // servers are reachable only by that match's organizer, while dedicated + // servers stay open to the moderation roles that police public play. + public async canAccessServer(serverId: string, user: User): Promise { + if (user.role === "administrator") { + return true; + } + + if (!isRoleAbove(user.role, "moderator")) { + return false; + } + + const { servers_by_pk: server } = await this.hasuraService.query({ + servers_by_pk: { + __args: { + id: serverId, + }, + is_dedicated: true, + }, + }); + + if (!server) { + return false; + } + + if (server.is_dedicated) { + return true; + } + + const { matches } = await this.hasuraService.query( + { + matches: { + __args: { + where: { + server_id: { + _eq: serverId, + }, + status: { + _nin: ["Canceled", "Finished", "Forfeit", "Surrendered", "Tie"], + }, + }, + }, + is_organizer: true, + }, + }, + user.steam_id, + ); + + return matches.some((match) => match.is_organizer); + } + public async connect(serverId: string): Promise { if (this.connections[serverId]) { this.setupConnectionTimeout(serverId); diff --git a/src/sanctions/sanctions.service.ts b/src/sanctions/sanctions.service.ts index 976973dd..d92af920 100644 --- a/src/sanctions/sanctions.service.ts +++ b/src/sanctions/sanctions.service.ts @@ -229,6 +229,27 @@ export class SanctionsService { }); } + private async hasLiveMatch(serverId: string): Promise { + const { matches } = await this.hasura.query({ + matches: { + __args: { + where: { + server_id: { + _eq: serverId, + }, + status: { + _nin: ["Canceled", "Finished", "Forfeit", "Surrendered", "Tie"], + }, + }, + limit: 1, + }, + id: true, + }, + }); + + return matches.length > 0; + } + private async syncServer( serverId: string, kickUserid: string | null, @@ -246,7 +267,19 @@ export class SanctionsService { await rcon.send(`kickid ${kickUserid} Banned`); } - await rcon.send("get_sanctions"); + // The plugins carry mute/gag/ban as flags on the match payload, so a + // match refresh is what actually re-applies them live. A server with no + // match has no command to push sanctions to yet. + if (!(await this.hasLiveMatch(serverId))) { + return { + enforced: kickUserid !== null, + message: kickUserid + ? "sanction saved and player kicked; server has no match to sync" + : "sanction saved; server has no match to sync", + }; + } + + await rcon.send("get_match"); return { enforced: true,