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

118 lines
4 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import { GiveawayStore } from '@/lib/giveaway-store';
import { ProviderFactory } from '@/providers/factory';
import { executeParticipantPipeline } from '@/core/pipeline/participant-enricher';
import { fetchParticipantsSchema, validateProviderCapabilities } from '@/core/validation/giveaway-schemas';
import { handleApiError } from '@/core/errors/http-errors';
import { expensiveApiRateLimiter, generalApiRateLimiter } from '@/lib/rate-limiter';
import { IdempotencyStore } from '@/lib/idempotency';
import { resolveClientIp } from '@/lib/client-ip';
import { requireGiveawayOwner } from '@/lib/auth/auth-guard';
export const dynamic = 'force-dynamic';
export async function GET(
req: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const { id } = params;
const clientIp = resolveClientIp(req);
generalApiRateLimiter.assertAllowed(`participants-get:${clientIp}`);
// Enforce giveaway ownership authorization (private participant PII data)
await requireGiveawayOwner(req, id);
const { searchParams } = new URL(req.url);
const page = Math.max(1, parseInt(searchParams.get('page') || '1', 10));
const pageSize = Math.min(100, Math.max(1, parseInt(searchParams.get('pageSize') || '50', 10)));
const tabParam = searchParams.get('tab') || 'all';
const tab = (tabParam === 'eligible' || tabParam === 'excluded') ? tabParam : 'all';
const result = await GiveawayStore.getParticipantsPaginated(id, page, pageSize, tab);
return NextResponse.json({
success: true,
...result,
});
} catch (error: any) {
return handleApiError(error);
}
}
export async function POST(
req: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const { id } = params;
const clientIp = resolveClientIp(req);
expensiveApiRateLimiter.assertAllowed(`participants-import:${clientIp}:${id}`);
// Enforce giveaway ownership authorization for importing participants
const { giveaway } = await requireGiveawayOwner(req, id);
const rawBody = await req.json();
const validated = fetchParticipantsSchema.parse(rawBody);
const idempotencyKey = req.headers.get('idempotency-key');
if (idempotencyKey) {
const cached = IdempotencyStore.get({
key: idempotencyKey,
operation: 'participants-import',
giveawayId: id,
requestPayload: validated,
});
if (cached) {
return NextResponse.json(cached.body, { status: cached.statusCode });
}
}
const provider = ProviderFactory.getVkProvider();
validateProviderCapabilities(validated.filterRules, provider.capabilities);
// Fetch raw participants from social provider
const rawParticipants = await provider.fetchParticipants({
ownerId: giveaway.platformOwnerId,
postId: giveaway.platformPostId,
includeLikes: validated.filterRules.requireLike,
includeComments: validated.filterRules.requireComment,
});
// Run participant fetch, enrichment, and filtering pipeline
const { allParticipants, eligibleParticipants, excludedParticipants } =
await executeParticipantPipeline({
rawParticipants,
rules: validated.filterRules,
provider,
ownerId: giveaway.platformOwnerId,
});
// Save atomic participant state in store
const updated = await GiveawayStore.updateParticipants(id, allParticipants);
// Return summary only (no massive arrays in POST response)
const responseBody = {
success: true,
giveawayId: updated.id,
totalCount: allParticipants.length,
eligibleCount: eligibleParticipants.length,
excludedCount: excludedParticipants.length,
};
if (idempotencyKey) {
IdempotencyStore.set({
key: idempotencyKey,
operation: 'participants-import',
giveawayId: id,
requestPayload: validated,
statusCode: 200,
body: responseBody,
});
}
return NextResponse.json(responseBody);
} catch (error: any) {
return handleApiError(error);
}
}