randomayzer/src/app/api/giveaways/[id]/snapshot/route.ts

93 lines
3 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import { GiveawayStore } from '@/lib/giveaway-store';
import { ProviderFactory } from '@/providers/factory';
import { applyFilterRules } from '@/core/filtering/filter-engine';
import { createSnapshotSchema, validateProviderCapabilities } from '@/core/validation/giveaway-schemas';
import { handleApiError, ConflictError } from '@/core/errors/http-errors';
import { expensiveApiRateLimiter } from '@/lib/rate-limiter';
import { IdempotencyStore } from '@/lib/idempotency';
import { resolveClientIp } from '@/lib/client-ip';
import { requireGiveawayOwner } from '@/lib/auth/auth-guard';
import { computeSeedCommitment } from '@/core/randomizer/hasher';
export const dynamic = 'force-dynamic';
export async function POST(
req: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const { id } = params;
const clientIp = resolveClientIp(req);
expensiveApiRateLimiter.assertAllowed(`snapshot-lock:${clientIp}:${id}`);
// Enforce giveaway ownership authorization
const { giveaway } = await requireGiveawayOwner(req, id);
if (giveaway.status === 'DRAWN' || giveaway.status === 'PUBLISHED') {
throw new ConflictError(`Cannot create new snapshot for giveaway in status "${giveaway.status}"`);
}
const rawBody = await req.json();
const validated = createSnapshotSchema.parse(rawBody);
const idempotencyKey = req.headers.get('idempotency-key');
if (idempotencyKey) {
const cached = IdempotencyStore.get({
key: idempotencyKey,
operation: 'snapshot-lock',
giveawayId: id,
requestPayload: validated,
});
if (cached) {
return NextResponse.json(cached.body, { status: cached.statusCode });
}
}
const provider = ProviderFactory.getVkProvider();
validateProviderCapabilities(validated.filterRules, provider.capabilities);
// Apply strict filtering on current participants
const { eligibleParticipants } = applyFilterRules(
giveaway.participants,
validated.filterRules
);
if (eligibleParticipants.length === 0) {
throw new ConflictError('Cannot create snapshot with 0 eligible participants. Check your filter rules.');
}
// Atomically create and lock snapshot + pre-commit seed in database
const snapshot = await GiveawayStore.createAndLockSnapshot(
id,
eligibleParticipants,
validated.filterRules
);
const updatedGw = await GiveawayStore.getById(id);
const seedCommitment = updatedGw?.seed ? computeSeedCommitment(updatedGw.seed) : null;
const responseBody = {
success: true,
giveawayId: id,
status: 'SNAPSHOT_LOCKED',
snapshot,
seedCommitment,
};
if (idempotencyKey) {
IdempotencyStore.set({
key: idempotencyKey,
operation: 'snapshot-lock',
giveawayId: id,
requestPayload: validated,
statusCode: 200,
body: responseBody,
});
}
return NextResponse.json(responseBody);
} catch (error: any) {
return handleApiError(error);
}
}