mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-29 21:02:13 +00:00
116 lines
3.2 KiB
TypeScript
116 lines
3.2 KiB
TypeScript
import { Cosmetics } from "../core/CosmeticSchemas";
|
|
import { decodePatternData } from "../core/PatternDecoder";
|
|
import {
|
|
FlagSchema,
|
|
PlayerColor,
|
|
PlayerCosmeticRefs,
|
|
PlayerCosmetics,
|
|
PlayerPattern,
|
|
} from "../core/Schemas";
|
|
|
|
type CosmeticResult =
|
|
| { type: "allowed"; cosmetics: PlayerCosmetics }
|
|
| { type: "forbidden"; reason: string };
|
|
|
|
export interface PrivilegeChecker {
|
|
isAllowed(flares: string[], refs: PlayerCosmeticRefs): CosmeticResult;
|
|
}
|
|
|
|
export class PrivilegeCheckerImpl implements PrivilegeChecker {
|
|
constructor(
|
|
private cosmetics: Cosmetics,
|
|
private b64urlDecode: (base64: string) => Uint8Array,
|
|
) {}
|
|
|
|
isAllowed(flares: string[], refs: PlayerCosmeticRefs): CosmeticResult {
|
|
const cosmetics: PlayerCosmetics = {};
|
|
if (refs.patternName) {
|
|
try {
|
|
cosmetics.pattern = this.isPatternAllowed(
|
|
flares,
|
|
refs.patternName,
|
|
refs.patternColorPaletteName ?? null,
|
|
);
|
|
} catch (e) {
|
|
return { type: "forbidden", reason: "invalid pattern: " + e.message };
|
|
}
|
|
}
|
|
if (refs.color) {
|
|
try {
|
|
cosmetics.color = this.isColorAllowed(flares, refs.color);
|
|
} catch (e) {
|
|
return { type: "forbidden", reason: "invalid color: " + e.message };
|
|
}
|
|
}
|
|
if (refs.flag) {
|
|
const result = FlagSchema.safeParse(refs.flag);
|
|
if (!result.success) {
|
|
return {
|
|
type: "forbidden",
|
|
reason: "invalid flag: " + result.error.message,
|
|
};
|
|
}
|
|
cosmetics.flag = result.data;
|
|
}
|
|
|
|
return { type: "allowed", cosmetics };
|
|
}
|
|
|
|
isPatternAllowed(
|
|
flares: readonly string[],
|
|
name: string,
|
|
colorPaletteName: string | null,
|
|
): PlayerPattern {
|
|
// Look for the pattern in the cosmetics.json config
|
|
const found = this.cosmetics.patterns[name];
|
|
if (!found) throw new Error(`Pattern ${name} not found`);
|
|
|
|
try {
|
|
decodePatternData(found.pattern, this.b64urlDecode);
|
|
} catch (e) {
|
|
throw new Error(`Invalid pattern ${name}`);
|
|
}
|
|
|
|
const colorPalette = this.cosmetics.colorPalettes?.[colorPaletteName ?? ""];
|
|
|
|
if (flares.includes("pattern:*")) {
|
|
return {
|
|
name: found.name,
|
|
patternData: found.pattern,
|
|
colorPalette,
|
|
} satisfies PlayerPattern;
|
|
}
|
|
|
|
const flareName =
|
|
`pattern:${found.name}` +
|
|
(colorPaletteName ? `:${colorPaletteName}` : "");
|
|
|
|
if (flares.includes(flareName)) {
|
|
// Player has a flare for this pattern
|
|
return {
|
|
name: found.name,
|
|
patternData: found.pattern,
|
|
colorPalette,
|
|
} satisfies PlayerPattern;
|
|
} else {
|
|
throw new Error(`No flares for pattern ${name}`);
|
|
}
|
|
}
|
|
|
|
isColorAllowed(flares: string[], color: string): PlayerColor {
|
|
const allowedColors = flares
|
|
.filter((flare) => flare.startsWith("color:"))
|
|
.map((flare) => "#" + flare.split(":")[1]);
|
|
if (!allowedColors.includes(color)) {
|
|
throw new Error(`Color ${color} not allowed`);
|
|
}
|
|
return { color };
|
|
}
|
|
}
|
|
|
|
export class FailOpenPrivilegeChecker implements PrivilegeChecker {
|
|
isAllowed(flares: string[], refs: PlayerCosmeticRefs): CosmeticResult {
|
|
return { type: "allowed", cosmetics: {} };
|
|
}
|
|
}
|