diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/group-page.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/group-page.js | 31 |
1 files changed, 28 insertions, 3 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js index 4fdf5f8..a25c07b 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js @@ -16,6 +16,7 @@ import { FilePreview } from './files-app.js'; import { VideoPlayer } from './video-player.js'; import { GroupSettingsPanel } from './group-settings.js'; import { reportIndexPush } from './index-dock.js'; +import { clearPending, nodePkFromLink, pendingFor } from './invite-link.js'; /** * The group shell: everything a group's "applications" (Chat, Files, and @@ -398,6 +399,17 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, // `transport` holds the attempt in progress, and keeps whichever one // answers — so it is null after the loop exactly when none did. let transport = null, ack = null, lastErr = null; + // An invitation link for this group names the node that holds its + // code: that node is tried first, and only it is handed the code — + // the transport refuses any other (docs/MESHBAY_DESIGN.md §3.4). A + // code typed into the form takes precedence and goes as it always has. + const link = session.pendingJoinCode ? null : pendingFor(groupId); + const joinCode = session.pendingJoinCode || (link ? link.c : null); + const joinNodePk = link ? nodePkFromLink(link.n) : undefined; + if (link) { + nodesData.nodes.sort((a, b) => + (b.pk_node === joinNodePk) - (a.pk_node === joinNodePk)); + } for (const n of nodesData.nodes) { // The same base the API calls use: signaling is a hub endpoint like // any other, and two sources for one address is how they drift. @@ -417,7 +429,7 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, try { ack = await transport.connect( n.node_id, live, groupId, null, sessionKeys, session.bundleKey, - username, userId, session.pendingJoinCode, session.recoveryKey); + username, userId, joinCode, session.recoveryKey, joinNodePk); break; } catch (e) { lastErr = e; @@ -432,12 +444,18 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, // A refusal that names a state of *this browser* — a code to enter, // a passphrase, a device to approve — is the same answer from every // node, and the operator to act on is this one's. Trying the next - // node would only replace it with a less useful message. - if (e.reason && e.reason !== 'not_hosted') throw e; + // node would only replace it with a less useful message. The one + // exception besides `not_hosted` is a link naming another host. + if (e.reason && e.reason !== 'not_hosted' && e.reason !== 'link_other_node') { + throw e; + } } } if (!transport) throw (lastErr || new Error('no node served this group')); session.pendingJoinCode = null; + // In: the invitation has done its job, whether its code was spent now or + // the node already knew us. + if (link) clearPending(); if (cancelled) return; applyAck(ack); transport.onAppsEnabled = (apps) => setEnabledApps(apps); @@ -617,6 +635,13 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs, // one-time code from the operator before it will hand over the group // key. Not an error to shout about — a step in joining. if (err.reason === 'code_required') setNeedsCode(true); + // The link's code was refused by the node that issued it — used, or + // cancelled. It will not work on another try; say so, and drop it. + if (err.reason === 'code_invalid' && pendingFor(groupId) + && !session.pendingJoinCode) { + clearPending(); + err.message = t('group.link_spent'); + } // The node has no bundle for us and this browser derived no key to make // one — the passphrase form below is the way in, not a support request. if (err.reason === 'no_keys') setNeedsPass(true); |