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
@@ -1,7 +1,7 @@
|
||||
"use server";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import { api, unwrap } from "@/lib/api";
|
||||
import { api, ApiError, unwrap } from "@/lib/api";
|
||||
import { attempt, field, type ActionState } from "@/lib/actions";
|
||||
|
||||
const done = () => revalidatePath("/account");
|
||||
@@ -71,3 +71,34 @@ export async function startAgeCheck(_: ActionState, form: FormData): Promise<Act
|
||||
if (url) redirect(url);
|
||||
return state;
|
||||
}
|
||||
|
||||
/** What making a token answers: the token itself, shown once. */
|
||||
export type TokenState = { error?: string; token?: string; name?: string } | null;
|
||||
|
||||
/** Makes an API token. `grants` is JSON: [{permission, scope}]. */
|
||||
export async function createToken(_: TokenState, form: FormData): Promise<TokenState> {
|
||||
let grants: { permission: string; scope: { type: "hub" } | { type: "catalog" | "product"; id: string } }[];
|
||||
try {
|
||||
grants = JSON.parse(field(form, "grants") || "[]");
|
||||
} catch {
|
||||
return { error: "Choose what it may do." };
|
||||
}
|
||||
if (!Array.isArray(grants) || grants.length === 0) return { error: "Choose what it may do." };
|
||||
try {
|
||||
const made = unwrap(await api.account.tokens.post({ name: field(form, "name"), days: Number(field(form, "days")), grants }));
|
||||
done();
|
||||
return { token: made.token, name: made.info.name };
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError) return { error: e.message };
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/** Revokes an API token. */
|
||||
export async function revokeToken(_: ActionState, form: FormData): Promise<ActionState> {
|
||||
return attempt(async () => {
|
||||
unwrap(await api.account.tokens({ id: field(form, "id") }).delete());
|
||||
done();
|
||||
return "Revoked.";
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user