@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.
- package/.env.example +100 -0
- package/LICENSE +190 -0
- package/NOTICE +18 -0
- package/README.md +133 -0
- package/app/components/actions/case-export/core-export.ts +328 -0
- package/app/components/actions/case-export/data-processing.ts +167 -0
- package/app/components/actions/case-export/download-handlers.ts +900 -0
- package/app/components/actions/case-export/index.ts +41 -0
- package/app/components/actions/case-export/metadata-helpers.ts +107 -0
- package/app/components/actions/case-export/types-constants.ts +56 -0
- package/app/components/actions/case-export/validation-utils.ts +25 -0
- package/app/components/actions/case-export.ts +4 -0
- package/app/components/actions/case-import/annotation-import.ts +35 -0
- package/app/components/actions/case-import/confirmation-import.ts +363 -0
- package/app/components/actions/case-import/image-operations.ts +61 -0
- package/app/components/actions/case-import/index.ts +39 -0
- package/app/components/actions/case-import/orchestrator.ts +420 -0
- package/app/components/actions/case-import/storage-operations.ts +270 -0
- package/app/components/actions/case-import/validation.ts +189 -0
- package/app/components/actions/case-import/zip-processing.ts +413 -0
- package/app/components/actions/case-manage.ts +524 -0
- package/app/components/actions/case-review.ts +4 -0
- package/app/components/actions/confirm-export.ts +351 -0
- package/app/components/actions/generate-pdf.ts +210 -0
- package/app/components/actions/image-manage.ts +385 -0
- package/app/components/actions/notes-manage.ts +33 -0
- package/app/components/actions/signout.module.css +15 -0
- package/app/components/actions/signout.tsx +50 -0
- package/app/components/audit/user-audit-viewer.tsx +975 -0
- package/app/components/audit/user-audit.module.css +568 -0
- package/app/components/auth/auth-provider.tsx +78 -0
- package/app/components/auth/mfa-enrollment.module.css +268 -0
- package/app/components/auth/mfa-enrollment.tsx +398 -0
- package/app/components/auth/mfa-verification.module.css +251 -0
- package/app/components/auth/mfa-verification.tsx +295 -0
- package/app/components/button/button.module.css +63 -0
- package/app/components/button/button.tsx +46 -0
- package/app/components/canvas/box-annotations/box-annotations.module.css +170 -0
- package/app/components/canvas/box-annotations/box-annotations.tsx +634 -0
- package/app/components/canvas/canvas.module.css +314 -0
- package/app/components/canvas/canvas.tsx +449 -0
- package/app/components/canvas/confirmation/confirmation.module.css +187 -0
- package/app/components/canvas/confirmation/confirmation.tsx +214 -0
- package/app/components/colors/colors.module.css +59 -0
- package/app/components/colors/colors.tsx +68 -0
- package/app/components/form/base-form.tsx +21 -0
- package/app/components/form/form-button.tsx +28 -0
- package/app/components/form/form-field.tsx +53 -0
- package/app/components/form/form-message.tsx +17 -0
- package/app/components/form/form-toggle.tsx +23 -0
- package/app/components/form/form.module.css +427 -0
- package/app/components/form/index.ts +6 -0
- package/app/components/icon/icon.module.css +3 -0
- package/app/components/icon/icon.tsx +27 -0
- package/app/components/icon/icons.svg +102 -0
- package/app/components/icon/manifest.json +110 -0
- package/app/components/sidebar/case-export/case-export.module.css +386 -0
- package/app/components/sidebar/case-export/case-export.tsx +317 -0
- package/app/components/sidebar/case-import/case-import.module.css +626 -0
- package/app/components/sidebar/case-import/case-import.tsx +404 -0
- package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +72 -0
- package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +72 -0
- package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +71 -0
- package/app/components/sidebar/case-import/components/ExistingCaseSection.tsx +40 -0
- package/app/components/sidebar/case-import/components/FileSelector.tsx +161 -0
- package/app/components/sidebar/case-import/components/ProgressSection.tsx +46 -0
- package/app/components/sidebar/case-import/hooks/useFilePreview.ts +101 -0
- package/app/components/sidebar/case-import/hooks/useImportExecution.ts +152 -0
- package/app/components/sidebar/case-import/hooks/useImportState.ts +88 -0
- package/app/components/sidebar/case-import/index.ts +18 -0
- package/app/components/sidebar/case-import/utils/file-validation.ts +43 -0
- package/app/components/sidebar/cases/case-sidebar.tsx +827 -0
- package/app/components/sidebar/cases/cases-modal.module.css +166 -0
- package/app/components/sidebar/cases/cases-modal.tsx +201 -0
- package/app/components/sidebar/cases/cases.module.css +713 -0
- package/app/components/sidebar/files/files-modal.module.css +209 -0
- package/app/components/sidebar/files/files-modal.tsx +239 -0
- package/app/components/sidebar/hash/hash-utility.module.css +366 -0
- package/app/components/sidebar/hash/hash-utility.tsx +982 -0
- package/app/components/sidebar/notes/notes-modal.tsx +51 -0
- package/app/components/sidebar/notes/notes-sidebar.tsx +491 -0
- package/app/components/sidebar/notes/notes.module.css +360 -0
- package/app/components/sidebar/sidebar-container.tsx +149 -0
- package/app/components/sidebar/sidebar.module.css +321 -0
- package/app/components/sidebar/sidebar.tsx +215 -0
- package/app/components/sidebar/upload/image-upload-zone.module.css +123 -0
- package/app/components/sidebar/upload/image-upload-zone.tsx +330 -0
- package/app/components/theme-provider/theme-provider.tsx +131 -0
- package/app/components/theme-provider/theme.ts +155 -0
- package/app/components/toast/toast.module.css +137 -0
- package/app/components/toast/toast.tsx +56 -0
- package/app/components/toolbar/toolbar-color-selector.module.css +171 -0
- package/app/components/toolbar/toolbar-color-selector.tsx +129 -0
- package/app/components/toolbar/toolbar.module.css +42 -0
- package/app/components/toolbar/toolbar.tsx +167 -0
- package/app/components/user/delete-account.module.css +274 -0
- package/app/components/user/delete-account.tsx +471 -0
- package/app/components/user/inactivity-warning.module.css +145 -0
- package/app/components/user/inactivity-warning.tsx +84 -0
- package/app/components/user/manage-profile.module.css +190 -0
- package/app/components/user/manage-profile.tsx +253 -0
- package/app/components/user/mfa-phone-update.tsx +739 -0
- package/app/config-example/admin-service.json +13 -0
- package/app/config-example/config.json +17 -0
- package/app/config-example/firebase.ts +21 -0
- package/app/config-example/inactivity.ts +13 -0
- package/app/config-example/meta-config.json +6 -0
- package/app/contexts/auth.context.ts +12 -0
- package/app/entry.client.tsx +12 -0
- package/app/entry.server.tsx +44 -0
- package/app/hooks/useInactivityTimeout.ts +110 -0
- package/app/root.tsx +170 -0
- package/app/routes/_index.tsx +16 -0
- package/app/routes/auth/emailActionHandler.module.css +232 -0
- package/app/routes/auth/emailActionHandler.tsx +405 -0
- package/app/routes/auth/emailVerification.tsx +120 -0
- package/app/routes/auth/login.module.css +523 -0
- package/app/routes/auth/login.tsx +654 -0
- package/app/routes/auth/passwordReset.module.css +274 -0
- package/app/routes/auth/passwordReset.tsx +154 -0
- package/app/routes/auth/route.ts +16 -0
- package/app/routes/mobile-prevented/mobilePrevented.module.css +47 -0
- package/app/routes/mobile-prevented/mobilePrevented.tsx +26 -0
- package/app/routes/mobile-prevented/route.ts +14 -0
- package/app/routes/striae/striae.module.css +30 -0
- package/app/routes/striae/striae.tsx +417 -0
- package/app/services/audit-export.service.ts +755 -0
- package/app/services/audit.service.ts +1454 -0
- package/app/services/firebase-errors.ts +106 -0
- package/app/services/firebase.ts +15 -0
- package/app/styles/legal-pages.module.css +113 -0
- package/app/styles/root.module.css +146 -0
- package/app/tailwind.css +225 -0
- package/app/types/annotations.ts +45 -0
- package/app/types/audit.ts +301 -0
- package/app/types/case.ts +90 -0
- package/app/types/export.ts +8 -0
- package/app/types/file.ts +30 -0
- package/app/types/import.ts +107 -0
- package/app/types/index.ts +24 -0
- package/app/types/user.ts +38 -0
- package/app/utils/SHA256.ts +461 -0
- package/app/utils/annotation-timestamp.ts +25 -0
- package/app/utils/audit-export-signature.ts +117 -0
- package/app/utils/auth-action-settings.ts +48 -0
- package/app/utils/auth.ts +34 -0
- package/app/utils/batch-operations.ts +135 -0
- package/app/utils/confirmation-signature.ts +193 -0
- package/app/utils/data-operations.ts +871 -0
- package/app/utils/device-detection.ts +5 -0
- package/app/utils/html-sanitizer.ts +80 -0
- package/app/utils/id-generator.ts +36 -0
- package/app/utils/meta.ts +48 -0
- package/app/utils/mfa-phone.ts +97 -0
- package/app/utils/mfa.ts +79 -0
- package/app/utils/password-policy.ts +28 -0
- package/app/utils/permissions.ts +562 -0
- package/app/utils/signature-utils.ts +160 -0
- package/app/utils/style.ts +83 -0
- package/app/utils/version.ts +5 -0
- package/firebase.json +11 -0
- package/functions/[[path]].ts +10 -0
- package/package.json +138 -0
- package/postcss.config.js +6 -0
- package/public/.well-known/publickey.info@striae.org.asc +17 -0
- package/public/.well-known/security.txt +7 -0
- package/public/_headers +28 -0
- package/public/_routes.json +13 -0
- package/public/assets/striae.jpg +0 -0
- package/public/clear.jpg +0 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +9 -0
- package/public/icon-256.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/logo-dark.png +0 -0
- package/public/manifest.json +25 -0
- package/public/oin-badge.png +0 -0
- package/public/shortcut.png +0 -0
- package/public/social-image.png +0 -0
- package/public/striae-ascii.txt +10 -0
- package/scripts/deploy-all.sh +100 -0
- package/scripts/deploy-config.sh +940 -0
- package/scripts/deploy-pages.sh +34 -0
- package/scripts/deploy-worker-secrets.sh +215 -0
- package/scripts/dev.cjs +23 -0
- package/scripts/install-workers.sh +88 -0
- package/scripts/run-eslint.cjs +35 -0
- package/scripts/update-compatibility-dates.cjs +124 -0
- package/scripts/update-markdown-versions.cjs +43 -0
- package/tailwind.config.ts +22 -0
- package/tsconfig.json +33 -0
- package/vite.config.ts +35 -0
- package/worker-configuration.d.ts +7490 -0
- package/workers/audit-worker/package.json +17 -0
- package/workers/audit-worker/src/audit-worker.example.ts +195 -0
- package/workers/audit-worker/worker-configuration.d.ts +7448 -0
- package/workers/audit-worker/wrangler.jsonc.example +29 -0
- package/workers/data-worker/package.json +17 -0
- package/workers/data-worker/src/data-worker.example.ts +267 -0
- package/workers/data-worker/src/signature-utils.ts +79 -0
- package/workers/data-worker/src/signing-payload-utils.ts +290 -0
- package/workers/data-worker/worker-configuration.d.ts +7448 -0
- package/workers/data-worker/wrangler.jsonc.example +30 -0
- package/workers/image-worker/package.json +17 -0
- package/workers/image-worker/src/image-worker.example.ts +180 -0
- package/workers/image-worker/worker-configuration.d.ts +7447 -0
- package/workers/image-worker/wrangler.jsonc.example +22 -0
- package/workers/keys-worker/package.json +17 -0
- package/workers/keys-worker/src/keys.example.ts +66 -0
- package/workers/keys-worker/src/keys.ts +66 -0
- package/workers/keys-worker/worker-configuration.d.ts +7447 -0
- package/workers/keys-worker/wrangler.jsonc.example +22 -0
- package/workers/pdf-worker/package.json +17 -0
- package/workers/pdf-worker/src/format-striae.ts +534 -0
- package/workers/pdf-worker/src/pdf-worker.example.ts +119 -0
- package/workers/pdf-worker/src/report-types.ts +69 -0
- package/workers/pdf-worker/worker-configuration.d.ts +7448 -0
- package/workers/pdf-worker/wrangler.jsonc.example +26 -0
- package/workers/user-worker/package.json +17 -0
- package/workers/user-worker/src/user-worker.example.ts +636 -0
- package/workers/user-worker/worker-configuration.d.ts +7448 -0
- package/workers/user-worker/wrangler.jsonc.example +29 -0
- 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
|
+
};
|