Add developer API key portal
This commit is contained in:
parent
eddd46c049
commit
c7ee638372
6
app/api/api-keys/[id]/route.ts
Normal file
6
app/api/api-keys/[id]/route.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { proxyRequest } from "@/lib/backend";
|
||||
|
||||
export async function DELETE(req: NextRequest, { params }: { params: { id: string } }) {
|
||||
return proxyRequest(req, `api-keys/${params.id}`);
|
||||
}
|
||||
10
app/api/api-keys/route.ts
Normal file
10
app/api/api-keys/route.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { proxyRequest } from "@/lib/backend";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
return proxyRequest(req, "api-keys");
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
return proxyRequest(req, "api-keys");
|
||||
}
|
||||
178
app/developer/page.tsx
Normal file
178
app/developer/page.tsx
Normal file
@ -0,0 +1,178 @@
|
||||
"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.ledgerone.app/api/public/v1/transactions?limit=25"`;
|
||||
|
||||
const sdkExample = `async function ledgerOne(path, apiKey) {
|
||||
const res = await fetch(\`https://api.ledgerone.app/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>
|
||||
);
|
||||
}
|
||||
@ -11,6 +11,7 @@ const navItems = [
|
||||
{ href: "/app/connect", label: "Accounts" },
|
||||
{ href: "/transactions", label: "Transactions" },
|
||||
{ href: "/planning", label: "Planning" },
|
||||
{ href: "/developer", label: "Developer" },
|
||||
{ href: "/bills", label: "Bills" },
|
||||
{ href: "/credit-score", label: "Credit Score" },
|
||||
{ href: "/rules", label: "Rules" },
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user