@striae-org/striae 5.2.1 → 5.3.1
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 +2 -10
- package/README.md +5 -46
- package/app/components/actions/case-export/core-export.ts +5 -174
- package/app/components/actions/case-export/download-handlers.ts +84 -751
- 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 +75 -36
- package/app/components/actions/case-import/confirmation-package.ts +68 -1
- package/app/components/actions/case-import/index.ts +1 -1
- package/app/components/actions/case-import/orchestrator.ts +78 -53
- package/app/components/actions/case-import/zip-processing.ts +160 -330
- 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/case-modals/export-case-modal.module.css +27 -0
- package/app/components/navbar/case-modals/export-case-modal.tsx +132 -0
- package/app/components/navbar/case-modals/export-confirmations-modal.module.css +24 -0
- package/app/components/navbar/case-modals/export-confirmations-modal.tsx +108 -0
- 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 +51 -3
- package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +2 -4
- package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +36 -5
- package/app/components/sidebar/case-import/hooks/useFilePreview.ts +5 -9
- package/app/components/sidebar/case-import/index.ts +1 -4
- 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/login.tsx +22 -103
- package/app/routes/auth/route.ts +1 -1
- package/app/routes/striae/striae.tsx +117 -59
- package/app/services/firebase/index.ts +0 -3
- package/app/types/case.ts +1 -0
- package/app/types/export.ts +2 -2
- package/app/types/import.ts +10 -0
- package/app/utils/auth/index.ts +0 -1
- package/app/utils/data/permissions.ts +3 -2
- package/package.json +9 -16
- 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 -344
- 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
|
@@ -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}
|
|
@@ -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