From d3ad243c4ae3a273f623bd5fc631e3266aa4d0e4 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Wed, 30 Sep 2026 11:49:56 +0200 Subject: fix: only the owner decides who hosts a group, and nobody is made a member unasked MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - hub: a node may host a group only if its account owns it or the owner approved that node (new `group_hosts`). Membership was the ceiling, and every member holds the group key, so any member's node could register as a host and be the one clients kept. A node claiming a group it may not host is recorded as a request; the owner is notified once and approves or refuses it (GET/POST/DELETE /v1/groups/{id}/hosts[/{node_id}]), which takes effect on a connected node at once. - hub: an owner adding a username creates an invitation (new `group_invitations`), accepted or declined by the invitee (/v1/groups/invitations, /{id}/invitation/accept|decline). Until then the group is not listed, not dialled, not searched and not in any token. Invitation links, open joins and group creation still make members directly: they are the account's own act. - hub: the MNP token names only the group it is minted for (group_id is now required), so a node operator no longer learns a member's other groups. - SPA: invitations on the home page; invited people and host requests in the group's settings; the transport sends group_id. Ten catalogues. - Browser probes for both screens, run in Chrome and Firefox. - Design §5.2, §7.2, §7.3, AV32, AV33; protocol §6.3; user guide. Co-Authored-By: Claude Opus 5.5 --- packages/meshbay-hub/src/meshbay_hub/static/app.js | 74 +++++++++++++++++++- .../src/meshbay_hub/static/group-settings.js | 78 ++++++++++++++++++++++ .../src/meshbay_hub/static/locales/de.js | 18 +++++ .../src/meshbay_hub/static/locales/en.js | 18 +++++ .../src/meshbay_hub/static/locales/es.js | 18 +++++ .../src/meshbay_hub/static/locales/fr.js | 18 +++++ .../src/meshbay_hub/static/locales/it.js | 18 +++++ .../src/meshbay_hub/static/locales/ja.js | 18 +++++ .../src/meshbay_hub/static/locales/nl.js | 18 +++++ .../src/meshbay_hub/static/locales/pl.js | 18 +++++ .../src/meshbay_hub/static/locales/pt-BR.js | 18 +++++ .../src/meshbay_hub/static/locales/zh-CN.js | 18 +++++ .../src/meshbay_hub/static/transport.js | 5 +- 13 files changed, 335 insertions(+), 2 deletions(-) (limited to 'packages/meshbay-hub/src/meshbay_hub/static') diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index 875b1aa..3a85bf7 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -528,7 +528,66 @@ function NotificationFeed({ notifications, onMarkRead, onPurge }) { `; } -function HomePage({ groups, notifications, onMarkRead, onPurge, allowPublicGroups = true }) { +/** + * Groups somebody added this account to, waiting for an answer. + * + * Being added is an invitation, not a membership (the hub's `group_invitations`): + * until it is accepted the group is not in the sidebar, not dialled, not in + * Search and not named in any token this browser hands a node. Accepting is + * what makes it a group of ours; declining forgets it. + */ +function PendingInvitations({ token, onAccepted }) { + const [items, setItems] = useState([]); + const [busy, setBusy] = useState(''); + const [error, setError] = useState(''); + const load = useCallback(() => { + if (!token) return; + hubFetch('/v1/groups/invitations', { token }) + .then(data => setItems(data.invitations || [])) + .catch(() => setItems([])); + }, [token]); + useEffect(() => { load(); }, [load]); + + const answer = async (inv, verb) => { + setBusy(inv.group_id); setError(''); + try { + await hubFetch(`/v1/groups/${inv.group_id}/invitation/${verb}`, + { method: 'POST', token }); + setItems(prev => prev.filter(i => i.group_id !== inv.group_id)); + if (verb === 'accept' && onAccepted) onAccepted(inv.group_id); + } catch (err) { + setError(err.message); + } finally { + setBusy(''); + } + }; + + if (!items.length) return null; + return html` +
+

${t('home.invitations')}

+

${t('home.invitation_hint')}

+ ${error && html`

${error}

`} + +
+ `; +} + +function HomePage({ groups, notifications, onMarkRead, onPurge, allowPublicGroups = true, + token, onGroupsChanged }) { const [setupDismissed, setSetupDismissed] = useState(false); if (groups.length === 0) { @@ -539,6 +598,7 @@ function HomePage({ groups, notifications, onMarkRead, onPurge, allowPublicGroup return html`

${t('home.welcome')}

+ <${PendingInvitations} token=${token} onAccepted=${onGroupsChanged} /> <${NotificationFeed} notifications=${notifications} onMarkRead=${onMarkRead} onPurge=${onPurge} /> <${JoinByLink} hubOrigin=${platform.hubOrigin()} /> @@ -555,6 +615,7 @@ function HomePage({ groups, notifications, onMarkRead, onPurge, allowPublicGroup return html`

${t('home.my_groups')}

+ <${PendingInvitations} token=${token} onAccepted=${onGroupsChanged} /> <${NotificationFeed} notifications=${notifications} onMarkRead=${onMarkRead} onPurge=${onPurge} />
@@ -958,6 +1019,15 @@ function App() { fetchNotifications(); }, [user]); + // After an invitation is accepted: the group is ours now, and only the hub + // knows its row the way `/mine` answers it. + const reloadGroups = useCallback(() => { + if (!user) return; + hubFetch('/v1/groups/mine', { token: user.token }) + .then(data => setGroups(data.groups || [])) + .catch(() => {}); + }, [user]); + // The group list lives here, so an edit made three components down has to come // back up rather than be re-fetched: a reload would drop the WebRTC connection // the page is holding. @@ -1216,6 +1286,7 @@ function App() { ? html`<${LazyAdminPage} token=${user.token} role=${user.role} />` : html`<${HomePage} groups=${groups} notifications=${notifications} allowPublicGroups=${allowPublicGroups} + token=${user.token} onGroupsChanged=${reloadGroups} onMarkRead=${markRead} onPurge=${purgeNotifications} />`; } else if (route === '/settings') { page = html`<${SettingsPage} user=${user} theme=${theme} @@ -1233,6 +1304,7 @@ function App() { } else { page = html`<${HomePage} groups=${groups} notifications=${notifications} allowPublicGroups=${allowPublicGroups} + token=${user.token} onGroupsChanged=${reloadGroups} onMarkRead=${markRead} onPurge=${purgeNotifications} />`; } diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js index fba8a0e..048f831 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js @@ -410,7 +410,14 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, onAppDirectories, onRefreshIndex, onPaired, onLeft }) { const [members, setMembers] = useState([]); + // Asked and not answered — the owner is the only one told (the hub leaves + // the field out for anyone else). + const [invited, setInvited] = useState([]); const [adminId, setAdminId] = useState(''); + // Nodes other than the owner's that asked to serve this group, and what the + // owner answered. Only the owner reads it. + const [hosts, setHosts] = useState([]); + const [hostBusy, setHostBusy] = useState(''); const [loading, setLoading] = useState(true); const [inviteUser, setInviteUser] = useState(''); const [inviting, setInviting] = useState(false); @@ -802,6 +809,7 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, hubFetch(`/v1/groups/${groupId}/members`, { token }) .then(data => { setMembers(data.members || []); + setInvited(data.invited || []); setAdminId(data.admin_id || ''); }) .catch(() => {}) @@ -810,6 +818,30 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, useEffect(() => { loadMembers(); }, [loadMembers]); + const ownsGroup = Boolean(group && group.is_admin); + const loadHosts = useCallback(() => { + if (!ownsGroup) return; + hubFetch(`/v1/groups/${groupId}/hosts`, { token }) + .then(data => setHosts(data.hosts || [])) + .catch(() => setHosts([])); + }, [groupId, token, ownsGroup]); + + useEffect(() => { loadHosts(); }, [loadHosts]); + + const decideHost = useCallback(async (host, approve) => { + setHostBusy(host.node_id); + setError(''); + try { + await hubFetch(`/v1/groups/${groupId}/hosts/${host.node_id}`, + { method: approve ? 'POST' : 'DELETE', token }); + loadHosts(); + } catch (err) { + setError(err.message); + } finally { + setHostBusy(''); + } + }, [groupId, token, loadHosts]); + useEffect(() => { hubFetch('/v1/users/me/preferences', { token }) .then(prefs => setInviteByEmail(prefs[INVITE_EMAIL_PREF] === 'true')) @@ -1382,12 +1414,58 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, `)} + ${invited.map(m => html` + + ${m.username} + ${t('members.invited')} + + ${isAdmin && html` + + `} + + + `)} ${isAdmin && members.length > 1 && html`

${t('members.remove_hint')}

`} + + ${ownsGroup && html` + <${CollapsibleSection} title=${t('hosts.title')}> +

${t('hosts.hint')}

+ ${hosts.length === 0 && html`

${t('hosts.none')}

`} + ${hosts.length > 0 && html` + + + ${hosts.map(h => html` + + + + + + `)} + +
${t('hosts.from', { name: h.username })} +
${h.pk_node}
${t(`hosts.status_${h.status}`)} + ${h.online && html` ${t('hosts.online')}`} + ${h.status !== 'approved' && html` + `} + ${h.status !== 'refused' && html` + `} +
+ `} + + `}
`; } diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js index a83f44b..3c0f1ec 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js @@ -1222,4 +1222,22 @@ export default { // Names made writable everywhere when saved (portable-name.js) 'transfers.renamed': "Umbenannt: „{name}“ ist nicht auf jedem System ein gültiger Name", 'transfers.renamed_n': "Namen geändert, damit sie auf jedem System gültig sind: {n}", + + // Invitations waiting for an answer, and the nodes a group owner approves + 'home.invitations': "Einladungen", + 'home.invitation_hint': "Jemand hat Sie zu diesen Gruppen hinzugefügt. Nehmen Sie nur die an, die Sie erwarten: Eine Gruppe, der Sie beitreten, sieht Ihre Adresse, wenn Sie sie öffnen.", + 'home.invited_by': "eingeladen von {name}", + 'home.accept': "Annehmen", + 'home.decline': "Ablehnen", + 'members.invited': "eingeladen", + 'hosts.title': "Hosts", + 'hosts.hint': "Ihre eigenen Nodes stellen diese Gruppe ohne Rückfrage bereit. Ein anderer Node tut es erst, wenn Sie ihn hier genehmigen; ein Node, der anfragt, steht unten.", + 'hosts.none': "Kein anderer Node hat angefragt, diese Gruppe bereitzustellen.", + 'hosts.from': "Ein Node von {name}", + 'hosts.status_pending': "wartet auf Ihre Antwort", + 'hosts.status_approved': "genehmigt", + 'hosts.status_refused': "abgelehnt", + 'hosts.approve': "Genehmigen", + 'hosts.refuse': "Ablehnen", + 'hosts.online': "online", }; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js index 46c5094..947c61d 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js @@ -1203,4 +1203,22 @@ export default { // Names made writable everywhere when saved (portable-name.js) 'transfers.renamed': "Renamed: “{name}” is not a valid name on every system", 'transfers.renamed_n': "Names changed to be valid on every system: {n}", + + // Invitations waiting for an answer, and the nodes a group owner approves + 'home.invitations': "Invitations", + 'home.invitation_hint': "Somebody added you to these groups. Accept only the ones you expect: a group you join can see your address when you open it.", + 'home.invited_by': "invited by {name}", + 'home.accept': "Accept", + 'home.decline': "Decline", + 'members.invited': "invited", + 'hosts.title': "Hosts", + 'hosts.hint': "Your own nodes serve this group without asking. Another node serves it only once you approve it here; a node that asks is listed below.", + 'hosts.none': "No other node has asked to host this group.", + 'hosts.from': "A node of {name}", + 'hosts.status_pending': "waiting for your answer", + 'hosts.status_approved': "approved", + 'hosts.status_refused': "refused", + 'hosts.approve': "Approve", + 'hosts.refuse': "Refuse", + 'hosts.online': "online", }; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js index 0d0e865..a0220d8 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js @@ -1216,4 +1216,22 @@ export default { // Names made writable everywhere when saved (portable-name.js) 'transfers.renamed': "Renombrado: «{name}» no es un nombre válido en todos los sistemas", 'transfers.renamed_n': "Nombres cambiados para ser válidos en todos los sistemas: {n}", + + // Invitations waiting for an answer, and the nodes a group owner approves + 'home.invitations': "Invitaciones", + 'home.invitation_hint': "Alguien le ha añadido a estos grupos. Acepte solo los que espera: un grupo al que se une ve su dirección cuando lo abre.", + 'home.invited_by': "invitado por {name}", + 'home.accept': "Aceptar", + 'home.decline': "Rechazar", + 'members.invited': "invitado", + 'hosts.title': "Anfitriones", + 'hosts.hint': "Sus propios nodes sirven este grupo sin preguntar. Otro node lo sirve solo cuando usted lo aprueba aquí; un node que lo pide aparece abajo.", + 'hosts.none': "Ningún otro node ha pedido alojar este grupo.", + 'hosts.from': "Un node de {name}", + 'hosts.status_pending': "esperando su respuesta", + 'hosts.status_approved': "aprobado", + 'hosts.status_refused': "rechazado", + 'hosts.approve': "Aprobar", + 'hosts.refuse': "Rechazar", + 'hosts.online': "en línea", }; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js index 3c86cd7..c4bc37e 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js @@ -1231,4 +1231,22 @@ export default { // Names made writable everywhere when saved (portable-name.js) 'transfers.renamed': "Renommé : « {name} » n’est pas un nom valide sur tous les systèmes", 'transfers.renamed_n': "Noms modifiés pour être valides sur tous les systèmes : {n}", + + // Invitations waiting for an answer, and the nodes a group owner approves + 'home.invitations': "Invitations", + 'home.invitation_hint': "Quelqu'un vous a ajouté à ces groupes. N'acceptez que ceux que vous attendez : un groupe que vous rejoignez voit votre adresse quand vous l'ouvrez.", + 'home.invited_by': "invité par {name}", + 'home.accept': "Accepter", + 'home.decline': "Refuser", + 'members.invited': "invité", + 'hosts.title': "Hôtes", + 'hosts.hint': "Vos propres nodes servent ce groupe sans rien demander. Un autre node ne le sert qu'une fois approuvé ici ; un node qui le demande apparaît ci-dessous.", + 'hosts.none': "Aucun autre node n'a demandé à héberger ce groupe.", + 'hosts.from': "Un node de {name}", + 'hosts.status_pending': "en attente de votre réponse", + 'hosts.status_approved': "approuvé", + 'hosts.status_refused': "refusé", + 'hosts.approve': "Approuver", + 'hosts.refuse': "Refuser", + 'hosts.online': "en ligne", }; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js index 3a74b4d..59505b8 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js @@ -1230,4 +1230,22 @@ export default { // Names made writable everywhere when saved (portable-name.js) 'transfers.renamed': "Rinominato: «{name}» non è un nome valido su tutti i sistemi", 'transfers.renamed_n': "Nomi modificati per essere validi su tutti i sistemi: {n}", + + // Invitations waiting for an answer, and the nodes a group owner approves + 'home.invitations': "Inviti", + 'home.invitation_hint': "Qualcuno ti ha aggiunto a questi gruppi. Accetta solo quelli che ti aspetti: un gruppo a cui ti unisci vede il tuo indirizzo quando lo apri.", + 'home.invited_by': "invitato da {name}", + 'home.accept': "Accetta", + 'home.decline': "Rifiuta", + 'members.invited': "invitato", + 'hosts.title': "Host", + 'hosts.hint': "I tuoi node servono questo gruppo senza chiedere. Un altro node lo serve solo dopo che lo approvi qui; un node che lo chiede compare qui sotto.", + 'hosts.none': "Nessun altro node ha chiesto di ospitare questo gruppo.", + 'hosts.from': "Un node di {name}", + 'hosts.status_pending': "in attesa della tua risposta", + 'hosts.status_approved': "approvato", + 'hosts.status_refused': "rifiutato", + 'hosts.approve': "Approva", + 'hosts.refuse': "Rifiuta", + 'hosts.online': "online", }; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js index e0c38de..bba9564 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js @@ -1214,4 +1214,22 @@ export default { // Names made writable everywhere when saved (portable-name.js) 'transfers.renamed': "名前を変更しました:「{name}」はすべてのシステムで有効な名前ではありません", 'transfers.renamed_n': "すべてのシステムで有効になるよう変更した名前:{n}", + + // Invitations waiting for an answer, and the nodes a group owner approves + 'home.invitations': "招待", + 'home.invitation_hint': "誰かがあなたをこれらのグループに追加しました。心当たりのあるものだけを承諾してください。参加したグループは、開いたときにあなたのアドレスを知ることができます。", + 'home.invited_by': "{name} からの招待", + 'home.accept': "承諾", + 'home.decline': "辞退", + 'members.invited': "招待中", + 'hosts.title': "ホスト", + 'hosts.hint': "あなた自身の node は確認なしでこのグループを提供します。他の node は、ここで承認した後にのみ提供します。申請した node は下に表示されます。", + 'hosts.none': "このグループのホストを申請した node は他にありません。", + 'hosts.from': "{name} の node", + 'hosts.status_pending': "あなたの返答待ち", + 'hosts.status_approved': "承認済み", + 'hosts.status_refused': "拒否済み", + 'hosts.approve': "承認", + 'hosts.refuse': "拒否", + 'hosts.online': "オンライン", }; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js index 83b9eed..87e5b87 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js @@ -1232,4 +1232,22 @@ export default { // Names made writable everywhere when saved (portable-name.js) 'transfers.renamed': "Hernoemd: ‘{name}’ is niet op elk systeem een geldige naam", 'transfers.renamed_n': "Namen aangepast zodat ze op elk systeem geldig zijn: {n}", + + // Invitations waiting for an answer, and the nodes a group owner approves + 'home.invitations': "Uitnodigingen", + 'home.invitation_hint': "Iemand heeft u aan deze groepen toegevoegd. Accepteer alleen de groepen die u verwacht: een groep waar u lid van wordt, ziet uw adres wanneer u hem opent.", + 'home.invited_by': "uitgenodigd door {name}", + 'home.accept': "Accepteren", + 'home.decline': "Weigeren", + 'members.invited': "uitgenodigd", + 'hosts.title': "Hosts", + 'hosts.hint': "Uw eigen nodes bedienen deze groep zonder te vragen. Een andere node doet dat pas nadat u hem hier goedkeurt; een node die erom vraagt, staat hieronder.", + 'hosts.none': "Geen andere node heeft gevraagd deze groep te hosten.", + 'hosts.from': "Een node van {name}", + 'hosts.status_pending': "wacht op uw antwoord", + 'hosts.status_approved': "goedgekeurd", + 'hosts.status_refused': "geweigerd", + 'hosts.approve': "Goedkeuren", + 'hosts.refuse': "Weigeren", + 'hosts.online': "online", }; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js index 3edba2d..6d81b25 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js @@ -1258,4 +1258,22 @@ export default { // Names made writable everywhere when saved (portable-name.js) 'transfers.renamed': "Zmieniono nazwę: „{name}” nie jest prawidłową nazwą w każdym systemie", 'transfers.renamed_n': "Nazwy zmienione, by były prawidłowe w każdym systemie: {n}", + + // Invitations waiting for an answer, and the nodes a group owner approves + 'home.invitations': "Zaproszenia", + 'home.invitation_hint': "Ktoś dodał Cię do tych grup. Akceptuj tylko te, których się spodziewasz: grupa, do której dołączysz, widzi Twój adres, gdy ją otwierasz.", + 'home.invited_by': "zaprasza {name}", + 'home.accept': "Akceptuj", + 'home.decline': "Odrzuć", + 'members.invited': "zaproszony", + 'hosts.title': "Hosty", + 'hosts.hint': "Twoje własne node'y obsługują tę grupę bez pytania. Inny node robi to dopiero po Twojej akceptacji tutaj; node, który o to prosi, jest widoczny poniżej.", + 'hosts.none': "Żaden inny node nie prosił o hostowanie tej grupy.", + 'hosts.from': "Node użytkownika {name}", + 'hosts.status_pending': "czeka na Twoją odpowiedź", + 'hosts.status_approved': "zaakceptowany", + 'hosts.status_refused': "odrzucony", + 'hosts.approve': "Akceptuj", + 'hosts.refuse': "Odrzuć", + 'hosts.online': "online", }; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js index 3f44570..7b12ea5 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js @@ -1217,4 +1217,22 @@ export default { // Names made writable everywhere when saved (portable-name.js) 'transfers.renamed': "Renomeado: “{name}” não é um nome válido em todos os sistemas", 'transfers.renamed_n': "Nomes alterados para serem válidos em todos os sistemas: {n}", + + // Invitations waiting for an answer, and the nodes a group owner approves + 'home.invitations': "Convites", + 'home.invitation_hint': "Alguém adicionou você a estes grupos. Aceite apenas os que espera: um grupo em que você entra vê o seu endereço quando você o abre.", + 'home.invited_by': "convidado por {name}", + 'home.accept': "Aceitar", + 'home.decline': "Recusar", + 'members.invited': "convidado", + 'hosts.title': "Hosts", + 'hosts.hint': "Seus próprios nodes servem este grupo sem perguntar. Outro node só o serve depois que você o aprova aqui; um node que pede aparece abaixo.", + 'hosts.none': "Nenhum outro node pediu para hospedar este grupo.", + 'hosts.from': "Um node de {name}", + 'hosts.status_pending': "aguardando sua resposta", + 'hosts.status_approved': "aprovado", + 'hosts.status_refused': "recusado", + 'hosts.approve': "Aprovar", + 'hosts.refuse': "Recusar", + 'hosts.online': "online", }; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js index 1168460..9f3c902 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js @@ -1203,4 +1203,22 @@ export default { // Names made writable everywhere when saved (portable-name.js) 'transfers.renamed': "已重命名:“{name}”并非在所有系统上都是有效的名称", 'transfers.renamed_n': "为在所有系统上有效而修改的名称:{n}", + + // Invitations waiting for an answer, and the nodes a group owner approves + 'home.invitations': "邀请", + 'home.invitation_hint': "有人把你加入了这些群组。只接受你预期的邀请:你加入的群组在你打开它时可以看到你的地址。", + 'home.invited_by': "由 {name} 邀请", + 'home.accept': "接受", + 'home.decline': "拒绝", + 'members.invited': "已邀请", + 'hosts.title': "主机", + 'hosts.hint': "你自己的 node 无需询问即可提供此群组。其他 node 只有在你于此处批准后才会提供;提出申请的 node 列在下方。", + 'hosts.none': "没有其他 node 申请托管此群组。", + 'hosts.from': "{name} 的 node", + 'hosts.status_pending': "等待你的答复", + 'hosts.status_approved': "已批准", + 'hosts.status_refused': "已拒绝", + 'hosts.approve': "批准", + 'hosts.refuse': "拒绝", + 'hosts.online': "在线", }; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js index 98d010e..04c2d23 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js @@ -653,7 +653,10 @@ class MeshBayTransport { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this._accessToken}`, }, - body: JSON.stringify({ node_pk: this._nodePkTarget || '' }), + // The token names this connection's group and no other: it is handed to + // the node's operator, who has no business learning every group this + // account belongs to. + body: JSON.stringify({ node_pk: this._nodePkTarget || '', group_id: this._groupId }), }); if (!r.ok) throw new Error(`Could not obtain a node token: ${r.status}`); return (await r.json()).mnp_token; -- cgit v1.2.3