93 lines
3 KiB
TypeScript
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);
|
|
}
|
|
}
|