118 lines
4 KiB
TypeScript
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);
|
|
}
|
|
}
|