From 45ee249e07dc8bf88461d2ac0610b1a1c518115f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Iv=C3=A1n=20Vieitez=20Parra?= <3857362+corrideat@users.noreply.github.com> Date: Wed, 25 Oct 2023 19:09:04 +0200 Subject: [PATCH 01/29] Login sync fixes --- frontend/controller/actions/group.js | 14 +- frontend/main.js | 3 +- frontend/model/contracts/group.js | 210 +++++++++++++----------- frontend/model/contracts/manifests.json | 2 +- shared/domains/chelonia/utils.js | 4 +- 5 files changed, 128 insertions(+), 105 deletions(-) diff --git a/frontend/controller/actions/group.js b/frontend/controller/actions/group.js index 145500d4c6..f339b5edea 100644 --- a/frontend/controller/actions/group.js +++ b/frontend/controller/actions/group.js @@ -322,12 +322,13 @@ export default (sbp('sbp/selectors/register', { // through an invite link, and we must send a key request to complete // the joining process. const sendKeyRequest = (!hasSecretKeys && params.originatingContractID) + const isWaitingForKeyShare = sbp('chelonia/contract/isWaitingForKeyShare', state) // If we are expecting to receive keys, set up an event listener // We are expecting to receive keys if: // (a) we are about to send a key request; or // (b) we have already sent a key request (!!pendingKeyRequests?.length) - if (sendKeyRequest || sbp('chelonia/contract/isWaitingForKeyShare', state)) { + if (sendKeyRequest || isWaitingForKeyShare) { console.log('@@@@@@@@ AT join[sendKeyRequest] for ' + params.contractID) // Event handler for continuing the join process if the keys are @@ -377,7 +378,7 @@ export default (sbp('sbp/selectors/register', { // current group. // This block must be run after having received the group's secret keys // (i.e., the CSK and the CEK) that were requested earlier. - } else if (hasSecretKeys && !sbp('chelonia/contract/isWaitingForKeyShare', state)) { + } else if (hasSecretKeys && !isWaitingForKeyShare) { console.log('@@@@@@@@ AT join[firstTimeJoin] for ' + params.contractID) // We're joining for the first time @@ -502,12 +503,19 @@ export default (sbp('sbp/selectors/register', { } sbp('okTurtles.data/set', 'JOINING_GROUP-' + params.contractID, false) + // We don't have the secret keys and we're not waiting for OP_KEY_SHARE + // This means that we've been removed from the group + } else if (!hasSecretKeys && !isWaitingForKeyShare) { // We have already sent a key request that hasn't been answered. We cannot // do much at this point, so we do nothing. // This could happen, for example, after logging in if we still haven't // received a response to the key request. - } else { + sbp('okTurtles.data/set', 'JOINING_GROUP-' + params.contractID, false) + console.warn('Requested to join group but we\'ve been removed. contractID=' + params.contractID) + } else if (isWaitingForKeyShare) { console.info('Requested to join group but already waiting for OP_KEY_SHARE. contractID=' + params.contractID) + } else { + console.warn('Requested to join group but the state appears invalid. contractID=' + params.contractID, { sendKeyRequest, hasSecretKeys, isWaitingForKeyShare }) } } catch (e) { console.error('gi.actions/group/join failed!', e) diff --git a/frontend/main.js b/frontend/main.js index ceafcf8b8b..1f2ebf973d 100644 --- a/frontend/main.js +++ b/frontend/main.js @@ -115,7 +115,8 @@ async function startApp () { 'gi.notifications/emit', 'gi.actions/out/rotateKeys', 'gi.actions/group/shareNewKeys', 'gi.actions/chatroom/shareNewKeys', 'gi.actions/identity/shareNewPEK', 'chelonia/out/keyDel', - 'chelonia/contract/disconnect' + 'chelonia/contract/disconnect', + 'gi.actions/identity/saveOurLoginState' ], allowedDomains: ['okTurtles.data', 'okTurtles.events', 'okTurtles.eventQueue', 'gi.db', 'gi.contracts'], preferSlim: true, diff --git a/frontend/model/contracts/group.js b/frontend/model/contracts/group.js index 70e94483db..41a7a109ed 100644 --- a/frontend/model/contracts/group.js +++ b/frontend/model/contracts/group.js @@ -939,100 +939,12 @@ sbp('chelonia/defineContract', { { contractID, meta, state, getters } ) }, - async sideEffect ({ data, meta, contractID }, { state, getters }) { - const rootState = sbp('state/vuex/state') - const rootGetters = sbp('state/vuex/getters') - const contracts = rootState.contracts || {} - const { username } = rootState.loggedIn - - // If this member is re-joining the group, ignore the rest - // so the member doesn't remove themself again. - if (data.member === username && sbp('okTurtles.data/get', 'JOINING_GROUP-' + contractID)) { - return - } - - if (data.member === username) { - // NOTE: should remove archived data from IndexedStorage - // regarding the current group (proposals, payments) - await sbp('gi.contracts/group/removeArchivedProposals', contractID) - await sbp('gi.contracts/group/removeArchivedPayments', contractID) - - // grab the groupID of any group that we've successfully finished joining - const groupIdToSwitch = Object.keys(contracts) - .filter(cID => contracts[cID].type === 'gi.contracts/group' && cID !== contractID) - .sort((cID1, cID2) => rootState[cID1].profiles?.[username] ? -1 : 1)[0] || null - sbp('state/vuex/commit', 'setCurrentChatRoomId', {}) - sbp('state/vuex/commit', 'setCurrentGroupId', groupIdToSwitch) - // we can't await on this in here, because it will cause a deadlock, since Chelonia processes - // this sideEffect on the eventqueue for this contractID, and /remove uses that same eventqueue - sbp('chelonia/contract/remove', contractID).catch(e => { - console.error(`sideEffect(removeMember): ${e.name} thrown by /remove ${contractID}:`, e) - }) - // this looks crazy, but doing this was necessary to fix a race condition in the - // group-member-removal Cypress tests where due to the ordering of asynchronous events - // we were getting the same latestHash upon re-logging in for test "user2 rejoins groupA". - // We add it to the same queue as '/remove' above gets run on so that it is run after - // contractID is removed. See also comments in 'gi.actions/identity/login'. - sbp('chelonia/queueInvocation', contractID, ['gi.actions/identity/saveOurLoginState']) - .then(function () { - const router = sbp('controller/router') - const switchFrom = router.currentRoute.path - const switchTo = groupIdToSwitch ? '/dashboard' : '/' - if (switchFrom !== '/join' && switchFrom !== switchTo) { - router.push({ path: switchTo }).catch(console.warn) - } - }) - .catch(e => { - console.error(`sideEffect(removeMember): ${e.name} thrown during queueEvent to ${contractID} by saveOurLoginState:`, e) - }) - .then(() => sbp('gi.contracts/group/revokeGroupKeyAndRotateOurPEK', contractID, true)) - .catch(e => { - console.error(`sideEffect(removeMember): ${e.name} thrown during revokeGroupKeyAndRotateOurPEK to ${contractID}:`, e) - }) - // TODO - #828 remove other group members contracts if applicable - - // NOTE: remove all notifications whose scope is in this group - for (const notification of rootGetters.notificationsByGroup(contractID)) { - sbp('state/vuex/commit', REMOVE_NOTIFICATION, notification) - } - } else { - const myProfile = getters.groupProfile(username) - - if (isActionYoungerThanUser(meta, myProfile)) { - const memberRemovedThemselves = data.member === meta.username - - sbp('gi.notifications/emit', // emit a notification for a member removal. - memberRemovedThemselves ? 'MEMBER_LEFT' : 'MEMBER_REMOVED', - { - createdDate: meta.createdDate, - groupID: contractID, - username: memberRemovedThemselves ? meta.username : data.member - }) - - // gi.contracts/group/removeOurselves will eventually trigger this - // as well - sbp('gi.contracts/group/rotateKeys', contractID, state).then(() => { - return sbp('gi.contracts/group/revokeGroupKeyAndRotateOurPEK', contractID, false) - }).catch((e) => { - console.error('Error rotating group keys or our PEK', e) - }) - - const rootGetters = sbp('state/vuex/getters') - const userID = rootGetters.ourContactProfiles[data.member]?.contractID - if (userID) { - sbp('gi.contracts/group/removeForeignKeys', contractID, userID, state) - } - } - // TODO - #828 remove the member contract if applicable. - // problem is, if they're in another group we're also a part of, or if we - // have a DM with them, we don't want to do this. may need to use manual reference counting - // sbp('chelonia/contract/release', getters.groupProfile(data.member).contractID) - } - - leaveAllChatRoomsUponLeaving(state, data.member, meta).catch((e) => { - console.error('[gi.contracts/group/removeMember/sideEffect]: Error while leaving all chatrooms', e) + sideEffect ({ data, meta, contractID }, { state, getters }) { + // Put this invocation at the end of a sync to ensure that leaving and + // re-joining works + sbp('chelonia/queueInvocation', contractID, () => sbp('gi.contracts/group/leaveGroup', { data, meta, contractID }, { state, getters })).catch(e => { + console.error(`[gi.contracts/group/removeMember/sideEffect] Error ${e.name} during queueInvocation for ${contractID}`, e) }) - // TODO - #850 verify open proposals and see if they need some re-adjustment. } }, 'gi.contracts/group/removeOurselves': { @@ -1302,7 +1214,7 @@ sbp('chelonia/defineContract', { }, async sideEffect ({ meta, data, contractID }, { state }) { const rootState = sbp('state/vuex/state') - if (meta.username === rootState.loggedIn.username && !sbp('okTurtles.data/get', 'JOINING_GROUP-' + contractID)) { + if (meta.username !== rootState.loggedIn.username || !sbp('okTurtles.data/get', 'JOINING_GROUP-' + contractID)) { await leaveChatRoomAction(state, data, meta) } } @@ -1547,6 +1459,106 @@ sbp('chelonia/defineContract', { }) } }, + 'gi.contracts/group/leaveGroup': async ({ data, meta, contractID }, { getters }) => { + const rootState = sbp('state/vuex/state') + const rootGetters = sbp('state/vuex/getters') + const state = rootState[contractID] + const contracts = rootState.contracts || {} + const { username } = rootState.loggedIn + + if (!state) { + console.info(`[gi.contracts/group/leaveGroup] for ${contractID}: contract has been removed`) + } + + if ((state.profiles?.[data.member] && !state.profiles[data.member].departedDate) || (data.member === username && sbp('okTurtles.data/get', 'JOINING_GROUP-' + contractID))) { + console.info(`[gi.contracts/group/leaveGroup] for ${contractID}: member has not left`, window.structuredClone(state.profiles)) + return + } + + console.log('@@@@gi.contracts/group/leaveGroup', { contractID }) + + if (data.member === username) { + // NOTE: should remove archived data from IndexedStorage + // regarding the current group (proposals, payments) + await sbp('gi.contracts/group/removeArchivedProposals', contractID) + await sbp('gi.contracts/group/removeArchivedPayments', contractID) + + // grab the groupID of any group that we've successfully finished joining + const groupIdToSwitch = Object.keys(contracts) + .filter(cID => contracts[cID].type === 'gi.contracts/group' && cID !== contractID) + .sort((cID1, cID2) => rootState[cID1].profiles?.[username] ? -1 : 1)[0] || null + sbp('state/vuex/commit', 'setCurrentChatRoomId', {}) + sbp('state/vuex/commit', 'setCurrentGroupId', groupIdToSwitch) + // we can't await on this in here, because it will cause a deadlock, since Chelonia processes + // this method on the eventqueue for this contractID, and /remove uses that same eventqueue + sbp('chelonia/contract/remove', contractID).catch(e => { + console.error(`sideEffect(removeMember): ${e.name} thrown by /remove ${contractID}:`, e) + }) + // this looks crazy, but doing this was necessary to fix a race condition in the + // group-member-removal Cypress tests where due to the ordering of asynchronous events + // we were getting the same latestHash upon re-logging in for test "user2 rejoins groupA". + // We add it to the same queue as '/remove' above gets run on so that it is run after + // contractID is removed. See also comments in 'gi.actions/identity/login'. + sbp('gi.actions/identity/saveOurLoginState') + .then(function () { + const router = sbp('controller/router') + const switchFrom = router.currentRoute.path + const switchTo = groupIdToSwitch ? '/dashboard' : '/' + if (switchFrom !== '/join' && switchFrom !== switchTo) { + router.push({ path: switchTo }).catch(console.warn) + } + }) + .catch(e => { + console.error(`sideEffect(removeMember): ${e.name} thrown by saveOurLoginState:`, e) + }) + .then(() => sbp('gi.contracts/group/revokeGroupKeyAndRotateOurPEK', contractID, true)) + .catch(e => { + console.error(`sideEffect(removeMember): ${e.name} thrown during revokeGroupKeyAndRotateOurPEK to ${contractID}:`, e) + }) + // TODO - #828 remove other group members contracts if applicable + + // NOTE: remove all notifications whose scope is in this group + for (const notification of rootGetters.notificationsByGroup(contractID)) { + sbp('state/vuex/commit', REMOVE_NOTIFICATION, notification) + } + } else { + const myProfile = getters.groupProfile(username) + + if (isActionYoungerThanUser(meta, myProfile)) { + const memberRemovedThemselves = data.member === meta.username + + sbp('gi.notifications/emit', // emit a notification for a member removal. + memberRemovedThemselves ? 'MEMBER_LEFT' : 'MEMBER_REMOVED', + { + createdDate: meta.createdDate, + groupID: contractID, + username: memberRemovedThemselves ? meta.username : data.member + }) + + // gi.contracts/group/removeOurselves will eventually trigger this + // as well + sbp('gi.contracts/group/rotateKeys', contractID, state).then(() => { + return sbp('gi.contracts/group/revokeGroupKeyAndRotateOurPEK', contractID, false) + }).catch((e) => { + console.error('Error rotating group keys or our PEK', e) + }) + + const userID = rootGetters.ourContactProfiles[data.member]?.contractID + if (userID) { + sbp('gi.contracts/group/removeForeignKeys', contractID, userID, state) + } + } + // TODO - #828 remove the member contract if applicable. + // problem is, if they're in another group we're also a part of, or if we + // have a DM with them, we don't want to do this. may need to use manual reference counting + // sbp('chelonia/contract/release', getters.groupProfile(data.member).contractID) + } + + leaveAllChatRoomsUponLeaving(state, data.member, meta).catch((e) => { + console.error('[gi.contracts/group/leaveGroup]: Error while leaving all chatrooms', e) + }) + // TODO - #850 verify open proposals and see if they need some re-adjustment. + }, 'gi.contracts/group/rotateKeys': (contractID, state) => { if (!state._volatile) Vue.set(state, '_volatile', Object.create(null)) if (!state._volatile.pendingKeyRevocations) Vue.set(state._volatile, 'pendingKeyRevocations', Object.create(null)) @@ -1557,8 +1569,8 @@ sbp('chelonia/defineContract', { Vue.set(state._volatile.pendingKeyRevocations, CSKid, true) Vue.set(state._volatile.pendingKeyRevocations, CEKid, true) - return sbp('chelonia/queueInvocation', contractID, ['gi.actions/out/rotateKeys', contractID, 'gi.contracts/group', 'pending', 'gi.actions/group/shareNewKeys']).catch(e => { - console.warn(`rotateKeys: ${e.name} thrown during queueEvent to ${contractID}:`, e) + return sbp('gi.actions/out/rotateKeys', contractID, 'gi.contracts/group', 'pending', 'gi.actions/group/shareNewKeys').catch(e => { + console.warn(`rotateKeys: ${e.name} thrown:`, e) }) }, 'gi.contracts/group/revokeGroupKeyAndRotateOurPEK': (groupContractID, disconnectGroup: ?boolean) => { @@ -1612,13 +1624,13 @@ sbp('chelonia/defineContract', { if (!CEKid) throw new Error('Missing encryption key') - sbp('chelonia/queueInvocation', contractID, ['chelonia/out/keyDel', { + sbp('chelonia/out/keyDel', { contractID, contractName: 'gi.contracts/group', data: keyIds, signingKeyId: CSKid - }]).catch(e => { - console.warn(`removeForeignKeys: ${e.name} thrown during queueEvent to ${contractID}:`, e) + }).catch(e => { + console.warn(`removeForeignKeys: ${e.name} error thrown:`, e) }) } } diff --git a/frontend/model/contracts/manifests.json b/frontend/model/contracts/manifests.json index 5bc48c2937..499daab8e1 100644 --- a/frontend/model/contracts/manifests.json +++ b/frontend/model/contracts/manifests.json @@ -1,7 +1,7 @@ { "manifests": { "gi.contracts/chatroom": "21XWnNV497KyM78NWtVUEB7o86SpKnqUsPg83GCwDnMV6omQkM", - "gi.contracts/group": "21XWnNH3nWnjrzvy6utyLKbo9nQhstXeEKQbdhfRLsHX9R1UeT", + "gi.contracts/group": "21XWnNKHSrD2c7B1MHMs7uoEQ4oCf7u3G8VxQf86dHvCxv7poF", "gi.contracts/identity": "21XWnNSxNrVFTMBCUVcZV3CG833gTbJ9V8ZyX7Fxgmpj9rNy4r" } } diff --git a/shared/domains/chelonia/utils.js b/shared/domains/chelonia/utils.js index 2dc1f83d7e..213a2ec5ba 100644 --- a/shared/domains/chelonia/utils.js +++ b/shared/domains/chelonia/utils.js @@ -468,11 +468,13 @@ export const recreateEvent = async (entry: GIMessage, rootState: Object): Promis } } else if (opT === GIMessage.OP_ATOMIC) { if (!Array.isArray(opV)) throw new Error('Invalid message format') - newOpV = ((((opV: any): GIOpAtomic).map(([t, v]) => recreateOperationInternal(t, v)).filter(Boolean): any): GIOpAtomic) + newOpV = ((((opV: any): GIOpAtomic).map(([t, v]) => [t, recreateOperationInternal(t, v)]).filter(([, v]) => !!v): any): GIOpAtomic) if (newOpV.length === 0) { console.info('Omitting empty OP_ATOMIC', { head }) } else if (newOpV.length === opV.length && newOpV.reduce((acc, cv, i) => acc && cv === opV[i], true)) { return opV + } else { + return newOpV } } else { return opV From f3bed17c03e7a073123e6541882931a00f804bf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Iv=C3=A1n=20Vieitez=20Parra?= <3857362+corrideat@users.noreply.github.com> Date: Thu, 26 Oct 2023 20:34:12 +0200 Subject: [PATCH 02/29] Fix various issues related to login and re-joining --- frontend/controller/actions/group.js | 7 +++--- frontend/controller/actions/identity.js | 28 ++++++++++++++---------- frontend/controller/actions/index.js | 5 +++++ frontend/controller/router.js | 2 +- frontend/model/contracts/group.js | 10 +++++++-- frontend/model/contracts/manifests.json | 2 +- frontend/model/state.js | 3 +++ frontend/views/pages/Join.vue | 9 ++++++-- frontend/views/pages/PendingApproval.vue | 5 +++-- shared/domains/chelonia/GIMessage.js | 25 ++++++++++----------- shared/domains/chelonia/internals.js | 7 ++++++ shared/domains/chelonia/signedData.js | 5 +++-- 12 files changed, 70 insertions(+), 38 deletions(-) diff --git a/frontend/controller/actions/group.js b/frontend/controller/actions/group.js index f339b5edea..b7e4768490 100644 --- a/frontend/controller/actions/group.js +++ b/frontend/controller/actions/group.js @@ -9,6 +9,7 @@ import { INVITE_INITIAL_CREATOR, MAX_GROUP_MEMBER_COUNT, MESSAGE_TYPES, + PROFILE_STATUS, PROPOSAL_GENERIC, PROPOSAL_GROUP_SETTING_CHANGE, PROPOSAL_INVITE_MEMBER, @@ -294,7 +295,7 @@ export default (sbp('sbp/selectors/register', { // secret keys to be shared with us, (b) ready to call the inviteAccept // action if we haven't done so yet (because we were previously waiting for // the keys), or (c) already a member and ready to interact with the group. - 'gi.actions/group/join': async function (params: $Exact & { options?: { skipUsableKeysCheck?: boolean; skipInviteAccept?: boolean } }) { + 'gi.actions/group/join': async function (params: $Exact & { options?: { skipUsableKeysCheck?: boolean; } }) { sbp('okTurtles.data/set', 'JOINING_GROUP-' + params.contractID, true) try { const rootState = sbp('state/vuex/state') @@ -384,7 +385,7 @@ export default (sbp('sbp/selectors/register', { // We're joining for the first time // In this case, we share our profile key with the group, call the // inviteAccept action and join the General chatroom - if (!state.profiles?.[username] || state.profiles[username].departedDate) { + if (!state.profiles?.[username]?.status !== PROFILE_STATUS.ACTIVE) { const generalChatRoomId = rootState[params.contractID].generalChatRoomId const CEKid = sbp('chelonia/contract/currentKeyIdByName', params.contractID, 'cek') @@ -524,7 +525,7 @@ export default (sbp('sbp/selectors/register', { saveLoginState('joining', params.contractID) } }, - 'gi.actions/group/joinAndSwitch': async function (params: $Exact & { options?: { skipUsableKeysCheck?: boolean; skipInviteAccept: boolean } }) { + 'gi.actions/group/joinAndSwitch': async function (params: $Exact & { options?: { skipUsableKeysCheck?: boolean; } }) { await sbp('gi.actions/group/join', params) // after joining, we can set the current group sbp('gi.actions/group/switch', params.contractID) diff --git a/frontend/controller/actions/identity.js b/frontend/controller/actions/identity.js index b68810ed64..831e535235 100644 --- a/frontend/controller/actions/identity.js +++ b/frontend/controller/actions/identity.js @@ -3,7 +3,8 @@ import { GIErrorUIRuntimeError, L, LError } from '@common/common.js' import { CHATROOM_PRIVACY_LEVEL, - CHATROOM_TYPES + CHATROOM_TYPES, + PROFILE_STATUS } from '@model/contracts/shared/constants.js' import { difference, omit, pickWhere, uniq } from '@model/contracts/shared/giLodash.js' import sbp from '@sbp/sbp' @@ -294,7 +295,7 @@ export default (sbp('sbp/selectors/register', { console.info('synchronizing login state:', { groupsJoined }) for (const contractID of groupsJoined) { try { - await sbp('gi.actions/group/join', { contractID, options: { skipInviteAccept: true } }) + await sbp('gi.actions/group/join', { contractID }) } catch (e) { console.error(`updateLoginStateUponLogin: ${e.name} attempting to join group ${contractID}`, e) if (state.contracts[contractID] || state[contractID]) { @@ -453,15 +454,18 @@ export default (sbp('sbp/selectors/register', { // Call 'gi.actions/group/join' on all groups which may need re-joining await Promise.all(groupsToRejoin.map(groupId => { return ( - // (1) Check whether the contract exists (may have been removed - // after sync) + // (1) Check whether the contract exists (may have been removed + // after sync) state.contracts[groupId] && - // (2) Check whether the join process is still incomplete - // This needs to be re-checked because it may have changed after - // sync - !state.profiles?.[username] && - // (3) Call join - sbp('gi.actions/group/join', { contractID: groupId, contractName: 'gi.contracts/group' }) + // (2) Check whether the join process is still incomplete + // This needs to be re-checked because it may have changed after + // sync + state[groupId]?.profiles?.[username]?.status !== PROFILE_STATUS.ACTIVE && + // (3) Call join + sbp('gi.actions/group/join', { + contractID: groupId, + contractName: 'gi.contracts/group' + }) ) })) @@ -471,8 +475,8 @@ export default (sbp('sbp/selectors/register', { .forEach(cId => { // We send this action only for groups we have fully joined (i.e., // accepted an invite add added our profile) - if (state[cId]?.profiles?.[username]) { - sbp('gi.actions/group/updateLastLoggedIn', { contractID: cId }).catch(console.error) + if (state[cId]?.profiles?.[username]?.status === PROFILE_STATUS.ACTIVE) { + sbp('gi.actions/group/updateLastLoggedIn', { contractID: cId }).catch((e) => console.error('Error sending updateLastLoggedIn', e)) } }) }).finally(() => { diff --git a/frontend/controller/actions/index.js b/frontend/controller/actions/index.js index 3e1a2a49b7..c0460aed4c 100644 --- a/frontend/controller/actions/index.js +++ b/frontend/controller/actions/index.js @@ -180,5 +180,10 @@ sbp('sbp/selectors/register', { signingKeyId }) } + + // TODO: Temporary sync until the server does signature validation + // This prevents us from sending messages signed with just-revoked keys + // Once the server enforces signatures, this can be removed + await sbp('chelonia/contract/sync', contractID, { force: true }) } }) diff --git a/frontend/controller/router.js b/frontend/controller/router.js index c73e3d9543..4bcf90cf34 100644 --- a/frontend/controller/router.js +++ b/frontend/controller/router.js @@ -55,7 +55,7 @@ const groupGuard = { } const pendingApprovalGuard = { - guard: (to, from) => store.state.currentGroupId && !store.getters.ourGroupProfile, + guard: (to, from) => store.state.currentGroupId && store.getters.ourPendingAccept, redirect: (to, from) => ({ path: '/pending-approval' }) } diff --git a/frontend/model/contracts/group.js b/frontend/model/contracts/group.js index 41a7a109ed..da07046ec0 100644 --- a/frontend/model/contracts/group.js +++ b/frontend/model/contracts/group.js @@ -336,6 +336,12 @@ sbp('chelonia/defineContract', { groupSettings (state, getters) { return getters.currentGroupState.settings || {} }, + pendingAccept (state, getters) { + return username => { + const profiles = getters.currentGroupState.profiles + return profiles?.[username]?.status === PROFILE_STATUS.PENDING + } + }, groupProfile (state, getters) { return username => { const profiles = getters.currentGroupState.profiles @@ -1470,8 +1476,8 @@ sbp('chelonia/defineContract', { console.info(`[gi.contracts/group/leaveGroup] for ${contractID}: contract has been removed`) } - if ((state.profiles?.[data.member] && !state.profiles[data.member].departedDate) || (data.member === username && sbp('okTurtles.data/get', 'JOINING_GROUP-' + contractID))) { - console.info(`[gi.contracts/group/leaveGroup] for ${contractID}: member has not left`, window.structuredClone(state.profiles)) + if (state.profiles?.[data.member]?.status !== PROFILE_STATUS.REMOVED || (data.member === username && sbp('okTurtles.data/get', 'JOINING_GROUP-' + contractID))) { + console.info(`[gi.contracts/group/leaveGroup] for ${contractID}: member has not left`, JSON.parse(JSON.stringify(state.profiles))) return } diff --git a/frontend/model/contracts/manifests.json b/frontend/model/contracts/manifests.json index 499daab8e1..5f2fb4bd65 100644 --- a/frontend/model/contracts/manifests.json +++ b/frontend/model/contracts/manifests.json @@ -1,7 +1,7 @@ { "manifests": { "gi.contracts/chatroom": "21XWnNV497KyM78NWtVUEB7o86SpKnqUsPg83GCwDnMV6omQkM", - "gi.contracts/group": "21XWnNKHSrD2c7B1MHMs7uoEQ4oCf7u3G8VxQf86dHvCxv7poF", + "gi.contracts/group": "21XWnNSQz4992RD7xxtwmsiBnkPQGVC4F1j6BGvqBsC9aoqQ3E", "gi.contracts/identity": "21XWnNSxNrVFTMBCUVcZV3CG833gTbJ9V8ZyX7Fxgmpj9rNy4r" } } diff --git a/frontend/model/state.js b/frontend/model/state.js index 25480a8a89..f0985564ce 100644 --- a/frontend/model/state.js +++ b/frontend/model/state.js @@ -228,6 +228,9 @@ const getters = { ourUsername (state) { return state.loggedIn && state.loggedIn.username }, + ourPendingAccept (state, getters) { + getters.pendingAccept(getters.ourUsername) + }, ourGroupProfile (state, getters) { return getters.groupProfile(getters.ourUsername) }, diff --git a/frontend/views/pages/Join.vue b/frontend/views/pages/Join.vue index c17fe68729..bde6db6133 100644 --- a/frontend/views/pages/Join.vue +++ b/frontend/views/pages/Join.vue @@ -52,12 +52,13 @@ import GroupWelcome from '@components/GroupWelcome.vue' import Loading from '@components/Loading.vue' import LoginForm from '@containers/access/LoginForm.vue' import SignupForm from '@containers/access/SignupForm.vue' -import { INVITE_STATUS } from '~/shared/domains/chelonia/constants.js' import sbp from '@sbp/sbp' import SvgBrokenLink from '@svgs/broken-link.svg' import { LOGIN } from '@utils/events.js' import { mapGetters, mapState } from 'vuex' +import { INVITE_STATUS } from '~/shared/domains/chelonia/constants.js' import { findKeyIdByName } from '~/shared/domains/chelonia/utils.js' +import { PROFILE_STATUS } from '@model/contracts/shared/constants.js' // Using relative path to crypto.js instead of ~-path to workaround some esbuild bug import { deserializeKey, keyId } from '../../../shared/domains/chelonia/crypto.js' @@ -130,7 +131,7 @@ export default ({ return } if (this.ourUsername) { - if (this.currentGroupId && this.$store.state.contracts[this.ephemeral.query.groupId]) { + if (this.currentGroupId && [PROFILE_STATUS.ACTIVE, PROFILE_STATUS.PENDING].includes(this.$store.state.contracts[this.ephemeral.query.groupId]?.profiles?.[this.ourUsername])) { this.$router.push({ path: '/dashboard' }) } else { await this.accept() @@ -150,6 +151,8 @@ export default ({ this.pageStatus = 'SIGNING' } catch (e) { console.error(e) + // eslint-disable-next-line no-debugger + debugger this.ephemeral.errorMsg = `${L('Something went wrong. Please, try again.')} ${e.message}` this.pageStatus = 'INVALID' } @@ -188,6 +191,8 @@ export default ({ // this.pageStatus = 'WELCOME' this.$router.push({ path: '/pending-approval' }) } catch (e) { + // eslint-disable-next-line no-debugger + debugger console.error('Join.vue accept() error:', e) this.ephemeral.errorMsg = e.message this.pageStatus = 'INVALID' diff --git a/frontend/views/pages/PendingApproval.vue b/frontend/views/pages/PendingApproval.vue index 23b036d155..3ee8608deb 100644 --- a/frontend/views/pages/PendingApproval.vue +++ b/frontend/views/pages/PendingApproval.vue @@ -13,9 +13,10 @@ div