Code review fixes: type safety, security hardening, and dead code removal

- Fix Socket.IO event types: draft-paused and draft-resumed were typed as
  () => void but are emitted with { seasonId, paused } data payloads

- Fix draft.force-manual-pick: add missing season.status === "draft" guard
  so commissioners cannot force picks outside an active draft; add duplicate
  pick-number check so a slot cannot be assigned two picks (the previous
  code only checked participant uniqueness, not slot uniqueness)

- Replace args: any with ActionFunctionArgs / Route.LoaderArgs across all
  API routes and league loaders; replace (auth as any).userId casts with
  proper const { userId } = await getAuth(args) destructuring

- Remove unused isSnakeDraft = true dead variable from draft.make-pick

- Replace autodraftSettings: any and draftSlots: any[] in draft-utils with
  properly typed InferSelectModel / DraftSlot types

- Update force-manual-pick tests: sequence draftPicks.findFirst mock for
  the two-call flow; add new tests for status-check and slot-uniqueness

https://claude.ai/code/session_01FKq2gPFYpgdfxr8cw4Z2AZ
This commit is contained in:
Claude 2026-02-23 02:41:43 +00:00
parent 3da27af036
commit c036cf6e28
No known key found for this signature in database
21 changed files with 116 additions and 64 deletions

View file

@ -1,6 +1,7 @@
import { database } from "~/database/context";
import * as schema from "~/database/schema";
import { eq, and, notInArray, desc, inArray } from "drizzle-orm";
import type { InferSelectModel } from "drizzle-orm";
import { getTeamQueue } from "./draft-queue";
import { isParticipantDrafted, getDraftPicksWithSports, getTeamDraftPicksWithSports } from "./draft-pick";
import { getParticipantsForSeasonWithSports } from "./participant";
@ -12,11 +13,13 @@ import { getSocketIO } from "../../server/socket";
* Check if the next team has autodraft enabled and immediately execute their pick
* This is called after a pick is made to chain autodraft picks
*/
type DraftSlot = { teamId: string; draftOrder: number };
export async function checkAndTriggerNextAutodraft(params: {
seasonId: string;
nextPickNumber: number;
totalTeams: number;
draftSlots: any[];
draftSlots: DraftSlot[];
db?: ReturnType<typeof database>;
}): Promise<void> {
const { seasonId, totalTeams, draftSlots, db: providedDb } = params;
@ -367,20 +370,26 @@ export function getTeamForPick(
* @param params.db - Database instance (optional, will use database() if not provided)
* @returns Result object with success status and pick data
*/
type AutodraftSettings = InferSelectModel<typeof schema.autodraftSettings>;
export async function executeAutoPick(params: {
seasonId: string;
teamId: string;
pickNumber: number;
triggeredBy: "commissioner" | "timer";
commissionerUserId?: string;
autodraftSettings?: any;
autodraftSettings?: AutodraftSettings | null;
db?: ReturnType<typeof database>;
chainEnabled?: boolean; // Set to false when called from within the autodraft chain to prevent recursion
}): Promise<{
success: boolean;
error?: string;
pick?: any;
participant?: any;
pick?: InferSelectModel<typeof schema.draftPicks>;
participant?: InferSelectModel<typeof schema.participants> & {
sportsSeason: InferSelectModel<typeof schema.sportsSeasons> & {
sport: InferSelectModel<typeof schema.sports>;
};
};
nextPickNumber?: number;
isDraftComplete?: boolean;
}> {

View file

@ -220,8 +220,35 @@ describe("draft.force-manual-pick action", () => {
expect(response.status).toBe(404);
});
it("returns 400 when the draft is not active", async () => {
mockDb.query.seasons.findFirst.mockResolvedValue({ ...mockSeason, status: "pre_draft" });
const response = await action({ request: defaultPickRequest(), params: {}, context: {} });
expect(response.status).toBe(400);
const data = await response.json();
expect(data.error).toMatch(/not currently active/i);
});
it("returns 400 when a pick already exists at the requested slot", async () => {
mockDb.query.draftPicks.findFirst.mockResolvedValueOnce({
id: "existing-pick",
participantId: "other-participant",
});
const response = await action({ request: defaultPickRequest(), params: {}, context: {} });
expect(response.status).toBe(400);
const data = await response.json();
expect(data.error).toMatch(/already exists at this slot/i);
});
it("returns 400 when the participant is already drafted", async () => {
mockDb.query.draftPicks.findFirst.mockResolvedValue({
// First call: slot check (no pick at this slot number)
// Second call: participant check (participant already drafted elsewhere)
mockDb.query.draftPicks.findFirst
.mockResolvedValueOnce(null)
.mockResolvedValueOnce({
id: "existing-pick",
participantId: PARTICIPANT_ID,
});

View file

@ -4,10 +4,10 @@ import * as schema from "~/database/schema";
import { eq, and } from "drizzle-orm";
import { getSocketIO } from "~/server/socket";
export async function action(args: any) {
import type { ActionFunctionArgs } from "react-router";
export async function action(args: ActionFunctionArgs) {
const { request } = args;
const auth = await getAuth(args);
const userId = (auth as any).userId as string | null;
const { userId } = await getAuth(args);
if (!userId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });

View file

@ -4,10 +4,10 @@ import { database } from "~/database/context";
import * as schema from "~/database/schema";
import { getSocketIO } from "../../../server/socket";
export async function action(args: any) {
import type { ActionFunctionArgs } from "react-router";
export async function action(args: ActionFunctionArgs) {
const { request } = args;
const auth = await getAuth(args);
const userId = (auth as any).userId as string | null;
const { userId } = await getAuth(args);
if (!userId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });

View file

@ -4,10 +4,10 @@ import * as schema from "~/database/schema";
import { eq, and } from "drizzle-orm";
import { executeAutoPick } from "~/models/draft-utils";
export async function action(args: any) {
import type { ActionFunctionArgs } from "react-router";
export async function action(args: ActionFunctionArgs) {
const { request } = args;
const auth = await getAuth(args);
const userId = (auth as any).userId as string | null;
const { userId } = await getAuth(args);
if (!userId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });

View file

@ -7,11 +7,11 @@ import { getDraftPicksWithSports, getTeamDraftPicksWithSports } from "~/models/d
import { getParticipantsForSeasonWithSports } from "~/models/participant";
import { getSeasonSportsSimple } from "~/models/season-sport";
import { getSocketIO } from "../../../server/socket";
import type { ActionFunctionArgs } from "react-router";
export async function action(args: any) {
export async function action(args: ActionFunctionArgs) {
const { request } = args;
const auth = await getAuth(args);
const userId = (auth as any).userId as string | null;
const { userId } = await getAuth(args);
if (!userId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
@ -50,6 +50,22 @@ export async function action(args: any) {
return Response.json({ error: "Only commissioners can force a manual pick" }, { status: 403 });
}
if (season.status !== "draft") {
return Response.json({ error: "Draft is not currently active" }, { status: 400 });
}
// Check if a pick already exists at this pick number slot (prevents duplicate picks at same slot)
const existingPickAtSlot = await db.query.draftPicks.findFirst({
where: and(
eq(schema.draftPicks.seasonId, seasonId),
eq(schema.draftPicks.pickNumber, pickNumber)
),
});
if (existingPickAtSlot) {
return Response.json({ error: "A pick already exists at this slot" }, { status: 400 });
}
// Check if participant is already drafted
const existingPick = await db.query.draftPicks.findFirst({
where: and(

View file

@ -8,10 +8,10 @@ import { getParticipantsForSeasonWithSports } from "~/models/participant";
import { getSeasonSportsSimple } from "~/models/season-sport";
import { getSocketIO } from "../../../server/socket";
export async function action(args: any) {
import type { ActionFunctionArgs } from "react-router";
export async function action(args: ActionFunctionArgs) {
const { request } = args;
const auth = await getAuth(args);
const userId = (auth as any).userId as string | null;
const { userId } = await getAuth(args);
if (!userId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
@ -56,12 +56,11 @@ export async function action(args: any) {
const totalTeams = draftSlots.length;
const currentRound = Math.ceil(currentPickNumber / totalTeams);
const isSnakeDraft = true; // Assuming snake draft
const isEvenRound = currentRound % 2 === 0;
// Calculate which team should pick
// Calculate which team should pick (snake draft)
let pickInRound = ((currentPickNumber - 1) % totalTeams) + 1;
if (isSnakeDraft && isEvenRound) {
if (isEvenRound) {
pickInRound = totalTeams - pickInRound + 1;
}

View file

@ -4,10 +4,10 @@ import { database } from "~/database/context";
import * as schema from "~/database/schema";
import { getSocketIO } from "../../../server/socket";
export async function action(args: any) {
import type { ActionFunctionArgs } from "react-router";
export async function action(args: ActionFunctionArgs) {
const { request } = args;
const auth = await getAuth(args);
const userId = (auth as any).userId as string | null;
const { userId } = await getAuth(args);
if (!userId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });

View file

@ -8,10 +8,10 @@ import { getParticipantsForSeasonWithSports } from "~/models/participant";
import { getSeasonSportsSimple } from "~/models/season-sport";
import { getSocketIO } from "../../../server/socket";
export async function action(args: any) {
import type { ActionFunctionArgs } from "react-router";
export async function action(args: ActionFunctionArgs) {
const { request } = args;
const auth = await getAuth(args);
const userId = (auth as any).userId as string | null;
const { userId } = await getAuth(args);
if (!userId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });

View file

@ -4,10 +4,10 @@ import { database } from "~/database/context";
import * as schema from "~/database/schema";
import { getSocketIO } from "../../../server/socket";
export async function action(args: any) {
import type { ActionFunctionArgs } from "react-router";
export async function action(args: ActionFunctionArgs) {
const { request } = args;
const auth = await getAuth(args);
const userId = (auth as any).userId as string | null;
const { userId } = await getAuth(args);
if (!userId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });

View file

@ -4,10 +4,10 @@ import * as schema from "~/database/schema";
import { eq, and, gte } from "drizzle-orm";
import { getSocketIO } from "../../../server/socket";
export async function action(args: any) {
import type { ActionFunctionArgs } from "react-router";
export async function action(args: ActionFunctionArgs) {
const { request } = args;
const auth = await getAuth(args);
const userId = (auth as any).userId as string | null;
const { userId } = await getAuth(args);
if (!userId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });

View file

@ -4,10 +4,10 @@ import * as schema from "~/database/schema";
import { eq, and } from "drizzle-orm";
import { getSocketIO } from "../../../server/socket";
export async function action(args: any) {
import type { ActionFunctionArgs } from "react-router";
export async function action(args: ActionFunctionArgs) {
const { request } = args;
const auth = await getAuth(args);
const userId = (auth as any).userId as string | null;
const { userId } = await getAuth(args);
if (!userId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });

View file

@ -4,10 +4,10 @@ import * as schema from "~/database/schema";
import { eq } from "drizzle-orm";
import { addToQueue, getTeamQueue, isParticipantInQueue } from "~/models/draft-queue";
export async function action(args: any) {
import type { ActionFunctionArgs } from "react-router";
export async function action(args: ActionFunctionArgs) {
const { request } = args;
const auth = await getAuth(args);
const userId = (auth as any).userId as string | null;
const { userId } = await getAuth(args);
if (!userId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });

View file

@ -3,11 +3,11 @@ import { database } from "~/database/context";
import * as schema from "~/database/schema";
import { eq } from "drizzle-orm";
import { clearTeamQueue } from "~/models/draft-queue";
import type { ActionFunctionArgs } from "react-router";
export async function action(args: any) {
export async function action(args: ActionFunctionArgs) {
const { request } = args;
const auth = await getAuth(args);
const userId = (auth as any).userId as string | null;
const { userId } = await getAuth(args);
if (!userId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });

View file

@ -4,10 +4,10 @@ import * as schema from "~/database/schema";
import { eq } from "drizzle-orm";
import { removeFromQueue, getTeamQueue } from "~/models/draft-queue";
export async function action(args: any) {
import type { ActionFunctionArgs } from "react-router";
export async function action(args: ActionFunctionArgs) {
const { request } = args;
const auth = await getAuth(args);
const userId = (auth as any).userId as string | null;
const { userId } = await getAuth(args);
if (!userId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });

View file

@ -4,10 +4,10 @@ import * as schema from "~/database/schema";
import { eq } from "drizzle-orm";
import { reorderQueueWithSeason, getTeamQueue } from "~/models/draft-queue";
export async function action(args: any) {
import type { ActionFunctionArgs } from "react-router";
export async function action(args: ActionFunctionArgs) {
const { request } = args;
const auth = await getAuth(args);
const userId = (auth as any).userId as string | null;
const { userId } = await getAuth(args);
if (!userId) {
return Response.json({ error: "Unauthorized" }, { status: 401 });

View file

@ -7,8 +7,9 @@ import { DraftGrid } from "~/components/DraftGrid";
import { useDraftSocket } from "~/hooks/useDraftSocket";
import { useState, useEffect } from "react";
import { buildOwnerMap } from "~/lib/owner-map";
import type { Route } from "./+types/$leagueId.draft-board.$seasonId";
export async function loader(args: any) {
export async function loader(args: Route.LoaderArgs) {
const { params } = args;
const { leagueId, seasonId } = params;
@ -38,8 +39,7 @@ export async function loader(args: any) {
// Check access: public boards are accessible to everyone without auth
if (!season.league.isPublicDraftBoard) {
// Not public - check if the user is a league member or commissioner
const auth = await getAuth(args);
const userId = (auth as any).userId as string | null;
const { userId } = await getAuth(args);
if (!userId) {
throw new Response("This draft board is not public", { status: 403 });

View file

@ -25,12 +25,12 @@ import { useDraftNotifications } from "~/hooks/useDraftNotifications";
import { NotificationSettings } from "~/components/NotificationSettings";
import { toast } from "sonner";
import { formatClockTime, getTimerColorClass } from "~/lib/draft-timer";
import type { Route } from "./+types/$leagueId.draft.$seasonId";
export async function loader(args: any) {
export async function loader(args: Route.LoaderArgs) {
const { params } = args;
const { seasonId, leagueId } = params;
const auth = await getAuth(args);
const userId = (auth as any).userId as string | null;
const { userId } = await getAuth(args);
if (!seasonId) {
throw new Response("Season ID is required", { status: 400 });

View file

@ -10,8 +10,9 @@ import {
findDraftSlotsBySeasonId,
} from "~/models";
import { findCurrentSeasonWithSports } from "~/models/season";
import type { Route } from "./+types/$leagueId";
export async function loader(args: any) {
export async function loader(args: Route.LoaderArgs) {
const { userId } = await getAuth(args);
const { params } = args;
const { leagueId } = params;

View file

@ -9,11 +9,11 @@ import { StandingsTable } from "~/components/standings/StandingsTable";
import { getSevenDayStandingsChange, getSeasonPointProgression } from "~/models/standings";
import { isSeasonComplete, getSeasonCompletionPercentage } from "~/lib/season-helpers.server";
import { PointProgressionChart } from "~/components/standings/PointProgressionChart";
import type { Route } from "./+types/$leagueId.standings.$seasonId";
export async function loader(args: any) {
export async function loader(args: Route.LoaderArgs) {
try {
const auth = await getAuth(args);
const userId = (auth as any).userId as string | null;
const { userId } = await getAuth(args);
const { params } = args;
const { leagueId, seasonId } = params;

View file

@ -26,8 +26,8 @@ interface ServerToClientEvents {
"team-connected": (data: { teamId: string }) => void;
"team-disconnected": (data: { teamId: string }) => void;
"connected-teams-list": (data: { teamIds: string[] }) => void;
"draft-paused": () => void;
"draft-resumed": () => void;
"draft-paused": (data: { seasonId: string; paused: boolean }) => void;
"draft-resumed": (data: { seasonId: string; paused: boolean }) => void;
"participant-removed-from-queues": (data: { participantId: string }) => void;
}