Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions api/src/models/leaderboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,21 @@ export class Leaderboard {
format,
tags,
isAdmin,
includeDisabled,
}: {
seasonId?: number;
faction?: Faction;
format?: Format;
tags?: string[];
isAdmin?: boolean;
includeDisabled?: boolean;
}): Promise<LeaderboardRow[]> {
const results = await Results.getExpanded({
seasonId,
faction,
format,
tags,
includeDisabled,
});

const rows: Record<number, LeaderboardRow> = {};
Expand Down
6 changes: 4 additions & 2 deletions api/src/models/results.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -56,6 +57,7 @@ export class Results {
faction = null,
format = null,
tags = null,
includeDisabled = false,
}: GetExpandedOptions): Promise<ResultExpanded[]> {
let tagModels: Tag[] = [];
if (tags) {
Expand Down Expand Up @@ -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<number>("rank")
Expand Down
1 change: 1 addition & 0 deletions api/src/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
11 changes: 11 additions & 0 deletions api/src/routes/leaderboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,24 @@ 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,
faction,
format,
tags,
isAdmin: req.is_admin,
includeDisabled,
});
for (const result of results) {
rows.push(LeaderboardRowComponent.parse(result));
Expand Down
3 changes: 3 additions & 0 deletions app/src/client/services/LeaderboardService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -29,6 +30,7 @@ export class LeaderboardService {
factionCode?: string,
format?: (Format & string),
tags?: (string | Array<string>),
includeDisabled?: boolean | null,
): CancelablePromise<Array<LeaderboardRow>> {
return __request(OpenAPI, {
method: 'GET',
Expand All @@ -38,6 +40,7 @@ export class LeaderboardService {
'factionCode': factionCode,
'format': format,
'tags': tags,
'includeDisabled': includeDisabled,
},
});
}
Expand Down
9 changes: 5 additions & 4 deletions app/src/output.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions app/src/routes/Profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ export function Profile() {
>
{tag.name}
</button>
<span className={"whitespace-nowrap text-sm text-gray-400"}>
<span className={"whitespace-nowrap text-gray-400 text-sm"}>
{tag.count} tournament{tag.count !== 1 ? "s" : ""}
</span>
</div>
Expand All @@ -387,7 +387,7 @@ export function Profile() {
<div className={"mb-4 grid grid-cols-2 gap-4"}>
{/* Left Column - Tournament Limits */}
<div className={"flex flex-col gap-2"}>
<label className={"text-sm font-medium text-gray-400"}>
<label className={"font-medium text-gray-400 text-sm"}>
Tournament Limits
</label>
<div className={"flex items-center"}>
Expand All @@ -413,12 +413,12 @@ export function Profile() {

{/* Right Column - Normalize Type */}
<div className={"flex flex-col gap-2"}>
<label className={"text-sm font-medium text-gray-400"}>
<label className={"font-medium text-gray-400 text-sm"}>
Normalize Type
</label>
<select
className={
"w-full rounded-lg border border-gray-600 bg-gray-900 px-3 py-2 text-sm text-gray-300"
"w-full rounded-lg border border-gray-600 bg-gray-900 px-3 py-2 text-gray-300 text-sm"
}
value={tag.normalized_tournament_type || ""}
onChange={(e) =>
Expand All @@ -440,7 +440,7 @@ export function Profile() {
</div>

{/* Footer Row - Delete Button */}
<div className={"flex justify-end border-t border-gray-700 pt-3"}>
<div className={"flex justify-end border-gray-700 border-t pt-3"}>
<Tooltip placement={"bottom"}>
<TooltipTrigger>
<button
Expand Down
28 changes: 25 additions & 3 deletions app/src/routes/leaderboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { Link } from "../components/Link";
import { PageHeading } from "../components/PageHeader";
import { Tooltip, TooltipContent, TooltipTrigger } from "../components/Tooltip";
import { getFilterValues, getSearchParamsFromValues } from "../filterUtils";
import useAuth from "../useAuth";
import { capStr } from "../util";

type ExpandedSectionProps = {
Expand Down Expand Up @@ -130,6 +131,8 @@ export function Leaderboard() {
const [searchParams] = useSearchParams();
const values = getFilterValues(searchParams);
const [selectedUser, setSelectedUser] = useState<User | null>(null);
const [includeDisabled, setIncludeDisabled] = useState(false);
const { user } = useAuth();

const { data: leaderboardRows } = useQuery<LeaderboardRow[]>({
queryKey: [
Expand All @@ -138,13 +141,15 @@ export function Leaderboard() {
values.faction,
values.format,
values.tags,
includeDisabled,
],
queryFn: () =>
LeaderboardService.getGetLeaderboard(
values.seasonId,
values.faction,
values.format,
values.tags,
includeDisabled,
),
});

Expand Down Expand Up @@ -201,6 +206,25 @@ export function Leaderboard() {
</h1>
)}
</PageHeading>
{user?.is_admin && (
<div className={"mb-4 flex items-center gap-2 px-4"}>
<input
type="checkbox"
id="includeDisabled"
checked={includeDisabled}
onChange={(e) => setIncludeDisabled(e.target.checked)}
className={
"h-4 w-4 cursor-pointer rounded border-gray-600 bg-gray-900 text-cyan-500 focus:ring-2 focus:ring-cyan-500"
}
/>
<label
htmlFor="includeDisabled"
className={"cursor-pointer text-gray-300 text-sm"}
>
Show all users (including disabled accounts)
</label>
</div>
)}
<FilterSection hasSearchBar={true} startSeason={3} />
<table
className={
Expand Down Expand Up @@ -277,9 +301,7 @@ export function Leaderboard() {
)}
>
{row.disabled ? (
<text className="text-gray-500 opacity-30">
{row.user_name}
</text>
<text className="text-gray-500">{row.user_name}</text>
) : (
<Link to={getLinkToUserSearchParams(row)}>
{row.user_name}
Expand Down
2 changes: 1 addition & 1 deletion spec.json

Large diffs are not rendered by default.