diff --git a/app/api/api-keys/[id]/route.ts b/app/api/api-keys/[id]/route.ts new file mode 100644 index 0000000..8f87b50 --- /dev/null +++ b/app/api/api-keys/[id]/route.ts @@ -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}`); +} diff --git a/app/api/api-keys/route.ts b/app/api/api-keys/route.ts new file mode 100644 index 0000000..5c80700 --- /dev/null +++ b/app/api/api-keys/route.ts @@ -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"); +} diff --git a/app/developer/page.tsx b/app/developer/page.tsx new file mode 100644 index 0000000..e4f0b08 --- /dev/null +++ b/app/developer/page.tsx @@ -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([]); + const [name, setName] = useState("Automation key"); + const [createdKey, setCreatedKey] = useState(null); + const [loading, setLoading] = useState(true); + const [message, setMessage] = useState(null); + + const loadKeys = async () => { + setLoading(true); + const res = await apiFetch("/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("/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 ( + +
+ {message ? ( +
{message}
+ ) : null} + + {createdKey ? ( +
+

New API key

+

This secret is shown once. Store it in your password manager or server environment.

+
{createdKey}
+
+ ) : null} + +
+
+

Create Key

+ + setName(event.target.value)} + className="mt-2 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" + /> + +
+ +
+

Read Endpoints

+
+ /public/v1/transactions + /public/v1/transactions/summary + /public/v1/transactions/cashflow + /public/v1/transactions/merchants +
+
{apiExample}
+
+
+ +
+
+

API Keys

+ +
+
+ + + + + + + + + + + + + {loading ? ( + + ) : keys.length ? ( + keys.map((key) => ( + + + + + + + + + )) + ) : ( + + )} + +
NamePrefixScopesLast usedStatusAction
Loading keys...
{key.name}{key.prefix}{key.scopes.join(", ")}{key.lastUsedAt ? new Date(key.lastUsedAt).toLocaleString() : "Never"}{key.revokedAt ? "Revoked" : "Active"} + {!key.revokedAt ? ( + + ) : null} +
No API keys yet.
+
+
+ +
+

Minimal JavaScript Client

+
{sdkExample}
+
+
+
+ ); +} diff --git a/components/app-shell.tsx b/components/app-shell.tsx index 2584e77..75e1ecf 100644 --- a/components/app-shell.tsx +++ b/components/app-shell.tsx @@ -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" },