Problem
In the admin challenge-moderation endpoint, the `end` action checks the challenge's current status before mutating it:
```ts
if (body.action === "end") {
if (challenge.status !== "active") {
return NextResponse.json({ error: "Only active challenges can be ended" }, { status: 409 });
}
...
}
```
But the `override-winner` action has no equivalent check — it only validates that `winnerId` is a participant (or null), then unconditionally sets `status: "completed"`:
```ts
if (body.action === "override-winner") {
const winnerId = body.winnerId ?? null;
if (winnerId !== null && winnerId !== challenge.challengerId && winnerId !== challenge.challengedId) {
return NextResponse.json({ error: "winnerId must be a participant or null" }, { status: 400 });
}
const [updated] = await db
.update(challenges)
.set({ status: "completed", winnerId })
.where(eq(challenges.id, challengeId))
.returning();
...
}
```
Impact
A direct API call (or a future UI regression that renders the override control for more statuses than intended) can force a `pending`, `declined`, or `cancelled` challenge straight to `completed` with a fabricated winner. This corrupts the stats shown on the admin challenges page (`byStatus` counts, `fromSuggestions` totals) and produces a "won: @user" result for a match that was never played, declined, or was cancelled. The client UI currently only renders override buttons for `active`/`completed` rows, but that's UI-only protection — the API itself doesn't enforce it, unlike the `end` action right above it in the same file.
Location
`app/api/admin/challenges/[id]/route.ts:77-96` (compare with the `end` action's guard at lines 42-45)
Suggested fix
Reject `override-winner` unless `challenge.status` is `"active"` or `"completed"` (mirroring the `end` action's status guard), returning a 409 otherwise.
Problem
In the admin challenge-moderation endpoint, the `end` action checks the challenge's current status before mutating it:
```ts
if (body.action === "end") {
if (challenge.status !== "active") {
return NextResponse.json({ error: "Only active challenges can be ended" }, { status: 409 });
}
...
}
```
But the `override-winner` action has no equivalent check — it only validates that `winnerId` is a participant (or null), then unconditionally sets `status: "completed"`:
```ts
if (body.action === "override-winner") {
const winnerId = body.winnerId ?? null;
if (winnerId !== null && winnerId !== challenge.challengerId && winnerId !== challenge.challengedId) {
return NextResponse.json({ error: "winnerId must be a participant or null" }, { status: 400 });
}
const [updated] = await db
.update(challenges)
.set({ status: "completed", winnerId })
.where(eq(challenges.id, challengeId))
.returning();
...
}
```
Impact
A direct API call (or a future UI regression that renders the override control for more statuses than intended) can force a `pending`, `declined`, or `cancelled` challenge straight to `completed` with a fabricated winner. This corrupts the stats shown on the admin challenges page (`byStatus` counts, `fromSuggestions` totals) and produces a "won: @user" result for a match that was never played, declined, or was cancelled. The client UI currently only renders override buttons for `active`/`completed` rows, but that's UI-only protection — the API itself doesn't enforce it, unlike the `end` action right above it in the same file.
Location
`app/api/admin/challenges/[id]/route.ts:77-96` (compare with the `end` action's guard at lines 42-45)
Suggested fix
Reject `override-winner` unless `challenge.status` is `"active"` or `"completed"` (mirroring the `end` action's status guard), returning a 409 otherwise.