Update selected item color in Pass menu
[ProtonMail-WebClient.git] / packages / pass / lib / items / item.requests.ts
blob9a73a409978ab72152a6f25f7d7cc1e88c67ac66
1 import { api } from '@proton/pass/lib/api/api';
2 import { createPageIterator } from '@proton/pass/lib/api/utils';
3 import { PassCrypto } from '@proton/pass/lib/crypto';
4 import type {
5     AliasAndItemCreateRequest,
6     BatchItemRevisionIDs,
7     BatchItemRevisions,
8     CustomAliasCreateRequest,
9     ImportItemBatchRequest,
10     ImportItemRequest,
11     ItemCreateIntent,
12     ItemEditIntent,
13     ItemImportIntent,
14     ItemMoveIndividualToShareRequest,
15     ItemMoveMultipleToShareRequest,
16     ItemRevision,
17     ItemRevisionContentsResponse,
18     ItemRevisionsIntent,
19     ItemType,
20     ItemUpdateFlagsRequest,
21     Maybe,
22 } from '@proton/pass/types';
23 import { truthy } from '@proton/pass/utils/fp/predicates';
24 import { logger } from '@proton/pass/utils/logger';
25 import { getEpoch } from '@proton/pass/utils/time/epoch';
26 import identity from '@proton/utils/identity';
28 import { serializeItemContent } from './item-proto.transformer';
29 import { parseItemRevision } from './item.parser';
30 import { batchByShareId, intoRevisionID } from './item.utils';
32 /* Item creation API request for all items
33  * except for alias items */
34 export const createItem = async (
35     createIntent: ItemCreateIntent<Exclude<ItemType, 'alias'>>
36 ): Promise<ItemRevisionContentsResponse> => {
37     const { shareId, ...item } = createIntent;
39     const content = serializeItemContent(item);
40     const data = await PassCrypto.createItem({ shareId, content });
42     const { Item } = await api({
43         url: `pass/v1/share/${shareId}/item`,
44         method: 'post',
45         data,
46     });
48     return Item!;
51 /* Specific alias item API request */
52 export const createAlias = async (createIntent: ItemCreateIntent<'alias'>): Promise<ItemRevisionContentsResponse> => {
53     const { shareId, ...item } = createIntent;
55     const content = serializeItemContent(item);
56     const encryptedItem = await PassCrypto.createItem({ shareId, content });
58     const data: CustomAliasCreateRequest = {
59         Item: encryptedItem,
60         Prefix: item.extraData.prefix,
61         SignedSuffix: item.extraData.signedSuffix,
62         MailboxIDs: item.extraData.mailboxes.map(({ id }) => id),
63     };
65     const { Item } = await api({
66         url: `pass/v1/share/${shareId}/alias/custom`,
67         method: 'post',
68         data,
69     });
71     return Item!;
74 /* Specific item with alias API request: the first item
75  * returned will be the login item, followed by the alias */
76 export const createItemWithAlias = async (
77     createIntent: ItemCreateIntent<'login'> & { extraData: { withAlias: true } }
78 ): Promise<[ItemRevisionContentsResponse, ItemRevisionContentsResponse]> => {
79     const { shareId, ...item } = createIntent;
81     const loginItemContent = serializeItemContent(item);
82     const aliasItemContent = serializeItemContent(item.extraData.alias);
84     const encryptedLoginItem = await PassCrypto.createItem({ shareId, content: loginItemContent });
85     const encryptedAliasItem = await PassCrypto.createItem({ shareId, content: aliasItemContent });
87     const data: AliasAndItemCreateRequest = {
88         Item: encryptedLoginItem,
89         Alias: {
90             Prefix: item.extraData.alias.extraData.prefix,
91             SignedSuffix: item.extraData.alias.extraData.signedSuffix,
92             MailboxIDs: item.extraData.alias.extraData.mailboxes.map(({ id }) => id),
93             Item: encryptedAliasItem,
94         },
95     };
97     const { Bundle } = await api({
98         url: `pass/v1/share/${shareId}/item/with_alias`,
99         method: 'post',
100         data,
101     });
103     return [Bundle!.Item, Bundle!.Alias];
106 export const editItem = async (
107     editIntent: ItemEditIntent,
108     lastRevision: number
109 ): Promise<ItemRevisionContentsResponse> => {
110     const { shareId, itemId, ...item } = editIntent;
111     const content = serializeItemContent(item);
113     const latestItemKey = (
114         await api({
115             url: `pass/v1/share/${shareId}/item/${itemId}/key/latest`,
116             method: 'get',
117         })
118     ).Key!;
120     const data = await PassCrypto.updateItem({ shareId, content, lastRevision, latestItemKey });
122     const { Item } = await api({
123         url: `pass/v1/share/${shareId}/item/${itemId}`,
124         method: 'put',
125         data,
126     });
128     return Item!;
131 export const moveItem = async (
132     item: ItemRevision,
133     shareId: string,
134     destinationShareId: string
135 ): Promise<ItemRevision> => {
136     const content = serializeItemContent(item.data);
137     const data = await PassCrypto.moveItem({ destinationShareId, content });
139     const encryptedItem = (
140         await api({
141             url: `pass/v1/share/${shareId}/item/${item.itemId}/share`,
142             method: 'put',
143             data,
144         })
145     ).Item!;
147     return parseItemRevision(destinationShareId, encryptedItem);
150 export const moveItems = async (
151     items: ItemRevision[],
152     destinationShareId: string,
153     onBatch?: (
154         data: BatchItemRevisions & { movedItems: ItemRevision[]; destinationShareId: string },
155         progress: number
156     ) => void,
157     progress: number = 0
158 ): Promise<ItemRevision[]> =>
159     (
160         await Promise.all(
161             batchByShareId(items, identity).map(async ({ shareId, items }) => {
162                 const data: ItemMoveMultipleToShareRequest = {
163                     ShareID: destinationShareId,
164                     Items: await Promise.all(
165                         items.map<Promise<ItemMoveIndividualToShareRequest>>(async (item) => {
166                             const content = serializeItemContent(item.data);
168                             return {
169                                 ItemID: item.itemId,
170                                 Item: (await PassCrypto.moveItem({ destinationShareId, content })).Item,
171                             };
172                         })
173                     ),
174                 };
176                 const { Items = [] } = await api({ url: `pass/v1/share/${shareId}/item/share`, method: 'put', data });
177                 const decryptedItems = Items!.map((encrypted) => parseItemRevision(destinationShareId, encrypted));
178                 const movedItems = await Promise.all(decryptedItems);
180                 onBatch?.({ batch: items, movedItems, shareId, destinationShareId }, (progress += movedItems.length));
181                 return movedItems;
182             })
183         )
184     ).flat();
186 export const trashItems = async (
187     items: ItemRevision[],
188     onBatch?: (data: BatchItemRevisionIDs, progress: number) => void,
189     progress: number = 0
190 ) =>
191     (
192         await Promise.all(
193             batchByShareId(items, intoRevisionID).map(async ({ shareId, items: Items }) => {
194                 const response = await api({
195                     url: `pass/v1/share/${shareId}/item/trash`,
196                     method: 'post',
197                     data: { Items },
198                 });
200                 onBatch?.({ shareId, batch: Items }, (progress += Items.length));
201                 return response;
202             })
203         )
204     ).flatMap(({ Items }) => Items ?? []);
206 export const restoreItems = async (
207     items: ItemRevision[],
208     onBatch?: (data: BatchItemRevisionIDs, progress: number) => void,
209     progress: number = 0
210 ) =>
211     (
212         await Promise.all(
213             batchByShareId(items, intoRevisionID).map(async ({ shareId, items: Items }) => {
214                 const response = await api({
215                     url: `pass/v1/share/${shareId}/item/untrash`,
216                     method: 'post',
217                     data: { Items },
218                 });
220                 onBatch?.({ shareId, batch: Items }, (progress += Items.length));
221                 return response;
222             })
223         )
224     ).flatMap(({ Items }) => Items ?? []);
226 export const deleteItems = async (
227     items: ItemRevision[],
228     onBatch?: (data: BatchItemRevisionIDs, progress: number) => void,
229     progress: number = 0
230 ) =>
231     (
232         await Promise.all(
233             batchByShareId(items, intoRevisionID).map(async ({ shareId, items: Items }) => {
234                 await api({
235                     url: `pass/v1/share/${shareId}/item`,
236                     method: 'delete',
237                     data: { Items },
238                 });
240                 onBatch?.({ shareId, batch: Items }, (progress += items.length));
241                 return Items;
242             })
243         )
244     ).flat();
246 export const updateItemLastUseTime = async (shareId: string, itemId: string) =>
247     (
248         await api({
249             url: `pass/v1/share/${shareId}/item/${itemId}/lastuse`,
250             method: 'put',
251             data: { LastUseTime: getEpoch() },
252         })
253     ).Revision!;
255 export const requestAllItemsForShareId = async (
256     options: { shareId: string; OnlyAlias?: boolean },
257     onBatch?: (progress: number) => void
258 ): Promise<ItemRevisionContentsResponse[]> =>
259     createPageIterator({
260         onBatch,
261         request: async (Since) => {
262             const { Items } = await api({
263                 url: `pass/v1/share/${options.shareId}/item`,
264                 method: 'get',
265                 params: Since ? { Since, OnlyAlias: options.OnlyAlias } : {},
266             });
268             return { data: Items?.RevisionsData ?? [], cursor: Items?.LastToken };
269         },
270     })();
272 /** Will not throw on decryption errors : this avoids blocking the
273  *  user if one item is corrupted or is using a newer proto version */
274 export async function requestItemsForShareId(
275     shareId: string,
276     onBatch?: (progress: number) => void
277 ): Promise<ItemRevision[]> {
278     const encryptedItems = await requestAllItemsForShareId({ shareId }, onBatch);
279     const items = await Promise.all(encryptedItems.map((item) => parseItemRevision(shareId, item).catch(() => null)));
280     return items.filter(truthy);
283 export const importItemsBatch = async (options: {
284     shareId: string;
285     importIntents: ItemImportIntent[];
286     onSkippedItem?: (skipped: ItemImportIntent) => void;
287 }): Promise<ItemRevisionContentsResponse[]> => {
288     const { shareId, importIntents, onSkippedItem } = options;
289     const data: ImportItemBatchRequest = {
290         Items: (
291             await Promise.all(
292                 importIntents.map(async (importIntent): Promise<Maybe<ImportItemRequest>> => {
293                     const { trashed, createTime, modifyTime, ...item } = importIntent;
295                     try {
296                         return {
297                             Item: await PassCrypto.createItem({ shareId, content: serializeItemContent(item) }),
298                             AliasEmail: item.type === 'alias' ? item.extraData.aliasEmail : null,
299                             Trashed: trashed,
300                             CreateTime: createTime ?? null,
301                             ModifyTime: modifyTime ?? null,
302                         };
303                     } catch (e) {
304                         logger.info(`[Import] could not import "${item.metadata.name}"`);
305                         onSkippedItem?.(importIntent);
306                         return;
307                     }
308                 })
309             )
310         ).filter(truthy),
311     };
313     if (data.Items.length === 0) return [];
315     const result = await api({
316         url: `pass/v1/share/${shareId}/item/import/batch`,
317         method: 'post',
318         data,
319     });
321     if (result.Revisions?.RevisionsData === undefined) {
322         throw new Error(`Error while batch importing data`);
323     }
325     return result.Revisions.RevisionsData;
328 /** Update the item monitoring flag */
329 export const updateItemFlags = async (
330     shareId: string,
331     itemId: string,
332     data: ItemUpdateFlagsRequest
333 ): Promise<ItemRevisionContentsResponse> =>
334     (await api({ url: `pass/v1/share/${shareId}/item/${itemId}/flags`, method: 'put', data })).Item!;
336 export const pinItem = (shareId: string, itemId: string) =>
337     api({ url: `pass/v1/share/${shareId}/item/${itemId}/pin`, method: 'post' });
339 export const unpinItem = (shareId: string, itemId: string) =>
340     api({ url: `pass/v1/share/${shareId}/item/${itemId}/pin`, method: 'delete' });
342 export const getItemRevisions = async (
343     { shareId, itemId, pageSize, since }: ItemRevisionsIntent,
344     signal?: AbortSignal
345 ) => {
346     return (
347         await api({
348             url: `pass/v1/share/${shareId}/item/${itemId}/revision`,
349             params: { PageSize: pageSize, ...(since ? { Since: since } : {}) },
350             method: 'get',
351             signal,
352         })
353     ).Revisions!;