Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / packages / docs-core / lib / UseCase / LoadDocument.ts
blobd7fc622b8dc35fc6cf96dba95bb34f966ea984a1
1 import { getCanWrite } from '@proton/shared/lib/drive/permissions'
2 import { Result } from '@proton/docs-shared'
3 import { DocumentRole, type DocumentMetaInterface } from '@proton/docs-shared'
4 import type { NodeMeta, PublicNodeMeta, DecryptedNode } from '@proton/drive-store'
5 import type { GetDocumentMeta } from './GetDocumentMeta'
6 import { getErrorString } from '../Util/GetErrorString'
7 import type { DocumentEntitlements, PublicDocumentEntitlements } from '../Types/DocumentEntitlements'
8 import { rawPermissionToRole } from '../Types/DocumentEntitlements'
9 import type { GetNode } from './GetNode'
10 import type { DriveCompatWrapper } from '@proton/drive-store/lib/DriveCompatWrapper'
11 import type { LoadCommit } from './LoadCommit'
12 import type { LoggerInterface } from '@proton/utils/logs'
13 import type { DecryptedCommit } from '../Models/DecryptedCommit'
15 type LoadDocumentResult<E extends DocumentEntitlements | PublicDocumentEntitlements> = {
16   entitlements: E
17   meta: DocumentMetaInterface
18   node: DecryptedNode
19   decryptedCommit?: DecryptedCommit
22 /**
23  * Performs initial loading procedure for document, including fetching keys and latest commit binary from DX.
24  */
25 export class LoadDocument {
26   constructor(
27     private compatWrapper: DriveCompatWrapper,
28     private getDocumentMeta: GetDocumentMeta,
29     private getNode: GetNode,
30     private loadCommit: LoadCommit,
31     private logger: LoggerInterface,
32   ) {}
34   async executePrivate(nodeMeta: NodeMeta): Promise<Result<LoadDocumentResult<DocumentEntitlements>>> {
35     if (!this.compatWrapper.userCompat) {
36       return Result.fail('User drive compat not found')
37     }
38     try {
39       const [nodeResult, keysResult, fetchResult, permissionsResult] = await Promise.all([
40         this.getNode.execute(nodeMeta).catch((error) => {
41           throw new Error(`Failed to load node: ${error}`)
42         }),
43         this.compatWrapper.userCompat.getDocumentKeys(nodeMeta).catch((error) => {
44           throw new Error(`Failed to load keys: ${error}`)
45         }),
46         this.getDocumentMeta.execute(nodeMeta).catch((error) => {
47           throw new Error(`Failed to fetch document metadata: ${error}`)
48         }),
49         this.compatWrapper.userCompat.getNodePermissions(nodeMeta).catch((error) => {
50           throw new Error(`Failed to load permissions: ${error}`)
51         }),
52       ])
54       if (fetchResult.isFailed()) {
55         return Result.fail(fetchResult.getError())
56       }
58       if (nodeResult.isFailed()) {
59         return Result.fail(nodeResult.getError())
60       }
61       const node = nodeResult.getValue().node
63       const serverBasedMeta: DocumentMetaInterface = fetchResult.getValue()
64       if (!serverBasedMeta) {
65         return Result.fail('Document meta not found')
66       }
68       const decryptedMeta = serverBasedMeta.copyWithNewValues({ name: node.name })
70       if (!permissionsResult) {
71         return Result.fail('Unable to load permissions')
72       }
74       if (!keysResult) {
75         return Result.fail('Unable to load all necessary data')
76       }
78       const entitlements: DocumentEntitlements = {
79         keys: keysResult,
80         role: permissionsResult ? rawPermissionToRole(permissionsResult) : new DocumentRole('PublicViewer'),
81         nodeMeta,
82       }
84       const latestCommitId = serverBasedMeta.latestCommitId()
85       let decryptedCommit: DecryptedCommit | undefined
87       if (latestCommitId) {
88         const decryptResult = await this.loadCommit.execute(
89           nodeMeta,
90           latestCommitId,
91           entitlements.keys.documentContentKey,
92         )
93         if (decryptResult.isFailed()) {
94           return Result.fail(decryptResult.getError())
95         }
97         decryptedCommit = decryptResult.getValue()
98         this.logger.info(`Downloaded and decrypted commit with ${decryptedCommit?.numberOfUpdates()} updates`)
99       }
101       return Result.ok({ entitlements, meta: decryptedMeta, node: node, decryptedCommit })
102     } catch (error) {
103       return Result.fail(getErrorString(error) ?? 'Failed to load document')
104     }
105   }
107   async executePublic(
108     nodeMeta: PublicNodeMeta,
109     publicEditingEnabled: boolean,
110   ): Promise<Result<LoadDocumentResult<PublicDocumentEntitlements>>> {
111     if (!this.compatWrapper.publicCompat) {
112       return Result.fail('Public drive compat not found')
113     }
115     const permissions = this.compatWrapper.publicCompat.permissions
117     if (!permissions) {
118       return Result.fail('Permissions not yet loaded')
119     }
121     try {
122       const [nodeResult, keysResult, fetchResult] = await Promise.all([
123         this.getNode.execute(nodeMeta).catch((error) => {
124           throw new Error(`Failed to load public node: ${error}`)
125         }),
126         this.compatWrapper.publicCompat.getDocumentKeys(nodeMeta).catch((error) => {
127           throw new Error(`Failed to load public keys: ${error}`)
128         }),
129         this.getDocumentMeta.execute(nodeMeta).catch((error) => {
130           throw new Error(`Failed to fetch document metadata: ${error}`)
131         }),
132       ])
134       const decryptedNode = nodeResult.getValue().node
136       if (fetchResult.isFailed()) {
137         return Result.fail(fetchResult.getError())
138       }
140       const serverBasedMeta: DocumentMetaInterface = fetchResult.getValue()
141       if (!serverBasedMeta) {
142         return Result.fail('Document meta not found')
143       }
145       const decryptedMeta = serverBasedMeta.copyWithNewValues({ name: decryptedNode.name })
147       if (!keysResult) {
148         return Result.fail('Unable to load all necessary data')
149       }
151       /**
152        * We attempt to determine if the current public session user can load the actual document meta via the
153        * authenticated API.
154        *
155        * If it succeeds, this means the user has some sort of access to this document, and can perform
156        * actions like duplicating it.
157        */
158       const authenticatedMetaAttempt = await this.getDocumentMeta.execute({
159         volumeId: decryptedMeta.volumeId,
160         linkId: nodeMeta.linkId,
161       })
163       const doesHaveAccessToDoc = !authenticatedMetaAttempt.isFailed()
165       const role = (() => {
166         if (publicEditingEnabled && getCanWrite(permissions)) {
167           return new DocumentRole('PublicEditor')
168         }
169         if (doesHaveAccessToDoc) {
170           return new DocumentRole('PublicViewerWithAccess')
171         }
172         return new DocumentRole('PublicViewer')
173       })()
175       const entitlements: PublicDocumentEntitlements = {
176         keys: keysResult,
177         role,
178         nodeMeta,
179       }
181       const latestCommitId = serverBasedMeta.latestCommitId()
182       let decryptedCommit: DecryptedCommit | undefined
184       if (latestCommitId) {
185         const decryptResult = await this.loadCommit.execute(
186           nodeMeta,
187           latestCommitId,
188           entitlements.keys.documentContentKey,
189         )
190         if (decryptResult.isFailed()) {
191           return Result.fail(decryptResult.getError())
192         }
194         decryptedCommit = decryptResult.getValue()
195         this.logger.info(`Downloaded and decrypted commit with ${decryptedCommit?.numberOfUpdates()} updates`)
196       }
198       return Result.ok({ entitlements, meta: decryptedMeta, node: decryptedNode, decryptedCommit })
199     } catch (error) {
200       return Result.fail(getErrorString(error) ?? 'Failed to load document')
201     }
202   }