179 lines
7.1 KiB
TypeScript
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>
|
|
);
|
|
}
|