Problem
Every mutating admin route under `app/api/admin/challenges/` calls `rateLimitAdminMutation(auth)` before writing (see `PATCH`/`DELETE` in `[id]/route.ts`, and `finalize/route.ts`). But `GET /api/admin/challenges` self-heals ended/expired challenges as a side effect of a plain read — issuing real `UPDATE`s via `resolveEndedChallenges`/`resolveExpiredPending` — with only `requireAdmin`, no `rateLimitAdminMutation`:
```ts
export async function GET(req: NextRequest) {
const auth = await requireAdmin(req);
if (auth instanceof NextResponse) return auth;
// ... no rateLimitAdminMutation call ...
const ended = await db.select().from(challenges).where(and(eq(challenges.status, "active"), lt(challenges.endsAt, now)));
await resolveEndedChallenges(ended, now);
const expiredPending = await db.select().from(challenges).where(and(eq(challenges.status, "pending"), lt(challenges.endsAt, now)));
await resolveExpiredPending(expiredPending, now);
...
}
```
Impact
The admin challenges page calls this endpoint on every filter change and on mount, and it's a GET (proxies/browsers may prefetch or retry GETs, and it's easy to poll accidentally via a bookmarklet or script). Because it performs real writes with no throttle, it's the one write path in this route group that lacks the protection every other mutation in the same directory already has. Note: this is distinct from the already-fixed #176 (which was about the cost of self-healing being N+1 — now addressed here via `mapWithConcurrency`); this issue is specifically about the missing rate limit on the write path itself.
Location
`app/api/admin/challenges/route.ts:22-49`
Suggested fix
Apply `rateLimitAdminMutation` (or a lighter per-admin throttle) before the self-heal writes, or move the self-heal logic to a scheduled job / restrict it to the existing `finalize` endpoint so `GET` stays read-only.
Problem
Every mutating admin route under `app/api/admin/challenges/` calls `rateLimitAdminMutation(auth)` before writing (see `PATCH`/`DELETE` in `[id]/route.ts`, and `finalize/route.ts`). But `GET /api/admin/challenges` self-heals ended/expired challenges as a side effect of a plain read — issuing real `UPDATE`s via `resolveEndedChallenges`/`resolveExpiredPending` — with only `requireAdmin`, no `rateLimitAdminMutation`:
```ts
export async function GET(req: NextRequest) {
const auth = await requireAdmin(req);
if (auth instanceof NextResponse) return auth;
// ... no rateLimitAdminMutation call ...
const ended = await db.select().from(challenges).where(and(eq(challenges.status, "active"), lt(challenges.endsAt, now)));
await resolveEndedChallenges(ended, now);
const expiredPending = await db.select().from(challenges).where(and(eq(challenges.status, "pending"), lt(challenges.endsAt, now)));
await resolveExpiredPending(expiredPending, now);
...
}
```
Impact
The admin challenges page calls this endpoint on every filter change and on mount, and it's a GET (proxies/browsers may prefetch or retry GETs, and it's easy to poll accidentally via a bookmarklet or script). Because it performs real writes with no throttle, it's the one write path in this route group that lacks the protection every other mutation in the same directory already has. Note: this is distinct from the already-fixed #176 (which was about the cost of self-healing being N+1 — now addressed here via `mapWithConcurrency`); this issue is specifically about the missing rate limit on the write path itself.
Location
`app/api/admin/challenges/route.ts:22-49`
Suggested fix
Apply `rateLimitAdminMutation` (or a lighter per-admin throttle) before the self-heal writes, or move the self-heal logic to a scheduled job / restrict it to the existing `finalize` endpoint so `GET` stays read-only.