forked from hikmet4434/winninghunter
feat: Apify Meta reklam ingest akisini ekle
This commit is contained in:
@@ -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,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>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user