"use client"; import { useEffect, useState } from "react"; import { Card } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { toast } from "sonner"; import { CheckCircle2, XCircle, Loader2, ExternalLink, Eye, EyeOff } from "lucide-react"; interface Credential { service: string; label: string; placeholder: string; link: string; linkLabel: string; isTextarea?: boolean; instructions?: string; } const CREDENTIALS: Credential[] = [ { service: "anymailfinder", label: "Anymailfinder API Key", placeholder: "amf_...", link: "https://anymailfinder.com/account", linkLabel: "Get API key", }, { service: "googlemaps", label: "Google Maps API Key", placeholder: "AIza...", link: "https://console.cloud.google.com/apis/credentials", linkLabel: "Google Cloud Console", instructions: 'Enable the "Places API (New)" in Google Cloud Console. Go to APIs & Services → Enable APIs → search "Places API (New)". Then create an API key under Credentials. Restrict the key to "Places API (New)" for security. Free tier: $200/month credit ≈ 6,000 searches.', }, { service: "apify", label: "Apify API Token", placeholder: "apify_api_...", link: "https://console.apify.com/account/integrations", linkLabel: "Get API token", }, { service: "vayne", label: "Vayne API Token", placeholder: "Bearer token from vayne.io dashboard", link: "https://www.vayne.io", linkLabel: "Vayne Dashboard", isTextarea: true, instructions: "Generate your API token in the API Settings section of your Vayne Dashboard at vayne.io. Use Bearer authentication — Vayne manages the LinkedIn session on their end.", }, ]; interface CredentialState { value: string; saved: boolean; testing: boolean; testResult: "ok" | "fail" | null; show: boolean; } export default function SettingsPage() { const [creds, setCreds] = useState>(() => Object.fromEntries( CREDENTIALS.map(c => [c.service, { value: "", saved: false, testing: false, testResult: null, show: false }]) ) ); const [saving, setSaving] = useState(false); // Load existing (masked) status useEffect(() => { fetch("/api/credentials") .then(r => r.json()) .then((data: Record) => { setCreds(prev => { const next = { ...prev }; for (const [k, v] of Object.entries(data)) { if (next[k]) { next[k] = { ...next[k], saved: v }; } } return next; }); }) .catch(() => {}); }, []); const update = (service: string, field: keyof CredentialState, val: unknown) => { setCreds(prev => ({ ...prev, [service]: { ...prev[service], [field]: val } })); }; const saveAll = async () => { setSaving(true); try { const body: Record = {}; for (const [service, state] of Object.entries(creds)) { if (state.value) body[service] = state.value; } const res = await fetch("/api/credentials", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!res.ok) throw new Error("Save failed"); toast.success("Credentials saved successfully"); setCreds(prev => { const next = { ...prev }; for (const service of Object.keys(body)) { next[service] = { ...next[service], saved: true }; } return next; }); } catch { toast.error("Failed to save credentials"); } finally { setSaving(false); } }; const testConnection = async (service: string) => { update(service, "testing", true); update(service, "testResult", null); try { const res = await fetch(`/api/credentials/test?service=${service}`); const data = await res.json() as { ok: boolean }; update(service, "testResult", data.ok ? "ok" : "fail"); if (data.ok) toast.success(`${service} connection verified`); else toast.error(`${service} connection failed — check your key`); } catch { update(service, "testResult", "fail"); toast.error("Connection test failed"); } finally { update(service, "testing", false); } }; return (
{/* Header */}

Settings

API credentials are encrypted with AES-256 and stored locally in SQLite.

{/* Credential cards */} {CREDENTIALS.map(cred => { const state = creds[cred.service]; return (
{state.saved ? ( Configured ) : ( Not configured )}
{cred.linkLabel}
{cred.instructions && (

{cred.instructions}

)}
{cred.isTextarea ? (