diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-10 09:57:17 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-10 09:57:29 +0200 |
| commit | 876f4a351f859201ca68fbf8d585c86905571686 (patch) | |
| tree | 7bf029b884ae427352e6c2e7af0cbba887dea824 /packages/meshbay-hub/src/meshbay_hub/static/node-page.js | |
| parent | 42b562fcf312ddceb78438b5daea49d4c9b465c8 (diff) | |
| download | meshbay-876f4a351f859201ca68fbf8d585c86905571686.tar.gz | |
fix(client): say when the account's node is elsewhere, and let go of it
Signing in on a desktop links this machine's node to the account, but never
over a key already linked (that would cut off the user's other machine) nor a
node set up for another account. On a real install both left the node reading
"Running" while the hub refused it in a loop, and nothing said why. And the
only way to unlink was the linked machine's own Node page, of no use once
that machine is gone.
- The Node page works out, from the node and the hub each time it looks,
whether this node can serve the signed-in account (nodeLinkProblem), and
says why not: "Link this node instead" (asked first) puts this node's key
on the account; "Use this node for my account" switches a node set up for
another account through node:start, which takeOver now lets past a node
that answers "running". The first version went through node:start for
both, and clicking it on a real install did nothing: a node signed in
before the account was linked elsewhere answers "running".
- The Profile page unlinks the account's node (DELETE /v1/users/me/node_key),
from any machine.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/node-page.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/node-page.js | 78 |
1 files changed, 77 insertions, 1 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/node-page.js b/packages/meshbay-hub/src/meshbay_hub/static/node-page.js index 11cd07e..56166c6 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/node-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/node-page.js @@ -6,7 +6,7 @@ import { t } from './i18n.js'; import { ask } from './ask.js'; import * as platform from './platform.js'; import { Icon } from './icon.js'; -import { HUB, session } from './hub-client.js'; +import { HUB, session, hubFetch } from './hub-client.js'; // ── Node management (D5) ──────────────────────────────────────────────────── // @@ -22,6 +22,80 @@ export function pairedFrom(result) { return v === true || v === false ? v : null; } +// Why this machine's node cannot serve the signed-in account, if it cannot: +// 'other_account' when it is set up for another account or hub, 'other_node' +// when the account is linked to another node's key. Signing in links this +// node (main.js ensureNode) but never over either, since that would cut off +// the user's other machine or someone else's node; both left a node reading +// "Running" that the hub refused in a loop, with nothing here saying why +// (found on a real install, 2026-10-10). Worked out from the node and the hub +// each time the page looks, not kept from sign-in: a link made or removed on +// the Profile page or on the other machine changes the answer. +export function nodeLinkProblem(info, linkedKey, username, hub) { + if (!info || !username) return null; + const sameHub = (a, b) => String(a || '').replace(/\/+$/, '') === String(b || '').replace(/\/+$/, ''); + if ((info.username && info.username !== username) + || (info.hub_url && hub && !sameHub(info.hub_url, hub))) return 'other_account'; + if (linkedKey && info.pk_node_ed25519 && linkedKey !== info.pk_node_ed25519) return 'other_node'; + return null; +} + +function NodeLinkBanner({ info, token, username, onLinked }) { + const [linkedKey, setLinkedKey] = useState(null); + const [busy, setBusy] = useState(false); + const [err, setErr] = useState(''); + const nodeKey = info && info.pk_node_ed25519; + + useEffect(() => { + if (!username || !token) return; + let cancelled = false; + hubFetch(`/v1/users/${encodeURIComponent(username)}/pubkeys`, { token }) + .then((k) => { if (!cancelled) setLinkedKey((k && k.pk_node_ed25519) || null); }) + .catch(() => {}); + return () => { cancelled = true; }; + }, [username, token, nodeKey]); + + const problem = nodeLinkProblem(info, linkedKey, username, HUB); + if (!problem) return null; + + // Another node's key: this node's replaces it on the account, and a node + // waiting for its account signs in on its next attempt. Not node:start, + // which leaves a node that answers "running" alone, and one can (signed in + // before the account was linked elsewhere). Another account: node:start + // switches it, after asking, restarts it and links it. + const takeOver = async () => { + if (problem === 'other_node' && !await ask(t('node.link_here_confirm'))) return; + setBusy(true); + setErr(''); + try { + if (problem === 'other_node') { + await hubFetch('/v1/users/me/node_key', { + method: 'PUT', token, body: { pk_node_ed25519: nodeKey }, + }); + } else { + await platform.node.start({ hubUrl: HUB, username, token, takeOver: true }); + } + setLinkedKey(nodeKey); + if (onLinked) onLinked(); + } catch (e) { + setErr(platform.bridgeMessage(e)); + } finally { + setBusy(false); + } + }; + + return html` + <div class="node-pair-banner node-link-banner"> + <p>${problem === 'other_node' + ? t('node.link_other_node') + : t('node.link_other_account', { account: `${info.username} @ ${info.hub_url}` })}</p> + <button class="btn btn-primary btn-small" disabled=${busy} onClick=${takeOver}> + ${busy ? t('node.link_linking') + : problem === 'other_node' ? t('node.link_here') : t('node.link_take_over')}</button> + ${err && html`<p class="error-msg">${err}</p>`} + </div>`; +} + function NodeServicePanel({ onChanged, token, username }) { const [info, setInfo] = useState(null); const [busy, setBusy] = useState(''); @@ -725,6 +799,8 @@ export function NodePage({ groups, token, username }) { ${t('node.reload')}</button> </div> <${NodeServicePanel} onChanged=${fetchStatus} token=${token} username=${username} /> + <${NodeLinkBanner} info=${nodeInfo} token=${token} username=${username} + onLinked=${fetchStatus} /> ${actionMsg && html`<div class="node-message">${actionMsg}</div>`} ${operatorPaired === false && nodeGroups.length > 0 && html` <div class="node-pair-banner"> |