Add API tokens, CLI server API and shared core crate
- Personal API tokens (full/read scope, expiry, revoke) and CLI password login - Cargo workspace: codecraft-core (types, providers, permission rules) shared by server and the upcoming terminal client; server re-exports the old paths - CLI server API: /cli/config, /cli/skill, /cli/tool (server MCP tools), /llm/stream model gateway with budgets and usage per user - Theme/language preferences mirrored to the server; personal memory endpoint - Statistics: provider and mode tables stacked; CLI usage labelled - cli/: work in progress (terminal client is being rewritten as a local TUI) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fca1c41087
commit
aa1f8cf016
40 files changed
+2454
-814
No files matched your search
@@ -253,15 +253,13 @@ export function Stats({ onMenu }: { onMenu: () => void }) {
|
||||
<h3>{t("stats.byModel")}</h3>
|
||||
<AggTable rows={data.by_model} keyName="model" label={t("stats.model")} currency={cur} />
|
||||
</div>
|
||||
<div className="two-col">
|
||||
<div className="panel">
|
||||
<h3>{t("stats.byProvider")}</h3>
|
||||
<AggTable rows={data.by_provider} keyName="provider" label={t("stats.provider")} currency={cur} />
|
||||
</div>
|
||||
<div className="panel">
|
||||
<h3>{t("stats.byAgent")}</h3>
|
||||
<AggTable rows={data.by_agent} keyName="agent" label={t("stats.agent")} currency={cur} />
|
||||
</div>
|
||||
<div className="panel">
|
||||
<h3>{t("stats.byProvider")}</h3>
|
||||
<AggTable rows={data.by_provider} keyName="provider" label={t("stats.provider")} currency={cur} />
|
||||
</div>
|
||||
<div className="panel">
|
||||
<h3>{t("stats.byAgent")}</h3>
|
||||
<AggTable rows={data.by_agent} keyName="agent" label={t("stats.agent")} currency={cur} />
|
||||
</div>
|
||||
{isAdmin && !userFilter && data.by_user.length > 1 && (
|
||||
<div className="panel">
|
||||
@@ -271,7 +269,7 @@ export function Stats({ onMenu }: { onMenu: () => void }) {
|
||||
)}
|
||||
<div className="panel">
|
||||
<h3>{t("stats.topSessions")}</h3>
|
||||
<AggTable rows={data.top_sessions} keyName="title" label={t("stats.session")} currency={cur} link={(r) => navigate(`/s/${r.session_id}`)} />
|
||||
<AggTable rows={data.top_sessions} keyName="title" label={t("stats.session")} currency={cur} link={(r) => !r.session_id.startsWith("cli:") && navigate(`/s/${r.session_id}`)} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { post } from "../../api";
|
||||
import { useT } from "../../i18n";
|
||||
import { useStore } from "../../store";
|
||||
import { GitAccount } from "./GitAccount";
|
||||
import { ApiTokens } from "./ApiTokens";
|
||||
|
||||
export function Account() {
|
||||
const t = useT();
|
||||
@@ -46,6 +47,7 @@ export function Account() {
|
||||
<div style={{ fontSize: 12, color: "var(--muted)", marginTop: 8 }}>{t("account.hint")}</div>
|
||||
</div>
|
||||
<GitAccount />
|
||||
<ApiTokens />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { del, get, post } from "../../api";
|
||||
import { fmtRelative, useT } from "../../i18n";
|
||||
import { confirmDialog, toast } from "../../dialogs";
|
||||
import { Icon } from "../Icon";
|
||||
|
||||
interface TokenInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
prefix: string;
|
||||
scope: "full" | "read";
|
||||
created_at: number;
|
||||
last_used?: number | null;
|
||||
expires_at?: number | null;
|
||||
}
|
||||
|
||||
/** Personal API tokens for the CLI and scripts. */
|
||||
export function ApiTokens() {
|
||||
const t = useT();
|
||||
const [list, setList] = useState<TokenInfo[]>([]);
|
||||
const [name, setName] = useState("");
|
||||
const [scope, setScope] = useState<"full" | "read">("full");
|
||||
const [days, setDays] = useState("");
|
||||
const [created, setCreated] = useState<string>();
|
||||
|
||||
const reload = () => get<TokenInfo[]>("/account/tokens").then(setList).catch((e) => toast(e.message));
|
||||
useEffect(() => {
|
||||
void reload();
|
||||
}, []);
|
||||
|
||||
const create = async () => {
|
||||
try {
|
||||
const r = await post<{ token: string }>("/account/tokens", { name: name.trim(), scope, days: days ? Number(days) : undefined });
|
||||
setCreated(r.token);
|
||||
setName("");
|
||||
void reload();
|
||||
} catch (e: any) {
|
||||
toast(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="panel">
|
||||
<h3>
|
||||
<Icon name="lock" /> {t("tokens.title")}
|
||||
</h3>
|
||||
<div className="hint" style={{ marginBottom: 10 }}>
|
||||
{t("tokens.hint")}
|
||||
</div>
|
||||
{created && (
|
||||
<div className="notice ok token-created">
|
||||
<div>{t("tokens.copyNow")}</div>
|
||||
<div className="row" style={{ gap: 6, marginTop: 6 }}>
|
||||
<code className="token-value">{created}</code>
|
||||
<button
|
||||
className="small"
|
||||
onClick={() => navigator.clipboard.writeText(created).then(() => toast(t("tokens.copied"), "success"))}
|
||||
>
|
||||
<Icon name="copy" /> {t("tokens.copy")}
|
||||
</button>
|
||||
</div>
|
||||
<pre className="mono token-example">{`codecraft login ${location.origin}`}</pre>
|
||||
</div>
|
||||
)}
|
||||
{list.length > 0 && (
|
||||
<table className="grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("tokens.name")}</th>
|
||||
<th>{t("tokens.scope")}</th>
|
||||
<th>{t("tokens.lastUsed")}</th>
|
||||
<th>{t("tokens.expires")}</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{list.map((k) => (
|
||||
<tr key={k.id}>
|
||||
<td>
|
||||
{k.name} <code className="muted">{k.prefix}…</code>
|
||||
</td>
|
||||
<td>{t(k.scope === "read" ? "tokens.scopeRead" : "tokens.scopeFull")}</td>
|
||||
<td>{k.last_used ? fmtRelative(k.last_used) : "–"}</td>
|
||||
<td>{k.expires_at ? new Date(k.expires_at).toLocaleDateString() : t("tokens.never")}</td>
|
||||
<td>
|
||||
<button
|
||||
className="small ghost danger"
|
||||
aria-label={t("tokens.revoke")}
|
||||
title={t("tokens.revoke")}
|
||||
onClick={async () => {
|
||||
if (await confirmDialog({ message: t("tokens.revokeConfirm", { name: k.name }), danger: true })) {
|
||||
await del(`/account/tokens/${k.id}`).catch((e) => toast(e.message));
|
||||
void reload();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
<div className="row" style={{ gap: 6, marginTop: 10, flexWrap: "wrap" }}>
|
||||
<input style={{ flex: 1, minWidth: 160 }} placeholder={t("tokens.namePlaceholder")} value={name} onChange={(e) => setName(e.target.value)} />
|
||||
<select value={scope} onChange={(e) => setScope(e.target.value as "full" | "read")}>
|
||||
<option value="full">{t("tokens.scopeFull")}</option>
|
||||
<option value="read">{t("tokens.scopeRead")}</option>
|
||||
</select>
|
||||
<select value={days} onChange={(e) => setDays(e.target.value)}>
|
||||
<option value="">{t("tokens.noExpiry")}</option>
|
||||
<option value="30">30 {t("tokens.days")}</option>
|
||||
<option value="90">90 {t("tokens.days")}</option>
|
||||
<option value="365">365 {t("tokens.days")}</option>
|
||||
</select>
|
||||
<button className="primary" disabled={!name.trim()} onClick={create}>
|
||||
<Icon name="plus" /> {t("tokens.create")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user