@striae-org/striae 3.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/.env.example +100 -0
  2. package/LICENSE +190 -0
  3. package/NOTICE +18 -0
  4. package/README.md +133 -0
  5. package/app/components/actions/case-export/core-export.ts +328 -0
  6. package/app/components/actions/case-export/data-processing.ts +167 -0
  7. package/app/components/actions/case-export/download-handlers.ts +900 -0
  8. package/app/components/actions/case-export/index.ts +41 -0
  9. package/app/components/actions/case-export/metadata-helpers.ts +107 -0
  10. package/app/components/actions/case-export/types-constants.ts +56 -0
  11. package/app/components/actions/case-export/validation-utils.ts +25 -0
  12. package/app/components/actions/case-export.ts +4 -0
  13. package/app/components/actions/case-import/annotation-import.ts +35 -0
  14. package/app/components/actions/case-import/confirmation-import.ts +363 -0
  15. package/app/components/actions/case-import/image-operations.ts +61 -0
  16. package/app/components/actions/case-import/index.ts +39 -0
  17. package/app/components/actions/case-import/orchestrator.ts +420 -0
  18. package/app/components/actions/case-import/storage-operations.ts +270 -0
  19. package/app/components/actions/case-import/validation.ts +189 -0
  20. package/app/components/actions/case-import/zip-processing.ts +413 -0
  21. package/app/components/actions/case-manage.ts +524 -0
  22. package/app/components/actions/case-review.ts +4 -0
  23. package/app/components/actions/confirm-export.ts +351 -0
  24. package/app/components/actions/generate-pdf.ts +210 -0
  25. package/app/components/actions/image-manage.ts +385 -0
  26. package/app/components/actions/notes-manage.ts +33 -0
  27. package/app/components/actions/signout.module.css +15 -0
  28. package/app/components/actions/signout.tsx +50 -0
  29. package/app/components/audit/user-audit-viewer.tsx +975 -0
  30. package/app/components/audit/user-audit.module.css +568 -0
  31. package/app/components/auth/auth-provider.tsx +78 -0
  32. package/app/components/auth/mfa-enrollment.module.css +268 -0
  33. package/app/components/auth/mfa-enrollment.tsx +398 -0
  34. package/app/components/auth/mfa-verification.module.css +251 -0
  35. package/app/components/auth/mfa-verification.tsx +295 -0
  36. package/app/components/button/button.module.css +63 -0
  37. package/app/components/button/button.tsx +46 -0
  38. package/app/components/canvas/box-annotations/box-annotations.module.css +170 -0
  39. package/app/components/canvas/box-annotations/box-annotations.tsx +634 -0
  40. package/app/components/canvas/canvas.module.css +314 -0
  41. package/app/components/canvas/canvas.tsx +449 -0
  42. package/app/components/canvas/confirmation/confirmation.module.css +187 -0
  43. package/app/components/canvas/confirmation/confirmation.tsx +214 -0
  44. package/app/components/colors/colors.module.css +59 -0
  45. package/app/components/colors/colors.tsx +68 -0
  46. package/app/components/form/base-form.tsx +21 -0
  47. package/app/components/form/form-button.tsx +28 -0
  48. package/app/components/form/form-field.tsx +53 -0
  49. package/app/components/form/form-message.tsx +17 -0
  50. package/app/components/form/form-toggle.tsx +23 -0
  51. package/app/components/form/form.module.css +427 -0
  52. package/app/components/form/index.ts +6 -0
  53. package/app/components/icon/icon.module.css +3 -0
  54. package/app/components/icon/icon.tsx +27 -0
  55. package/app/components/icon/icons.svg +102 -0
  56. package/app/components/icon/manifest.json +110 -0
  57. package/app/components/sidebar/case-export/case-export.module.css +386 -0
  58. package/app/components/sidebar/case-export/case-export.tsx +317 -0
  59. package/app/components/sidebar/case-import/case-import.module.css +626 -0
  60. package/app/components/sidebar/case-import/case-import.tsx +404 -0
  61. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +72 -0
  62. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +72 -0
  63. package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +71 -0
  64. package/app/components/sidebar/case-import/components/ExistingCaseSection.tsx +40 -0
  65. package/app/components/sidebar/case-import/components/FileSelector.tsx +161 -0
  66. package/app/components/sidebar/case-import/components/ProgressSection.tsx +46 -0
  67. package/app/components/sidebar/case-import/hooks/useFilePreview.ts +101 -0
  68. package/app/components/sidebar/case-import/hooks/useImportExecution.ts +152 -0
  69. package/app/components/sidebar/case-import/hooks/useImportState.ts +88 -0
  70. package/app/components/sidebar/case-import/index.ts +18 -0
  71. package/app/components/sidebar/case-import/utils/file-validation.ts +43 -0
  72. package/app/components/sidebar/cases/case-sidebar.tsx +827 -0
  73. package/app/components/sidebar/cases/cases-modal.module.css +166 -0
  74. package/app/components/sidebar/cases/cases-modal.tsx +201 -0
  75. package/app/components/sidebar/cases/cases.module.css +713 -0
  76. package/app/components/sidebar/files/files-modal.module.css +209 -0
  77. package/app/components/sidebar/files/files-modal.tsx +239 -0
  78. package/app/components/sidebar/hash/hash-utility.module.css +366 -0
  79. package/app/components/sidebar/hash/hash-utility.tsx +982 -0
  80. package/app/components/sidebar/notes/notes-modal.tsx +51 -0
  81. package/app/components/sidebar/notes/notes-sidebar.tsx +491 -0
  82. package/app/components/sidebar/notes/notes.module.css +360 -0
  83. package/app/components/sidebar/sidebar-container.tsx +149 -0
  84. package/app/components/sidebar/sidebar.module.css +321 -0
  85. package/app/components/sidebar/sidebar.tsx +215 -0
  86. package/app/components/sidebar/upload/image-upload-zone.module.css +123 -0
  87. package/app/components/sidebar/upload/image-upload-zone.tsx +330 -0
  88. package/app/components/theme-provider/theme-provider.tsx +131 -0
  89. package/app/components/theme-provider/theme.ts +155 -0
  90. package/app/components/toast/toast.module.css +137 -0
  91. package/app/components/toast/toast.tsx +56 -0
  92. package/app/components/toolbar/toolbar-color-selector.module.css +171 -0
  93. package/app/components/toolbar/toolbar-color-selector.tsx +129 -0
  94. package/app/components/toolbar/toolbar.module.css +42 -0
  95. package/app/components/toolbar/toolbar.tsx +167 -0
  96. package/app/components/user/delete-account.module.css +274 -0
  97. package/app/components/user/delete-account.tsx +471 -0
  98. package/app/components/user/inactivity-warning.module.css +145 -0
  99. package/app/components/user/inactivity-warning.tsx +84 -0
  100. package/app/components/user/manage-profile.module.css +190 -0
  101. package/app/components/user/manage-profile.tsx +253 -0
  102. package/app/components/user/mfa-phone-update.tsx +739 -0
  103. package/app/config-example/admin-service.json +13 -0
  104. package/app/config-example/config.json +17 -0
  105. package/app/config-example/firebase.ts +21 -0
  106. package/app/config-example/inactivity.ts +13 -0
  107. package/app/config-example/meta-config.json +6 -0
  108. package/app/contexts/auth.context.ts +12 -0
  109. package/app/entry.client.tsx +12 -0
  110. package/app/entry.server.tsx +44 -0
  111. package/app/hooks/useInactivityTimeout.ts +110 -0
  112. package/app/root.tsx +170 -0
  113. package/app/routes/_index.tsx +16 -0
  114. package/app/routes/auth/emailActionHandler.module.css +232 -0
  115. package/app/routes/auth/emailActionHandler.tsx +405 -0
  116. package/app/routes/auth/emailVerification.tsx +120 -0
  117. package/app/routes/auth/login.module.css +523 -0
  118. package/app/routes/auth/login.tsx +654 -0
  119. package/app/routes/auth/passwordReset.module.css +274 -0
  120. package/app/routes/auth/passwordReset.tsx +154 -0
  121. package/app/routes/auth/route.ts +16 -0
  122. package/app/routes/mobile-prevented/mobilePrevented.module.css +47 -0
  123. package/app/routes/mobile-prevented/mobilePrevented.tsx +26 -0
  124. package/app/routes/mobile-prevented/route.ts +14 -0
  125. package/app/routes/striae/striae.module.css +30 -0
  126. package/app/routes/striae/striae.tsx +417 -0
  127. package/app/services/audit-export.service.ts +755 -0
  128. package/app/services/audit.service.ts +1454 -0
  129. package/app/services/firebase-errors.ts +106 -0
  130. package/app/services/firebase.ts +15 -0
  131. package/app/styles/legal-pages.module.css +113 -0
  132. package/app/styles/root.module.css +146 -0
  133. package/app/tailwind.css +225 -0
  134. package/app/types/annotations.ts +45 -0
  135. package/app/types/audit.ts +301 -0
  136. package/app/types/case.ts +90 -0
  137. package/app/types/export.ts +8 -0
  138. package/app/types/file.ts +30 -0
  139. package/app/types/import.ts +107 -0
  140. package/app/types/index.ts +24 -0
  141. package/app/types/user.ts +38 -0
  142. package/app/utils/SHA256.ts +461 -0
  143. package/app/utils/annotation-timestamp.ts +25 -0
  144. package/app/utils/audit-export-signature.ts +117 -0
  145. package/app/utils/auth-action-settings.ts +48 -0
  146. package/app/utils/auth.ts +34 -0
  147. package/app/utils/batch-operations.ts +135 -0
  148. package/app/utils/confirmation-signature.ts +193 -0
  149. package/app/utils/data-operations.ts +871 -0
  150. package/app/utils/device-detection.ts +5 -0
  151. package/app/utils/html-sanitizer.ts +80 -0
  152. package/app/utils/id-generator.ts +36 -0
  153. package/app/utils/meta.ts +48 -0
  154. package/app/utils/mfa-phone.ts +97 -0
  155. package/app/utils/mfa.ts +79 -0
  156. package/app/utils/password-policy.ts +28 -0
  157. package/app/utils/permissions.ts +562 -0
  158. package/app/utils/signature-utils.ts +160 -0
  159. package/app/utils/style.ts +83 -0
  160. package/app/utils/version.ts +5 -0
  161. package/firebase.json +11 -0
  162. package/functions/[[path]].ts +10 -0
  163. package/package.json +138 -0
  164. package/postcss.config.js +6 -0
  165. package/public/.well-known/publickey.info@striae.org.asc +17 -0
  166. package/public/.well-known/security.txt +7 -0
  167. package/public/_headers +28 -0
  168. package/public/_routes.json +13 -0
  169. package/public/assets/striae.jpg +0 -0
  170. package/public/clear.jpg +0 -0
  171. package/public/favicon.ico +0 -0
  172. package/public/favicon.svg +9 -0
  173. package/public/icon-256.png +0 -0
  174. package/public/icon-512.png +0 -0
  175. package/public/logo-dark.png +0 -0
  176. package/public/manifest.json +25 -0
  177. package/public/oin-badge.png +0 -0
  178. package/public/shortcut.png +0 -0
  179. package/public/social-image.png +0 -0
  180. package/public/striae-ascii.txt +10 -0
  181. package/scripts/deploy-all.sh +100 -0
  182. package/scripts/deploy-config.sh +940 -0
  183. package/scripts/deploy-pages.sh +34 -0
  184. package/scripts/deploy-worker-secrets.sh +215 -0
  185. package/scripts/dev.cjs +23 -0
  186. package/scripts/install-workers.sh +88 -0
  187. package/scripts/run-eslint.cjs +35 -0
  188. package/scripts/update-compatibility-dates.cjs +124 -0
  189. package/scripts/update-markdown-versions.cjs +43 -0
  190. package/tailwind.config.ts +22 -0
  191. package/tsconfig.json +33 -0
  192. package/vite.config.ts +35 -0
  193. package/worker-configuration.d.ts +7490 -0
  194. package/workers/audit-worker/package.json +17 -0
  195. package/workers/audit-worker/src/audit-worker.example.ts +195 -0
  196. package/workers/audit-worker/worker-configuration.d.ts +7448 -0
  197. package/workers/audit-worker/wrangler.jsonc.example +29 -0
  198. package/workers/data-worker/package.json +17 -0
  199. package/workers/data-worker/src/data-worker.example.ts +267 -0
  200. package/workers/data-worker/src/signature-utils.ts +79 -0
  201. package/workers/data-worker/src/signing-payload-utils.ts +290 -0
  202. package/workers/data-worker/worker-configuration.d.ts +7448 -0
  203. package/workers/data-worker/wrangler.jsonc.example +30 -0
  204. package/workers/image-worker/package.json +17 -0
  205. package/workers/image-worker/src/image-worker.example.ts +180 -0
  206. package/workers/image-worker/worker-configuration.d.ts +7447 -0
  207. package/workers/image-worker/wrangler.jsonc.example +22 -0
  208. package/workers/keys-worker/package.json +17 -0
  209. package/workers/keys-worker/src/keys.example.ts +66 -0
  210. package/workers/keys-worker/src/keys.ts +66 -0
  211. package/workers/keys-worker/worker-configuration.d.ts +7447 -0
  212. package/workers/keys-worker/wrangler.jsonc.example +22 -0
  213. package/workers/pdf-worker/package.json +17 -0
  214. package/workers/pdf-worker/src/format-striae.ts +534 -0
  215. package/workers/pdf-worker/src/pdf-worker.example.ts +119 -0
  216. package/workers/pdf-worker/src/report-types.ts +69 -0
  217. package/workers/pdf-worker/worker-configuration.d.ts +7448 -0
  218. package/workers/pdf-worker/wrangler.jsonc.example +26 -0
  219. package/workers/user-worker/package.json +17 -0
  220. package/workers/user-worker/src/user-worker.example.ts +636 -0
  221. package/workers/user-worker/worker-configuration.d.ts +7448 -0
  222. package/workers/user-worker/wrangler.jsonc.example +29 -0
  223. package/wrangler.toml.example +8 -0
@@ -0,0 +1,385 @@
1
+ import { User } from 'firebase/auth';
2
+ import paths from '~/config/config.json';
3
+ import {
4
+ getImageApiKey,
5
+ getAccountHash
6
+ } from '~/utils/auth';
7
+ import { canUploadFile } from '~/utils/permissions';
8
+ import { getCaseData, updateCaseData, deleteFileAnnotations } from '~/utils/data-operations';
9
+ import { CaseData, FileData, ImageUploadResponse } from '~/types';
10
+ import { auditService } from '~/services/audit.service';
11
+
12
+ const IMAGE_URL = paths.image_worker_url;
13
+
14
+ export const fetchFiles = async (
15
+ user: User,
16
+ caseNumber: string,
17
+ options?: { skipValidation?: boolean }
18
+ ): Promise<FileData[]> => {
19
+ const caseData = await getCaseData(user, caseNumber, { skipValidation: options?.skipValidation });
20
+ return caseData?.files || [];
21
+ };
22
+
23
+ export const uploadFile = async (
24
+ user: User,
25
+ caseNumber: string,
26
+ file: File,
27
+ onProgress?: (progress: number) => void
28
+ ): Promise<FileData> => {
29
+ const startTime = Date.now();
30
+
31
+ // First, get current files to check count
32
+ const currentFiles = await fetchFiles(user, caseNumber);
33
+
34
+ // Check if user can upload another file
35
+ const permission = await canUploadFile(user, currentFiles.length);
36
+ if (!permission.canUpload) {
37
+ // Log permission denied
38
+ try {
39
+ await auditService.logFileUpload(
40
+ user,
41
+ file.name,
42
+ file.size,
43
+ file.type,
44
+ 'file-picker',
45
+ caseNumber,
46
+ 'failure',
47
+ Date.now() - startTime
48
+ );
49
+ } catch (auditError) {
50
+ console.error('Failed to log file upload permission denial:', auditError);
51
+ }
52
+ throw new Error(permission.reason || 'You cannot upload more files to this case.');
53
+ }
54
+
55
+ const imagesApiToken = await getImageApiKey();
56
+
57
+ return new Promise((resolve, reject) => {
58
+ const xhr = new XMLHttpRequest();
59
+ const formData = new FormData();
60
+ formData.append('file', file);
61
+
62
+ xhr.upload.addEventListener('progress', (event) => {
63
+ if (event.lengthComputable && onProgress) {
64
+ const progress = Math.round((event.loaded / event.total) * 100);
65
+ onProgress(progress);
66
+ }
67
+ });
68
+
69
+ xhr.addEventListener('load', async () => {
70
+ const endTime = Date.now();
71
+
72
+ if (xhr.status === 200) {
73
+ try {
74
+ const imageData = JSON.parse(xhr.responseText) as ImageUploadResponse;
75
+ if (!imageData.success) throw new Error('Upload failed');
76
+
77
+ const newFile: FileData = {
78
+ id: imageData.result.id,
79
+ originalFilename: file.name,
80
+ uploadedAt: new Date().toISOString()
81
+ };
82
+
83
+ // Update case data using centralized function
84
+ const existingData = await getCaseData(user, caseNumber);
85
+ if (!existingData) {
86
+ throw new Error('Case not found');
87
+ }
88
+
89
+ const updatedData = {
90
+ ...existingData,
91
+ files: [...(existingData.files || []), newFile]
92
+ };
93
+
94
+ await updateCaseData(user, caseNumber, updatedData);
95
+
96
+ // Log successful file upload
97
+ try {
98
+ await auditService.logFileUpload(
99
+ user,
100
+ file.name,
101
+ file.size,
102
+ file.type,
103
+ 'file-picker',
104
+ caseNumber,
105
+ 'success',
106
+ endTime - startTime,
107
+ imageData.result.id
108
+ );
109
+ } catch (auditError) {
110
+ console.error('Failed to log successful file upload:', auditError);
111
+ }
112
+
113
+ console.log(`✅ File uploaded: ${file.name} (${file.size} bytes) (${endTime - startTime}ms)`);
114
+ resolve(newFile);
115
+ } catch (error) {
116
+ // Log failed file upload
117
+ try {
118
+ await auditService.logFileUpload(
119
+ user,
120
+ file.name,
121
+ file.size,
122
+ file.type,
123
+ 'file-picker',
124
+ caseNumber,
125
+ 'failure',
126
+ endTime - startTime
127
+ );
128
+ } catch (auditError) {
129
+ console.error('Failed to log file upload failure:', auditError);
130
+ }
131
+ reject(error);
132
+ }
133
+ } else {
134
+ // Log failed file upload
135
+ try {
136
+ await auditService.logFileUpload(
137
+ user,
138
+ file.name,
139
+ file.size,
140
+ file.type,
141
+ 'file-picker',
142
+ caseNumber,
143
+ 'failure',
144
+ endTime - startTime
145
+ );
146
+ } catch (auditError) {
147
+ console.error('Failed to log file upload failure:', auditError);
148
+ }
149
+ reject(new Error('Upload failed'));
150
+ }
151
+ });
152
+
153
+ xhr.addEventListener('error', async () => {
154
+ // Log upload error
155
+ try {
156
+ await auditService.logFileUpload(
157
+ user,
158
+ file.name,
159
+ file.size,
160
+ file.type,
161
+ 'file-picker',
162
+ caseNumber,
163
+ 'failure',
164
+ Date.now() - startTime
165
+ );
166
+ } catch (auditError) {
167
+ console.error('Failed to log file upload error:', auditError);
168
+ }
169
+ reject(new Error('Upload failed'));
170
+ });
171
+
172
+ xhr.open('POST', IMAGE_URL);
173
+ xhr.setRequestHeader('Authorization', `Bearer ${imagesApiToken}`);
174
+ xhr.send(formData);
175
+ });
176
+ };
177
+
178
+ export const deleteFile = async (user: User, caseNumber: string, fileId: string, deleteReason: string = 'User-requested deletion via file list'): Promise<void> => {
179
+ const startTime = Date.now();
180
+
181
+ // Get file info for audit logging (outside try block so it's available in catch)
182
+ let fileName = fileId; // Default to fileId
183
+ let fileToDelete: FileData | undefined;
184
+
185
+ try {
186
+ // Get the case data using centralized function
187
+ const caseData = await getCaseData(user, caseNumber);
188
+ if (!caseData) {
189
+ throw new Error('Case not found');
190
+ }
191
+
192
+ fileToDelete = (caseData.files || []).find((f: FileData) => f.id === fileId);
193
+ fileName = fileToDelete?.originalFilename || fileId;
194
+ const fileSize = 0; // We don't store file size, so use 0
195
+
196
+ let imageDeleteFailed = false;
197
+ let imageDeleteError = '';
198
+
199
+ // Attempt to delete image file
200
+ const imagesApiToken = await getImageApiKey();
201
+ const imageResponse = await fetch(`${IMAGE_URL}/${fileId}`, {
202
+ method: 'DELETE',
203
+ headers: {
204
+ 'Authorization': `Bearer ${imagesApiToken}`
205
+ }
206
+ });
207
+
208
+ // Handle image deletion response
209
+ if (!imageResponse.ok) {
210
+ if (imageResponse.status === 404) {
211
+ // Image already doesn't exist - proceed with data cleanup
212
+ console.warn(`Image ${fileId} not found (404) - proceeding with data cleanup`);
213
+ } else {
214
+ // Other errors should still fail the operation
215
+ imageDeleteFailed = true;
216
+ imageDeleteError = `Failed to delete image: ${imageResponse.statusText}`;
217
+ }
218
+ }
219
+
220
+ // If image deletion failed with non-404 error, don't proceed with data cleanup
221
+ if (imageDeleteFailed) {
222
+ throw new Error(imageDeleteError);
223
+ }
224
+
225
+ // Clean up data files regardless of image deletion success/404
226
+ // Try to delete notes file using centralized function
227
+ try {
228
+ await deleteFileAnnotations(user, caseNumber, fileId);
229
+ } catch (error) {
230
+ // Ignore 404 errors - notes file might not exist
231
+ console.log('Notes file deletion result:', error);
232
+ }
233
+
234
+ // Update case data.json to remove file reference using centralized function
235
+ const updatedData: CaseData = {
236
+ ...caseData,
237
+ files: (caseData.files || []).filter((f: FileData) => f.id !== fileId)
238
+ };
239
+
240
+ await updateCaseData(user, caseNumber, updatedData);
241
+
242
+ // Log successful file deletion
243
+ const endTime = Date.now();
244
+ try {
245
+ await auditService.logFileDeletion(
246
+ user,
247
+ fileName,
248
+ fileSize,
249
+ deleteReason,
250
+ caseNumber,
251
+ fileId,
252
+ fileToDelete?.originalFilename
253
+ );
254
+ } catch (auditError) {
255
+ console.error('Failed to log file deletion:', auditError);
256
+ }
257
+
258
+ console.log(`✅ File deleted: ${fileName} (${endTime - startTime}ms)`);
259
+
260
+ } catch (error) {
261
+ // Log failed file deletion
262
+ const endTime = Date.now();
263
+ try {
264
+ await auditService.logEvent({
265
+ userId: user.uid,
266
+ userEmail: user.email || '',
267
+ action: 'file-delete',
268
+ result: 'failure',
269
+ fileName: fileName, // Now uses the original filename
270
+ fileType: 'unknown',
271
+ validationErrors: [error instanceof Error ? error.message : 'Unknown error'],
272
+ caseNumber,
273
+ fileDetails: {
274
+ fileId: fileId,
275
+ fileSize: 0,
276
+ deleteReason: 'Failed deletion attempt',
277
+ originalFileName: fileToDelete?.originalFilename
278
+ },
279
+ performanceMetrics: {
280
+ processingTimeMs: endTime - startTime,
281
+ fileSizeBytes: 0
282
+ }
283
+ });
284
+ } catch (auditError) {
285
+ console.error('Failed to log file deletion failure:', auditError);
286
+ }
287
+
288
+ console.error('Error in deleteFile:', error);
289
+ throw error;
290
+ }
291
+ };
292
+
293
+ const DEFAULT_VARIANT = 'striae';
294
+ interface ImageDeliveryConfig {
295
+ accountHash: string;
296
+ }
297
+
298
+ const getImageConfig = async (): Promise<ImageDeliveryConfig> => {
299
+ const accountHash = await getAccountHash();
300
+ return { accountHash };
301
+ };
302
+
303
+
304
+ export const getImageUrl = async (user: User, fileData: FileData, caseNumber: string, accessReason?: string): Promise<string> => {
305
+ const startTime = Date.now();
306
+ const defaultAccessReason = accessReason || 'Image viewer access';
307
+
308
+ try {
309
+ const { accountHash } = await getImageConfig();
310
+ const imagesApiToken = await getImageApiKey();
311
+ const imageDeliveryUrl = `https://imagedelivery.net/${accountHash}/${fileData.id}/${DEFAULT_VARIANT}`;
312
+
313
+ const workerResponse = await fetch(`${IMAGE_URL}/${imageDeliveryUrl}`, {
314
+ method: 'GET',
315
+ headers: {
316
+ 'Authorization': `Bearer ${imagesApiToken}`,
317
+ 'Accept': 'text/plain'
318
+ }
319
+ });
320
+
321
+ if (!workerResponse.ok) {
322
+ // Log failed image access
323
+ await auditService.logFileAccess(
324
+ user,
325
+ fileData.originalFilename || fileData.id,
326
+ fileData.id,
327
+ 'signed-url',
328
+ caseNumber,
329
+ 'failure',
330
+ Date.now() - startTime,
331
+ 'Image URL generation failed',
332
+ fileData.originalFilename
333
+ );
334
+ throw new Error('Failed to get signed image URL');
335
+ }
336
+
337
+ const signedUrl = await workerResponse.text();
338
+ if (!signedUrl.includes('sig=') || !signedUrl.includes('exp=')) {
339
+ // Log invalid URL response
340
+ await auditService.logFileAccess(
341
+ user,
342
+ fileData.originalFilename || fileData.id,
343
+ fileData.id,
344
+ 'signed-url',
345
+ caseNumber,
346
+ 'failure',
347
+ Date.now() - startTime,
348
+ 'Invalid signed URL returned',
349
+ fileData.originalFilename
350
+ );
351
+ throw new Error('Invalid signed URL returned');
352
+ }
353
+
354
+ // Log successful image access
355
+ await auditService.logFileAccess(
356
+ user,
357
+ fileData.originalFilename || fileData.id,
358
+ fileData.id,
359
+ 'signed-url',
360
+ caseNumber,
361
+ 'success',
362
+ Date.now() - startTime,
363
+ defaultAccessReason,
364
+ fileData.originalFilename
365
+ );
366
+
367
+ return signedUrl;
368
+ } catch (error) {
369
+ // Log any unexpected errors if not already logged
370
+ if (!(error instanceof Error && error.message.includes('Failed to get signed image URL'))) {
371
+ await auditService.logFileAccess(
372
+ user,
373
+ fileData.originalFilename || fileData.id,
374
+ fileData.id,
375
+ 'signed-url',
376
+ caseNumber,
377
+ 'failure',
378
+ Date.now() - startTime,
379
+ `Unexpected error during ${accessReason || 'image access'}`,
380
+ fileData.originalFilename
381
+ );
382
+ }
383
+ throw error;
384
+ }
385
+ };
@@ -0,0 +1,33 @@
1
+ import { User } from 'firebase/auth';
2
+ import { AnnotationData } from '~/types/annotations';
3
+ import { saveFileAnnotations, getFileAnnotations, DataOperationOptions } from '~/utils/data-operations';
4
+
5
+ export const saveNotes = async (
6
+ user: User,
7
+ caseNumber: string,
8
+ imageId: string,
9
+ annotationData: AnnotationData,
10
+ options: DataOperationOptions = {}
11
+ ): Promise<void> => {
12
+ try {
13
+ // Use centralized function with built-in validation and error handling
14
+ await saveFileAnnotations(user, caseNumber, imageId, annotationData, options);
15
+ } catch (error) {
16
+ console.error('Error saving notes:', error);
17
+ throw error;
18
+ }
19
+ };
20
+
21
+ export const getNotes = async (
22
+ user: User,
23
+ caseNumber: string,
24
+ imageId: string
25
+ ): Promise<AnnotationData | null> => {
26
+ try {
27
+ // Use centralized function with built-in validation and error handling
28
+ return await getFileAnnotations(user, caseNumber, imageId);
29
+ } catch (error) {
30
+ console.error('Error fetching notes:', error);
31
+ return null;
32
+ }
33
+ };
@@ -0,0 +1,15 @@
1
+ .signOutButton {
2
+ padding: 0.5rem 1rem;
3
+ background-color: #dc3545;
4
+ color: white;
5
+ border: none;
6
+ border-radius: 4px;
7
+ font-size: 0.85rem;
8
+ cursor: pointer;
9
+ transition: all 0.2s;
10
+ align-self: flex-start;
11
+ }
12
+
13
+ .signOutButton:hover {
14
+ background-color: #bb2d3b;
15
+ }
@@ -0,0 +1,50 @@
1
+ import { auth } from '~/services/firebase';
2
+ import { auditService } from '~/services/audit.service';
3
+ import { generateUniqueId } from '~/utils/id-generator';
4
+ import styles from './signout.module.css';
5
+
6
+ interface SignOutProps {
7
+ redirectTo?: string;
8
+ disabled?: boolean;
9
+ }
10
+
11
+ export const SignOut = ({ redirectTo = '/', disabled = false }: SignOutProps) => {
12
+ const handleSignOut = async () => {
13
+ try {
14
+ const user = auth.currentUser;
15
+
16
+ // Log logout audit before signing out
17
+ if (user) {
18
+ try {
19
+ const sessionId = `session_${user.uid}_logout_${Date.now()}_${generateUniqueId(8)}`;
20
+ await auditService.logUserLogout(
21
+ user,
22
+ sessionId,
23
+ 0, // sessionDuration - we don't track session start time here
24
+ 'user-initiated'
25
+ );
26
+ } catch (auditError) {
27
+ console.error('Failed to log user logout audit:', auditError);
28
+ // Continue with logout even if audit logging fails
29
+ }
30
+ }
31
+
32
+ await auth.signOut();
33
+ localStorage.clear();
34
+ window.location.href = redirectTo;
35
+ } catch (error) {
36
+ console.error('Sign out error:', error);
37
+ }
38
+ };
39
+
40
+ return (
41
+ <button
42
+ onClick={handleSignOut}
43
+ className={styles.signOutButton}
44
+ disabled={disabled}
45
+ title={disabled ? "Cannot sign out while uploading files" : undefined}
46
+ >
47
+ Sign Out
48
+ </button>
49
+ );
50
+ };