70 lines
2.5 KiB
TypeScript
70 lines
2.5 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { GiveawayStore } from '@/lib/giveaway-store';
|
|
import { executeDeterministicDrawV1 } from '@/core/randomizer/deterministic';
|
|
import { generateCryptoSecureSeed } from '@/core/randomizer/hasher';
|
|
import { GiveawayFSM } from '@/core/fsm/giveaway-fsm';
|
|
|
|
export async function POST(
|
|
req: NextRequest,
|
|
{ params }: { params: { id: string } }
|
|
) {
|
|
try {
|
|
const { id } = params;
|
|
const body = await req.json().catch(() => ({}));
|
|
const giveaway = await GiveawayStore.getById(id);
|
|
|
|
if (!giveaway) {
|
|
return NextResponse.json({ error: 'Giveaway not found' }, { status: 404 });
|
|
}
|
|
|
|
// 1. Guard check with FSM
|
|
if (giveaway.status === 'DRAWN') {
|
|
return NextResponse.json({
|
|
error: 'Розыгрыш уже проведен. Повторный запуск строго запрещен.',
|
|
}, { status: 400 });
|
|
}
|
|
|
|
// 2. Fetch locked snapshot (or lock current eligible if ready)
|
|
let snapshot = await GiveawayStore.getLatestSnapshot(id);
|
|
if (!snapshot) {
|
|
const eligible = giveaway.participants.filter(p => p.eligible);
|
|
if (eligible.length === 0) {
|
|
return NextResponse.json({
|
|
error: 'Нет допущенных участников для создания слепка и розыгрыша'
|
|
}, { status: 400 });
|
|
}
|
|
snapshot = await GiveawayStore.createAndLockSnapshot(id, eligible, giveaway.filterRules);
|
|
}
|
|
|
|
// Validate status after snapshot lock
|
|
GiveawayFSM.assertCanDraw('SNAPSHOT_LOCKED');
|
|
|
|
const winnersCount = body.winnersCount || giveaway.winnersCount || 1;
|
|
const reserveWinnersCount = body.reserveWinnersCount ?? giveaway.reserveWinnersCount ?? 0;
|
|
|
|
// Seed must be generated with CSPRNG if not provided
|
|
const seed = body.seed?.trim() || giveaway.seed || generateCryptoSecureSeed();
|
|
|
|
// 3. Execute Provably Fair Randomizer V1
|
|
const drawResult = executeDeterministicDrawV1({
|
|
giveawayId: id,
|
|
snapshot,
|
|
totalLoadedCount: giveaway.participants.length,
|
|
winnersCount,
|
|
reserveWinnersCount,
|
|
seed,
|
|
filterRules: giveaway.filterRules,
|
|
});
|
|
|
|
// 4. Persist DrawResult and AuditRecord atomically
|
|
const updatedGiveaway = await GiveawayStore.saveDrawResult(id, snapshot.id, drawResult);
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
drawResult,
|
|
giveaway: updatedGiveaway,
|
|
});
|
|
} catch (error: any) {
|
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
}
|
|
}
|