From ea93d674a211b39883bbf9382eef3d4714ec9ab1 Mon Sep 17 00:00:00 2001 From: Timo Uttenweiler Date: Fri, 20 Mar 2026 13:58:41 +0100 Subject: [PATCH] feat: API Keys via Umgebungsvariablen konfigurierbar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Neuer getApiKey() Helper: prüft zuerst ENV-Vars, dann DB - Alle Job-Routes nutzen getApiKey() statt direktem DB-Lookup - Credentials-Status berücksichtigt ENV-Vars (Sidebar-Haken) - .env.local.example: Platzhalter für alle 4 API Keys Co-Authored-By: Claude Sonnet 4.6 --- .env.local.example | 6 ++++++ app/api/credentials/route.ts | 3 ++- app/api/credentials/test/route.ts | 10 +++------- app/api/jobs/airscale-enrich/route.ts | 9 +++------ app/api/jobs/linkedin-enrich/route.ts | 9 +++------ app/api/jobs/maps-enrich/route.ts | 22 +++++++--------------- app/api/jobs/serp-enrich/route.ts | 12 ++++-------- app/api/jobs/vayne-scrape/route.ts | 9 +++------ lib/utils/apiKey.ts | 23 +++++++++++++++++++++++ 9 files changed, 54 insertions(+), 49 deletions(-) create mode 100644 lib/utils/apiKey.ts diff --git a/.env.local.example b/.env.local.example index 1fb672f..f12b5af 100644 --- a/.env.local.example +++ b/.env.local.example @@ -1,2 +1,8 @@ APP_ENCRYPTION_SECRET=your-32-character-secret-here!! DATABASE_URL=file:./leadflow.db + +# API Keys — optional, überschreiben die Einstellungen in der UI +ANYMAILFINDER_API_KEY= +APIFY_API_KEY= +VAYNE_API_KEY= +GOOGLE_MAPS_API_KEY= diff --git a/app/api/credentials/route.ts b/app/api/credentials/route.ts index d95eddb..725c7ae 100644 --- a/app/api/credentials/route.ts +++ b/app/api/credentials/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import { prisma } from "@/lib/db"; import { encrypt, decrypt } from "@/lib/utils/encryption"; +import { hasApiKeyFromEnv } from "@/lib/utils/apiKey"; const SERVICES = ["anymailfinder", "apify", "vayne", "airscale", "googlemaps"] as const; @@ -9,7 +10,7 @@ export async function GET() { const creds = await prisma.apiCredential.findMany(); const result: Record = {}; for (const svc of SERVICES) { - result[svc] = creds.some(c => c.service === svc && c.value); + result[svc] = hasApiKeyFromEnv(svc) || creds.some(c => c.service === svc && c.value); } return NextResponse.json(result); } catch (err) { diff --git a/app/api/credentials/test/route.ts b/app/api/credentials/test/route.ts index b8b4678..9641ffd 100644 --- a/app/api/credentials/test/route.ts +++ b/app/api/credentials/test/route.ts @@ -1,17 +1,13 @@ import { NextRequest, NextResponse } from "next/server"; -import { prisma } from "@/lib/db"; -import { decrypt } from "@/lib/utils/encryption"; +import { getApiKey } from "@/lib/utils/apiKey"; import axios from "axios"; export async function GET(req: NextRequest) { const service = req.nextUrl.searchParams.get("service"); if (!service) return NextResponse.json({ ok: false, error: "Missing service" }, { status: 400 }); - const cred = await prisma.apiCredential.findUnique({ where: { service } }); - if (!cred?.value) return NextResponse.json({ ok: false, error: "Not configured" }); - - const key = decrypt(cred.value); - if (!key) return NextResponse.json({ ok: false, error: "Empty key" }); + const key = await getApiKey(service); + if (!key) return NextResponse.json({ ok: false, error: "Not configured" }); try { switch (service) { diff --git a/app/api/jobs/airscale-enrich/route.ts b/app/api/jobs/airscale-enrich/route.ts index f2b43b9..0a3a6f6 100644 --- a/app/api/jobs/airscale-enrich/route.ts +++ b/app/api/jobs/airscale-enrich/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { prisma } from "@/lib/db"; -import { decrypt } from "@/lib/utils/encryption"; +import { getApiKey } from "@/lib/utils/apiKey"; import { cleanDomain } from "@/lib/utils/domains"; import { bulkSearchDomains, type DecisionMakerCategory } from "@/lib/services/anymailfinder"; @@ -16,11 +16,8 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: "No companies provided" }, { status: 400 }); } - const cred = await prisma.apiCredential.findUnique({ where: { service: "anymailfinder" } }); - if (!cred?.value) { - return NextResponse.json({ error: "Anymailfinder API key not configured" }, { status: 400 }); - } - const apiKey = decrypt(cred.value); + const apiKey = await getApiKey("anymailfinder"); + if (!apiKey) return NextResponse.json({ error: "Anymailfinder API key not configured" }, { status: 400 }); // Build domain → company map const domainMap = new Map(); diff --git a/app/api/jobs/linkedin-enrich/route.ts b/app/api/jobs/linkedin-enrich/route.ts index dbe1255..6827508 100644 --- a/app/api/jobs/linkedin-enrich/route.ts +++ b/app/api/jobs/linkedin-enrich/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { prisma } from "@/lib/db"; -import { decrypt } from "@/lib/utils/encryption"; +import { getApiKey } from "@/lib/utils/apiKey"; import { submitBulkPersonSearch, getBulkSearchStatus, @@ -18,11 +18,8 @@ export async function POST(req: NextRequest) { }; const { jobId, resultIds, categories } = body; - const cred = await prisma.apiCredential.findUnique({ where: { service: "anymailfinder" } }); - if (!cred?.value) { - return NextResponse.json({ error: "Anymailfinder API key not configured" }, { status: 400 }); - } - const apiKey = decrypt(cred.value); + const apiKey = await getApiKey("anymailfinder"); + if (!apiKey) return NextResponse.json({ error: "Anymailfinder API key not configured" }, { status: 400 }); const results = await prisma.leadResult.findMany({ where: { id: { in: resultIds }, jobId, domain: { not: null } }, diff --git a/app/api/jobs/maps-enrich/route.ts b/app/api/jobs/maps-enrich/route.ts index 981506b..e63cec6 100644 --- a/app/api/jobs/maps-enrich/route.ts +++ b/app/api/jobs/maps-enrich/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { prisma } from "@/lib/db"; -import { decrypt } from "@/lib/utils/encryption"; +import { getApiKey } from "@/lib/utils/apiKey"; import { searchPlacesMultiQuery } from "@/lib/services/googlemaps"; import { bulkSearchDomains, type DecisionMakerCategory } from "@/lib/services/anymailfinder"; @@ -20,17 +20,11 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: "No search queries provided" }, { status: 400 }); } - const mapsCredential = await prisma.apiCredential.findUnique({ where: { service: "googlemaps" } }); - if (!mapsCredential?.value) { - return NextResponse.json({ error: "Google Maps API key not configured" }, { status: 400 }); - } - const mapsApiKey = decrypt(mapsCredential.value); + const mapsApiKey = await getApiKey("googlemaps"); + if (!mapsApiKey) return NextResponse.json({ error: "Google Maps API key not configured" }, { status: 400 }); - if (enrichEmails) { - const anymailCred = await prisma.apiCredential.findUnique({ where: { service: "anymailfinder" } }); - if (!anymailCred?.value) { - return NextResponse.json({ error: "Anymailfinder API key not configured" }, { status: 400 }); - } + if (enrichEmails && !(await getApiKey("anymailfinder"))) { + return NextResponse.json({ error: "Anymailfinder API key not configured" }, { status: 400 }); } const job = await prisma.job.create({ @@ -98,10 +92,8 @@ async function runMapsEnrich( // 3. Optionally enrich with Anymailfinder if (params.enrichEmails && places.length > 0) { - const anymailCred = await prisma.apiCredential.findUnique({ where: { service: "anymailfinder" } }); - if (!anymailCred?.value) throw new Error("Anymailfinder key missing"); - - const anymailKey = decrypt(anymailCred.value); + const anymailKey = await getApiKey("anymailfinder"); + if (!anymailKey) throw new Error("Anymailfinder key missing"); const domains = places.filter(p => p.domain).map(p => p.domain!); // Map domain → placeId for updating results diff --git a/app/api/jobs/serp-enrich/route.ts b/app/api/jobs/serp-enrich/route.ts index 53a75bb..f7c6e56 100644 --- a/app/api/jobs/serp-enrich/route.ts +++ b/app/api/jobs/serp-enrich/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { prisma } from "@/lib/db"; -import { decrypt } from "@/lib/utils/encryption"; +import { getApiKey } from "@/lib/utils/apiKey"; import { isSocialOrDirectory } from "@/lib/utils/domains"; import { runGoogleSerpScraper, pollRunStatus, fetchDatasetItems } from "@/lib/services/apify"; import { bulkSearchDomains, type DecisionMakerCategory } from "@/lib/services/anymailfinder"; @@ -17,14 +17,10 @@ export async function POST(req: NextRequest) { selectedDomains?: string[]; }; - const apifyCred = await prisma.apiCredential.findUnique({ where: { service: "apify" } }); - const anymailCred = await prisma.apiCredential.findUnique({ where: { service: "anymailfinder" } }); + const [apifyToken, anymailKey] = await Promise.all([getApiKey("apify"), getApiKey("anymailfinder")]); - if (!apifyCred?.value) return NextResponse.json({ error: "Apify API token not configured" }, { status: 400 }); - if (!anymailCred?.value) return NextResponse.json({ error: "Anymailfinder API key not configured" }, { status: 400 }); - - const apifyToken = decrypt(apifyCred.value); - const anymailKey = decrypt(anymailCred.value); + if (!apifyToken) return NextResponse.json({ error: "Apify API token not configured" }, { status: 400 }); + if (!anymailKey) return NextResponse.json({ error: "Anymailfinder API key not configured" }, { status: 400 }); const job = await prisma.job.create({ data: { diff --git a/app/api/jobs/vayne-scrape/route.ts b/app/api/jobs/vayne-scrape/route.ts index 28f40f7..be94b2b 100644 --- a/app/api/jobs/vayne-scrape/route.ts +++ b/app/api/jobs/vayne-scrape/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { prisma } from "@/lib/db"; -import { decrypt } from "@/lib/utils/encryption"; +import { getApiKey } from "@/lib/utils/apiKey"; import { createOrder, getOrderStatus, triggerExport, downloadOrderCSV } from "@/lib/services/vayne"; export async function POST(req: NextRequest) { @@ -12,11 +12,8 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: "Invalid Sales Navigator URL" }, { status: 400 }); } - const cred = await prisma.apiCredential.findUnique({ where: { service: "vayne" } }); - if (!cred?.value) { - return NextResponse.json({ error: "Vayne API token not configured" }, { status: 400 }); - } - const apiToken = decrypt(cred.value); + const apiToken = await getApiKey("vayne"); + if (!apiToken) return NextResponse.json({ error: "Vayne API token not configured" }, { status: 400 }); const job = await prisma.job.create({ data: { diff --git a/lib/utils/apiKey.ts b/lib/utils/apiKey.ts new file mode 100644 index 0000000..7478ff6 --- /dev/null +++ b/lib/utils/apiKey.ts @@ -0,0 +1,23 @@ +import { prisma } from "@/lib/db"; +import { decrypt } from "./encryption"; + +const ENV_VARS: Record = { + anymailfinder: "ANYMAILFINDER_API_KEY", + apify: "APIFY_API_KEY", + vayne: "VAYNE_API_KEY", + googlemaps: "GOOGLE_MAPS_API_KEY", +}; + +export async function getApiKey(service: string): Promise { + const envVar = ENV_VARS[service]; + if (envVar && process.env[envVar]) return process.env[envVar]!; + + const cred = await prisma.apiCredential.findUnique({ where: { service } }); + if (!cred?.value) return null; + return decrypt(cred.value); +} + +export function hasApiKeyFromEnv(service: string): boolean { + const envVar = ENV_VARS[service]; + return !!(envVar && process.env[envVar]); +}