feat: personal API tokens
Tokens for scripts, made, listed and revoked on the account page. Each grants some of the person's own permissions at chosen places (a product of theirs, a catalog whose team they're on, or the hub for moderators); every check is the intersection with what they hold now. Tokens work only on routes that check a permission (session-only routes answer token_not_allowed), are shown once and stored as hashes, note their last use, and mark the audit entries of what they do. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
58a3e70a63
commit
2b88a52bcb
17 files changed
+4358
-98
No files matched your search
@@ -0,0 +1,97 @@
|
||||
// API tokens on the account page: the person's tokens, and making one.
|
||||
import { ActionForm, SubmitButton } from "@/components/forms";
|
||||
import { Tag } from "@/components/status";
|
||||
import { DateTime } from "@/components/time";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import type { TokenEntry, TokenList } from "@/lib/api";
|
||||
import { revokeToken } from "./actions";
|
||||
import { TokenForm } from "./token-form";
|
||||
|
||||
export function TokensCard({ data }: { data: TokenList }) {
|
||||
const active = data.tokens.filter((t) => t.status === "active");
|
||||
const past = data.tokens.filter((t) => t.status !== "active").slice(0, 10);
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>API Tokens</CardTitle>
|
||||
<CardDescription>
|
||||
For your own scripts. A token can do only what you give it, and never more than you can do yourself.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{active.length > 0 && (
|
||||
<ul className="divide-y">
|
||||
{active.map((t) => (
|
||||
<TokenRow key={t.id} token={t} />
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{data.scopes.length > 0 ? (
|
||||
<TokenForm scopes={data.scopes} maxDays={data.maxDays} />
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Tokens act on what you manage: your products or a catalog's team. You don't manage anything yet.
|
||||
</p>
|
||||
)}
|
||||
{past.length > 0 && (
|
||||
<details className="text-sm">
|
||||
<summary className="cursor-pointer text-muted-foreground hover:text-foreground">Revoked and expired</summary>
|
||||
<ul className="mt-2 divide-y">
|
||||
{past.map((t) => (
|
||||
<TokenRow key={t.id} token={t} />
|
||||
))}
|
||||
</ul>
|
||||
</details>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function TokenRow({ token: t }: { token: TokenEntry }) {
|
||||
return (
|
||||
<li className="space-y-1 py-3 first:pt-0 last:pb-0">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="font-medium">{t.name}</span>
|
||||
{t.status !== "active" && <Tag tone={t.status === "revoked" ? "danger" : "neutral"}>{t.status}</Tag>}
|
||||
<span className="flex-1" />
|
||||
{t.status === "active" && (
|
||||
<ActionForm action={revokeToken} fields={{ id: t.id }}>
|
||||
<SubmitButton size="sm" variant="outline" pendingLabel="Revoking…" confirm={`Revoke ${t.name}? Scripts using it stop working.`}>
|
||||
Revoke
|
||||
</SubmitButton>
|
||||
</ActionForm>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t.grants.map((g, i) => (
|
||||
<span key={i}>
|
||||
{i > 0 && ", "}
|
||||
<span className="font-mono">{g.permission}</span> on {g.scope.type === "hub" ? "everything" : g.name}
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Made <DateTime iso={t.createdAt} dateOnly />
|
||||
{" · "}
|
||||
{t.status === "revoked" && t.revokedAt ? (
|
||||
<>
|
||||
revoked <DateTime iso={t.revokedAt} dateOnly />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{t.status === "expired" ? "expired" : "expires"} <DateTime iso={t.expiresAt} dateOnly />
|
||||
</>
|
||||
)}
|
||||
{" · "}
|
||||
{t.lastUsedAt ? (
|
||||
<>
|
||||
last used <DateTime iso={t.lastUsedAt} />
|
||||
</>
|
||||
) : (
|
||||
"never used"
|
||||
)}
|
||||
</p>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user