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:
parent
3da27af036
commit
c036cf6e28
21 changed files with 116 additions and 64 deletions
|
|
@ -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;
|
||||
}> {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue