@nordsym/apiclaw 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +74 -0
- package/HEARTBEAT.md +4 -0
- package/IDENTITY.md +22 -0
- package/README.md +193 -202
- package/SOUL.md +36 -0
- package/STATUS.md +237 -0
- package/TOOLS.md +36 -0
- package/USER.md +17 -0
- package/{backend/convex → convex}/_generated/api.d.ts +12 -6
- package/convex/analytics.ts +90 -0
- package/convex/credits.ts +211 -0
- package/convex/http.ts +578 -0
- package/convex/providers.ts +516 -0
- package/convex/purchases.ts +183 -0
- package/convex/ratelimit.ts +104 -0
- package/convex/schema.ts +220 -0
- package/convex/telemetry.ts +81 -0
- package/convex.json +3 -0
- package/dist/credentials.d.ts +19 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +158 -0
- package/dist/credentials.js.map +1 -0
- package/dist/credits.d.ts +14 -11
- package/dist/credits.d.ts.map +1 -1
- package/dist/credits.js +151 -99
- package/dist/credits.js.map +1 -1
- package/dist/discovery.d.ts +7 -16
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js +33 -40
- package/dist/discovery.js.map +1 -1
- package/dist/execute.d.ts +19 -0
- package/dist/execute.d.ts.map +1 -0
- package/dist/execute.js +285 -0
- package/dist/execute.js.map +1 -0
- package/dist/index.js +175 -31
- package/dist/index.js.map +1 -1
- package/dist/proxy.d.ts +6 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +19 -0
- package/dist/proxy.js.map +1 -0
- package/dist/registry/apis.json +95362 -202
- package/dist/registry/apis_expanded.json +100853 -0
- package/dist/stripe.d.ts +68 -0
- package/dist/stripe.d.ts.map +1 -0
- package/dist/stripe.js +196 -0
- package/dist/stripe.js.map +1 -0
- package/dist/telemetry.d.ts +28 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +50 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/test.d.ts +3 -2
- package/dist/test.d.ts.map +1 -1
- package/dist/test.js +105 -75
- package/dist/test.js.map +1 -1
- package/dist/types.d.ts +0 -28
- package/dist/types.d.ts.map +1 -1
- package/dist/webhook.d.ts +2 -0
- package/dist/webhook.d.ts.map +1 -0
- package/dist/webhook.js +90 -0
- package/dist/webhook.js.map +1 -0
- package/landing/DESIGN.md +343 -0
- package/landing/package-lock.json +1196 -7
- package/landing/package.json +5 -1
- package/landing/public/android-chrome-192x192.png +0 -0
- package/landing/public/android-chrome-512x512.png +0 -0
- package/landing/public/apple-touch-icon.png +0 -0
- package/landing/public/demo.gif +0 -0
- package/landing/public/demo.mp4 +0 -0
- package/landing/public/favicon-16x16.png +0 -0
- package/landing/public/favicon-32x32.png +0 -0
- package/landing/public/favicon.ico +0 -0
- package/landing/public/favicon.svg +3 -0
- package/landing/public/icon.svg +47 -0
- package/landing/public/logo-mono.svg +37 -0
- package/landing/public/logo-simple.svg +45 -0
- package/landing/public/logo.svg +84 -0
- package/landing/public/og-template.html +184 -0
- package/landing/public/site.webmanifest +31 -0
- package/landing/scripts/generate-assets.js +284 -0
- package/landing/scripts/generate-pngs.js +48 -0
- package/landing/scripts/generate-stats.js +42 -0
- package/landing/src/app/admin/page.tsx +348 -0
- package/landing/src/app/api/auth/magic-link/route.ts +73 -0
- package/landing/src/app/api/auth/session/route.ts +38 -0
- package/landing/src/app/api/auth/verify/route.ts +43 -0
- package/landing/src/app/api/og/route.tsx +84 -0
- package/landing/src/app/globals.css +439 -100
- package/landing/src/app/layout.tsx +37 -7
- package/landing/src/app/page.tsx +627 -552
- package/landing/src/app/providers/dashboard/login/page.tsx +176 -0
- package/landing/src/app/providers/dashboard/page.tsx +589 -0
- package/landing/src/app/providers/dashboard/verify/page.tsx +106 -0
- package/landing/src/app/providers/layout.tsx +14 -0
- package/landing/src/app/providers/page.tsx +402 -0
- package/landing/src/app/providers/register/page.tsx +670 -0
- package/landing/src/components/ProviderDashboard.tsx +794 -0
- package/landing/src/hooks/useDashboardData.ts +99 -0
- package/landing/src/lib/apis.json +116054 -0
- package/landing/src/lib/convex-client.ts +106 -0
- package/landing/src/lib/mock-data.ts +285 -0
- package/landing/src/lib/stats.json +6 -0
- package/landing/tailwind.config.ts +12 -11
- package/landing/tsconfig.tsbuildinfo +1 -0
- package/package.json +21 -20
- package/scripts/SYMBOT-FIX.md +238 -0
- package/scripts/demo-simulation.py +177 -0
- package/scripts/expand-more.py +502 -0
- package/scripts/expand-registry.py +434 -0
- package/scripts/history-sanitizer.ts +272 -0
- package/scripts/mass-scrape.py +1308 -0
- package/scripts/sync-and-deploy.sh +36 -0
- package/src/credentials.ts +177 -0
- package/src/credits.ts +190 -122
- package/src/discovery.ts +45 -58
- package/src/execute.ts +350 -0
- package/src/index.ts +184 -32
- package/src/proxy.ts +24 -0
- package/src/registry/apis.json +95362 -202
- package/src/registry/apis_expanded.json +100853 -0
- package/src/stripe.ts +243 -0
- package/src/telemetry.ts +71 -0
- package/src/test.ts +127 -89
- package/src/types.ts +0 -34
- package/src/webhook.ts +107 -0
- package/.github/ISSUE_TEMPLATE/add-api.yml +0 -123
- package/BRIEFING.md +0 -30
- package/backend/convex/apiKeys.ts +0 -75
- package/backend/convex/purchases.ts +0 -74
- package/backend/convex/schema.ts +0 -45
- package/backend/convex/transactions.ts +0 -57
- package/backend/convex/users.ts +0 -94
- package/backend/package-lock.json +0 -521
- package/backend/package.json +0 -15
- package/dist/registry/parse_apis.py +0 -146
- package/dist/revenuecat.d.ts +0 -61
- package/dist/revenuecat.d.ts.map +0 -1
- package/dist/revenuecat.js +0 -166
- package/dist/revenuecat.js.map +0 -1
- package/dist/webhooks/revenuecat.d.ts +0 -48
- package/dist/webhooks/revenuecat.d.ts.map +0 -1
- package/dist/webhooks/revenuecat.js +0 -119
- package/dist/webhooks/revenuecat.js.map +0 -1
- package/docs/revenuecat-setup.md +0 -89
- package/landing/src/app/api/keys/route.ts +0 -71
- package/landing/src/app/api/log/route.ts +0 -37
- package/landing/src/app/api/stats/route.ts +0 -37
- package/landing/src/app/page.tsx.bak +0 -567
- package/landing/src/components/AddKeyModal.tsx +0 -159
- package/newsletter-template.html +0 -71
- package/outreach/OUTREACH-SYSTEM.md +0 -211
- package/outreach/email-template.html +0 -179
- package/outreach/targets.md +0 -133
- package/src/registry/parse_apis.py +0 -146
- package/src/revenuecat.ts +0 -239
- package/src/webhooks/revenuecat.ts +0 -187
- /package/{backend/convex → convex}/README.md +0 -0
- /package/{backend/convex → convex}/_generated/api.js +0 -0
- /package/{backend/convex → convex}/_generated/dataModel.d.ts +0 -0
- /package/{backend/convex → convex}/_generated/server.d.ts +0 -0
- /package/{backend/convex → convex}/_generated/server.js +0 -0
- /package/{backend/convex → convex}/tsconfig.json +0 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import { Shield, Users, Zap, TrendingUp, Check, X, Clock, ExternalLink, RefreshCw, Eye } from "lucide-react";
|
|
5
|
+
import Link from "next/link";
|
|
6
|
+
|
|
7
|
+
const CONVEX_URL = process.env.NEXT_PUBLIC_CONVEX_URL || "https://brilliant-puffin-712.eu-west-1.convex.cloud";
|
|
8
|
+
|
|
9
|
+
interface Stats {
|
|
10
|
+
totalProviders: number;
|
|
11
|
+
approvedProviders: number;
|
|
12
|
+
pendingProviders: number;
|
|
13
|
+
totalAPIs: number;
|
|
14
|
+
approvedAPIs: number;
|
|
15
|
+
pendingAPIs: number;
|
|
16
|
+
totalDiscoveries: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface Provider {
|
|
20
|
+
_id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
email: string;
|
|
23
|
+
company?: string;
|
|
24
|
+
website?: string;
|
|
25
|
+
status: string;
|
|
26
|
+
createdAt: number;
|
|
27
|
+
approvedAt?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ProviderAPI {
|
|
31
|
+
_id: string;
|
|
32
|
+
providerId: string;
|
|
33
|
+
name: string;
|
|
34
|
+
description: string;
|
|
35
|
+
category: string;
|
|
36
|
+
status: string;
|
|
37
|
+
pricingModel: string;
|
|
38
|
+
discoveryCount?: number;
|
|
39
|
+
createdAt: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Simple admin password check (set via env or hardcode for MVP)
|
|
43
|
+
const ADMIN_PASSWORD = "nordsym2026";
|
|
44
|
+
|
|
45
|
+
export default function AdminPage() {
|
|
46
|
+
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
47
|
+
const [password, setPassword] = useState("");
|
|
48
|
+
const [stats, setStats] = useState<Stats | null>(null);
|
|
49
|
+
const [providers, setProviders] = useState<Provider[]>([]);
|
|
50
|
+
const [apis, setApis] = useState<ProviderAPI[]>([]);
|
|
51
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
52
|
+
const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
// Check if already authenticated this session
|
|
56
|
+
const auth = sessionStorage.getItem("admin_auth");
|
|
57
|
+
if (auth === "true") {
|
|
58
|
+
setIsAuthenticated(true);
|
|
59
|
+
}
|
|
60
|
+
}, []);
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!isAuthenticated) return;
|
|
64
|
+
loadData();
|
|
65
|
+
}, [isAuthenticated]);
|
|
66
|
+
|
|
67
|
+
const handleLogin = (e: React.FormEvent) => {
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
if (password === ADMIN_PASSWORD) {
|
|
70
|
+
setIsAuthenticated(true);
|
|
71
|
+
sessionStorage.setItem("admin_auth", "true");
|
|
72
|
+
} else {
|
|
73
|
+
alert("Invalid password");
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const loadData = async () => {
|
|
78
|
+
setIsLoading(true);
|
|
79
|
+
try {
|
|
80
|
+
// Load stats
|
|
81
|
+
const statsRes = await fetch(`${CONVEX_URL}/api/query`, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: { "Content-Type": "application/json" },
|
|
84
|
+
body: JSON.stringify({ path: "providers:getProviderStats", args: {} }),
|
|
85
|
+
});
|
|
86
|
+
const statsData = await statsRes.json();
|
|
87
|
+
setStats(statsData);
|
|
88
|
+
|
|
89
|
+
// Load all providers (using a simple query)
|
|
90
|
+
const providersRes = await fetch(`${CONVEX_URL}/api/query`, {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: { "Content-Type": "application/json" },
|
|
93
|
+
body: JSON.stringify({ path: "providers:getAllProviders", args: {} }),
|
|
94
|
+
});
|
|
95
|
+
const providersData = await providersRes.json();
|
|
96
|
+
setProviders(providersData || []);
|
|
97
|
+
|
|
98
|
+
// Load all APIs
|
|
99
|
+
const apisRes = await fetch(`${CONVEX_URL}/api/query`, {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: { "Content-Type": "application/json" },
|
|
102
|
+
body: JSON.stringify({ path: "providers:getAllAPIs", args: {} }),
|
|
103
|
+
});
|
|
104
|
+
const apisData = await apisRes.json();
|
|
105
|
+
setApis(apisData || []);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error("Failed to load admin data:", error);
|
|
108
|
+
} finally {
|
|
109
|
+
setIsLoading(false);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
if (!isAuthenticated) {
|
|
114
|
+
return (
|
|
115
|
+
<main className="min-h-screen flex items-center justify-center px-6">
|
|
116
|
+
<div className="max-w-sm w-full">
|
|
117
|
+
<div className="text-center mb-8">
|
|
118
|
+
<Shield className="w-16 h-16 text-accent mx-auto mb-4" />
|
|
119
|
+
<h1 className="text-2xl font-bold">Admin Access</h1>
|
|
120
|
+
<p className="text-text-muted">Enter password to continue</p>
|
|
121
|
+
</div>
|
|
122
|
+
<form onSubmit={handleLogin} className="space-y-4">
|
|
123
|
+
<input
|
|
124
|
+
type="password"
|
|
125
|
+
value={password}
|
|
126
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
127
|
+
placeholder="Password"
|
|
128
|
+
className="w-full px-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none"
|
|
129
|
+
/>
|
|
130
|
+
<button type="submit" className="btn-primary w-full justify-center">
|
|
131
|
+
Access Admin Panel
|
|
132
|
+
</button>
|
|
133
|
+
</form>
|
|
134
|
+
</div>
|
|
135
|
+
</main>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (isLoading) {
|
|
140
|
+
return (
|
|
141
|
+
<main className="min-h-screen flex items-center justify-center">
|
|
142
|
+
<RefreshCw className="w-12 h-12 text-accent animate-spin" />
|
|
143
|
+
</main>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const providerAPIs = selectedProvider
|
|
148
|
+
? apis.filter((a) => a.providerId === selectedProvider)
|
|
149
|
+
: [];
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<main className="min-h-screen bg-background">
|
|
153
|
+
{/* Header */}
|
|
154
|
+
<header className="border-b border-border bg-surface/50 backdrop-blur-xl sticky top-0 z-50">
|
|
155
|
+
<div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
|
|
156
|
+
<div className="flex items-center gap-4">
|
|
157
|
+
<Link href="/" className="w-10 h-10 rounded-xl bg-accent/20 flex items-center justify-center text-xl">
|
|
158
|
+
🦞
|
|
159
|
+
</Link>
|
|
160
|
+
<div>
|
|
161
|
+
<h1 className="font-bold text-lg">APIClaw Admin</h1>
|
|
162
|
+
<p className="text-sm text-text-muted">Provider Management</p>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
<button onClick={loadData} className="btn-secondary !py-2 !px-4 text-sm">
|
|
166
|
+
<RefreshCw className="w-4 h-4" />
|
|
167
|
+
Refresh
|
|
168
|
+
</button>
|
|
169
|
+
</div>
|
|
170
|
+
</header>
|
|
171
|
+
|
|
172
|
+
<div className="max-w-7xl mx-auto px-6 py-8">
|
|
173
|
+
{/* Stats */}
|
|
174
|
+
{stats && (
|
|
175
|
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
|
176
|
+
<StatCard
|
|
177
|
+
icon={Users}
|
|
178
|
+
label="Providers"
|
|
179
|
+
value={stats.totalProviders}
|
|
180
|
+
subValue={`${stats.approvedProviders} approved`}
|
|
181
|
+
/>
|
|
182
|
+
<StatCard
|
|
183
|
+
icon={Zap}
|
|
184
|
+
label="APIs"
|
|
185
|
+
value={stats.totalAPIs}
|
|
186
|
+
subValue={`${stats.approvedAPIs} approved`}
|
|
187
|
+
/>
|
|
188
|
+
<StatCard
|
|
189
|
+
icon={TrendingUp}
|
|
190
|
+
label="Discoveries"
|
|
191
|
+
value={stats.totalDiscoveries}
|
|
192
|
+
/>
|
|
193
|
+
<StatCard
|
|
194
|
+
icon={Clock}
|
|
195
|
+
label="Pending"
|
|
196
|
+
value={stats.pendingProviders + stats.pendingAPIs}
|
|
197
|
+
subValue="need review"
|
|
198
|
+
accent
|
|
199
|
+
/>
|
|
200
|
+
</div>
|
|
201
|
+
)}
|
|
202
|
+
|
|
203
|
+
{/* Providers Table */}
|
|
204
|
+
<div className="bg-surface-elevated rounded-2xl border border-border overflow-hidden mb-8">
|
|
205
|
+
<div className="p-6 border-b border-border">
|
|
206
|
+
<h2 className="font-bold text-lg">All Providers</h2>
|
|
207
|
+
</div>
|
|
208
|
+
<div className="overflow-x-auto">
|
|
209
|
+
<table className="w-full">
|
|
210
|
+
<thead className="bg-surface">
|
|
211
|
+
<tr>
|
|
212
|
+
<th className="text-left px-6 py-3 text-sm font-medium text-text-muted">Provider</th>
|
|
213
|
+
<th className="text-left px-6 py-3 text-sm font-medium text-text-muted">Email</th>
|
|
214
|
+
<th className="text-left px-6 py-3 text-sm font-medium text-text-muted">Status</th>
|
|
215
|
+
<th className="text-left px-6 py-3 text-sm font-medium text-text-muted">APIs</th>
|
|
216
|
+
<th className="text-left px-6 py-3 text-sm font-medium text-text-muted">Registered</th>
|
|
217
|
+
<th className="text-left px-6 py-3 text-sm font-medium text-text-muted">Actions</th>
|
|
218
|
+
</tr>
|
|
219
|
+
</thead>
|
|
220
|
+
<tbody className="divide-y divide-border">
|
|
221
|
+
{providers.map((provider) => {
|
|
222
|
+
const apiCount = apis.filter((a) => a.providerId === provider._id).length;
|
|
223
|
+
return (
|
|
224
|
+
<tr key={provider._id} className="hover:bg-surface/50">
|
|
225
|
+
<td className="px-6 py-4">
|
|
226
|
+
<div>
|
|
227
|
+
<p className="font-medium">{provider.name}</p>
|
|
228
|
+
{provider.company && (
|
|
229
|
+
<p className="text-sm text-text-muted">{provider.company}</p>
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
</td>
|
|
233
|
+
<td className="px-6 py-4 text-sm">{provider.email}</td>
|
|
234
|
+
<td className="px-6 py-4">
|
|
235
|
+
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
|
|
236
|
+
provider.status === "approved"
|
|
237
|
+
? "bg-green-500/20 text-green-500"
|
|
238
|
+
: provider.status === "pending"
|
|
239
|
+
? "bg-yellow-500/20 text-yellow-600"
|
|
240
|
+
: "bg-red-500/20 text-red-500"
|
|
241
|
+
}`}>
|
|
242
|
+
{provider.status}
|
|
243
|
+
</span>
|
|
244
|
+
</td>
|
|
245
|
+
<td className="px-6 py-4 text-sm">{apiCount}</td>
|
|
246
|
+
<td className="px-6 py-4 text-sm text-text-muted">
|
|
247
|
+
{new Date(provider.createdAt).toLocaleDateString()}
|
|
248
|
+
</td>
|
|
249
|
+
<td className="px-6 py-4">
|
|
250
|
+
<button
|
|
251
|
+
onClick={() => setSelectedProvider(
|
|
252
|
+
selectedProvider === provider._id ? null : provider._id
|
|
253
|
+
)}
|
|
254
|
+
className="p-2 rounded-lg hover:bg-surface transition"
|
|
255
|
+
title="View APIs"
|
|
256
|
+
>
|
|
257
|
+
<Eye className="w-4 h-4 text-text-muted" />
|
|
258
|
+
</button>
|
|
259
|
+
</td>
|
|
260
|
+
</tr>
|
|
261
|
+
);
|
|
262
|
+
})}
|
|
263
|
+
{providers.length === 0 && (
|
|
264
|
+
<tr>
|
|
265
|
+
<td colSpan={6} className="px-6 py-12 text-center text-text-muted">
|
|
266
|
+
No providers registered yet
|
|
267
|
+
</td>
|
|
268
|
+
</tr>
|
|
269
|
+
)}
|
|
270
|
+
</tbody>
|
|
271
|
+
</table>
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
{/* Selected Provider APIs */}
|
|
276
|
+
{selectedProvider && (
|
|
277
|
+
<div className="bg-surface-elevated rounded-2xl border border-border overflow-hidden">
|
|
278
|
+
<div className="p-6 border-b border-border flex items-center justify-between">
|
|
279
|
+
<h2 className="font-bold text-lg">
|
|
280
|
+
APIs for {providers.find((p) => p._id === selectedProvider)?.name}
|
|
281
|
+
</h2>
|
|
282
|
+
<button
|
|
283
|
+
onClick={() => setSelectedProvider(null)}
|
|
284
|
+
className="text-text-muted hover:text-text-primary"
|
|
285
|
+
>
|
|
286
|
+
<X className="w-5 h-5" />
|
|
287
|
+
</button>
|
|
288
|
+
</div>
|
|
289
|
+
<div className="divide-y divide-border">
|
|
290
|
+
{providerAPIs.map((api) => (
|
|
291
|
+
<div key={api._id} className="p-6">
|
|
292
|
+
<div className="flex items-start justify-between mb-2">
|
|
293
|
+
<div>
|
|
294
|
+
<h3 className="font-semibold">{api.name}</h3>
|
|
295
|
+
<p className="text-sm text-text-muted">{api.category} • {api.pricingModel}</p>
|
|
296
|
+
</div>
|
|
297
|
+
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
|
|
298
|
+
api.status === "approved"
|
|
299
|
+
? "bg-green-500/20 text-green-500"
|
|
300
|
+
: "bg-yellow-500/20 text-yellow-600"
|
|
301
|
+
}`}>
|
|
302
|
+
{api.status}
|
|
303
|
+
</span>
|
|
304
|
+
</div>
|
|
305
|
+
<p className="text-text-secondary text-sm mb-3">{api.description}</p>
|
|
306
|
+
<div className="flex items-center gap-4 text-sm text-text-muted">
|
|
307
|
+
<span>{api.discoveryCount || 0} discoveries</span>
|
|
308
|
+
<span>Created {new Date(api.createdAt).toLocaleDateString()}</span>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
))}
|
|
312
|
+
{providerAPIs.length === 0 && (
|
|
313
|
+
<div className="p-12 text-center text-text-muted">
|
|
314
|
+
No APIs listed for this provider
|
|
315
|
+
</div>
|
|
316
|
+
)}
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
)}
|
|
320
|
+
</div>
|
|
321
|
+
</main>
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function StatCard({
|
|
326
|
+
icon: Icon,
|
|
327
|
+
label,
|
|
328
|
+
value,
|
|
329
|
+
subValue,
|
|
330
|
+
accent,
|
|
331
|
+
}: {
|
|
332
|
+
icon: typeof Users;
|
|
333
|
+
label: string;
|
|
334
|
+
value: number;
|
|
335
|
+
subValue?: string;
|
|
336
|
+
accent?: boolean;
|
|
337
|
+
}) {
|
|
338
|
+
return (
|
|
339
|
+
<div className={`rounded-2xl border p-5 ${accent ? "bg-accent/10 border-accent/30" : "bg-surface-elevated border-border"}`}>
|
|
340
|
+
<div className="flex items-center gap-2 mb-2">
|
|
341
|
+
<Icon className={`w-5 h-5 ${accent ? "text-accent" : "text-text-muted"}`} />
|
|
342
|
+
<span className="text-sm text-text-muted">{label}</span>
|
|
343
|
+
</div>
|
|
344
|
+
<p className={`text-3xl font-bold ${accent ? "text-accent" : ""}`}>{value}</p>
|
|
345
|
+
{subValue && <p className="text-xs text-text-muted mt-1">{subValue}</p>}
|
|
346
|
+
</div>
|
|
347
|
+
);
|
|
348
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
|
|
3
|
+
const CONVEX_URL = process.env.NEXT_PUBLIC_CONVEX_URL || "https://brilliant-puffin-712.eu-west-1.convex.cloud";
|
|
4
|
+
|
|
5
|
+
export async function POST(req: NextRequest) {
|
|
6
|
+
try {
|
|
7
|
+
const { email } = await req.json();
|
|
8
|
+
|
|
9
|
+
if (!email || !email.includes("@")) {
|
|
10
|
+
return NextResponse.json({ error: "Valid email required" }, { status: 400 });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Create magic link in Convex
|
|
14
|
+
const response = await fetch(`${CONVEX_URL}/api/mutation`, {
|
|
15
|
+
method: "POST",
|
|
16
|
+
headers: { "Content-Type": "application/json" },
|
|
17
|
+
body: JSON.stringify({
|
|
18
|
+
path: "providers:createMagicLink",
|
|
19
|
+
args: { email: email.toLowerCase() },
|
|
20
|
+
}),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
throw new Error("Failed to create magic link");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { token } = await response.json();
|
|
28
|
+
|
|
29
|
+
// Send magic link email via n8n
|
|
30
|
+
const magicLinkUrl = `${process.env.NEXT_PUBLIC_APP_URL || "https://apiclaw.nordsym.com"}/providers/dashboard/verify?token=${token}`;
|
|
31
|
+
|
|
32
|
+
await fetch("https://nordsym.app.n8n.cloud/webhook/symbot-gmail", {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: { "Content-Type": "application/json" },
|
|
35
|
+
body: JSON.stringify({
|
|
36
|
+
action: "smtp",
|
|
37
|
+
to: email,
|
|
38
|
+
subject: "🦞 Sign in to APIClaw Dashboard",
|
|
39
|
+
message: `
|
|
40
|
+
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 500px; margin: 0 auto; padding: 40px 20px;">
|
|
41
|
+
<div style="text-align: center; margin-bottom: 32px;">
|
|
42
|
+
<span style="font-size: 48px;">🦞</span>
|
|
43
|
+
<h1 style="margin: 16px 0 8px; font-size: 24px; font-weight: 700;">Sign in to APIClaw</h1>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<p style="color: #525252; font-size: 16px; line-height: 1.6; text-align: center;">
|
|
47
|
+
Click the button below to sign in to your provider dashboard.
|
|
48
|
+
</p>
|
|
49
|
+
|
|
50
|
+
<div style="text-align: center; margin: 32px 0;">
|
|
51
|
+
<a href="${magicLinkUrl}" style="display: inline-block; background: #ef4444; color: white; text-decoration: none; padding: 14px 32px; border-radius: 8px; font-weight: 600; font-size: 16px;">
|
|
52
|
+
Sign In to Dashboard
|
|
53
|
+
</a>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<p style="color: #737373; font-size: 14px; text-align: center;">
|
|
57
|
+
This link expires in 15 minutes.
|
|
58
|
+
</p>
|
|
59
|
+
|
|
60
|
+
<p style="color: #737373; font-size: 12px; margin-top: 40px; text-align: center;">
|
|
61
|
+
If you didn't request this email, you can safely ignore it.
|
|
62
|
+
</p>
|
|
63
|
+
</div>
|
|
64
|
+
`,
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return NextResponse.json({ success: true });
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error("Magic link error:", error);
|
|
71
|
+
return NextResponse.json({ error: "Failed to send magic link" }, { status: 500 });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
|
|
3
|
+
const CONVEX_URL = process.env.NEXT_PUBLIC_CONVEX_URL || "https://brilliant-puffin-712.eu-west-1.convex.cloud";
|
|
4
|
+
|
|
5
|
+
export async function POST(req: NextRequest) {
|
|
6
|
+
try {
|
|
7
|
+
const { token } = await req.json();
|
|
8
|
+
|
|
9
|
+
if (!token) {
|
|
10
|
+
return NextResponse.json({ error: "Token required" }, { status: 400 });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Get session from Convex
|
|
14
|
+
const response = await fetch(`${CONVEX_URL}/api/query`, {
|
|
15
|
+
method: "POST",
|
|
16
|
+
headers: { "Content-Type": "application/json" },
|
|
17
|
+
body: JSON.stringify({
|
|
18
|
+
path: "providers:getSession",
|
|
19
|
+
args: { token },
|
|
20
|
+
}),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
throw new Error("Session check failed");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const session = await response.json();
|
|
28
|
+
|
|
29
|
+
if (!session) {
|
|
30
|
+
return NextResponse.json({ error: "Invalid session" }, { status: 401 });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return NextResponse.json({ session });
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error("Session error:", error);
|
|
36
|
+
return NextResponse.json({ error: "Session check failed" }, { status: 500 });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
|
|
3
|
+
const CONVEX_URL = process.env.NEXT_PUBLIC_CONVEX_URL || "https://brilliant-puffin-712.eu-west-1.convex.cloud";
|
|
4
|
+
|
|
5
|
+
export async function POST(req: NextRequest) {
|
|
6
|
+
try {
|
|
7
|
+
const { token } = await req.json();
|
|
8
|
+
|
|
9
|
+
if (!token) {
|
|
10
|
+
return NextResponse.json({ error: "Token required" }, { status: 400 });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Verify magic link in Convex
|
|
14
|
+
const response = await fetch(`${CONVEX_URL}/api/mutation`, {
|
|
15
|
+
method: "POST",
|
|
16
|
+
headers: { "Content-Type": "application/json" },
|
|
17
|
+
body: JSON.stringify({
|
|
18
|
+
path: "providers:verifyMagicLink",
|
|
19
|
+
args: { token },
|
|
20
|
+
}),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
throw new Error("Verification failed");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const result = await response.json();
|
|
28
|
+
|
|
29
|
+
if (!result.success) {
|
|
30
|
+
return NextResponse.json({ error: result.error || "Invalid token" }, { status: 400 });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Return session token
|
|
34
|
+
return NextResponse.json({
|
|
35
|
+
success: true,
|
|
36
|
+
sessionToken: result.sessionToken,
|
|
37
|
+
provider: result.provider,
|
|
38
|
+
});
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error("Verify error:", error);
|
|
41
|
+
return NextResponse.json({ error: "Verification failed" }, { status: 500 });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { ImageResponse } from '@vercel/og';
|
|
2
|
+
import statsData from '@/lib/stats.json';
|
|
3
|
+
|
|
4
|
+
export const runtime = 'edge';
|
|
5
|
+
|
|
6
|
+
export async function GET() {
|
|
7
|
+
return new ImageResponse(
|
|
8
|
+
(
|
|
9
|
+
<div
|
|
10
|
+
style={{
|
|
11
|
+
height: '100%',
|
|
12
|
+
width: '100%',
|
|
13
|
+
display: 'flex',
|
|
14
|
+
flexDirection: 'column',
|
|
15
|
+
alignItems: 'center',
|
|
16
|
+
justifyContent: 'center',
|
|
17
|
+
backgroundColor: '#0d0d0d',
|
|
18
|
+
padding: '40px',
|
|
19
|
+
}}
|
|
20
|
+
>
|
|
21
|
+
<div style={{ fontSize: 120, marginBottom: 20 }}>🦞</div>
|
|
22
|
+
<div
|
|
23
|
+
style={{
|
|
24
|
+
fontSize: 72,
|
|
25
|
+
fontWeight: 'bold',
|
|
26
|
+
color: '#ef4444',
|
|
27
|
+
marginBottom: 20,
|
|
28
|
+
}}
|
|
29
|
+
>
|
|
30
|
+
APIClaw
|
|
31
|
+
</div>
|
|
32
|
+
<div
|
|
33
|
+
style={{
|
|
34
|
+
fontSize: 36,
|
|
35
|
+
color: '#ffffff',
|
|
36
|
+
textAlign: 'center',
|
|
37
|
+
maxWidth: 800,
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
The API Layer for AI Agents
|
|
41
|
+
</div>
|
|
42
|
+
<div
|
|
43
|
+
style={{
|
|
44
|
+
fontSize: 24,
|
|
45
|
+
color: '#a3a3a3',
|
|
46
|
+
marginTop: 16,
|
|
47
|
+
textAlign: 'center',
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
Find, evaluate, and integrate APIs in milliseconds.
|
|
51
|
+
</div>
|
|
52
|
+
<div
|
|
53
|
+
style={{
|
|
54
|
+
display: 'flex',
|
|
55
|
+
gap: 40,
|
|
56
|
+
marginTop: 40,
|
|
57
|
+
color: '#ef4444',
|
|
58
|
+
fontSize: 28,
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
<span>{statsData.apiCount.toLocaleString()}+ APIs</span>
|
|
62
|
+
<span>•</span>
|
|
63
|
+
<span>{statsData.categoryCount}+ Categories</span>
|
|
64
|
+
<span>•</span>
|
|
65
|
+
<span>MCP Native</span>
|
|
66
|
+
</div>
|
|
67
|
+
<div
|
|
68
|
+
style={{
|
|
69
|
+
fontSize: 20,
|
|
70
|
+
color: '#737373',
|
|
71
|
+
marginTop: 24,
|
|
72
|
+
fontStyle: 'italic',
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
Built for the agentic era
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
),
|
|
79
|
+
{
|
|
80
|
+
width: 1200,
|
|
81
|
+
height: 630,
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
}
|