diff options
Diffstat (limited to 'packages/meshbay-hub/src')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/app.js | 46 | ||||
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/transport.js | 7 |
2 files changed, 43 insertions, 10 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index 1ef878a..6fefe0f 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -765,7 +765,7 @@ async function pipelinedDownload(transport, gekKey, fileId, totalChunks, onChunk return results; } -function GroupPage({ groupId, group, token, username, userId }) { +function GroupPage({ groupId, group, token, username, userId, onRefreshAuth }) { const [status, setStatus] = useState('idle'); const [entries, setEntries] = useState([]); const [cached, setCached] = useState(false); @@ -786,6 +786,9 @@ function GroupPage({ groupId, group, token, username, userId }) { const [retryKey, setRetryKey] = useState(0); const transportRef = useRef(null); const gekRef = useRef(null); + // One refresh per mount: if a fresh token still says we are not a member, we + // really are not, and retrying forever would hide that. + const refreshedRef = useRef(false); const submitJoinCode = useCallback((e) => { e.preventDefault(); @@ -902,14 +905,24 @@ function GroupPage({ groupId, group, token, username, userId }) { cacheGroupIndex(groupId, group ? group.name : groupId, freshEntries); } catch (err) { - if (!cancelled) { - // The node has never seen this browser for this account: it needs a - // 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); - setError(err.message); - setStatus('error'); + if (cancelled) return; + + // Our token predates being added to this group. Refresh once and retry + // rather than telling someone who was just invited that they are not a + // member — which is what the node honestly sees, and is useless to them. + if (err.reason === 'not_a_member' && !refreshedRef.current && onRefreshAuth) { + refreshedRef.current = true; + try { + if (await onRefreshAuth()) return; // new token → effect re-runs + } catch { /* fall through to the message below */ } } + + // The node has never seen this browser for this account: it needs a + // 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); + setError(err.message); + setStatus('error'); } }; @@ -2653,6 +2666,20 @@ function App() { }, }; + // Group membership is baked into the access token at login and the hub does not + // push updates, so someone invited after they signed in carries a token that + // says they are in nothing. Refreshing re-reads membership from the database. + const refreshAuth = useCallback(async () => { + if (!user || !user.refreshToken) return null; + const data = await hubFetch('/v1/users/token/refresh', { + method: 'POST', body: { refresh_token: user.refreshToken }, + }); + const u = { ...user, token: data.access_token }; + setUser(u); + saveAuth(u); + return data.access_token; + }, [user]); + let page; if (route === '/login' || route === '/register') { page = route === '/register' @@ -2677,7 +2704,8 @@ function App() { const group = groups.find(g => g.id === groupId); page = html`<${GroupPage} groupId=${groupId} group=${group} token=${user.token} - username=${user.username} userId=${user.userId} />`; + username=${user.username} userId=${user.userId} + onRefreshAuth=${refreshAuth} />`; } else if (route === '/admin') { page = (user.role === 'moderator' || user.role === 'admin') ? html`<${AdminPage} token=${user.token} />` diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js index 271d11f..c200674 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js @@ -314,8 +314,13 @@ class MeshBayTransport { // A node that answers a handshake with anything other than a challenge is not // running the mutual protocol. Accepting a bare handshake_ack here would let a // peer skip proving GEK possession entirely (C3/C6). - throw new Error( + const rejected = new Error( 'MNP handshake rejected: ' + (reply.detail || `unexpected ${reply.type}`)); + // `not_a_member` usually means our token predates being added to the group; + // the caller refreshes it and tries again rather than showing that to someone + // who was invited thirty seconds ago. + rejected.reason = reply.code || ''; + throw rejected; } /** |