179 lines
7.1 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { AppShell } from "@/components/app-shell";
import { apiFetch } from "@/lib/api";
type ApiKey = {
id: string;
name: string;
prefix: string;
scopes: string[];
lastUsedAt: string | null;
revokedAt: string | null;
expiresAt: string | null;
createdAt: string;
};
type KeyListResponse = { keys: ApiKey[] };
type CreatedKey = ApiKey & { key: string };
const apiExample = `curl -H "Authorization: Bearer l1_your_api_key" \\
"https://api.aarthalabs.com/api/public/v1/transactions?limit=25"`;
const sdkExample = `async function ledgerOne(path, apiKey) {
const res = await fetch(\`https://api.aarthalabs.com/api/public/v1\${path}\`, {
headers: { Authorization: \`Bearer \${apiKey}\` },
});
if (!res.ok) throw new Error(await res.text());
return res.json();
}
const transactions = await ledgerOne("/transactions?limit=25", process.env.LEDGERONE_API_KEY);`;
export default function DeveloperPage() {
const [keys, setKeys] = useState<ApiKey[]>([]);
const [name, setName] = useState("Automation key");
const [createdKey, setCreatedKey] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [message, setMessage] = useState<string | null>(null);
const loadKeys = async () => {
setLoading(true);
const res = await apiFetch<KeyListResponse>("/api/api-keys");
if (res.error) {
setMessage(res.error.message);
} else {
setKeys(res.data.keys);
setMessage(null);
}
setLoading(false);
};
useEffect(() => {
void loadKeys();
}, []);
const createKey = async () => {
const res = await apiFetch<CreatedKey>("/api/api-keys", {
method: "POST",
body: JSON.stringify({ name, scopes: ["transactions:read"] }),
});
if (res.error) {
setMessage(res.error.message);
return;
}
setCreatedKey(res.data.key);
setName("Automation key");
await loadKeys();
};
const revokeKey = async (id: string) => {
const res = await apiFetch<{ revoked: boolean }>(`/api/api-keys/${id}`, { method: "DELETE" });
if (res.error) {
setMessage(res.error.message);
return;
}
await loadKeys();
};
return (
<AppShell title="Developer" subtitle="API keys, public endpoint docs, and integration examples.">
<div className="mx-auto flex w-full max-w-6xl flex-col gap-6">
{message ? (
<div className="rounded-md border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900">{message}</div>
) : null}
{createdKey ? (
<section className="rounded-lg border border-emerald-200 bg-emerald-50 p-4">
<h2 className="text-sm font-semibold text-emerald-950">New API key</h2>
<p className="mt-1 text-xs text-emerald-800">This secret is shown once. Store it in your password manager or server environment.</p>
<pre className="mt-3 overflow-x-auto rounded-md bg-white p-3 text-xs text-slate-900">{createdKey}</pre>
</section>
) : null}
<section className="grid gap-4 lg:grid-cols-[1fr_1.2fr]">
<div className="rounded-lg border border-slate-200 bg-white p-4">
<h2 className="text-base font-semibold text-slate-950">Create Key</h2>
<label className="mt-4 block text-sm font-medium text-slate-700">Key name</label>
<input
value={name}
onChange={(event) => setName(event.target.value)}
className="mt-2 w-full rounded-md border border-slate-300 px-3 py-2 text-sm"
/>
<button
onClick={createKey}
className="mt-4 rounded-md bg-slate-950 px-4 py-2 text-sm font-medium text-white hover:bg-slate-800"
>
Create API key
</button>
</div>
<div className="rounded-lg border border-slate-200 bg-white p-4">
<h2 className="text-base font-semibold text-slate-950">Read Endpoints</h2>
<div className="mt-3 grid gap-2 text-sm text-slate-700 sm:grid-cols-2">
<span>/public/v1/transactions</span>
<span>/public/v1/transactions/summary</span>
<span>/public/v1/transactions/cashflow</span>
<span>/public/v1/transactions/merchants</span>
</div>
<pre className="mt-4 overflow-x-auto rounded-md bg-slate-950 p-3 text-xs text-slate-50">{apiExample}</pre>
</div>
</section>
<section className="rounded-lg border border-slate-200 bg-white p-4">
<div className="flex items-center justify-between gap-3">
<h2 className="text-base font-semibold text-slate-950">API Keys</h2>
<button onClick={loadKeys} className="rounded-md border border-slate-300 px-3 py-2 text-sm text-slate-700">
Refresh
</button>
</div>
<div className="mt-4 overflow-x-auto">
<table className="w-full min-w-[680px] text-left text-sm">
<thead className="border-b border-slate-200 text-xs uppercase text-slate-500">
<tr>
<th className="py-2">Name</th>
<th className="py-2">Prefix</th>
<th className="py-2">Scopes</th>
<th className="py-2">Last used</th>
<th className="py-2">Status</th>
<th className="py-2 text-right">Action</th>
</tr>
</thead>
<tbody>
{loading ? (
<tr><td className="py-4 text-slate-500" colSpan={6}>Loading keys...</td></tr>
) : keys.length ? (
keys.map((key) => (
<tr key={key.id} className="border-b border-slate-100">
<td className="py-3 font-medium text-slate-900">{key.name}</td>
<td className="py-3 text-slate-600">{key.prefix}</td>
<td className="py-3 text-slate-600">{key.scopes.join(", ")}</td>
<td className="py-3 text-slate-600">{key.lastUsedAt ? new Date(key.lastUsedAt).toLocaleString() : "Never"}</td>
<td className="py-3 text-slate-600">{key.revokedAt ? "Revoked" : "Active"}</td>
<td className="py-3 text-right">
{!key.revokedAt ? (
<button onClick={() => revokeKey(key.id)} className="rounded-md border border-red-200 px-3 py-1.5 text-xs font-medium text-red-700">
Revoke
</button>
) : null}
</td>
</tr>
))
) : (
<tr><td className="py-4 text-slate-500" colSpan={6}>No API keys yet.</td></tr>
)}
</tbody>
</table>
</div>
</section>
<section className="rounded-lg border border-slate-200 bg-white p-4">
<h2 className="text-base font-semibold text-slate-950">Minimal JavaScript Client</h2>
<pre className="mt-3 overflow-x-auto rounded-md bg-slate-950 p-3 text-xs text-slate-50">{sdkExample}</pre>
</section>
</div>
</AppShell>
);
}