@striae-org/striae 3.0.4 → 3.1.0
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/NOTICE +0 -5
- package/app/components/actions/case-export/core-export.ts +1 -1
- package/app/components/actions/case-export/download-handlers.ts +10 -12
- package/app/components/actions/case-export/metadata-helpers.ts +1 -1
- package/app/components/actions/case-import/confirmation-import.ts +24 -9
- package/app/components/actions/case-import/orchestrator.ts +3 -4
- package/app/components/actions/case-import/validation.ts +3 -3
- package/app/components/actions/case-import/zip-processing.ts +12 -48
- package/app/components/actions/case-manage.ts +0 -1
- package/app/components/actions/confirm-export.ts +2 -2
- package/app/components/audit/user-audit-viewer.tsx +53 -15
- package/app/components/audit/user-audit.module.css +11 -4
- package/app/components/canvas/box-annotations/box-annotations.tsx +36 -7
- package/app/components/canvas/canvas.tsx +35 -24
- package/app/components/canvas/confirmation/confirmation.module.css +5 -2
- package/app/components/canvas/confirmation/confirmation.tsx +25 -8
- package/app/components/sidebar/case-export/case-export.module.css +194 -5
- package/app/components/sidebar/case-export/case-export.tsx +291 -11
- package/app/components/sidebar/case-import/case-import.module.css +9 -5
- package/app/components/sidebar/case-import/case-import.tsx +30 -7
- package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +2 -2
- package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +1 -1
- package/app/components/sidebar/case-import/components/ExistingCaseSection.tsx +1 -1
- package/app/components/sidebar/case-import/hooks/useFilePreview.ts +34 -9
- package/app/components/sidebar/cases/case-sidebar.tsx +13 -13
- package/app/components/sidebar/cases/cases-modal.tsx +12 -2
- package/app/components/sidebar/files/files-modal.tsx +28 -8
- package/app/components/sidebar/sidebar.module.css +2 -3
- package/app/components/sidebar/sidebar.tsx +1 -16
- package/app/components/sidebar/upload/image-upload-zone.tsx +4 -4
- package/app/components/toolbar/toolbar-color-selector.module.css +2 -2
- package/app/components/toolbar/toolbar-color-selector.tsx +3 -3
- package/app/components/toolbar/toolbar.tsx +19 -9
- package/app/components/user/delete-account.module.css +4 -1
- package/app/components/user/delete-account.tsx +22 -3
- package/app/components/user/manage-profile.tsx +0 -2
- package/app/entry.server.tsx +2 -3
- package/app/hooks/useInactivityTimeout.ts +5 -1
- package/app/root.tsx +0 -3
- package/app/routes/_index.tsx +1 -16
- package/app/routes/auth/emailVerification.tsx +1 -1
- package/app/routes/auth/login.tsx +7 -5
- package/app/routes/auth/route.ts +3 -12
- package/app/routes/striae/striae.tsx +1 -1
- package/app/services/audit.service.ts +29 -9
- package/app/tailwind.css +16 -1
- package/app/types/audit.ts +3 -3
- package/app/types/case.ts +1 -1
- package/app/types/import.ts +0 -2
- package/app/utils/SHA256.ts +3 -3
- package/app/utils/batch-operations.ts +6 -6
- package/app/utils/data-operations.ts +14 -7
- package/app/utils/permissions.ts +0 -2
- package/functions/[[path]].ts +0 -1
- package/package.json +1 -2
- package/public/_headers +0 -12
- package/public/assets/striae.jpg +0 -0
- package/scripts/deploy-config.sh +0 -7
- package/scripts/run-eslint.cjs +14 -6
- package/worker-configuration.d.ts +2 -2
- package/workers/audit-worker/src/audit-worker.example.ts +9 -7
- package/workers/audit-worker/worker-configuration.d.ts +2 -2
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/src/data-worker.example.ts +1 -1
- package/workers/data-worker/worker-configuration.d.ts +2 -2
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/worker-configuration.d.ts +2 -2
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/keys-worker/worker-configuration.d.ts +2 -2
- package/workers/keys-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/src/pdf-worker.example.ts +3 -3
- package/workers/pdf-worker/worker-configuration.d.ts +7448 -7448
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/src/user-worker.example.ts +10 -10
- package/workers/user-worker/worker-configuration.d.ts +2 -2
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
- package/app/components/sidebar/hash/hash-utility.module.css +0 -366
- package/app/components/sidebar/hash/hash-utility.tsx +0 -982
- package/app/config-example/meta-config.json +0 -6
- package/app/routes/mobile-prevented/mobilePrevented.module.css +0 -47
- package/app/routes/mobile-prevented/mobilePrevented.tsx +0 -26
- package/app/routes/mobile-prevented/route.ts +0 -14
- package/app/utils/device-detection.ts +0 -5
- package/app/utils/html-sanitizer.ts +0 -80
- package/app/utils/meta.ts +0 -48
- package/public/icon-256.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/manifest.json +0 -25
- package/public/shortcut.png +0 -0
- package/public/social-image.png +0 -0
|
@@ -23,6 +23,11 @@ export const CasesModal = ({ isOpen, onClose, onSelectCase, currentCase, user }:
|
|
|
23
23
|
}>({});
|
|
24
24
|
const CASES_PER_PAGE = 10;
|
|
25
25
|
|
|
26
|
+
const startLoading = () => {
|
|
27
|
+
setIsLoading(true);
|
|
28
|
+
setError('');
|
|
29
|
+
};
|
|
30
|
+
|
|
26
31
|
useEffect(() => {
|
|
27
32
|
const handleEscape = (e: KeyboardEvent) => {
|
|
28
33
|
if (e.key === 'Escape') {
|
|
@@ -38,8 +43,9 @@ export const CasesModal = ({ isOpen, onClose, onSelectCase, currentCase, user }:
|
|
|
38
43
|
|
|
39
44
|
useEffect(() => {
|
|
40
45
|
if (isOpen) {
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
const loadingTimer = window.setTimeout(() => {
|
|
47
|
+
startLoading();
|
|
48
|
+
}, 0);
|
|
43
49
|
|
|
44
50
|
listCases(user)
|
|
45
51
|
.then(fetchedCases => {
|
|
@@ -52,6 +58,10 @@ export const CasesModal = ({ isOpen, onClose, onSelectCase, currentCase, user }:
|
|
|
52
58
|
.finally(() => {
|
|
53
59
|
setIsLoading(false);
|
|
54
60
|
});
|
|
61
|
+
|
|
62
|
+
return () => {
|
|
63
|
+
window.clearTimeout(loadingTimer);
|
|
64
|
+
};
|
|
55
65
|
}
|
|
56
66
|
}, [isOpen, user]);
|
|
57
67
|
|
|
@@ -41,7 +41,10 @@ export const FilesModal = ({ isOpen, onClose, onFileSelect, currentCase, files,
|
|
|
41
41
|
// Fetch confirmation status only for currently visible paginated files
|
|
42
42
|
useEffect(() => {
|
|
43
43
|
const fetchConfirmationStatuses = async () => {
|
|
44
|
-
const visibleFiles =
|
|
44
|
+
const visibleFiles = files.slice(
|
|
45
|
+
currentPage * FILES_PER_PAGE,
|
|
46
|
+
currentPage * FILES_PER_PAGE + FILES_PER_PAGE
|
|
47
|
+
);
|
|
45
48
|
|
|
46
49
|
if (!isOpen || !currentCase || !user || visibleFiles.length === 0) {
|
|
47
50
|
return;
|
|
@@ -84,6 +87,21 @@ export const FilesModal = ({ isOpen, onClose, onFileSelect, currentCase, files,
|
|
|
84
87
|
fetchConfirmationStatuses();
|
|
85
88
|
}, [isOpen, currentCase, currentPage, files, user]);
|
|
86
89
|
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
const handleEscape = (event: KeyboardEvent) => {
|
|
92
|
+
if (event.key === 'Escape' && isOpen) {
|
|
93
|
+
onClose();
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
if (isOpen) {
|
|
98
|
+
document.addEventListener('keydown', handleEscape);
|
|
99
|
+
return () => {
|
|
100
|
+
document.removeEventListener('keydown', handleEscape);
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}, [isOpen, onClose]);
|
|
104
|
+
|
|
87
105
|
const handleFileSelect = (file: FileData) => {
|
|
88
106
|
onFileSelect?.(file);
|
|
89
107
|
onClose();
|
|
@@ -123,12 +141,6 @@ export const FilesModal = ({ isOpen, onClose, onFileSelect, currentCase, files,
|
|
|
123
141
|
}
|
|
124
142
|
};
|
|
125
143
|
|
|
126
|
-
const handleKeyDown = (event: React.KeyboardEvent) => {
|
|
127
|
-
if (event.key === 'Escape') {
|
|
128
|
-
onClose();
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
|
|
132
144
|
const formatDate = (dateString: string) => {
|
|
133
145
|
try {
|
|
134
146
|
return new Date(dateString).toLocaleDateString();
|
|
@@ -153,7 +165,7 @@ export const FilesModal = ({ isOpen, onClose, onFileSelect, currentCase, files,
|
|
|
153
165
|
if (!isOpen) return null;
|
|
154
166
|
|
|
155
167
|
return (
|
|
156
|
-
<div className={styles.modalOverlay}
|
|
168
|
+
<div className={styles.modalOverlay}>
|
|
157
169
|
<div className={styles.modal}>
|
|
158
170
|
<div className={styles.modalHeader}>
|
|
159
171
|
<h2>Files in Case {currentCase}</h2>
|
|
@@ -188,6 +200,14 @@ export const FilesModal = ({ isOpen, onClose, onFileSelect, currentCase, files,
|
|
|
188
200
|
key={file.id}
|
|
189
201
|
className={`${styles.fileItem} ${selectedFileId === file.id ? styles.active : ''} ${confirmationClass}`}
|
|
190
202
|
onClick={() => handleFileSelect(file)}
|
|
203
|
+
onKeyDown={(event) => {
|
|
204
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
205
|
+
event.preventDefault();
|
|
206
|
+
handleFileSelect(file);
|
|
207
|
+
}
|
|
208
|
+
}}
|
|
209
|
+
role="button"
|
|
210
|
+
tabIndex={0}
|
|
191
211
|
>
|
|
192
212
|
<div className={styles.fileInfo}>
|
|
193
213
|
<div className={styles.fileName} title={file.originalFilename}>
|
|
@@ -255,7 +255,8 @@
|
|
|
255
255
|
|
|
256
256
|
.oinBadgeLink {
|
|
257
257
|
display: inline-block;
|
|
258
|
-
transition: opacity var(--durationS, 0.2s)
|
|
258
|
+
transition: opacity var(--durationS, 0.2s)
|
|
259
|
+
var(--bezierFastoutSlowin, ease-out);
|
|
259
260
|
}
|
|
260
261
|
|
|
261
262
|
.oinBadgeLink:hover {
|
|
@@ -317,5 +318,3 @@
|
|
|
317
318
|
background: #5c636a;
|
|
318
319
|
box-shadow: 0 2px 6px color-mix(in lab, #6c757d 40%, transparent);
|
|
319
320
|
}
|
|
320
|
-
|
|
321
|
-
|
|
@@ -6,10 +6,8 @@ import { SignOut } from '../actions/signout';
|
|
|
6
6
|
import { CaseSidebar } from './cases/case-sidebar';
|
|
7
7
|
import { NotesSidebar } from './notes/notes-sidebar';
|
|
8
8
|
import { CaseImport } from './case-import/case-import';
|
|
9
|
-
import { HashUtility } from './hash/hash-utility';
|
|
10
9
|
import { Toast } from '../toast/toast';
|
|
11
|
-
import { FileData } from '~/types';
|
|
12
|
-
import { ImportResult, ConfirmationImportResult } from '~/types';
|
|
10
|
+
import { FileData, ImportResult, ConfirmationImportResult } from '~/types';
|
|
13
11
|
|
|
14
12
|
interface SidebarProps {
|
|
15
13
|
user: User;
|
|
@@ -64,7 +62,6 @@ export const Sidebar = ({
|
|
|
64
62
|
}: SidebarProps) => {
|
|
65
63
|
const [isProfileModalOpen, setIsProfileModalOpen] = useState(false);
|
|
66
64
|
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
|
|
67
|
-
const [isHashModalOpen, setIsHashModalOpen] = useState(false);
|
|
68
65
|
const [isUploading, setIsUploading] = useState(initialIsUploading);
|
|
69
66
|
const [toastMessage, setToastMessage] = useState('');
|
|
70
67
|
const [toastType, setToastType] = useState<'success' | 'error' | 'warning'>('success');
|
|
@@ -143,10 +140,6 @@ export const Sidebar = ({
|
|
|
143
140
|
onClose={() => setIsImportModalOpen(false)}
|
|
144
141
|
onImportComplete={handleImportComplete}
|
|
145
142
|
/>
|
|
146
|
-
<HashUtility
|
|
147
|
-
isOpen={isHashModalOpen}
|
|
148
|
-
onClose={() => setIsHashModalOpen(false)}
|
|
149
|
-
/>
|
|
150
143
|
{showNotes ? (
|
|
151
144
|
<NotesSidebar
|
|
152
145
|
currentCase={currentCase}
|
|
@@ -193,14 +186,6 @@ export const Sidebar = ({
|
|
|
193
186
|
>
|
|
194
187
|
Import/Clear RO Case
|
|
195
188
|
</button>
|
|
196
|
-
<button
|
|
197
|
-
onClick={() => setIsHashModalOpen(true)}
|
|
198
|
-
className={styles.hashButton}
|
|
199
|
-
disabled={isUploading}
|
|
200
|
-
title={isUploading ? 'Cannot open hash utility while uploading files' : undefined}
|
|
201
|
-
>
|
|
202
|
-
Hash Utility
|
|
203
|
-
</button>
|
|
204
189
|
</div>
|
|
205
190
|
</>
|
|
206
191
|
)}
|
|
@@ -198,7 +198,7 @@ export const ImageUploadZone = ({
|
|
|
198
198
|
}
|
|
199
199
|
};
|
|
200
200
|
|
|
201
|
-
const handleFileInputChange =
|
|
201
|
+
const handleFileInputChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
202
202
|
if (isReadOnly) {
|
|
203
203
|
return;
|
|
204
204
|
}
|
|
@@ -209,7 +209,7 @@ export const ImageUploadZone = ({
|
|
|
209
209
|
// Convert FileList to Array
|
|
210
210
|
const filesToUpload = Array.from(files);
|
|
211
211
|
await processFileQueue(filesToUpload);
|
|
212
|
-
}
|
|
212
|
+
};
|
|
213
213
|
|
|
214
214
|
const handleDragEnter = useCallback((e: React.DragEvent<HTMLDivElement>) => {
|
|
215
215
|
e.preventDefault();
|
|
@@ -234,7 +234,7 @@ export const ImageUploadZone = ({
|
|
|
234
234
|
e.stopPropagation();
|
|
235
235
|
}, []);
|
|
236
236
|
|
|
237
|
-
const handleDrop =
|
|
237
|
+
const handleDrop = async (e: React.DragEvent<HTMLDivElement>) => {
|
|
238
238
|
e.preventDefault();
|
|
239
239
|
e.stopPropagation();
|
|
240
240
|
setIsDraggingFiles(false);
|
|
@@ -249,7 +249,7 @@ export const ImageUploadZone = ({
|
|
|
249
249
|
// Convert FileList to Array and process all files
|
|
250
250
|
const filesToUpload = Array.from(files);
|
|
251
251
|
await processFileQueue(filesToUpload);
|
|
252
|
-
}
|
|
252
|
+
};
|
|
253
253
|
|
|
254
254
|
// If read-only or uploads restricted, show only error message
|
|
255
255
|
if (isReadOnly || !canUploadNewFile) {
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
border-radius: 3px;
|
|
85
85
|
padding: var(--spaceXS) var(--spaceS);
|
|
86
86
|
cursor: pointer;
|
|
87
|
-
font-size: var(--
|
|
87
|
+
font-size: var(--fontSizeBodyXS);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
.toggleButton:hover {
|
|
@@ -157,7 +157,7 @@
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
.previewLabel {
|
|
160
|
-
font-size: var(--
|
|
160
|
+
font-size: var(--fontSizeBodyXS);
|
|
161
161
|
color: var(--foreground);
|
|
162
162
|
font-weight: 500;
|
|
163
163
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react';
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
2
|
import styles from './toolbar-color-selector.module.css';
|
|
3
3
|
|
|
4
4
|
interface ToolbarColorSelectorProps {
|
|
@@ -39,11 +39,11 @@ export const ToolbarColorSelector = ({
|
|
|
39
39
|
onColorConfirm(tempSelectedColor);
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
-
const handleCancel = () => {
|
|
42
|
+
const handleCancel = useCallback(() => {
|
|
43
43
|
setTempSelectedColor(selectedColor); // Reset to original color
|
|
44
44
|
setShowColorWheel(false); // Reset to presets view
|
|
45
45
|
onCancel();
|
|
46
|
-
};
|
|
46
|
+
}, [selectedColor, onCancel]);
|
|
47
47
|
|
|
48
48
|
// Handle keyboard events for escape
|
|
49
49
|
useEffect(() => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react';
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
2
|
import { Button } from '../button/button';
|
|
3
3
|
import { ToolbarColorSelector } from './toolbar-color-selector';
|
|
4
4
|
import styles from './toolbar.module.css';
|
|
@@ -34,18 +34,28 @@ export const Toolbar = ({
|
|
|
34
34
|
const [isVisible, setIsVisible] = useState(true);
|
|
35
35
|
const [showColorSelector, setShowColorSelector] = useState(false);
|
|
36
36
|
|
|
37
|
+
const deactivateBoxTool = useCallback(() => {
|
|
38
|
+
setActiveTools(prev => {
|
|
39
|
+
const next = new Set(prev);
|
|
40
|
+
next.delete('box');
|
|
41
|
+
onToolSelect?.('box', false);
|
|
42
|
+
return next;
|
|
43
|
+
});
|
|
44
|
+
setShowColorSelector(false);
|
|
45
|
+
}, [onToolSelect]);
|
|
46
|
+
|
|
37
47
|
// Automatically deactivate box annotations when notes are opened
|
|
38
48
|
useEffect(() => {
|
|
39
49
|
if (isNotesOpen && activeTools.has('box')) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
50
|
+
const deactivateTimer = window.setTimeout(() => {
|
|
51
|
+
deactivateBoxTool();
|
|
52
|
+
}, 0);
|
|
53
|
+
|
|
54
|
+
return () => {
|
|
55
|
+
window.clearTimeout(deactivateTimer);
|
|
56
|
+
};
|
|
47
57
|
}
|
|
48
|
-
}, [isNotesOpen, activeTools,
|
|
58
|
+
}, [isNotesOpen, activeTools, deactivateBoxTool]);
|
|
49
59
|
|
|
50
60
|
const handleToolClick = (toolId: ToolId) => {
|
|
51
61
|
if (toolId === 'print') {
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
justify-content: center;
|
|
8
8
|
align-items: center;
|
|
9
9
|
z-index: var(--zIndex5);
|
|
10
|
+
cursor: default;
|
|
10
11
|
transition: background-color var(--durationM) var(--bezierFastoutSlowin);
|
|
11
12
|
}
|
|
12
13
|
|
|
@@ -16,11 +17,13 @@
|
|
|
16
17
|
border-radius: var(--spaceXS);
|
|
17
18
|
width: 90%;
|
|
18
19
|
max-width: 600px;
|
|
19
|
-
box-shadow: 0 var(--spaceXS) var(--spaceL)
|
|
20
|
+
box-shadow: 0 var(--spaceXS) var(--spaceL)
|
|
21
|
+
color-mix(in lab, var(--black) 10%, transparent);
|
|
20
22
|
transition: background-color var(--durationM) var(--bezierFastoutSlowin);
|
|
21
23
|
overflow: hidden;
|
|
22
24
|
max-height: 90vh;
|
|
23
25
|
overflow-y: auto;
|
|
26
|
+
cursor: default;
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
/* Modal Header */
|
|
@@ -317,18 +317,37 @@ export const DeleteAccount = ({ isOpen, onClose, user, company }: DeleteAccountP
|
|
|
317
317
|
? `Deleting case ${deletionProgress.currentCaseNumber}...`
|
|
318
318
|
: 'Preparing account deletion...');
|
|
319
319
|
|
|
320
|
+
const handleOverlayClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
|
321
|
+
if (event.target === event.currentTarget) {
|
|
322
|
+
onClose();
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const handleOverlayKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
327
|
+
if (event.target !== event.currentTarget) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
332
|
+
event.preventDefault();
|
|
333
|
+
onClose();
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
|
|
320
337
|
return (
|
|
321
338
|
<div
|
|
322
339
|
className={styles.modalOverlay}
|
|
323
|
-
onClick={
|
|
324
|
-
|
|
340
|
+
onClick={handleOverlayClick}
|
|
341
|
+
onKeyDown={handleOverlayKeyDown}
|
|
342
|
+
role="button"
|
|
343
|
+
tabIndex={0}
|
|
344
|
+
aria-label="Close delete account dialog"
|
|
325
345
|
>
|
|
326
346
|
<div
|
|
327
347
|
className={styles.modal}
|
|
328
348
|
role="dialog"
|
|
329
349
|
aria-modal="true"
|
|
330
350
|
aria-labelledby="modal-title"
|
|
331
|
-
onClick={(e) => e.stopPropagation()}
|
|
332
351
|
>
|
|
333
352
|
{/* Header */}
|
|
334
353
|
<header className={styles.modalHeader}>
|
|
@@ -21,7 +21,6 @@ export const ManageProfile = ({ isOpen, onClose }: ManageProfileProps) => {
|
|
|
21
21
|
const [displayName, setDisplayName] = useState(user?.displayName || '');
|
|
22
22
|
const [company, setCompany] = useState('');
|
|
23
23
|
const [email, setEmail] = useState('');
|
|
24
|
-
const [permitted, setPermitted] = useState(false); // Default to false for safety - updated after data loads.
|
|
25
24
|
const [isLoading, setIsLoading] = useState(false);
|
|
26
25
|
const [isMfaBusy, setIsMfaBusy] = useState(false);
|
|
27
26
|
const [error, setError] = useState('');
|
|
@@ -52,7 +51,6 @@ export const ManageProfile = ({ isOpen, onClose }: ManageProfileProps) => {
|
|
|
52
51
|
if (userData) {
|
|
53
52
|
setCompany(userData.company || '');
|
|
54
53
|
setEmail(userData.email || '');
|
|
55
|
-
setPermitted(userData.permitted === true);
|
|
56
54
|
}
|
|
57
55
|
} catch (err) {
|
|
58
56
|
console.error('Failed to load user data:', err);
|
package/app/entry.server.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { EntryContext } from "@remix-run/cloudflare";
|
|
2
2
|
import { RemixServer } from "@remix-run/react";
|
|
3
3
|
import { isbot } from "isbot";
|
|
4
4
|
import { renderToReadableStream } from "react-dom/server";
|
|
@@ -7,8 +7,7 @@ export default async function handleRequest(
|
|
|
7
7
|
request: Request,
|
|
8
8
|
responseStatusCode: number,
|
|
9
9
|
responseHeaders: Headers,
|
|
10
|
-
remixContext: EntryContext
|
|
11
|
-
loadContext: AppLoadContext
|
|
10
|
+
remixContext: EntryContext
|
|
12
11
|
) {
|
|
13
12
|
const body = await renderToReadableStream(
|
|
14
13
|
<RemixServer context={remixContext} url={request.url} />,
|
|
@@ -22,11 +22,15 @@ export const useInactivityTimeout = ({
|
|
|
22
22
|
const location = useLocation();
|
|
23
23
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
24
24
|
const warningTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
25
|
-
const lastActivityRef = useRef<number>(
|
|
25
|
+
const lastActivityRef = useRef<number>(0);
|
|
26
26
|
|
|
27
27
|
const isAuthRoute = location.pathname.startsWith('/auth');
|
|
28
28
|
const shouldEnable = enabled && isAuthRoute;
|
|
29
29
|
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
lastActivityRef.current = Date.now();
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
30
34
|
const clearTimeouts = useCallback(() => {
|
|
31
35
|
if (timeoutRef.current) {
|
|
32
36
|
clearTimeout(timeoutRef.current);
|
package/app/root.tsx
CHANGED
|
@@ -30,11 +30,8 @@ export const links: LinksFunction = () => [
|
|
|
30
30
|
rel: "stylesheet",
|
|
31
31
|
href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
|
|
32
32
|
},
|
|
33
|
-
{ rel: 'manifest', href: '/manifest.json' },
|
|
34
33
|
{ rel: 'icon', href: '/favicon.ico' },
|
|
35
34
|
{ rel: 'icon', href: '/favicon.svg', type: 'image/svg+xml' },
|
|
36
|
-
{ rel: 'shortcut_icon', href: '/shortcut.png', type: 'image/png', sizes: '64x64' },
|
|
37
|
-
{ rel: 'apple-touch-icon', href: '/icon-256.png', sizes: '256x256' },
|
|
38
35
|
];
|
|
39
36
|
|
|
40
37
|
type AppTheme = 'dark' | 'light';
|
package/app/routes/_index.tsx
CHANGED
|
@@ -1,16 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { isMobileOrTabletUserAgent } from '~/utils/device-detection';
|
|
3
|
-
|
|
4
|
-
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|
5
|
-
const requestUrl = new URL(request.url);
|
|
6
|
-
const search = requestUrl.search ?? '';
|
|
7
|
-
const userAgent = request.headers.get('user-agent') ?? '';
|
|
8
|
-
|
|
9
|
-
if (isMobileOrTabletUserAgent(userAgent)) {
|
|
10
|
-
throw redirect(`/mobile-prevented${search}`);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
return null;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export { Login as default, meta } from './auth/login';
|
|
1
|
+
export { Login as default, meta } from './auth/login';
|
|
@@ -107,7 +107,7 @@ export const EmailVerification = ({
|
|
|
107
107
|
</div>
|
|
108
108
|
|
|
109
109
|
<div className={styles.verificationHints}>
|
|
110
|
-
<p className={styles.hint}>Didn
|
|
110
|
+
<p className={styles.hint}>Didn't receive the email?</p>
|
|
111
111
|
<ul className={styles.hintList}>
|
|
112
112
|
<li>Check your spam or junk folder</li>
|
|
113
113
|
<li>Make sure {user?.email} is correct</li>
|
|
@@ -20,7 +20,6 @@ import { MFAVerification } from '~/components/auth/mfa-verification';
|
|
|
20
20
|
import { MFAEnrollment } from '~/components/auth/mfa-enrollment';
|
|
21
21
|
import { Icon } from '~/components/icon/icon';
|
|
22
22
|
import styles from './login.module.css';
|
|
23
|
-
import { baseMeta } from '~/utils/meta';
|
|
24
23
|
import { Striae } from '~/routes/striae/striae';
|
|
25
24
|
import { getUserData, createUser } from '~/utils/permissions';
|
|
26
25
|
import { auditService } from '~/services/audit.service';
|
|
@@ -30,10 +29,13 @@ import { buildActionCodeSettings } from '~/utils/auth-action-settings';
|
|
|
30
29
|
import { userHasMFA } from '~/utils/mfa';
|
|
31
30
|
|
|
32
31
|
export const meta = () => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
const titleText = 'Striae | Welcome to Striae';
|
|
33
|
+
const description = 'Login to your Striae account to access your projects and data';
|
|
34
|
+
|
|
35
|
+
return [
|
|
36
|
+
{ title: titleText },
|
|
37
|
+
{ name: 'description', content: description },
|
|
38
|
+
];
|
|
37
39
|
};
|
|
38
40
|
|
|
39
41
|
const SUPPORTED_EMAIL_ACTION_MODES = new Set(['resetPassword', 'verifyEmail', 'recoverEmail']);
|
package/app/routes/auth/route.ts
CHANGED
|
@@ -1,16 +1,7 @@
|
|
|
1
|
-
import { redirect
|
|
2
|
-
import { isMobileOrTabletUserAgent } from '~/utils/device-detection';
|
|
1
|
+
import { redirect } from '@remix-run/cloudflare';
|
|
3
2
|
|
|
4
|
-
export const loader = async (
|
|
5
|
-
|
|
6
|
-
const search = requestUrl.search ?? '';
|
|
7
|
-
const userAgent = request.headers.get('user-agent') ?? '';
|
|
8
|
-
|
|
9
|
-
if (isMobileOrTabletUserAgent(userAgent)) {
|
|
10
|
-
throw redirect(`/mobile-prevented${search}`);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
throw redirect(`/${search}`);
|
|
3
|
+
export const loader = async () => {
|
|
4
|
+
throw redirect('/');
|
|
14
5
|
};
|
|
15
6
|
|
|
16
7
|
export { Login as default, meta } from './login';
|
|
@@ -18,6 +18,20 @@ import { generateWorkflowId } from '../utils/id-generator';
|
|
|
18
18
|
|
|
19
19
|
const AUDIT_WORKER_URL = paths.audit_worker_url;
|
|
20
20
|
|
|
21
|
+
type AnnotationSnapshot = Record<string, unknown> & {
|
|
22
|
+
type?: 'measurement' | 'identification' | 'comparison' | 'note' | 'region';
|
|
23
|
+
position?: { x: number; y: number };
|
|
24
|
+
size?: { width: number; height: number };
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const toAnnotationSnapshot = (value: unknown): AnnotationSnapshot | undefined => {
|
|
28
|
+
if (typeof value !== 'object' || value === null) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return value as AnnotationSnapshot;
|
|
33
|
+
};
|
|
34
|
+
|
|
21
35
|
/**
|
|
22
36
|
* Audit Service for ValidationAuditEntry system
|
|
23
37
|
* Provides comprehensive audit logging throughout the confirmation workflow
|
|
@@ -552,7 +566,7 @@ export class AuditService {
|
|
|
552
566
|
fileId,
|
|
553
567
|
originalFileName,
|
|
554
568
|
fileSize: 0, // File size not available for access events
|
|
555
|
-
uploadMethod: accessMethod
|
|
569
|
+
uploadMethod: accessMethod,
|
|
556
570
|
processingTime,
|
|
557
571
|
sourceLocation: accessReason || 'Image viewer'
|
|
558
572
|
},
|
|
@@ -570,12 +584,14 @@ export class AuditService {
|
|
|
570
584
|
user: User,
|
|
571
585
|
annotationId: string,
|
|
572
586
|
annotationType: 'measurement' | 'identification' | 'comparison' | 'note' | 'region',
|
|
573
|
-
annotationData:
|
|
587
|
+
annotationData: unknown,
|
|
574
588
|
caseNumber: string,
|
|
575
589
|
tool?: string,
|
|
576
590
|
imageFileId?: string,
|
|
577
591
|
originalImageFileName?: string
|
|
578
592
|
): Promise<void> {
|
|
593
|
+
const annotationSnapshot = toAnnotationSnapshot(annotationData);
|
|
594
|
+
|
|
579
595
|
await this.logEvent({
|
|
580
596
|
userId: user.uid,
|
|
581
597
|
userEmail: user.email || '',
|
|
@@ -591,8 +607,8 @@ export class AuditService {
|
|
|
591
607
|
annotationType,
|
|
592
608
|
annotationData,
|
|
593
609
|
tool,
|
|
594
|
-
canvasPosition:
|
|
595
|
-
annotationSize:
|
|
610
|
+
canvasPosition: annotationSnapshot?.position,
|
|
611
|
+
annotationSize: annotationSnapshot?.size
|
|
596
612
|
},
|
|
597
613
|
fileDetails: imageFileId || originalImageFileName ? {
|
|
598
614
|
fileId: imageFileId,
|
|
@@ -610,13 +626,15 @@ export class AuditService {
|
|
|
610
626
|
public async logAnnotationEdit(
|
|
611
627
|
user: User,
|
|
612
628
|
annotationId: string,
|
|
613
|
-
previousValue:
|
|
614
|
-
newValue:
|
|
629
|
+
previousValue: unknown,
|
|
630
|
+
newValue: unknown,
|
|
615
631
|
caseNumber: string,
|
|
616
632
|
tool?: string,
|
|
617
633
|
imageFileId?: string,
|
|
618
634
|
originalImageFileName?: string
|
|
619
635
|
): Promise<void> {
|
|
636
|
+
const newValueSnapshot = toAnnotationSnapshot(newValue);
|
|
637
|
+
|
|
620
638
|
await this.logEvent({
|
|
621
639
|
userId: user.uid,
|
|
622
640
|
userEmail: user.email || '',
|
|
@@ -629,7 +647,7 @@ export class AuditService {
|
|
|
629
647
|
workflowPhase: 'casework',
|
|
630
648
|
annotationDetails: {
|
|
631
649
|
annotationId,
|
|
632
|
-
annotationType:
|
|
650
|
+
annotationType: newValueSnapshot?.type,
|
|
633
651
|
annotationData: newValue,
|
|
634
652
|
previousValue,
|
|
635
653
|
tool
|
|
@@ -650,12 +668,14 @@ export class AuditService {
|
|
|
650
668
|
public async logAnnotationDelete(
|
|
651
669
|
user: User,
|
|
652
670
|
annotationId: string,
|
|
653
|
-
annotationData:
|
|
671
|
+
annotationData: unknown,
|
|
654
672
|
caseNumber: string,
|
|
655
673
|
deleteReason?: string,
|
|
656
674
|
imageFileId?: string,
|
|
657
675
|
originalImageFileName?: string
|
|
658
676
|
): Promise<void> {
|
|
677
|
+
const annotationSnapshot = toAnnotationSnapshot(annotationData);
|
|
678
|
+
|
|
659
679
|
await this.logEvent({
|
|
660
680
|
userId: user.uid,
|
|
661
681
|
userEmail: user.email || '',
|
|
@@ -668,7 +688,7 @@ export class AuditService {
|
|
|
668
688
|
workflowPhase: 'casework',
|
|
669
689
|
annotationDetails: {
|
|
670
690
|
annotationId,
|
|
671
|
-
annotationType:
|
|
691
|
+
annotationType: annotationSnapshot?.type,
|
|
672
692
|
annotationData,
|
|
673
693
|
tool: deleteReason
|
|
674
694
|
},
|
package/app/tailwind.css
CHANGED
|
@@ -43,8 +43,23 @@
|
|
|
43
43
|
display: block;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
ul,
|
|
47
|
+
ol {
|
|
48
|
+
padding-left: var(--spaceL);
|
|
49
|
+
margin: 0;
|
|
50
|
+
list-style-position: outside;
|
|
51
|
+
}
|
|
52
|
+
|
|
46
53
|
ul {
|
|
47
|
-
|
|
54
|
+
list-style-type: disc;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
ol {
|
|
58
|
+
list-style-type: decimal;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
ol li::marker {
|
|
62
|
+
content: counter(list-item) ") ";
|
|
48
63
|
}
|
|
49
64
|
|
|
50
65
|
html,
|
package/app/types/audit.ts
CHANGED
|
@@ -211,7 +211,7 @@ export interface FileAuditDetails {
|
|
|
211
211
|
originalFileName?: string;
|
|
212
212
|
fileSize: number;
|
|
213
213
|
mimeType?: string;
|
|
214
|
-
uploadMethod?: 'drag-drop' | 'file-picker' | 'api' | 'import';
|
|
214
|
+
uploadMethod?: 'drag-drop' | 'file-picker' | 'api' | 'import' | 'direct-url' | 'signed-url' | 'download';
|
|
215
215
|
processingTime?: number;
|
|
216
216
|
thumbnailGenerated?: boolean;
|
|
217
217
|
deleteReason?: string;
|
|
@@ -224,10 +224,10 @@ export interface FileAuditDetails {
|
|
|
224
224
|
export interface AnnotationAuditDetails {
|
|
225
225
|
annotationId?: string;
|
|
226
226
|
annotationType?: 'measurement' | 'identification' | 'comparison' | 'note' | 'region';
|
|
227
|
-
annotationData?:
|
|
227
|
+
annotationData?: unknown; // The actual annotation data structure
|
|
228
228
|
canvasPosition?: { x: number; y: number };
|
|
229
229
|
annotationSize?: { width: number; height: number };
|
|
230
|
-
previousValue?:
|
|
230
|
+
previousValue?: unknown; // For edit operations
|
|
231
231
|
tool?: string; // Which tool was used to create/edit
|
|
232
232
|
}
|
|
233
233
|
|
package/app/types/case.ts
CHANGED
|
@@ -82,7 +82,7 @@ export interface CaseConfirmations {
|
|
|
82
82
|
export interface CaseDataWithConfirmations {
|
|
83
83
|
createdAt: string;
|
|
84
84
|
caseNumber: string;
|
|
85
|
-
files:
|
|
85
|
+
files: FileData[];
|
|
86
86
|
isReadOnly?: boolean;
|
|
87
87
|
importedAt?: string;
|
|
88
88
|
originalImageIds?: { [originalId: string]: string };
|