Paramètres : accès API et MCP (jetons) ; documentation
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nh6YQSjM9ni5jiTc8u5Tj1
This commit is contained in:
1 parent
9b8f53d0d0
commit
6ac2dbd227
7 files changed
+251
-1
No files matched your search
@@ -0,0 +1,154 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { api, type ApiToken } from "./api";
|
||||
import { CopyIcon, TrashIcon } from "./icons";
|
||||
|
||||
const fmtDate = (d: string) => new Date(d).toLocaleDateString(undefined, { dateStyle: "short" });
|
||||
|
||||
/** Section « Accès API et MCP » des paramètres : adresse du serveur MCP et jetons de l'utilisateur. */
|
||||
export function ApiAccess({ onError }: { onError: (msg: string | null) => void }) {
|
||||
const [tokens, setTokens] = useState<ApiToken[] | null>(null);
|
||||
const [name, setName] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [created, setCreated] = useState<string | null>(null);
|
||||
const [copied, setCopied] = useState<string | null>(null);
|
||||
|
||||
const mcpUrl = `${location.origin}/mcp`;
|
||||
const cmd = created
|
||||
? `claude mcp add --transport http excalicloud ${mcpUrl} --header "Authorization: Bearer ${created}"`
|
||||
: null;
|
||||
|
||||
const load = () =>
|
||||
api
|
||||
.tokens()
|
||||
.then(setTokens)
|
||||
.catch((e) => onError(e.message));
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, []);
|
||||
|
||||
const copy = (what: string, text: string) =>
|
||||
navigator.clipboard?.writeText(text).then(() => {
|
||||
setCopied(what);
|
||||
setTimeout(() => setCopied(null), 1500);
|
||||
});
|
||||
|
||||
const create = async () => {
|
||||
if (!name.trim()) return;
|
||||
setBusy(true);
|
||||
onError(null);
|
||||
try {
|
||||
const t = await api.createToken(name);
|
||||
setCreated(t.token);
|
||||
setName("");
|
||||
load();
|
||||
} catch (e) {
|
||||
onError(e instanceof Error ? e.message : String(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const revoke = async (t: ApiToken) => {
|
||||
const what = t.kind === "oauth" ? `l'accès de « ${t.name} » (${t.client})` : `le jeton « ${t.name} »`;
|
||||
if (!confirm(`Révoquer ${what} ? Les clients qui l'utilisent n'auront plus accès.`)) return;
|
||||
onError(null);
|
||||
try {
|
||||
await api.revokeToken(t.id);
|
||||
} catch (e) {
|
||||
onError(e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
load();
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="field api-access">
|
||||
<h4>Accès API et MCP</h4>
|
||||
<span>Adresse du serveur MCP</span>
|
||||
<div className="cmd">
|
||||
<code>{mcpUrl}</code>
|
||||
<button type="button" className="icon-btn" onClick={() => copy("url", mcpUrl)} title="Copier" aria-label="Copier l'adresse">
|
||||
<CopyIcon />
|
||||
</button>
|
||||
</div>
|
||||
<small className="muted">
|
||||
{copied === "url" ? (
|
||||
"Adresse copiée."
|
||||
) : (
|
||||
<>
|
||||
claude.ai, Claude Desktop ou mobile : <i>Paramètres › Connecteurs › Ajouter un connecteur personnalisé</i>{" "}
|
||||
avec cette adresse, sans identifiant OAuth ; la connexion ouvre ici une page d'autorisation. Claude Code et
|
||||
scripts : créer un jeton personnel.
|
||||
</>
|
||||
)}
|
||||
</small>
|
||||
|
||||
<div className="token-create">
|
||||
<input
|
||||
className="text-input"
|
||||
value={name}
|
||||
maxLength={80}
|
||||
placeholder="Nom du jeton, ex. Claude Code portable"
|
||||
aria-label="Nom du nouveau jeton"
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
create();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button type="button" disabled={busy || !name.trim()} onClick={create}>
|
||||
Créer un jeton
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{created && cmd && (
|
||||
<div className="token-new">
|
||||
<small>Jeton créé : le copier maintenant, il ne sera plus affiché.</small>
|
||||
<div className="cmd">
|
||||
<code>{created}</code>
|
||||
<button type="button" className="icon-btn" onClick={() => copy("token", created)} title="Copier" aria-label="Copier le jeton">
|
||||
<CopyIcon />
|
||||
</button>
|
||||
</div>
|
||||
<small className="muted">{copied === "token" ? "Jeton copié." : "Commande Claude Code :"}</small>
|
||||
<div className="cmd">
|
||||
<code>{cmd}</code>
|
||||
<button type="button" className="icon-btn" onClick={() => copy("cmd", cmd)} title="Copier" aria-label="Copier la commande">
|
||||
<CopyIcon />
|
||||
</button>
|
||||
</div>
|
||||
{copied === "cmd" && <small className="muted">Commande copiée.</small>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tokens?.length ? (
|
||||
<ul className="share-list all">
|
||||
{tokens.map((t) => (
|
||||
<li key={t.id}>
|
||||
<div className="cmd">
|
||||
<code title={t.name}>{t.name}</code>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-btn danger"
|
||||
onClick={() => revoke(t)}
|
||||
title="Révoquer"
|
||||
aria-label={`Révoquer ${t.name}`}
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</div>
|
||||
<small className="muted">
|
||||
{t.kind === "oauth" ? `Connecteur autorisé (${t.client})` : "Jeton personnel"} · créé le {fmtDate(t.created)}
|
||||
{t.lastUsed ? ` · utilisé le ${fmtDate(t.lastUsed)}` : " · jamais utilisé"}
|
||||
</small>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
tokens && <small className="muted">Aucun jeton ni connecteur autorisé.</small>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { ApiAccess } from "./ApiAccess";
|
||||
import { api, type Channel, type Settings, type Share, type UpdateStatus } from "./api";
|
||||
import { CopyIcon, TrashIcon } from "./icons";
|
||||
import { fmtExpiry } from "./ShareDialog";
|
||||
@@ -260,6 +261,8 @@ export function SettingsDialog({ settings, update, focusExcalidraw, onChange, on
|
||||
)}
|
||||
</section>
|
||||
|
||||
<ApiAccess onError={setError} />
|
||||
|
||||
{error && <div className="error">{error}</div>}
|
||||
|
||||
<div className="modal-actions">
|
||||
|
||||
@@ -72,6 +72,16 @@ export interface Share {
|
||||
expires?: string;
|
||||
}
|
||||
|
||||
/** Jeton d'accès à l'API et au serveur MCP (api : créé ici ; oauth : autorisé depuis un client). */
|
||||
export interface ApiToken {
|
||||
id: string;
|
||||
name: string;
|
||||
kind: "api" | "oauth";
|
||||
client?: string;
|
||||
created: string;
|
||||
lastUsed?: string;
|
||||
}
|
||||
|
||||
/** Schéma partagé tel que servi sans authentification. */
|
||||
export interface PublicScene {
|
||||
name: string;
|
||||
@@ -203,6 +213,16 @@ export const api = {
|
||||
await request("/api/shares/revoke-all", { method: "POST" });
|
||||
},
|
||||
|
||||
tokens: async (): Promise<ApiToken[]> => (await request("/api/tokens")).json(),
|
||||
|
||||
/** Le jeton en clair (token) n'est renvoyé qu'à la création. */
|
||||
createToken: async (name: string): Promise<ApiToken & { token: string }> =>
|
||||
(await request("/api/tokens", json({ name }))).json(),
|
||||
|
||||
revokeToken: async (id: string): Promise<void> => {
|
||||
await request(`/api/tokens/${q(id)}`, { method: "DELETE" });
|
||||
},
|
||||
|
||||
publicScene: async (token: string): Promise<PublicScene> =>
|
||||
(await request(`/api/public/${q(token)}`)).json(),
|
||||
|
||||
|
||||
@@ -1340,6 +1340,23 @@ button.chip.on,
|
||||
text-overflow: ellipsis;
|
||||
overflow-wrap: normal;
|
||||
}
|
||||
.token-create {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.token-create .text-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.token-new {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 8px;
|
||||
border-radius: var(--ex-radius-lg);
|
||||
border: 1px solid var(--warn);
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.share-view .status.readonly {
|
||||
display: inline-flex;
|
||||
|
||||
Reference in new issue
Block a user