diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/app.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/app.js | 104 |
1 files changed, 58 insertions, 46 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index a5b74f0..0f101d2 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -890,6 +890,11 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth, const [nodeDirs, setNodeDirs] = useState([]); const [menuOpen, setMenuOpen] = useState(null); const [isNodeAdmin, setIsNodeAdmin] = useState(false); + // Paired ≠ operator account. `is_node_admin` says the hub account owning this + // node is the one connecting; this says the node pinned *this browser's* key + // as an operator key. Only the second one lets you sign an invite, and only + // the second one should make the pairing form go away. + const [operatorPaired, setOperatorPaired] = useState(false); const [needsCode, setNeedsCode] = useState(false); const [codeInput, setCodeInput] = useState(''); const [retryKey, setRetryKey] = useState(0); @@ -955,6 +960,7 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth, _pendingJoinCode = null; if (cancelled) return; setIsNodeAdmin(!!ack.is_node_admin); + setOperatorPaired(transport.memberRole === 'operator'); // A first join to this node generated an identity for it; leave it with // the node so any other browser can become the same person here with the @@ -1421,7 +1427,9 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth, ${tab === 'members' && html` <${MembersPanel} groupId=${groupId} group=${group} token=${token} transportRef=${transportRef} gekRef=${gekRef} - isNodeAdmin=${isNodeAdmin} userId=${userId} /> + isNodeAdmin=${isNodeAdmin} userId=${userId} + operatorPaired=${operatorPaired} + onPaired=${() => setOperatorPaired(true)} /> `} `} ${status === 'offline' && html` @@ -1576,7 +1584,7 @@ function _b64ToU8(b64) { // ── Members Panel ──────────────────────────────────────────────────────── function MembersPanel({ groupId, group, token, transportRef, gekRef, - isNodeAdmin, userId }) { + isNodeAdmin, userId, operatorPaired, onPaired }) { const [members, setMembers] = useState([]); const [adminId, setAdminId] = useState(''); const [loading, setLoading] = useState(true); @@ -1604,6 +1612,10 @@ function MembersPanel({ groupId, group, token, transportRef, gekRef, await transport.pairOperator(userId, code); setPairCode(''); setPairStatus('paired'); + // The node has pinned this key as an operator key; the form has nothing + // left to do. It used to stay put through a refresh, because what governed + // it was the account, which pairing does not change. + if (onPaired) onPaired(); } catch (err) { setPairStatus(err.message); } finally { @@ -1675,7 +1687,50 @@ function MembersPanel({ groupId, group, token, transportRef, gekRef, return html` <div class="members-panel"> - ${isNodeAdmin && html` + ${isAdmin && html` + <form class="invite-form" onSubmit=${doInvite}> + <h4>${t('members.invite_title')}</h4> + ${error && html`<p class="error-msg">${error}</p>`} + ${inviteCode && html` + <div class="success-msg" style="margin-bottom:8px"> + <p>${t('members.invite_code_ready', { user: inviteCode.username })}</p> + <p style="font-family:monospace;font-size:1.4em;letter-spacing:2px;margin:6px 0"> + ${inviteCode.code} + </p> + <p>${t('members.invite_code_hint')}</p> + </div> + `} + <div style="display:flex;gap:8px"> + <input type="text" placeholder="${t('members.username_placeholder')}" + value=${inviteUser} onInput=${e => setInviteUser(e.target.value)} required /> + <button class="admin-btn" type="submit" disabled=${inviting}> + ${inviting ? '...' : t('members.invite_btn')} + </button> + </div> + </form> + `} + <table class="admin-table"> + <thead> + <tr> + <th>${t('admin.col_username')}</th> + <th>${t('members.group_role')}</th> + </tr> + </thead> + <tbody> + ${members.map(m => html` + <tr key=${m.user_id}> + <td>${m.username}</td> + <td> + ${m.user_id === adminId + ? html`<span class="badge" style="background:var(--accent);color:var(--accent-text)">${t('members.owner')}</span>` + : html`<span class="badge">${t('members.member')}</span>` + } + </td> + </tr> + `)} + </tbody> + </table> + ${isNodeAdmin && !operatorPaired && html` <form class="invite-form" onSubmit=${doPair}> <h4>${t('members.pair_title')}</h4> <p class="settings-hint">${t('members.pair_hint')}</p> @@ -2571,49 +2626,6 @@ function AdminPage({ token }) { value=${userSearch} onInput=${e => { setUserSearch(e.target.value); loadUsers(e.target.value); }} /> <span class="settings-value">${usersTotal} total</span> </div> - ${isAdmin && html` - <form class="invite-form" onSubmit=${doInvite}> - <h4>${t('members.invite_title')}</h4> - ${error && html`<p class="error-msg">${error}</p>`} - ${inviteCode && html` - <div class="success-msg" style="margin-bottom:8px"> - <p>${t('members.invite_code_ready', { user: inviteCode.username })}</p> - <p style="font-family:monospace;font-size:1.4em;letter-spacing:2px;margin:6px 0"> - ${inviteCode.code} - </p> - <p>${t('members.invite_code_hint')}</p> - </div> - `} - <div style="display:flex;gap:8px"> - <input type="text" placeholder="${t('members.username_placeholder')}" - value=${inviteUser} onInput=${e => setInviteUser(e.target.value)} required /> - <button class="admin-btn" type="submit" disabled=${inviting}> - ${inviting ? '...' : t('members.invite_btn')} - </button> - </div> - </form> - `} - <table class="admin-table"> - <thead> - <tr> - <th>${t('admin.col_username')}</th> - <th>${t('members.group_role')}</th> - </tr> - </thead> - <tbody> - ${members.map(m => html` - <tr key=${m.user_id}> - <td>${m.username}</td> - <td> - ${m.user_id === adminId - ? html`<span class="badge" style="background:var(--accent);color:var(--accent-text)">${t('members.owner')}</span>` - : html`<span class="badge">${t('members.member')}</span>` - } - </td> - </tr> - `)} - </tbody> - </table> <table class="admin-table"> <thead><tr> <th>${t('admin.col_username')}</th> |