Files

120 lines
4.1 KiB
TypeScript

"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { Loader2, Copy, KeyRound, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent } from "@/components/ui/card";
import { ConfirmDialog } from "@/components/admin/ui/confirm-dialog";
import { EmptyState } from "@/components/ui/empty-state";
import { createApiKeyAction, revokeApiKeyAction } from "@/app/(app)/api-keys/actions";
interface KeyRow {
id: string;
name: string;
prefix: string;
lastUsedAt: string | null;
createdAt: string;
}
export function ApiKeysClient({ keys }: { keys: KeyRow[] }) {
const router = useRouter();
const [name, setName] = useState("");
const [creating, setCreating] = useState(false);
const [newKey, setNewKey] = useState<string | null>(null);
async function create(e: React.FormEvent) {
e.preventDefault();
setCreating(true);
const res = await createApiKeyAction(name);
setCreating(false);
if (!res.ok || !res.key) {
toast.error(res.error ?? "Could not create");
return;
}
setNewKey(res.key);
setName("");
router.refresh();
}
return (
<div className="max-w-2xl space-y-6">
{newKey && (
<Card className="ring-2 ring-brand">
<CardContent className="space-y-2 py-4">
<p className="text-sm font-medium">Your new API key copy it now, it won&apos;t be shown again.</p>
<div className="flex items-center gap-2">
<code className="flex-1 truncate rounded-lg bg-secondary px-2.5 py-1.5 text-xs">{newKey}</code>
<Button
size="sm"
variant="outline"
onClick={() => {
navigator.clipboard.writeText(newKey);
toast.success("Copied");
}}
>
<Copy className="h-4 w-4" /> Copy
</Button>
</div>
</CardContent>
</Card>
)}
<Card>
<CardContent className="py-4">
<form onSubmit={create} className="flex gap-2">
<Input
placeholder="Key name (e.g. Production)"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<Button type="submit" disabled={creating}>
{creating ? <Loader2 className="h-4 w-4 animate-spin" /> : <KeyRound className="h-4 w-4" />}
Create key
</Button>
</form>
</CardContent>
</Card>
{keys.length === 0 ? (
<EmptyState
icon={KeyRound}
title="No API keys yet"
description="Create a key above to start generating episodes programmatically."
/>
) : (
<div className="divide-y rounded-2xl border">
{keys.map((k) => (
<div key={k.id} className="flex items-center justify-between gap-3 p-4">
<div className="min-w-0">
<p className="font-medium">{k.name}</p>
<p className="text-xs text-muted-foreground">
<code>{k.prefix}</code> · created {new Date(k.createdAt).toLocaleDateString()}
{k.lastUsedAt ? ` · last used ${new Date(k.lastUsedAt).toLocaleDateString()}` : " · never used"}
</p>
</div>
<ConfirmDialog
trigger={
<Button variant="ghost" size="sm" className="text-destructive">
<Trash2 className="h-4 w-4" /> Revoke
</Button>
}
title="Revoke this key?"
description={`Apps using "${k.name}" will immediately stop working. This cannot be undone.`}
confirmLabel="Revoke key"
successMessage="Key revoked"
onConfirm={async () => {
const res = await revokeApiKeyAction(k.id);
return { ok: res.ok, error: res.error };
}}
/>
</div>
))}
</div>
)}
</div>
);
}