1 import type { Action } from '@reduxjs/toolkit';
3 import { CryptoProxy } from '@proton/crypto';
4 import type { SharedStartListening } from '@proton/redux-shared-store-types';
5 import { CacheType } from '@proton/redux-utilities';
6 import { getIsAddressConfirmed, getIsAddressEnabled } from '@proton/shared/lib/helpers/address';
7 import { hasBit } from '@proton/shared/lib/helpers/bitset';
8 import { captureMessage } from '@proton/shared/lib/helpers/sentry';
9 import { getSentryError } from '@proton/shared/lib/keys';
10 import noop from '@proton/utils/noop';
12 import { addressesThunk, selectAddresses } from '../addresses';
13 import { serverEvent } from '../eventLoop';
14 import { selectMembers } from '../members';
15 import { selectOrganization } from '../organization';
17 changeOrganizationSignature,
18 migrateOrganizationKeyPasswordless,
19 migrateOrganizationKeyPasswordlessPrivateAdmin,
21 import { type OrganizationKeyState, organizationKeyThunk, selectOrganizationKey } from './index';
23 export const organizationKeysListener = (startListening: SharedStartListening<OrganizationKeyState>) => {
25 predicate: (action: Action, currentState: OrganizationKeyState) => {
26 // Warning: There is no event update coming for organization key changes, however, an update for the organization
27 // is received as the keys are changed. So each time it changes, it will redo this.
29 serverEvent.match(action) &&
30 action.payload.Organization &&
31 !!selectOrganizationKey(currentState).value
34 effect: async (action, listenerApi) => {
35 await listenerApi.dispatch(organizationKeyThunk({ cache: CacheType.None }));
40 predicate: (action, currentState, nextState) => {
41 const oldValue = selectOrganizationKey(currentState).value;
42 return !!(oldValue && oldValue !== selectOrganizationKey(nextState).value);
44 effect: async (action, listenerApi) => {
45 const oldValue = selectOrganizationKey(listenerApi.getOriginalState())?.value;
46 if (oldValue?.privateKey) {
47 await CryptoProxy.clearKey({ key: oldValue.privateKey }).catch(noop);
53 export const organizationKeysManagementListener = (startListening: SharedStartListening<OrganizationKeyState>) => {
55 predicate: (action, currentState, nextState) => {
57 selectOrganizationKey(nextState).value &&
58 selectMembers(nextState).value?.length &&
59 selectOrganization(nextState).value
62 effect: async (action, listenerApi) => {
63 const state = listenerApi.getState();
64 const organization = selectOrganization(state).value;
65 const members = selectMembers(state).value;
66 const self = members?.find((member) => Boolean(member.Self));
67 const organizationKey = selectOrganizationKey(state).value;
68 if (!organization || !self || !organizationKey?.privateKey) {
72 listenerApi.unsubscribe();
74 if (hasBit(organization.ToMigrate, 2)) {
75 await listenerApi.dispatch(migrateOrganizationKeyPasswordless());
76 } else if (hasBit(self.ToMigrate, 2)) {
77 await listenerApi.dispatch(migrateOrganizationKeyPasswordlessPrivateAdmin());
84 predicate: (action, currentState, nextState) => {
85 const orgKey = selectOrganizationKey(nextState).value;
86 const addresses = selectAddresses(nextState).value;
87 return Boolean(orgKey?.privateKey && !orgKey.Key.FingerprintSignature && (addresses || [])?.length >= 1);
89 effect: async (action, listenerApi) => {
90 const state = listenerApi.getState();
91 const organizationKey = selectOrganizationKey(state).value;
92 if (!organizationKey?.privateKey) {
96 listenerApi.unsubscribe();
98 const addresses = await listenerApi.dispatch(addressesThunk());
99 const activeAddresses = addresses.filter(
100 (address) => getIsAddressEnabled(address) && getIsAddressConfirmed(address)
102 const primaryActiveAddress = activeAddresses[0];
103 if (!primaryActiveAddress) {
108 await listenerApi.dispatch(changeOrganizationSignature({ address: primaryActiveAddress }));
110 const error = getSentryError(e);
112 captureMessage('Organization identity: Error generating signature', {