Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / packages / account / members / unprivatizeMembers.ts
blobeb688f71e3660e0c571df5166096cb84190c3dcc
1 import type { UnknownAction } from '@reduxjs/toolkit';
2 import { createNextState, createSelector } from '@reduxjs/toolkit';
3 import type { ThunkAction } from 'redux-thunk';
4 import { c } from 'ttag';
6 import type { ProtonThunkArguments } from '@proton/redux-shared-store-types';
7 import { getApiErrorMessage } from '@proton/shared/lib/api/helpers/apiErrorHelper';
8 import { getSilentApi } from '@proton/shared/lib/api/helpers/customConfig';
9 import { unprivatizeMemberKeysRoute } from '@proton/shared/lib/api/members';
10 import { captureMessage } from '@proton/shared/lib/helpers/sentry';
11 import type {
12     EnhancedMember,
13     Member,
14     MemberReadyForUnprivatization,
15     MemberReadyForUnprivatizationApproval,
16     VerifyOutboundPublicKeys,
17 } from '@proton/shared/lib/interfaces';
18 import {
19     UnprivatizationRevisionError,
20     getMemberReadyForUnprivatization,
21     getMemberReadyForUnprivatizationApproval,
22     getSentryError,
23     unprivatizeMember,
24     unprivatizeMemberHelper,
25 } from '@proton/shared/lib/keys';
27 import type { OrganizationKeyState } from '../organizationKey';
28 import { organizationKeyThunk } from '../organizationKey';
29 import { getMember } from './actions';
30 import {
31     type UnprivatizationMemberApproval,
32     type UnprivatizationMemberFailure,
33     type UnprivatizationMemberSuccess,
34     upsertMember,
35 } from './index';
36 import { type MembersState, selectMembers, setUnprivatizationState } from './index';
37 import { getMemberAddresses } from './index';
39 export const getMemberToUnprivatize = (member: Member): member is MemberReadyForUnprivatization => {
40     return getMemberReadyForUnprivatization(member.Unprivatization);
43 export const getMemberToUnprivatizeApproval = (member: Member): member is MemberReadyForUnprivatizationApproval => {
44     return getMemberReadyForUnprivatizationApproval(member.Unprivatization);
47 export const selectUnprivatizationState = (state: MembersState) => state.members.unprivatization;
49 type JoinedResult =
50     | (UnprivatizationMemberApproval & {
51           member: MemberReadyForUnprivatizationApproval;
52       })
53     | (UnprivatizationMemberFailure & {
54           member: MemberReadyForUnprivatization;
55       })
56     | (UnprivatizationMemberSuccess & {
57           member: EnhancedMember;
58       });
60 const getJoinedUnprivatizationMemberList = (
61     unprivatizationState: ReturnType<typeof selectUnprivatizationState>,
62     membersState: ReturnType<typeof selectMembers>
63 ): JoinedResult[] => {
64     const membersList = Object.entries(unprivatizationState.members).map(([key, value]) => {
65         return {
66             id: key,
67             value,
68         };
69     });
70     let membersMap: { [key: string]: EnhancedMember } = {};
71     if (membersList.length > 0 && membersState.value) {
72         membersMap = Object.fromEntries(membersState.value.map((member) => [member.ID, member]));
73     }
74     return membersList.reduce<JoinedResult[]>((acc, cur) => {
75         const member = membersMap[cur.id];
76         if (!cur.value || !member) {
77             return acc;
78         }
79         if (cur.value.type === 'approval' && getMemberToUnprivatizeApproval(member)) {
80             acc.push({
81                 ...cur.value,
82                 member: member,
83             });
84         }
85         if (cur.value.type === 'error' && getMemberToUnprivatize(member)) {
86             acc.push({
87                 ...cur.value,
88                 member: member,
89             });
90         }
91         if (cur.value.type === 'success') {
92             acc.push({
93                 ...cur.value,
94                 member: member,
95             });
96         }
97         return acc;
98     }, []);
101 const reportSentryError = (error: any) => {
102     const sentryError = getSentryError(error);
103     if (sentryError) {
104         captureMessage('Unprivatization: Error unprivatizing member', {
105             level: 'error',
106             extra: { error: sentryError },
107         });
108     }
111 const getErrorState = (error: any) => {
112     const apiErrorMessage = getApiErrorMessage(error);
113     const errorMessage = apiErrorMessage || error?.message || c('Error').t`Unknown error`;
114     return {
115         type: 'error',
116         error: errorMessage,
117         revision: error instanceof UnprivatizationRevisionError,
118     } as const;
121 export const selectJoinedUnprivatizationState = createSelector(
122     [selectUnprivatizationState, selectMembers],
123     (unprivatizationState, membersState) => {
124         const result = getJoinedUnprivatizationMemberList(unprivatizationState, membersState);
125         const filteredResult = result.reduce<{
126             failures: (UnprivatizationMemberFailure & { member: MemberReadyForUnprivatization })[];
127             approval: (UnprivatizationMemberApproval & { member: MemberReadyForUnprivatizationApproval })[];
128         }>(
129             (acc, cur) => {
130                 if (cur.type === 'error' && cur.revision) {
131                     acc.failures.push(cur);
132                 }
133                 if (cur.type === 'approval') {
134                     acc.approval.push(cur);
135                 }
136                 return acc;
137             },
138             { failures: [], approval: [] }
139         );
140         return {
141             ...filteredResult,
142             loading: unprivatizationState.loading,
143         };
144     }
147 export const unprivatizeApprovalMembersHelper = ({
148     membersToUnprivatize,
149 }: {
150     membersToUnprivatize: MemberReadyForUnprivatizationApproval[];
151 }): ThunkAction<
152     Promise<{
153         membersToUpdate: Member[];
154         membersToError: { member: Member; error: any }[];
155     }>,
156     MembersState & OrganizationKeyState,
157     ProtonThunkArguments,
158     UnknownAction
159 > => {
160     return async (dispatch, getState, extra) => {
161         const membersToUpdate: Member[] = [];
162         const membersToError: { member: Member; error: any }[] = [];
163         extra.eventManager.stop();
164         const api = getSilentApi(extra.api);
165         for (const member of membersToUnprivatize) {
166             try {
167                 const [organizationKey, memberAddresses] = await Promise.all([
168                     dispatch(organizationKeyThunk()), // Fetch org key again to ensure it's up-to-date.
169                     dispatch(getMemberAddresses({ member, retry: true })),
170                 ]);
171                 const payload = await unprivatizeMemberHelper({
172                     data: {
173                         ActivationToken: member.Unprivatization.ActivationToken,
174                         PrivateKeys: member.Unprivatization.PrivateKeys,
175                     },
176                     memberAddresses,
177                     verificationKeys: null,
178                     organizationKey: organizationKey.privateKey,
179                 });
180                 await api(unprivatizeMemberKeysRoute(member.ID, payload));
181                 const newMember = await getMember(api, member.ID);
182                 membersToUpdate.push(newMember);
183             } catch (error: any) {
184                 membersToError.push({ member, error });
185             }
186         }
187         extra.eventManager.start();
188         return { membersToUpdate, membersToError };
189     };
192 export const unprivatizeApprovalMembers = ({
193     membersToUnprivatize,
194 }: {
195     membersToUnprivatize: MemberReadyForUnprivatizationApproval[];
196 }): ThunkAction<Promise<Member[]>, MembersState & OrganizationKeyState, ProtonThunkArguments, UnknownAction> => {
197     return async (dispatch, getState) => {
198         if (!membersToUnprivatize.length) {
199             return [];
200         }
202         {
203             const oldState = selectUnprivatizationState(getState());
204             if (oldState.loading.approval) {
205                 return [];
206             }
207             const newState = createNextState(oldState, (state) => {
208                 state.loading.approval = true;
209             });
210             dispatch(setUnprivatizationState(newState));
211         }
213         const { membersToUpdate, membersToError } = await dispatch(
214             unprivatizeApprovalMembersHelper({ membersToUnprivatize })
215         );
217         membersToError.forEach(({ error }) => {
218             reportSentryError(error);
219         });
221         {
222             const oldState = selectUnprivatizationState(getState());
223             const newState = createNextState(oldState, (state) => {
224                 state.loading.approval = false;
226                 membersToUpdate.forEach((member) => {
227                     state.members[member.ID] = { type: 'success' };
228                 });
230                 membersToError.forEach(({ member, error }) => {
231                     state.members[member.ID] = getErrorState(error);
232                 });
233             });
235             if (newState !== oldState) {
236                 dispatch(setUnprivatizationState(newState));
237             }
238         }
240         membersToUpdate.forEach((member) => {
241             dispatch(upsertMember({ member }));
242         });
244         return membersToUpdate;
245     };
248 const getMembersToUnprivatize = ({
249     members,
250     oldState,
251 }: {
252     members: EnhancedMember[];
253     oldState: ReturnType<typeof selectUnprivatizationState>;
254 }): {
255     membersToApprove: Member[];
256     membersToDelete: string[];
257     membersToUnprivatize: MemberReadyForUnprivatization[];
258 } => {
259     const membersToDelete: string[] = [];
260     const membersToApprove: Member[] = [];
261     const membersToUnprivatize: MemberReadyForUnprivatization[] = [];
263     members.forEach((member) => {
264         const item = oldState.members[member.ID];
265         if (getMemberToUnprivatize(member)) {
266             if (!item) {
267                 membersToUnprivatize.push(member);
268             }
269         } else if (getMemberToUnprivatizeApproval(member)) {
270             if (!item) {
271                 membersToApprove.push(member);
272             }
273             // If there is a previous error and the user is no longer to unprivatize, delete it
274         } else if (item && item.type === 'error') {
275             membersToDelete.push(member.ID);
276         }
277     });
279     return { membersToUnprivatize, membersToDelete, membersToApprove };
282 export const unprivatizeMembersBackgroundHelper = ({
283     membersToUnprivatize,
284     verifyOutboundPublicKeys,
285     options,
286 }: {
287     membersToUnprivatize: MemberReadyForUnprivatization[];
288     verifyOutboundPublicKeys: VerifyOutboundPublicKeys;
289     options?: Parameters<typeof unprivatizeMember>[0]['options'];
290 }): ThunkAction<
291     Promise<{
292         membersToUpdate: Member[];
293         membersToError: { member: Member; error: any }[];
294     }>,
295     MembersState & OrganizationKeyState,
296     ProtonThunkArguments,
297     UnknownAction
298 > => {
299     return async (dispatch, getState, extra) => {
300         const membersToUpdate: Member[] = [];
301         const membersToError: { member: Member; error: any }[] = [];
303         extra.eventManager.stop();
304         const api = getSilentApi(extra.api);
305         for (const member of membersToUnprivatize) {
306             try {
307                 const [organizationKey, memberAddresses] = await Promise.all([
308                     dispatch(organizationKeyThunk()), // Fetch org key again to ensure it's up-to-date.
309                     dispatch(getMemberAddresses({ member, retry: true })),
310                 ]);
311                 const payload = await unprivatizeMember({
312                     api,
313                     member,
314                     memberAddresses,
315                     organizationKey: organizationKey.privateKey,
316                     verifyOutboundPublicKeys,
317                     options,
318                 });
319                 await api(unprivatizeMemberKeysRoute(member.ID, payload));
320                 const newMember = await getMember(api, member.ID);
321                 membersToUpdate.push(newMember);
322             } catch (error: any) {
323                 membersToError.push({ member, error });
324             }
325         }
326         extra.eventManager.start();
327         return { membersToUpdate, membersToError };
328     };
331 export const unprivatizeMembersBackground = ({
332     verifyOutboundPublicKeys,
333     options,
334     target,
335 }: {
336     verifyOutboundPublicKeys: VerifyOutboundPublicKeys;
337     options?: Parameters<typeof unprivatizeMember>[0]['options'];
338     target:
339         | {
340               type: 'background';
341               members: EnhancedMember[];
342           }
343         | {
344               type: 'action';
345               members: MemberReadyForUnprivatization[];
346           };
347 }): ThunkAction<Promise<void>, MembersState & OrganizationKeyState, ProtonThunkArguments, UnknownAction> => {
348     return async (dispatch, getState) => {
349         if (!target.members.length) {
350             return;
351         }
353         const oldState = selectUnprivatizationState(getState());
354         if (oldState.loading.automatic) {
355             return;
356         }
358         const { membersToUnprivatize, membersToDelete, membersToApprove } =
359             target.type === 'action'
360                 ? { membersToUnprivatize: target.members, membersToDelete: [], membersToApprove: [] }
361                 : getMembersToUnprivatize({
362                       members: target.members,
363                       oldState,
364                   });
366         if (membersToUnprivatize.length || membersToApprove.length || membersToDelete.length) {
367             const newState = createNextState(oldState, (state) => {
368                 state.loading.automatic = Boolean(membersToUnprivatize.length);
370                 membersToApprove.forEach((member) => {
371                     state.members[member.ID] = { type: 'approval' };
372                 });
374                 membersToDelete.forEach((memberID) => {
375                     delete state.members[memberID];
376                 });
377             });
379             if (oldState !== newState) {
380                 dispatch(setUnprivatizationState(newState));
381             }
382         }
384         if (!membersToUnprivatize.length) {
385             return;
386         }
388         const { membersToError, membersToUpdate } = await dispatch(
389             unprivatizeMembersBackgroundHelper({
390                 membersToUnprivatize,
391                 verifyOutboundPublicKeys,
392                 options,
393             })
394         );
396         membersToError.forEach(({ error }) => {
397             reportSentryError(error);
398         });
400         {
401             const oldState = selectUnprivatizationState(getState());
402             const newState = createNextState(oldState, (state) => {
403                 state.loading.automatic = false;
405                 membersToUpdate.forEach((member) => {
406                     state.members[member.ID] = { type: 'success' };
407                 });
409                 membersToError.forEach(({ member, error }) => {
410                     state.members[member.ID] = getErrorState(error);
411                 });
412             });
414             if (newState !== oldState) {
415                 dispatch(setUnprivatizationState(newState));
416             }
417         }
419         membersToUpdate.forEach((member) => {
420             dispatch(upsertMember({ member }));
421         });
422     };