feat: Apify Meta reklam ingest akisini ekle

This commit is contained in:
2026-08-02 23:00:22 +03:00
parent 3af61c82a2
commit 0ade96903d
9 changed files with 464 additions and 130 deletions
+44
View File
@@ -0,0 +1,44 @@
import { NextResponse } from "next/server";
import { z } from "zod";
import { getApiAdmin, requestAuditContext } from "@/lib/admin-api";
import { importApifyAds, runApifyActor } from "@/lib/apify";
import { prisma } from "@/lib/db";
const schema = z.object({
searchTerms: z.array(z.string().trim().min(2).max(100)).min(1).max(10),
country: z.string().trim().toUpperCase().refine((value) => value === "ALL" || /^[A-Z]{2}$/.test(value)),
adActiveStatus: z.enum(["ACTIVE", "INACTIVE", "ALL"]).default("ACTIVE"),
mediaType: z.enum(["ALL", "IMAGE", "VIDEO", "MEME", "NONE"]).default("ALL"),
maxResults: z.coerce.number().int().min(1).max(1000).default(100),
scrapeAdDetails: z.boolean().default(true),
includeAboutPage: z.boolean().default(false),
maxCostUsd: z.coerce.number().min(0.1).max(10).default(1)
});
export async function POST(request: Request) {
const actor = await getApiAdmin();
if (!actor) return NextResponse.json({ error: "FORBIDDEN" }, { status: 403 });
const parsed = schema.safeParse(await request.json().catch(() => null));
if (!parsed.success) return NextResponse.json({ error: "INVALID_INPUT" }, { status: 400 });
const auditContext = requestAuditContext(request);
const job = await prisma.ingestJob.create({
data: { source: "apify", type: "meta-ads-library", status: "RUNNING", startedAt: new Date(), metadata: { searchTerms: parsed.data.searchTerms, country: parsed.data.country, maxResults: parsed.data.maxResults } }
});
try {
const records = await runApifyActor(parsed.data);
const result = await importApifyAds(records);
await prisma.$transaction([
prisma.ingestJob.update({ where: { id: job.id }, data: { status: "COMPLETED", finishedAt: new Date(), recordsImported: result.imported, recordsFailed: result.failed, metadata: { searchTerms: parsed.data.searchTerms, country: parsed.data.country, received: records.length } } }),
prisma.adminAuditLog.create({ data: { actorId: actor.id, actorEmail: actor.email, action: "APIFY_INGEST_COMPLETED", targetType: "IngestJob", targetId: job.id, summary: `${result.imported} Meta reklamı Apify üzerinden işlendi.`, details: { ...result, received: records.length, searchTerms: parsed.data.searchTerms, country: parsed.data.country }, ...auditContext } })
]);
return NextResponse.json({ ok: true, jobId: job.id, ...result, received: records.length });
} catch (error) {
const code = error instanceof Error && /^APIFY_[A-Z0-9_]+$/.test(error.message) ? error.message : "APIFY_INGEST_FAILED";
await prisma.$transaction([
prisma.ingestJob.update({ where: { id: job.id }, data: { status: "FAILED", finishedAt: new Date(), errorMessage: code } }),
prisma.adminAuditLog.create({ data: { actorId: actor.id, actorEmail: actor.email, action: "APIFY_INGEST_FAILED", targetType: "IngestJob", targetId: job.id, summary: `Apify ingest başarısız: ${code}`, details: { searchTerms: parsed.data.searchTerms, country: parsed.data.country }, ...auditContext } })
]);
return NextResponse.json({ error: code }, { status: code === "APIFY_NOT_CONFIGURED" ? 503 : 502 });
}
}
+2 -1
View File
@@ -2,9 +2,10 @@ import { requireAdmin } from "@/lib/auth/current-user";
import { prisma } from "@/lib/db";
import { Card } from "@/components/ui/card";
import { AdminNav } from "@/components/admin/admin-nav";
import { ApifyIngestForm } from "@/components/admin/apify-ingest-form";
export default async function AdminDataPage() {
await requireAdmin();
const [ads, stores, users, jobs] = await Promise.all([prisma.ad.count(), prisma.store.count(), prisma.user.count(), prisma.ingestJob.findMany({ orderBy: { createdAt: "desc" }, take: 10 })]);
return <div><h1 className="mb-2 text-3xl font-black">Veri İşleri</h1><p className="mb-6 text-slate-500">Reklam, mağaza ve ingest operasyonları.</p><AdminNav /><div className="grid gap-4 md:grid-cols-3"><Card><b>{ads}</b><br />Ads</Card><Card><b>{stores}</b><br />Stores</Card><Card><b>{users}</b><br />Users</Card></div><Card className="mt-5"><h2 className="mb-3 font-black">Ingest Jobs</h2>{jobs.map((j) => <div key={j.id} className="border-t py-2 text-sm">{j.source} · {j.type} · {j.status} · {j.recordsImported} records</div>)}</Card></div>;
return <div><h1 className="mb-2 text-3xl font-black">Veri İşleri</h1><p className="mb-6 text-slate-500">Reklam, mağaza ve ingest operasyonları.</p><AdminNav /><div className="grid gap-4 md:grid-cols-3"><Card><b className="text-3xl">{ads}</b><br />Ads</Card><Card><b className="text-3xl">{stores}</b><br />Stores</Card><Card><b className="text-3xl">{users}</b><br />Users</Card></div><Card className="mt-5"><div className="mb-4"><h2 className="text-xl font-black">Apify · Meta Ads Library</h2><p className="mt-1 text-sm text-slate-500">Anahtar kelimeyle gerçek Meta reklamlarını çekin. Her çalışmada maliyet üst sınırı uygulanır ve tekrar eden reklamlar güncellenir.</p></div><ApifyIngestForm configured={Boolean(process.env.APIFY_TOKEN)} /></Card><Card className="mt-5"><h2 className="mb-3 font-black">Ingest Jobs</h2>{jobs.map((j) => <div key={j.id} className="border-t py-3 text-sm"><b>{j.source} · {j.type}</b><div className="text-slate-500">{j.status} · {j.recordsImported} başarılı · {j.recordsFailed} hatalı</div>{j.errorMessage && <div className="text-rose-600">{j.errorMessage}</div>}</div>)}{!jobs.length && <p className="text-sm text-slate-500">Henüz ingest işi yok.</p>}</Card></div>;
}