From 42d99e385675ded99da86d6085c32e25b68fc16d Mon Sep 17 00:00:00 2001 From: enkoder Date: Tue, 25 Nov 2025 11:06:28 -0800 Subject: [PATCH] Add admin toggle --- api/src/models/leaderboard.ts | 3 ++ api/src/models/results.ts | 6 ++-- api/src/openapi.ts | 1 + api/src/routes/leaderboard.ts | 11 ++++++++ app/src/client/services/LeaderboardService.ts | 3 ++ app/src/output.css | 9 +++--- app/src/routes/Profile.tsx | 10 +++---- app/src/routes/leaderboard.tsx | 28 +++++++++++++++++-- spec.json | 2 +- 9 files changed, 58 insertions(+), 15 deletions(-) diff --git a/api/src/models/leaderboard.ts b/api/src/models/leaderboard.ts index 44f370a..6307e18 100644 --- a/api/src/models/leaderboard.ts +++ b/api/src/models/leaderboard.ts @@ -20,18 +20,21 @@ export class Leaderboard { format, tags, isAdmin, + includeDisabled, }: { seasonId?: number; faction?: Faction; format?: Format; tags?: string[]; isAdmin?: boolean; + includeDisabled?: boolean; }): Promise { const results = await Results.getExpanded({ seasonId, faction, format, tags, + includeDisabled, }); const rows: Record = {}; diff --git a/api/src/models/results.ts b/api/src/models/results.ts index a0ee77f..65b27a7 100644 --- a/api/src/models/results.ts +++ b/api/src/models/results.ts @@ -31,6 +31,7 @@ type GetExpandedOptions = { faction?: Faction | null; format?: Format | null; tags?: string[] | null; + includeDisabled?: boolean; }; export async function get(user_id: number, tournament_id: number) { @@ -56,6 +57,7 @@ export class Results { faction = null, format = null, tags = null, + includeDisabled = false, }: GetExpandedOptions): Promise { let tagModels: Tag[] = []; if (tags) { @@ -87,8 +89,8 @@ export class Results { "tournaments.format as format", "tournaments.season_id as season_id", ]) - // Only fetch results for non-disabled users - .where("users.disabled", "=", 0) + // Only fetch results for non-disabled users unless includeDisabled is true + .$if(!includeDisabled, (qb) => qb.where("users.disabled", "=", 0)) .select((eb) => [ eb.fn .agg("rank") diff --git a/api/src/openapi.ts b/api/src/openapi.ts index 9e9591a..f388a33 100644 --- a/api/src/openapi.ts +++ b/api/src/openapi.ts @@ -419,6 +419,7 @@ export const GetLeaderboardSchema = { factionCode: Query(z.string().optional()), format: Query(FormatComponent.optional()), tags: Query(z.string().or(z.array(z.string())).optional()), + includeDisabled: Query(z.coerce.boolean().optional()), }, responses: { "200": { diff --git a/api/src/routes/leaderboard.ts b/api/src/routes/leaderboard.ts index 5e3103e..acd22b5 100644 --- a/api/src/routes/leaderboard.ts +++ b/api/src/routes/leaderboard.ts @@ -50,6 +50,16 @@ export class GetLeaderboard extends OpenAPIRoute { ? [req.query.tags] : null; + // Security check: only admins can include disabled users + if (req.query.includeDisabled === "true" && !req.is_admin) { + return new Response("Forbidden: Only admins can view disabled users", { + status: 403, + }); + } + + const includeDisabled = + req.is_admin && req.query.includeDisabled === "true"; + const rows: LeaderboardRowComponentType[] = []; const results = await Leaderboard.getExpanded({ seasonId, @@ -57,6 +67,7 @@ export class GetLeaderboard extends OpenAPIRoute { format, tags, isAdmin: req.is_admin, + includeDisabled, }); for (const result of results) { rows.push(LeaderboardRowComponent.parse(result)); diff --git a/app/src/client/services/LeaderboardService.ts b/app/src/client/services/LeaderboardService.ts index 27f98f5..f58c94d 100644 --- a/app/src/client/services/LeaderboardService.ts +++ b/app/src/client/services/LeaderboardService.ts @@ -21,6 +21,7 @@ export class LeaderboardService { * @param factionCode * @param format * @param tags + * @param includeDisabled * @returns LeaderboardRow Returns a array of rows compromising the full leaderboard for the given season * @throws ApiError */ @@ -29,6 +30,7 @@ export class LeaderboardService { factionCode?: string, format?: (Format & string), tags?: (string | Array), + includeDisabled?: boolean | null, ): CancelablePromise> { return __request(OpenAPI, { method: 'GET', @@ -38,6 +40,7 @@ export class LeaderboardService { 'factionCode': factionCode, 'format': format, 'tags': tags, + 'includeDisabled': includeDisabled, }, }); } diff --git a/app/src/output.css b/app/src/output.css index 0907476..abe0e4f 100644 --- a/app/src/output.css +++ b/app/src/output.css @@ -1638,10 +1638,6 @@ html { opacity: 1; } -.opacity-30 { - opacity: 0.3; -} - .opacity-95 { opacity: 0.95; } @@ -1897,6 +1893,11 @@ html { --tw-ring-inset: inset; } +.focus\:ring-cyan-500:focus { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(6 182 212 / var(--tw-ring-opacity, 1)); +} + .focus\:ring-cyan-600:focus { --tw-ring-opacity: 1; --tw-ring-color: rgb(8 145 178 / var(--tw-ring-opacity, 1)); diff --git a/app/src/routes/Profile.tsx b/app/src/routes/Profile.tsx index 72c6c3c..4425654 100644 --- a/app/src/routes/Profile.tsx +++ b/app/src/routes/Profile.tsx @@ -378,7 +378,7 @@ export function Profile() { > {tag.name} - + {tag.count} tournament{tag.count !== 1 ? "s" : ""} @@ -387,7 +387,7 @@ export function Profile() {
{/* Left Column - Tournament Limits */}
-