Merge branch 'translations_2024-12-05_050340' into 'main'
[ProtonMail-WebClient.git] / packages / pass / lib / items / item.requests.ts
blob1c924c94a8f052eb5c0b5a24cb0f987ac39205ce
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                                 /* TODO: add array of revisions to not lose history
172                                  * after moving an item to another vault */
173                                 History: [],
174                             };
175                         })
176                     ),
177                 };
179                 const { Items = [] } = await api({ url: `pass/v1/share/${shareId}/item/share`, method: 'put', data });
180                 const decryptedItems = Items!.map((encrypted) => parseItemRevision(destinationShareId, encrypted));
181                 const movedItems = await Promise.all(decryptedItems);
183                 onBatch?.({ batch: items, movedItems, shareId, destinationShareId }, (progress += movedItems.length));
184                 return movedItems;
185             })
186         )
187     ).flat();
189 export const trashItems = async (
190     items: ItemRevision[],
191     onBatch?: (data: BatchItemRevisionIDs, progress: number) => void,
192     progress: number = 0
193 ) =>
194     (
195         await Promise.all(
196             batchByShareId(items, intoRevisionID).map(async ({ shareId, items: Items }) => {
197                 const response = await api({
198                     url: `pass/v1/share/${shareId}/item/trash`,
199                     method: 'post',
200                     data: { Items },
201                 });
203                 onBatch?.({ shareId, batch: Items }, (progress += Items.length));
204                 return response;
205             })
206         )
207     ).flatMap(({ Items }) => Items ?? []);
209 export const restoreItems = async (
210     items: ItemRevision[],
211     onBatch?: (data: BatchItemRevisionIDs, progress: number) => void,
212     progress: number = 0
213 ) =>
214     (
215         await Promise.all(
216             batchByShareId(items, intoRevisionID).map(async ({ shareId, items: Items }) => {
217                 const response = await api({
218                     url: `pass/v1/share/${shareId}/item/untrash`,
219                     method: 'post',
220                     data: { Items },
221                 });
223                 onBatch?.({ shareId, batch: Items }, (progress += Items.length));
224                 return response;
225             })
226         )
227     ).flatMap(({ Items }) => Items ?? []);
229 export const deleteItems = async (
230     items: ItemRevision[],
231     onBatch?: (data: BatchItemRevisionIDs, progress: number) => void,
232     progress: number = 0
233 ) =>
234     (
235         await Promise.all(
236             batchByShareId(items, intoRevisionID).map(async ({ shareId, items: Items }) => {
237                 await api({
238                     url: `pass/v1/share/${shareId}/item`,
239                     method: 'delete',
240                     data: { Items },
241                 });
243                 onBatch?.({ shareId, batch: Items }, (progress += items.length));
244                 return Items;
245             })
246         )
247     ).flat();
249 export const updateItemLastUseTime = async (shareId: string, itemId: string) =>
250     (
251         await api({
252             url: `pass/v1/share/${shareId}/item/${itemId}/lastuse`,
253             method: 'put',
254             data: { LastUseTime: getEpoch() },
255         })
256     ).Revision!;
258 export const requestAllItemsForShareId = async (
259     options: { shareId: string; OnlyAlias?: boolean },
260     onBatch?: (progress: number) => void
261 ): Promise<ItemRevisionContentsResponse[]> =>
262     createPageIterator({
263         onBatch,
264         request: async (Since) => {
265             const { Items } = await api({
266                 url: `pass/v1/share/${options.shareId}/item`,
267                 method: 'get',
268                 params: Since ? { Since, OnlyAlias: options.OnlyAlias } : {},
269             });
271             return { data: Items?.RevisionsData ?? [], cursor: Items?.LastToken };
272         },
273     })();
275 /** Will not throw on decryption errors : this avoids blocking the
276  *  user if one item is corrupted or is using a newer proto version */
277 export async function requestItemsForShareId(
278     shareId: string,
279     onBatch?: (progress: number) => void
280 ): Promise<ItemRevision[]> {
281     const encryptedItems = await requestAllItemsForShareId({ shareId }, onBatch);
282     const items = await Promise.all(encryptedItems.map((item) => parseItemRevision(shareId, item).catch(() => null)));
283     return items.filter(truthy);
286 export const importItemsBatch = async (options: {
287     shareId: string;
288     importIntents: ItemImportIntent[];
289     onSkippedItem?: (skipped: ItemImportIntent) => void;
290 }): Promise<ItemRevisionContentsResponse[]> => {
291     const { shareId, importIntents, onSkippedItem } = options;
292     const data: ImportItemBatchRequest = {
293         Items: (
294             await Promise.all(
295                 importIntents.map(async (importIntent): Promise<Maybe<ImportItemRequest>> => {
296                     const { trashed, createTime, modifyTime, ...item } = importIntent;
298                     try {
299                         return {
300                             Item: await PassCrypto.createItem({ shareId, content: serializeItemContent(item) }),
301                             AliasEmail: item.type === 'alias' ? item.extraData.aliasEmail : null,
302                             Trashed: trashed,
303                             CreateTime: createTime ?? null,
304                             ModifyTime: modifyTime ?? null,
305                         };
306                     } catch (e) {
307                         logger.info(`[Import] could not import "${item.metadata.name}"`);
308                         onSkippedItem?.(importIntent);
309                         return;
310                     }
311                 })
312             )
313         ).filter(truthy),
314     };
316     if (data.Items.length === 0) return [];
318     const result = await api({
319         url: `pass/v1/share/${shareId}/item/import/batch`,
320         method: 'post',
321         data,
322     });
324     if (result.Revisions?.RevisionsData === undefined) {
325         throw new Error(`Error while batch importing data`);
326     }
328     return result.Revisions.RevisionsData;
331 /** Update the item monitoring flag */
332 export const updateItemFlags = async (
333     shareId: string,
334     itemId: string,
335     data: ItemUpdateFlagsRequest
336 ): Promise<ItemRevisionContentsResponse> =>
337     (await api({ url: `pass/v1/share/${shareId}/item/${itemId}/flags`, method: 'put', data })).Item!;
339 export const pinItem = (shareId: string, itemId: string) =>
340     api({ url: `pass/v1/share/${shareId}/item/${itemId}/pin`, method: 'post' });
342 export const unpinItem = (shareId: string, itemId: string) =>
343     api({ url: `pass/v1/share/${shareId}/item/${itemId}/pin`, method: 'delete' });
345 export const getItemRevisions = async (
346     { shareId, itemId, pageSize, since }: ItemRevisionsIntent,
347     signal?: AbortSignal
348 ) => {
349     return (
350         await api({
351             url: `pass/v1/share/${shareId}/item/${itemId}/revision`,
352             params: { PageSize: pageSize, ...(since ? { Since: since } : {}) },
353             method: 'get',
354             signal,
355         })
356     ).Revisions!;