The matrix, forge and github link routes wrote their credential unconditionally, so linking a name that was already linked replaced the working account. For matrix that lost the device the agent's crypto store belongs to (#4838). Each route now reads the account's store path first and answers 409, naming the existing account, when something is stored there. Nothing is written. Replacing an account takes the delete from #4899, then a link. The matrix route checks before password mode's login, so a refused link mints no new device at the homeserver. The check is a read then a write, not an atomic step; two concurrent links to one name can still both pass it. Closes #4856
109 lines
3.4 KiB
TypeScript
109 lines
3.4 KiB
TypeScript
// <LinkGithubAccountForm> — writes a GitHub personal access token for one
|
|
// agent into the swarm secret store.
|
|
// PUTs `/api/hives/{hive}/agents/{agent}/github-account` — 204 on success,
|
|
// 400/409/500 as `problem+json`, shown via `ApiErrorPanel` like
|
|
// `LinkForgeAccountForm`.
|
|
//
|
|
// One token per agent. A blind set: no route hands a token back.
|
|
// Whether one is stored shows on the agent panel (`LinkedAccounts`).
|
|
import { useState } from "preact/hooks";
|
|
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
|
|
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
|
|
import { Panel } from "../ui/panel/Panel.js";
|
|
import { TextField } from "../ui/text-field/TextField.js";
|
|
import { Button } from "../ui/button/Button.js";
|
|
import "./LinkMatrixAccountForm.css";
|
|
|
|
type SubmitState =
|
|
| { status: "idle" }
|
|
| { status: "submitting" }
|
|
| { status: "done" }
|
|
| { status: "error"; problem: ProblemDetails };
|
|
|
|
export function LinkGithubAccountForm({
|
|
hive,
|
|
agent,
|
|
onClose,
|
|
}: {
|
|
hive: string;
|
|
agent: string;
|
|
onClose?: () => void;
|
|
}) {
|
|
const [token, setToken] = useState("");
|
|
const [result, setResult] = useState<SubmitState>({ status: "idle" });
|
|
|
|
async function submit(e: Event) {
|
|
e.preventDefault();
|
|
setResult({ status: "submitting" });
|
|
try {
|
|
const r = await fetch(
|
|
`/api/hives/${encodeURIComponent(hive)}/agents/${encodeURIComponent(agent)}/github-account`,
|
|
{
|
|
method: "PUT",
|
|
headers: { "content-type": "application/json" },
|
|
body: JSON.stringify({ token }),
|
|
},
|
|
);
|
|
if (!r.ok) {
|
|
setResult({ status: "error", problem: await readApiError(r) });
|
|
return;
|
|
}
|
|
setResult({ status: "done" });
|
|
// The store holds the token; nothing here needs it.
|
|
setToken("");
|
|
} catch (err) {
|
|
setResult({ status: "error", problem: { detail: String(err) } });
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Panel
|
|
title={`link a github account — ${agent}`}
|
|
icon="🔗"
|
|
onClose={onClose}
|
|
>
|
|
<p>
|
|
Writes the token to the swarm secret store. The agent fetches it within
|
|
two minutes, and its <code>gh</code> and <code>git push</code> to
|
|
github.com then authenticate with it. Use a dedicated bot account and a
|
|
minimally scoped token, created at{" "}
|
|
<a
|
|
href="https://github.com/settings/tokens"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
github.com/settings/tokens
|
|
</a>
|
|
; GitHub notifications also need the <code>notifications</code> scope.
|
|
</p>
|
|
<form class="link-matrix-account-form" onSubmit={submit}>
|
|
<TextField
|
|
id="github-account-token"
|
|
label="personal access token"
|
|
type="password"
|
|
value={token}
|
|
required
|
|
onInput={setToken}
|
|
/>
|
|
<Button
|
|
variant="primary"
|
|
type="submit"
|
|
disabled={result.status === "submitting"}
|
|
>
|
|
{result.status === "submitting" ? "linking…" : "link account"}
|
|
</Button>
|
|
</form>
|
|
{result.status === "done" && (
|
|
<p class="link-matrix-account-result-ok">
|
|
stored a github token for <strong>{agent}</strong>
|
|
</p>
|
|
)}
|
|
{result.status === "error" && (
|
|
<ApiErrorPanel
|
|
context="failed to link the account"
|
|
problem={result.problem}
|
|
/>
|
|
)}
|
|
</Panel>
|
|
);
|
|
}
|