@striae-org/striae 5.2.0 → 5.3.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/.env.example +36 -33
- package/README.md +5 -46
- package/app/components/actions/case-export/core-export.ts +2 -174
- package/app/components/actions/case-export/download-handlers.ts +83 -750
- package/app/components/actions/case-export/index.ts +6 -30
- package/app/components/actions/case-export/metadata-helpers.ts +0 -78
- package/app/components/actions/case-export/types-constants.ts +0 -43
- package/app/components/actions/case-import/confirmation-import.ts +13 -14
- package/app/components/actions/case-import/zip-processing.ts +92 -12
- package/app/components/actions/generate-pdf.ts +3 -2
- package/app/components/audit/user-audit-viewer.tsx +0 -19
- package/app/components/audit/viewer/audit-viewer-header.tsx +0 -33
- package/app/components/navbar/case-modals/archive-case-modal.tsx +1 -1
- package/app/components/navbar/navbar.tsx +1 -1
- package/app/components/sidebar/case-import/case-import.module.css +35 -0
- package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +59 -3
- package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +2 -4
- package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +1 -1
- package/app/components/sidebar/notes/class-details-shared.ts +2 -2
- package/app/components/toast/toast.module.css +36 -0
- package/app/components/toast/toast.tsx +6 -2
- package/app/components/user/manage-profile.tsx +4 -3
- package/app/config-example/config.json +1 -2
- package/app/root.tsx +0 -7
- package/app/routes/_index.tsx +1 -1
- package/app/routes/auth/login.example.tsx +22 -103
- package/app/routes/auth/route.ts +1 -1
- package/app/routes/striae/striae.tsx +53 -59
- package/app/services/firebase/index.ts +0 -3
- package/app/types/export.ts +1 -2
- package/app/utils/auth/index.ts +0 -1
- package/app/utils/data/permissions.ts +3 -2
- package/package.json +10 -17
- package/public/_headers +0 -4
- package/public/_routes.json +0 -1
- package/worker-configuration.d.ts +20 -17
- package/workers/audit-worker/src/audit-worker.example.ts +9 -806
- package/workers/audit-worker/src/config.ts +7 -0
- package/workers/audit-worker/src/crypto/data-at-rest.ts +410 -0
- package/workers/audit-worker/src/handlers/audit-routes.ts +125 -0
- package/workers/audit-worker/src/storage/audit-storage.ts +99 -0
- package/workers/audit-worker/src/types.ts +56 -0
- package/workers/audit-worker/worker-configuration.d.ts +1 -1
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/src/config.ts +11 -0
- package/workers/data-worker/src/data-worker.example.ts +21 -942
- package/workers/data-worker/src/handlers/decrypt-export.ts +118 -0
- package/workers/data-worker/src/handlers/signing.ts +174 -0
- package/workers/data-worker/src/handlers/storage-routes.ts +129 -0
- package/workers/data-worker/src/registry/key-registry.ts +368 -0
- package/workers/data-worker/src/types.ts +46 -0
- package/workers/data-worker/worker-configuration.d.ts +1 -1
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/worker-configuration.d.ts +1 -1
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/worker-configuration.d.ts +2 -3
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/src/auth.ts +30 -0
- package/workers/user-worker/src/cleanup/account-deletion.ts +337 -0
- package/workers/user-worker/src/config.ts +4 -0
- package/workers/user-worker/src/encryption-utils.ts +25 -0
- package/workers/user-worker/src/firebase/admin.ts +152 -0
- package/workers/user-worker/src/handlers/user-routes.ts +242 -0
- package/workers/user-worker/src/registry/user-kv.ts +172 -0
- package/workers/user-worker/src/storage/user-records.ts +34 -0
- package/workers/user-worker/src/types.ts +106 -0
- package/workers/user-worker/src/user-worker.example.ts +18 -964
- package/workers/user-worker/worker-configuration.d.ts +4 -2
- package/workers/user-worker/wrangler.jsonc.example +12 -1
- package/wrangler.toml.example +1 -1
- package/app/components/actions/case-export/data-processing.ts +0 -223
- package/app/components/sidebar/case-export/case-export.module.css +0 -418
- package/app/components/sidebar/case-export/case-export.tsx +0 -310
- package/app/types/exceljs-bare.d.ts +0 -9
- package/app/utils/auth/auth.ts +0 -11
- package/public/.well-known/security.txt +0 -6
- package/public/favicon.ico +0 -0
- package/public/icon-256.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/manifest.json +0 -39
- package/public/shortcut.png +0 -0
- package/public/social-image.png +0 -0
- package/public/vendor/exceljs.LICENSE +0 -22
- package/public/vendor/exceljs.bare.min.js +0 -45
- package/scripts/deploy-all.sh +0 -166
- package/scripts/deploy-config/modules/env-utils.sh +0 -322
- package/scripts/deploy-config/modules/keys.sh +0 -404
- package/scripts/deploy-config/modules/prompt.sh +0 -372
- package/scripts/deploy-config/modules/scaffolding.sh +0 -336
- package/scripts/deploy-config/modules/validation.sh +0 -365
- package/scripts/deploy-config.sh +0 -236
- package/scripts/deploy-pages-secrets.sh +0 -231
- package/scripts/deploy-pages.sh +0 -34
- package/scripts/deploy-primershear-emails.sh +0 -167
- package/scripts/deploy-worker-secrets.sh +0 -374
- package/scripts/dev.cjs +0 -23
- package/scripts/install-workers.sh +0 -88
- package/scripts/run-eslint.cjs +0 -43
- package/scripts/update-compatibility-dates.cjs +0 -124
- package/scripts/update-markdown-versions.cjs +0 -43
- package/workers/keys-worker/package.json +0 -18
- package/workers/keys-worker/src/keys.example.ts +0 -67
- package/workers/keys-worker/src/keys.ts +0 -67
- package/workers/keys-worker/worker-configuration.d.ts +0 -7447
- package/workers/keys-worker/wrangler.jsonc.example +0 -15
|
@@ -90,11 +90,47 @@
|
|
|
90
90
|
font-size: 14px;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
.toast.loading {
|
|
94
|
+
background: var(--backgroundLight);
|
|
95
|
+
border-color: var(--primary);
|
|
96
|
+
box-shadow: 0 8px 32px color-mix(in lab, var(--primary) 20%, transparent);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.toast.loading .icon {
|
|
100
|
+
color: var(--primary);
|
|
101
|
+
background: color-mix(in lab, var(--primary) 15%, transparent);
|
|
102
|
+
border-radius: 50%;
|
|
103
|
+
width: 28px;
|
|
104
|
+
height: 28px;
|
|
105
|
+
display: flex;
|
|
106
|
+
align-items: center;
|
|
107
|
+
justify-content: center;
|
|
108
|
+
}
|
|
109
|
+
|
|
93
110
|
.icon {
|
|
94
111
|
font-weight: bold;
|
|
95
112
|
flex-shrink: 0;
|
|
96
113
|
}
|
|
97
114
|
|
|
115
|
+
.spinner {
|
|
116
|
+
width: 14px;
|
|
117
|
+
height: 14px;
|
|
118
|
+
border: 2px solid color-mix(in lab, var(--primary) 20%, transparent);
|
|
119
|
+
border-top-color: var(--primary);
|
|
120
|
+
border-radius: 50%;
|
|
121
|
+
animation: spin 0.8s linear infinite;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@keyframes spin {
|
|
125
|
+
from {
|
|
126
|
+
transform: rotate(0deg);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
to {
|
|
130
|
+
transform: rotate(360deg);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
98
134
|
.message {
|
|
99
135
|
flex: 1;
|
|
100
136
|
font-size: 16px;
|
|
@@ -2,9 +2,11 @@ import { useEffect, type ReactNode } from 'react';
|
|
|
2
2
|
import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
|
|
3
3
|
import styles from './toast.module.css';
|
|
4
4
|
|
|
5
|
+
export type ToastType = 'success' | 'error' | 'warning' | 'loading';
|
|
6
|
+
|
|
5
7
|
interface ToastProps {
|
|
6
8
|
message: ReactNode;
|
|
7
|
-
type:
|
|
9
|
+
type: ToastType;
|
|
8
10
|
isVisible: boolean;
|
|
9
11
|
onClose: () => void;
|
|
10
12
|
duration?: number;
|
|
@@ -45,7 +47,9 @@ export const Toast = ({ message, type, isVisible, onClose, duration = 4000 }: To
|
|
|
45
47
|
></div>
|
|
46
48
|
<div className={`${styles.toast} ${styles[type]} ${isVisible ? styles.show : ''}`}>
|
|
47
49
|
<div className={styles.icon}>
|
|
48
|
-
{type === '
|
|
50
|
+
{type === 'loading' ? (
|
|
51
|
+
<span className={styles.spinner} aria-hidden="true" />
|
|
52
|
+
) : type === 'success' ? '✓' : type === 'warning' ? '!' : '✗'}
|
|
49
53
|
</div>
|
|
50
54
|
<span className={styles.message}>{message}</span>
|
|
51
55
|
<button
|
|
@@ -101,7 +101,6 @@ export const ManageProfile = ({ isOpen, onClose }: ManageProfileProps) => {
|
|
|
101
101
|
email: user.email,
|
|
102
102
|
firstName: firstName || '',
|
|
103
103
|
lastName: lastName || '',
|
|
104
|
-
badgeId: normalizedBadgeId,
|
|
105
104
|
});
|
|
106
105
|
|
|
107
106
|
await auditService.logUserProfileUpdate(
|
|
@@ -253,12 +252,14 @@ export const ManageProfile = ({ isOpen, onClose }: ManageProfileProps) => {
|
|
|
253
252
|
id="badgeId"
|
|
254
253
|
type="text"
|
|
255
254
|
value={badgeId}
|
|
256
|
-
|
|
255
|
+
disabled
|
|
256
|
+
readOnly
|
|
257
257
|
className={styles.input}
|
|
258
258
|
autoComplete="off"
|
|
259
|
+
style={{ backgroundColor: '#f8f9fa', cursor: 'not-allowed' }}
|
|
259
260
|
/>
|
|
260
261
|
<p className={styles.helpText}>
|
|
261
|
-
|
|
262
|
+
Badge/ID number can only be changed by an administrator. Contact support if changes are needed.
|
|
262
263
|
</p>
|
|
263
264
|
</div>
|
|
264
265
|
|
package/app/root.tsx
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { LinksFunction } from 'react-router';
|
|
2
2
|
import {
|
|
3
3
|
Links,
|
|
4
|
-
Meta,
|
|
5
4
|
Outlet,
|
|
6
5
|
Scripts,
|
|
7
6
|
ScrollRestoration,
|
|
@@ -29,8 +28,6 @@ export const links: LinksFunction = () => [
|
|
|
29
28
|
rel: "stylesheet",
|
|
30
29
|
href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
|
|
31
30
|
},
|
|
32
|
-
{ rel: 'manifest', href: '/manifest.json' },
|
|
33
|
-
{ rel: 'icon', href: '/favicon.ico' },
|
|
34
31
|
];
|
|
35
32
|
|
|
36
33
|
type AppTheme = 'dark' | 'light';
|
|
@@ -60,17 +57,13 @@ const resolveRouteTheme = (matches: ReturnType<typeof useMatches>): AppTheme =>
|
|
|
60
57
|
export function Layout({ children }: { children: React.ReactNode }) {
|
|
61
58
|
const matches = useMatches();
|
|
62
59
|
const theme = resolveRouteTheme(matches);
|
|
63
|
-
const themeColor = theme === 'dark' ? '#000000' : '#377087';
|
|
64
60
|
|
|
65
61
|
return (
|
|
66
62
|
<html lang="en" data-theme={theme}>
|
|
67
63
|
<head>
|
|
68
64
|
<meta charSet="utf-8" />
|
|
69
65
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
70
|
-
<meta name="theme-color" content={themeColor} />
|
|
71
|
-
<meta name="color-scheme" content={theme} />
|
|
72
66
|
<style dangerouslySetInnerHTML={{ __html: themeStyles }} />
|
|
73
|
-
<Meta />
|
|
74
67
|
<Links />
|
|
75
68
|
</head>
|
|
76
69
|
<body className="flex flex-col h-screen w-full overflow-x-hidden">
|
package/app/routes/_index.tsx
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { Login as default
|
|
1
|
+
export { Login as default } from './auth/login';
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useState, useEffect, useRef } from 'react';
|
|
2
|
-
import { Link, useSearchParams
|
|
2
|
+
import { Link, useSearchParams } from 'react-router';
|
|
3
3
|
import { auth } from '~/services/firebase';
|
|
4
4
|
import {
|
|
5
|
-
signInWithEmailAndPassword,
|
|
5
|
+
signInWithEmailAndPassword,
|
|
6
6
|
createUserWithEmailAndPassword,
|
|
7
7
|
onAuthStateChanged,
|
|
8
8
|
sendEmailVerification,
|
|
@@ -28,95 +28,7 @@ import { generateUniqueId } from '~/utils/common';
|
|
|
28
28
|
import { evaluatePasswordPolicy, buildActionCodeSettings, userHasMFA } from '~/utils/auth';
|
|
29
29
|
import type { UserData } from '~/types';
|
|
30
30
|
|
|
31
|
-
const
|
|
32
|
-
const SOCIAL_IMAGE_PATH = '/social-image.png';
|
|
33
|
-
const SOCIAL_IMAGE_ALT = 'Striae forensic annotation and comparison workspace';
|
|
34
|
-
const LOGIN_PATH_ALIASES = new Set(['/auth', '/auth/', '/auth/login', '/auth/login/']);
|
|
35
|
-
|
|
36
|
-
type AuthMetaContent = {
|
|
37
|
-
title: string;
|
|
38
|
-
description: string;
|
|
39
|
-
robots: string;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const getCanonicalPath = (pathname: string): string => {
|
|
43
|
-
if (!pathname || LOGIN_PATH_ALIASES.has(pathname)) {
|
|
44
|
-
return '/';
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return pathname.startsWith('/') ? pathname : `/${pathname}`;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const getAuthMetaContent = (mode: string | null, hasActionCode: boolean): AuthMetaContent => {
|
|
51
|
-
if (!mode && !hasActionCode) {
|
|
52
|
-
return {
|
|
53
|
-
title: 'Striae: A Firearms Examiner\'s Comparison Companion',
|
|
54
|
-
description: 'Sign in to Striae to access your comparison annotation workspace, case files, and review tools.',
|
|
55
|
-
robots: 'index,follow,max-image-preview:large,max-snippet:-1,max-video-preview:-1',
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (mode === 'resetPassword') {
|
|
60
|
-
return {
|
|
61
|
-
title: 'Striae | Reset Your Password',
|
|
62
|
-
description: 'Use this secure page to reset your Striae account password and restore access to your workspace.',
|
|
63
|
-
robots: 'noindex,nofollow,noarchive',
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (mode === 'verifyEmail') {
|
|
68
|
-
return {
|
|
69
|
-
title: 'Striae | Verify Your Email Address',
|
|
70
|
-
description: 'Confirm your email address to complete Striae account activation and continue securely.',
|
|
71
|
-
robots: 'noindex,nofollow,noarchive',
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (mode === 'recoverEmail') {
|
|
76
|
-
return {
|
|
77
|
-
title: 'Striae | Recover Email Access',
|
|
78
|
-
description: 'Complete your Striae account email recovery steps securely.',
|
|
79
|
-
robots: 'noindex,nofollow,noarchive',
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return {
|
|
84
|
-
title: 'Striae | Account Action',
|
|
85
|
-
description: 'Complete your Striae account action securely.',
|
|
86
|
-
robots: 'noindex,nofollow,noarchive',
|
|
87
|
-
};
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
export const meta: MetaFunction = ({ location }) => {
|
|
91
|
-
const searchParams = new URLSearchParams(location.search);
|
|
92
|
-
const mode = searchParams.get('mode');
|
|
93
|
-
const hasActionCode = Boolean(searchParams.get('oobCode'));
|
|
94
|
-
|
|
95
|
-
const canonicalPath = getCanonicalPath(location.pathname);
|
|
96
|
-
const canonicalHref = `${APP_CANONICAL_ORIGIN}${canonicalPath}`;
|
|
97
|
-
const socialImageHref = `${APP_CANONICAL_ORIGIN}${SOCIAL_IMAGE_PATH}`;
|
|
98
|
-
const { title, description, robots } = getAuthMetaContent(mode, hasActionCode);
|
|
99
|
-
|
|
100
|
-
return [
|
|
101
|
-
{ title },
|
|
102
|
-
{ name: 'description', content: description },
|
|
103
|
-
{ name: 'robots', content: robots },
|
|
104
|
-
{ property: 'og:site_name', content: 'Striae' },
|
|
105
|
-
{ property: 'og:type', content: 'website' },
|
|
106
|
-
{ property: 'og:url', content: canonicalHref },
|
|
107
|
-
{ property: 'og:title', content: title },
|
|
108
|
-
{ property: 'og:description', content: description },
|
|
109
|
-
{ property: 'og:image', content: socialImageHref },
|
|
110
|
-
{ property: 'og:image:secure_url', content: socialImageHref },
|
|
111
|
-
{ property: 'og:image:alt', content: SOCIAL_IMAGE_ALT },
|
|
112
|
-
{ name: 'twitter:card', content: 'summary_large_image' },
|
|
113
|
-
{ name: 'twitter:title', content: title },
|
|
114
|
-
{ name: 'twitter:description', content: description },
|
|
115
|
-
{ name: 'twitter:image', content: socialImageHref },
|
|
116
|
-
{ name: 'twitter:image:alt', content: SOCIAL_IMAGE_ALT },
|
|
117
|
-
{ tagName: 'link', rel: 'canonical', href: canonicalHref },
|
|
118
|
-
];
|
|
119
|
-
};
|
|
31
|
+
const DEMO_COMPANY_NAME = 'STRIAE DEMO';
|
|
120
32
|
|
|
121
33
|
const SUPPORTED_EMAIL_ACTION_MODES = new Set(['resetPassword', 'verifyEmail', 'recoverEmail']);
|
|
122
34
|
|
|
@@ -157,7 +69,8 @@ export const Login = () => {
|
|
|
157
69
|
const [isClient, setIsClient] = useState(false);
|
|
158
70
|
const [firstName, setFirstName] = useState('');
|
|
159
71
|
const [lastName, setLastName] = useState('');
|
|
160
|
-
const [company, setCompany] = useState(
|
|
72
|
+
const [company, setCompany] = useState(DEMO_COMPANY_NAME);
|
|
73
|
+
const [badgeId, setBadgeId] = useState('');
|
|
161
74
|
const [confirmPasswordValue, setConfirmPasswordValue] = useState('');
|
|
162
75
|
|
|
163
76
|
// MFA state
|
|
@@ -251,7 +164,6 @@ export const Login = () => {
|
|
|
251
164
|
}
|
|
252
165
|
|
|
253
166
|
// Check if user exists in the USER_DB
|
|
254
|
-
let hasBadgeId = true;
|
|
255
167
|
setIsCheckingUser(true);
|
|
256
168
|
try {
|
|
257
169
|
const userData = await checkUserExists(currentUser);
|
|
@@ -262,8 +174,6 @@ export const Login = () => {
|
|
|
262
174
|
setError('This account does not exist or has been deleted');
|
|
263
175
|
return;
|
|
264
176
|
}
|
|
265
|
-
|
|
266
|
-
hasBadgeId = Boolean(userData.badgeId?.trim());
|
|
267
177
|
} catch (error) {
|
|
268
178
|
setIsCheckingUser(false);
|
|
269
179
|
handleSignOut();
|
|
@@ -282,13 +192,8 @@ export const Login = () => {
|
|
|
282
192
|
setShowMfaEnrollment(false);
|
|
283
193
|
|
|
284
194
|
if (shouldShowWelcomeToastRef.current) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
setWelcomeToastMessage(`Welcome to Striae, ${getUserFirstName(currentUser)}!`);
|
|
288
|
-
} else {
|
|
289
|
-
setWelcomeToastType('warning');
|
|
290
|
-
setWelcomeToastMessage('Your badge or ID number is not set. You can set one in Manage Profile.');
|
|
291
|
-
}
|
|
195
|
+
setWelcomeToastType('success');
|
|
196
|
+
setWelcomeToastMessage(`Welcome to Striae, ${getUserFirstName(currentUser)}!`);
|
|
292
197
|
setIsWelcomeToastVisible(true);
|
|
293
198
|
shouldShowWelcomeToastRef.current = false;
|
|
294
199
|
}
|
|
@@ -373,6 +278,7 @@ export const Login = () => {
|
|
|
373
278
|
const formFirstName = firstName;
|
|
374
279
|
const formLastName = lastName;
|
|
375
280
|
const formCompany = company;
|
|
281
|
+
const formBadgeId = badgeId;
|
|
376
282
|
|
|
377
283
|
try {
|
|
378
284
|
if (!isLogin) {
|
|
@@ -411,7 +317,8 @@ export const Login = () => {
|
|
|
411
317
|
formFirstName,
|
|
412
318
|
formLastName,
|
|
413
319
|
companyName || '',
|
|
414
|
-
true
|
|
320
|
+
true,
|
|
321
|
+
formBadgeId.trim()
|
|
415
322
|
);
|
|
416
323
|
|
|
417
324
|
// Log user registration audit event
|
|
@@ -691,6 +598,17 @@ export const Login = () => {
|
|
|
691
598
|
disabled={isLoading}
|
|
692
599
|
value={company}
|
|
693
600
|
onChange={(e) => setCompany(e.target.value)}
|
|
601
|
+
/>
|
|
602
|
+
<input
|
|
603
|
+
type="text"
|
|
604
|
+
name="badgeId"
|
|
605
|
+
required
|
|
606
|
+
placeholder="Badge/ID # (required)"
|
|
607
|
+
autoComplete="off"
|
|
608
|
+
className={styles.input}
|
|
609
|
+
disabled={isLoading}
|
|
610
|
+
value={badgeId}
|
|
611
|
+
onChange={(e) => setBadgeId(e.target.value)}
|
|
694
612
|
/>
|
|
695
613
|
{passwordStrength && (
|
|
696
614
|
<div className={styles.passwordStrength}>
|
|
@@ -740,6 +658,7 @@ export const Login = () => {
|
|
|
740
658
|
setFirstName('');
|
|
741
659
|
setLastName('');
|
|
742
660
|
setCompany('');
|
|
661
|
+
setBadgeId('');
|
|
743
662
|
setConfirmPasswordValue('');
|
|
744
663
|
}}
|
|
745
664
|
className={styles.toggleButton}
|
package/app/routes/auth/route.ts
CHANGED
|
@@ -7,11 +7,11 @@ import { ArchiveCaseModal } from '~/components/navbar/case-modals/archive-case-m
|
|
|
7
7
|
import { OpenCaseModal } from '~/components/navbar/case-modals/open-case-modal';
|
|
8
8
|
import { Toolbar } from '~/components/toolbar/toolbar';
|
|
9
9
|
import { Canvas } from '~/components/canvas/canvas';
|
|
10
|
-
import { Toast } from '~/components/toast/toast';
|
|
10
|
+
import { Toast, type ToastType } from '~/components/toast/toast';
|
|
11
11
|
import { getImageUrl, fetchFiles, deleteFile } from '~/components/actions/image-manage';
|
|
12
12
|
import { getNotes, saveNotes } from '~/components/actions/notes-manage';
|
|
13
13
|
import { generatePDF } from '~/components/actions/generate-pdf';
|
|
14
|
-
import {
|
|
14
|
+
import { exportConfirmationData } from '~/components/actions/confirm-export';
|
|
15
15
|
import { CasesModal } from '~/components/sidebar/cases/cases-modal';
|
|
16
16
|
import { FilesModal } from '~/components/sidebar/files/files-modal';
|
|
17
17
|
import { NotesEditorModal } from '~/components/sidebar/notes/notes-editor-modal';
|
|
@@ -77,8 +77,8 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
77
77
|
const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
|
|
78
78
|
const [showToast, setShowToast] = useState(false);
|
|
79
79
|
const [toastMessage, setToastMessage] = useState('');
|
|
80
|
-
const [toastType, setToastType] = useState<
|
|
81
|
-
const [
|
|
80
|
+
const [toastType, setToastType] = useState<ToastType>('success');
|
|
81
|
+
const [toastDuration, setToastDuration] = useState(4000);
|
|
82
82
|
const [isAuditTrailOpen, setIsAuditTrailOpen] = useState(false);
|
|
83
83
|
const [isRenameCaseModalOpen, setIsRenameCaseModalOpen] = useState(false);
|
|
84
84
|
const [isOpenCaseModalOpen, setIsOpenCaseModalOpen] = useState(false);
|
|
@@ -253,13 +253,19 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
253
253
|
setIsGeneratingPDF,
|
|
254
254
|
setToastType,
|
|
255
255
|
setToastMessage,
|
|
256
|
-
setShowToast
|
|
256
|
+
setShowToast,
|
|
257
|
+
setToastDuration
|
|
257
258
|
});
|
|
258
259
|
};
|
|
259
260
|
|
|
260
|
-
const showNotification = (
|
|
261
|
+
const showNotification = (
|
|
262
|
+
message: string,
|
|
263
|
+
type: ToastType = 'success',
|
|
264
|
+
duration = 4000
|
|
265
|
+
) => {
|
|
261
266
|
setToastType(type);
|
|
262
267
|
setToastMessage(message);
|
|
268
|
+
setToastDuration(duration);
|
|
263
269
|
setShowToast(true);
|
|
264
270
|
};
|
|
265
271
|
|
|
@@ -270,60 +276,45 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
270
276
|
|
|
271
277
|
const handleExport = async (
|
|
272
278
|
exportCaseNumber: string,
|
|
273
|
-
format: ExportFormat,
|
|
274
|
-
includeImages?: boolean,
|
|
275
279
|
onProgress?: (progress: number, label: string) => void
|
|
276
280
|
) => {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if (includeImages) {
|
|
280
|
-
await caseExportActions.downloadCaseAsZip(user, exportCaseNumber, format, (progress) => {
|
|
281
|
-
const label = getExportProgressLabel(progress);
|
|
282
|
-
onProgress?.(Math.round(progress), label);
|
|
283
|
-
});
|
|
284
|
-
showNotification(`Case ${exportCaseNumber} exported successfully.`, 'success');
|
|
281
|
+
if (!exportCaseNumber) {
|
|
282
|
+
showNotification('Select a case before exporting.', 'error');
|
|
285
283
|
return;
|
|
286
284
|
}
|
|
287
285
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
(
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
286
|
+
showNotification(`Exporting case ${exportCaseNumber}...`, 'loading', 0);
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
const caseExportActions = await loadCaseExportActions();
|
|
290
|
+
|
|
291
|
+
await caseExportActions.downloadCaseAsZip(user, exportCaseNumber, (progress) => {
|
|
292
|
+
const roundedProgress = Math.round(progress);
|
|
293
|
+
const label = getExportProgressLabel(progress);
|
|
294
|
+
setToastType('loading');
|
|
295
|
+
setToastMessage(`Exporting case ${exportCaseNumber}... ${label} (${roundedProgress}%)`);
|
|
296
|
+
setToastDuration(0);
|
|
297
|
+
setShowToast(true);
|
|
298
|
+
onProgress?.(roundedProgress, label);
|
|
299
|
+
});
|
|
298
300
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
} else {
|
|
303
|
-
await caseExportActions.downloadCaseAsCSV(user, exportData);
|
|
301
|
+
showNotification(`Case ${exportCaseNumber} exported successfully.`, 'success');
|
|
302
|
+
} catch (error) {
|
|
303
|
+
showNotification(error instanceof Error ? error.message : 'Export failed. Please try again.', 'error');
|
|
304
304
|
}
|
|
305
|
-
onProgress?.(100, 'Complete');
|
|
306
|
-
showNotification(`Case ${exportCaseNumber} exported successfully.`, 'success');
|
|
307
305
|
};
|
|
308
306
|
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
format: ExportFormat
|
|
312
|
-
) => {
|
|
313
|
-
const caseExportActions = await loadCaseExportActions();
|
|
314
|
-
const exportData = await caseExportActions.exportAllCases(
|
|
315
|
-
user,
|
|
316
|
-
{ includeMetadata: true },
|
|
317
|
-
onProgress
|
|
318
|
-
);
|
|
307
|
+
const handleExportConfirmations = async () => {
|
|
308
|
+
if (!currentCase || !user) return;
|
|
319
309
|
|
|
320
|
-
|
|
321
|
-
await caseExportActions.downloadAllCasesAsJSON(user, exportData);
|
|
322
|
-
} else {
|
|
323
|
-
await caseExportActions.downloadAllCasesAsCSV(user, exportData);
|
|
324
|
-
}
|
|
310
|
+
showNotification(`Exporting confirmations for case ${currentCase}...`, 'loading', 0);
|
|
325
311
|
|
|
326
|
-
|
|
312
|
+
try {
|
|
313
|
+
await exportConfirmationData(user, currentCase);
|
|
314
|
+
showNotification(`Confirmations for case ${currentCase} exported successfully.`, 'success');
|
|
315
|
+
} catch (e) {
|
|
316
|
+
showNotification(e instanceof Error ? e.message : 'Confirmation export failed. Please try again.', 'error');
|
|
317
|
+
}
|
|
327
318
|
};
|
|
328
319
|
|
|
329
320
|
const handleRenameCaseSubmit = async (newCaseName: string) => {
|
|
@@ -370,6 +361,8 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
370
361
|
}
|
|
371
362
|
|
|
372
363
|
setIsDeletingCase(true);
|
|
364
|
+
showNotification(`Deleting case ${currentCase}...`, 'loading', 0);
|
|
365
|
+
|
|
373
366
|
try {
|
|
374
367
|
const deleteResult = await deleteCase(user, currentCase);
|
|
375
368
|
clearLoadedCaseState();
|
|
@@ -474,6 +467,8 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
474
467
|
}
|
|
475
468
|
|
|
476
469
|
setIsArchivingCase(true);
|
|
470
|
+
showNotification(`Archiving case ${currentCase}... Preparing archive package.`, 'loading', 0);
|
|
471
|
+
|
|
477
472
|
try {
|
|
478
473
|
await archiveCase(user, currentCase, archiveReason);
|
|
479
474
|
setIsReadOnlyCase(true);
|
|
@@ -767,7 +762,13 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
767
762
|
void handleOpenCaseModal();
|
|
768
763
|
}}
|
|
769
764
|
onOpenListAllCases={() => setIsListCasesModalOpen(true)}
|
|
770
|
-
onOpenCaseExport={() =>
|
|
765
|
+
onOpenCaseExport={() => {
|
|
766
|
+
if (isReadOnlyCase) {
|
|
767
|
+
void handleExportConfirmations();
|
|
768
|
+
} else {
|
|
769
|
+
void handleExport(currentCase || '');
|
|
770
|
+
}
|
|
771
|
+
}}
|
|
771
772
|
onOpenAuditTrail={() => setIsAuditTrailOpen(true)}
|
|
772
773
|
onOpenRenameCase={() => setIsRenameCaseModalOpen(true)}
|
|
773
774
|
onDeleteCase={() => {
|
|
@@ -790,7 +791,7 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
790
791
|
onOpenCase={() => {
|
|
791
792
|
void handleOpenCaseModal();
|
|
792
793
|
}}
|
|
793
|
-
onOpenCaseExport={() =>
|
|
794
|
+
onOpenCaseExport={() => void handleExportConfirmations()}
|
|
794
795
|
imageId={imageId}
|
|
795
796
|
currentCase={currentCase}
|
|
796
797
|
imageLoaded={imageLoaded}
|
|
@@ -883,14 +884,6 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
883
884
|
isUploading={isUploading}
|
|
884
885
|
showNotification={showNotification}
|
|
885
886
|
/>
|
|
886
|
-
<CaseExport
|
|
887
|
-
isOpen={isCaseExportModalOpen}
|
|
888
|
-
onClose={() => setIsCaseExportModalOpen(false)}
|
|
889
|
-
onExport={handleExport}
|
|
890
|
-
onExportAll={handleExportAll}
|
|
891
|
-
currentCaseNumber={currentCase}
|
|
892
|
-
isReadOnly={isReadOnlyCase}
|
|
893
|
-
/>
|
|
894
887
|
<UserAuditViewer
|
|
895
888
|
caseNumber={currentCase || ''}
|
|
896
889
|
isOpen={isAuditTrailOpen}
|
|
@@ -916,6 +909,7 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
916
909
|
type={toastType}
|
|
917
910
|
isVisible={showToast}
|
|
918
911
|
onClose={closeToast}
|
|
912
|
+
duration={toastDuration}
|
|
919
913
|
/>
|
|
920
914
|
</div>
|
|
921
915
|
);
|
|
@@ -6,14 +6,11 @@ import {
|
|
|
6
6
|
//connectAuthEmulator,
|
|
7
7
|
} from 'firebase/auth';
|
|
8
8
|
import firebaseConfig from '~/config/firebase';
|
|
9
|
-
import { getAppVersion } from '~/utils/common';
|
|
10
9
|
|
|
11
10
|
export const app = initializeApp(firebaseConfig, "Striae");
|
|
12
11
|
export const auth = getAuth(app);
|
|
13
12
|
|
|
14
13
|
setPersistence(auth, browserSessionPersistence);
|
|
15
14
|
|
|
16
|
-
console.log(`Welcome to ${app.name} v${getAppVersion()}`);
|
|
17
|
-
|
|
18
15
|
//Connect to the Firebase Auth emulator if running locally
|
|
19
16
|
//connectAuthEmulator(auth, 'http://127.0.0.1:9099');
|
package/app/types/export.ts
CHANGED
package/app/utils/auth/index.ts
CHANGED
|
@@ -103,7 +103,8 @@ export const createUser = async (
|
|
|
103
103
|
firstName: string,
|
|
104
104
|
lastName: string,
|
|
105
105
|
company: string,
|
|
106
|
-
permitted: boolean = false
|
|
106
|
+
permitted: boolean = false,
|
|
107
|
+
badgeId: string = ''
|
|
107
108
|
): Promise<UserData> => {
|
|
108
109
|
try {
|
|
109
110
|
const userData: UserData = {
|
|
@@ -112,7 +113,7 @@ export const createUser = async (
|
|
|
112
113
|
firstName,
|
|
113
114
|
lastName,
|
|
114
115
|
company,
|
|
115
|
-
badgeId
|
|
116
|
+
badgeId,
|
|
116
117
|
permitted,
|
|
117
118
|
cases: [],
|
|
118
119
|
readOnlyCases: [],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@striae-org/striae",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Striae is a specialized, cloud-native platform designed to streamline forensic firearms identification by providing an intuitive environment for digital comparison image annotation, authenticated confirmations, and automated report generation.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -49,14 +49,15 @@
|
|
|
49
49
|
"load-context.ts",
|
|
50
50
|
"functions/",
|
|
51
51
|
"public/",
|
|
52
|
-
"scripts/",
|
|
53
52
|
"workers/*/package.json",
|
|
54
|
-
"workers/*/src
|
|
55
|
-
"workers/*/src
|
|
56
|
-
"workers/*/src
|
|
53
|
+
"workers/*/src/**/*.example.ts",
|
|
54
|
+
"workers/*/src/**/*.example.js",
|
|
55
|
+
"workers/*/src/**/*.ts",
|
|
57
56
|
"workers/pdf-worker/scripts/*.js",
|
|
58
|
-
"!workers/*/src
|
|
57
|
+
"!workers/*/src/**/*worker.ts",
|
|
58
|
+
"!workers/pdf-worker/src/assets/**/*",
|
|
59
59
|
"workers/pdf-worker/src/assets/generated-assets.example.ts",
|
|
60
|
+
"!workers/pdf-worker/src/formats/**/*",
|
|
60
61
|
"workers/pdf-worker/src/formats/format-striae.ts",
|
|
61
62
|
"workers/pdf-worker/src/report-types.ts",
|
|
62
63
|
"workers/*/wrangler.jsonc.example",
|
|
@@ -96,7 +97,7 @@
|
|
|
96
97
|
"deploy-config": "bash ./scripts/deploy-config.sh",
|
|
97
98
|
"update-env": "bash ./scripts/deploy-config.sh --update-env",
|
|
98
99
|
"install-workers": "bash ./scripts/install-workers.sh",
|
|
99
|
-
"deploy-workers": "npm run deploy-workers:audit && npm run deploy-workers:data && npm run deploy-workers:image && npm run deploy-workers:
|
|
100
|
+
"deploy-workers": "npm run deploy-workers:audit && npm run deploy-workers:data && npm run deploy-workers:image && npm run deploy-workers:pdf && npm run deploy-workers:user",
|
|
100
101
|
"deploy-workers:secrets": "bash ./scripts/deploy-worker-secrets.sh",
|
|
101
102
|
"deploy-pages:secrets": "bash ./scripts/deploy-pages-secrets.sh --production-only",
|
|
102
103
|
"deploy-pages": "bash ./scripts/deploy-pages.sh",
|
|
@@ -104,13 +105,11 @@
|
|
|
104
105
|
"deploy-workers:audit": "cd workers/audit-worker && npm run deploy",
|
|
105
106
|
"deploy-workers:data": "cd workers/data-worker && npm run deploy",
|
|
106
107
|
"deploy-workers:image": "cd workers/image-worker && npm run deploy",
|
|
107
|
-
"deploy-workers:keys": "cd workers/keys-worker && npm run deploy",
|
|
108
108
|
"deploy-workers:pdf": "cd workers/pdf-worker && npm run deploy",
|
|
109
109
|
"deploy-workers:user": "cd workers/user-worker && npm run deploy"
|
|
110
110
|
},
|
|
111
111
|
"dependencies": {
|
|
112
112
|
"@react-router/cloudflare": "^7.13.2",
|
|
113
|
-
"exceljs": "^4.4.0",
|
|
114
113
|
"firebase": "^12.10.0",
|
|
115
114
|
"isbot": "^5.1.36",
|
|
116
115
|
"jszip": "^3.10.1",
|
|
@@ -135,17 +134,11 @@
|
|
|
135
134
|
"typescript": "^5.9.3",
|
|
136
135
|
"vite": "^6.4.1",
|
|
137
136
|
"vite-tsconfig-paths": "^6.1.1",
|
|
138
|
-
"wrangler": "^4.
|
|
137
|
+
"wrangler": "^4.77.0"
|
|
139
138
|
},
|
|
140
139
|
"overrides": {
|
|
141
140
|
"tar": "7.5.11",
|
|
142
|
-
"undici": "7.24.1"
|
|
143
|
-
"exceljs": {
|
|
144
|
-
"archiver": "7.0.1",
|
|
145
|
-
"fast-csv": "5.0.5",
|
|
146
|
-
"unzipper": "0.12.3",
|
|
147
|
-
"glob": "13.0.6"
|
|
148
|
-
}
|
|
141
|
+
"undici": "7.24.1"
|
|
149
142
|
},
|
|
150
143
|
"engines": {
|
|
151
144
|
"node": ">=20.0.0"
|