diff --git a/src/client/TerritoryPatternsModal.ts b/src/client/TerritoryPatternsModal.ts index 02c5e10bb..4658977cd 100644 --- a/src/client/TerritoryPatternsModal.ts +++ b/src/client/TerritoryPatternsModal.ts @@ -71,47 +71,37 @@ export class TerritoryPatternsModal extends LitElement { } private checkPatternPermission(roles: string[]) { - if ( - roles.includes("1286745100411473930") || // creator - roles.includes("1286738076386856991") || // admin - roles.includes("1338654590043820148") // mod - ) { - return; + const patterns = territoryPatterns.pattern ?? {}; + + for (const [key, patternData] of Object.entries(patterns)) { + const roleGroup: string[] | string | undefined = patternData.role_group; + + if (!roleGroup || (Array.isArray(roleGroup) && roleGroup.length === 0)) + continue; + + const groupList = Array.isArray(roleGroup) ? roleGroup : [roleGroup]; + + if (groupList.includes("all")) { + continue; // Allow all users + } + + const isAllowed = groupList.some((required) => roles.includes(required)); + + if (!isAllowed) { + let reason: string; + + if (groupList.includes("donor")) { + reason = + "This pattern is available only to donors (money haters or early access supporters)."; + } else if (groupList.includes("staff")) { + reason = "This pattern is available only to moderators and above."; + } else { + reason = `This pattern is available only to specific roles. (${groupList.join(", ")})`; + } + + this.setLockedPatterns([key], reason); + } } - - this.setLockedPatterns( - ["evan", "openfront"], - "This pattern is available only to moderators and above.", - ); - - if ( - roles.includes("1359441841371480176") || // money haters - roles.includes("1330243292306341969") // early access supporter - ) { - return; - } - - const restrictedForDonorsOnly = [ - "diagonal", - "cross", - "mini_cross", - "horizontal_stripes", - "sparse_dots", - "diagonal_stripe", - "mountain_ridge", - "scattered_dots", - "circuit_board", - "vertical_bars", - ".w.", - ]; - - this.setLockedPatterns( - restrictedForDonorsOnly, - "This pattern is available only to donors (money haters or early access supporters).", - ); - - // Future permission logic here - return; } createRenderRoot() { diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 9fb503cfe..2ce2e47a1 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -77,7 +77,7 @@ export abstract class DefaultServerConfig implements ServerConfig { jwtIssuer(): string { const audience = this.jwtAudience(); return audience === "localhost" - ? "http://localhost:8787" + ? "https://api-worker-dev.evanpelle.workers.dev" : `https://api.${audience}`; } async jwkPublicKey(): Promise { diff --git a/src/server/Privilege.ts b/src/server/Privilege.ts new file mode 100644 index 000000000..0791ccde9 --- /dev/null +++ b/src/server/Privilege.ts @@ -0,0 +1,60 @@ +import { createRequire } from "module"; +const require = createRequire(import.meta.url); +const territory_patterns = require("../../resources/cosmetic/territory_patterns.json"); + +type RoleGroups = Record; +type PatternEntry = { + pattern: string; + role_group: string[]; +}; +type TerritoryPatterns = { + role_groups: RoleGroups; + pattern: Record; +}; + +const patternData = territory_patterns as TerritoryPatterns; + +export class PrivilegeChecker { + private patternData: TerritoryPatterns; + + constructor(patternData: TerritoryPatterns) { + this.patternData = patternData; + } + + isPatternAllowed(base64: string, roleIDs: string[]): boolean { + const found = Object.entries(this.patternData.pattern).find( + ([, entry]) => entry.pattern === base64, + ); + + if (!found) { + // fallback to staff privilege check + const staffRoles = this.patternData.role_groups["staff"] || []; + return roleIDs.some((role) => staffRoles.includes(role)); + } + + const [, entry] = found; + const allowedGroups = entry.role_group; + + if (allowedGroups.includes("all")) { + return true; + } + + for (const groupName of allowedGroups) { + const groupRoles = this.patternData.role_groups[groupName] || []; + if (roleIDs.some((role) => groupRoles.includes(role))) { + return true; + } + } + + return false; + } +} + +let cachedChecker: PrivilegeChecker | null = null; + +export function getPrivilegeChecker(): PrivilegeChecker { + if (!cachedChecker) { + cachedChecker = new PrivilegeChecker(patternData); + } + return cachedChecker; +} diff --git a/src/server/Worker.ts b/src/server/Worker.ts index 6a2afadbd..b0f357699 100644 --- a/src/server/Worker.ts +++ b/src/server/Worker.ts @@ -2,7 +2,6 @@ import express, { NextFunction, Request, Response } from "express"; import rateLimit from "express-rate-limit"; import http from "http"; import ipAnonymize from "ip-anonymize"; -import { createRequire } from "module"; import path from "path"; import { fileURLToPath } from "url"; import { WebSocket, WebSocketServer } from "ws"; @@ -22,9 +21,8 @@ import { GameManager } from "./GameManager"; import { gatekeeper, LimiterType } from "./Gatekeeper"; import { getUserMe, verifyClientToken } from "./jwt"; import { logger } from "./Logger"; +import { getPrivilegeChecker } from "./Privilege"; import { initWorkerMetrics } from "./WorkerMetrics"; -const require = createRequire(import.meta.url); -const territory_patterns = require("../../resources/territory_patterns.json"); const config = getServerConfigFromServer(); @@ -333,57 +331,12 @@ export function startWorker() { } if (clientMsg.pattern !== undefined) { - const isCreator = roles?.includes("1286745100411473930"); - const isAdmin = roles?.includes("1286738076386856991"); - const isMod = roles?.includes("1338654590043820148"); - const isMoneyHater = roles?.includes("1359441841371480176"); - const isEarlyAccess = roles?.includes("1330243292306341969"); - - const isAllowedBase64 = Object.values( - territory_patterns, - ).includes(clientMsg.pattern); - - const evanBlockedPatterns = [ - territory_patterns["openfront"], - territory_patterns["evan"], - ]; - const isEvanPattern = evanBlockedPatterns.includes( - clientMsg.pattern, - ); - - const restrictedBase64Patterns = [ - territory_patterns["diagonal"], - territory_patterns["cross"], - territory_patterns["mini_cross"], - territory_patterns["horizontal_stripes"], - territory_patterns["sparse_dots"], - territory_patterns["diagonal_stripe"], - territory_patterns["mountain_ridge"], - territory_patterns["scattered_dots"], - territory_patterns["circuit_board"], - territory_patterns["vertical_bars"], - territory_patterns[".w."], - ]; - const isRestrictedPattern = restrictedBase64Patterns.includes( - clientMsg.pattern, - ); - - if (!(isCreator || isAdmin || isMod)) { - if (isMoneyHater || isEarlyAccess) { - if (!isAllowedBase64 || isEvanPattern) { - log.warn(`pattern blocked (evan/openfront or unlisted)`); - return; - } - } else { - if ( - !isAllowedBase64 || - isRestrictedPattern || - isEvanPattern - ) { - log.warn(`pattern blocked (restricted or unlisted)`); - return; - } - } + const checker = getPrivilegeChecker(); + if (!checker.isPatternAllowed(clientMsg.pattern, roles ?? [])) { + log.warn( + `pattern blocked (restricted or unlisted): ${clientMsg.pattern}`, + ); + return; } }