@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.
Files changed (117) hide show
  1. package/.env.example +2 -10
  2. package/README.md +5 -46
  3. package/app/components/actions/case-export/core-export.ts +5 -174
  4. package/app/components/actions/case-export/download-handlers.ts +84 -751
  5. package/app/components/actions/case-export/index.ts +6 -30
  6. package/app/components/actions/case-export/metadata-helpers.ts +0 -78
  7. package/app/components/actions/case-export/types-constants.ts +0 -43
  8. package/app/components/actions/case-import/confirmation-import.ts +75 -36
  9. package/app/components/actions/case-import/confirmation-package.ts +68 -1
  10. package/app/components/actions/case-import/index.ts +1 -1
  11. package/app/components/actions/case-import/orchestrator.ts +78 -53
  12. package/app/components/actions/case-import/zip-processing.ts +160 -330
  13. package/app/components/actions/generate-pdf.ts +3 -2
  14. package/app/components/audit/user-audit-viewer.tsx +0 -19
  15. package/app/components/audit/viewer/audit-viewer-header.tsx +0 -33
  16. package/app/components/navbar/case-modals/archive-case-modal.tsx +1 -1
  17. package/app/components/navbar/case-modals/export-case-modal.module.css +27 -0
  18. package/app/components/navbar/case-modals/export-case-modal.tsx +132 -0
  19. package/app/components/navbar/case-modals/export-confirmations-modal.module.css +24 -0
  20. package/app/components/navbar/case-modals/export-confirmations-modal.tsx +108 -0
  21. package/app/components/navbar/navbar.tsx +1 -1
  22. package/app/components/sidebar/case-import/case-import.module.css +35 -0
  23. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +51 -3
  24. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +2 -4
  25. package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +36 -5
  26. package/app/components/sidebar/case-import/hooks/useFilePreview.ts +5 -9
  27. package/app/components/sidebar/case-import/index.ts +1 -4
  28. package/app/components/sidebar/notes/class-details-shared.ts +2 -2
  29. package/app/components/toast/toast.module.css +36 -0
  30. package/app/components/toast/toast.tsx +6 -2
  31. package/app/components/user/manage-profile.tsx +4 -3
  32. package/app/config-example/config.json +1 -2
  33. package/app/root.tsx +0 -7
  34. package/app/routes/_index.tsx +1 -1
  35. package/app/routes/auth/login.example.tsx +22 -103
  36. package/app/routes/auth/login.tsx +22 -103
  37. package/app/routes/auth/route.ts +1 -1
  38. package/app/routes/striae/striae.tsx +117 -59
  39. package/app/services/firebase/index.ts +0 -3
  40. package/app/types/case.ts +1 -0
  41. package/app/types/export.ts +2 -2
  42. package/app/types/import.ts +10 -0
  43. package/app/utils/auth/index.ts +0 -1
  44. package/app/utils/data/permissions.ts +3 -2
  45. package/package.json +9 -16
  46. package/public/_headers +0 -4
  47. package/public/_routes.json +0 -1
  48. package/worker-configuration.d.ts +20 -17
  49. package/workers/audit-worker/src/audit-worker.example.ts +9 -806
  50. package/workers/audit-worker/src/config.ts +7 -0
  51. package/workers/audit-worker/src/crypto/data-at-rest.ts +410 -0
  52. package/workers/audit-worker/src/handlers/audit-routes.ts +125 -0
  53. package/workers/audit-worker/src/storage/audit-storage.ts +99 -0
  54. package/workers/audit-worker/src/types.ts +56 -0
  55. package/workers/audit-worker/worker-configuration.d.ts +1 -1
  56. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  57. package/workers/data-worker/src/config.ts +11 -0
  58. package/workers/data-worker/src/data-worker.example.ts +21 -942
  59. package/workers/data-worker/src/handlers/decrypt-export.ts +118 -0
  60. package/workers/data-worker/src/handlers/signing.ts +174 -0
  61. package/workers/data-worker/src/handlers/storage-routes.ts +129 -0
  62. package/workers/data-worker/src/registry/key-registry.ts +368 -0
  63. package/workers/data-worker/src/types.ts +46 -0
  64. package/workers/data-worker/worker-configuration.d.ts +1 -1
  65. package/workers/data-worker/wrangler.jsonc.example +1 -1
  66. package/workers/image-worker/worker-configuration.d.ts +1 -1
  67. package/workers/image-worker/wrangler.jsonc.example +1 -1
  68. package/workers/pdf-worker/worker-configuration.d.ts +2 -3
  69. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  70. package/workers/user-worker/src/auth.ts +30 -0
  71. package/workers/user-worker/src/cleanup/account-deletion.ts +337 -0
  72. package/workers/user-worker/src/config.ts +4 -0
  73. package/workers/user-worker/src/encryption-utils.ts +25 -0
  74. package/workers/user-worker/src/firebase/admin.ts +152 -0
  75. package/workers/user-worker/src/handlers/user-routes.ts +242 -0
  76. package/workers/user-worker/src/registry/user-kv.ts +172 -0
  77. package/workers/user-worker/src/storage/user-records.ts +34 -0
  78. package/workers/user-worker/src/types.ts +106 -0
  79. package/workers/user-worker/src/user-worker.example.ts +18 -964
  80. package/workers/user-worker/worker-configuration.d.ts +4 -2
  81. package/workers/user-worker/wrangler.jsonc.example +12 -1
  82. package/wrangler.toml.example +1 -1
  83. package/app/components/actions/case-export/data-processing.ts +0 -223
  84. package/app/components/sidebar/case-export/case-export.module.css +0 -418
  85. package/app/components/sidebar/case-export/case-export.tsx +0 -310
  86. package/app/types/exceljs-bare.d.ts +0 -9
  87. package/app/utils/auth/auth.ts +0 -11
  88. package/public/.well-known/security.txt +0 -6
  89. package/public/favicon.ico +0 -0
  90. package/public/icon-256.png +0 -0
  91. package/public/icon-512.png +0 -0
  92. package/public/manifest.json +0 -39
  93. package/public/shortcut.png +0 -0
  94. package/public/social-image.png +0 -0
  95. package/public/vendor/exceljs.LICENSE +0 -22
  96. package/public/vendor/exceljs.bare.min.js +0 -45
  97. package/scripts/deploy-all.sh +0 -166
  98. package/scripts/deploy-config/modules/env-utils.sh +0 -322
  99. package/scripts/deploy-config/modules/keys.sh +0 -404
  100. package/scripts/deploy-config/modules/prompt.sh +0 -372
  101. package/scripts/deploy-config/modules/scaffolding.sh +0 -344
  102. package/scripts/deploy-config/modules/validation.sh +0 -365
  103. package/scripts/deploy-config.sh +0 -236
  104. package/scripts/deploy-pages-secrets.sh +0 -231
  105. package/scripts/deploy-pages.sh +0 -34
  106. package/scripts/deploy-primershear-emails.sh +0 -167
  107. package/scripts/deploy-worker-secrets.sh +0 -374
  108. package/scripts/dev.cjs +0 -23
  109. package/scripts/install-workers.sh +0 -88
  110. package/scripts/run-eslint.cjs +0 -43
  111. package/scripts/update-compatibility-dates.cjs +0 -124
  112. package/scripts/update-markdown-versions.cjs +0 -43
  113. package/workers/keys-worker/package.json +0 -18
  114. package/workers/keys-worker/src/keys.example.ts +0 -67
  115. package/workers/keys-worker/src/keys.ts +0 -67
  116. package/workers/keys-worker/worker-configuration.d.ts +0 -7447
  117. 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: 'success' | 'error' | 'warning';
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 === 'success' ? '✓' : type === 'warning' ? '!' : '✗'}
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
- onChange={(e) => setBadgeId(e.target.value)}
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
- Enter your Badge/ID number for confirmations and reports. This can be updated as needed.
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
 
@@ -1,6 +1,5 @@
1
1
  {
2
- "url": "PAGES_CUSTOM_DOMAIN",
3
- "account_hash": "ACCOUNT_HASH",
2
+ "url": "PAGES_CUSTOM_DOMAIN",
4
3
  "manifest_signing_key_id": "MANIFEST_SIGNING_KEY_ID",
5
4
  "manifest_signing_public_key": "MANIFEST_SIGNING_PUBLIC_KEY",
6
5
  "manifest_signing_public_keys": {
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">
@@ -1 +1 @@
1
- export { Login as default, meta } from './auth/login';
1
+ export { Login as default } from './auth/login';
@@ -1,8 +1,8 @@
1
1
  import { useState, useEffect, useRef } from 'react';
2
- import { Link, useSearchParams, type MetaFunction } from 'react-router';
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 APP_CANONICAL_ORIGIN = 'PAGES_CUSTOM_DOMAIN';
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
- if (hasBadgeId) {
286
- setWelcomeToastType('success');
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, type MetaFunction } from 'react-router';
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 APP_CANONICAL_ORIGIN = 'https://striae.app';
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
- if (hasBadgeId) {
286
- setWelcomeToastType('success');
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}
@@ -5,4 +5,4 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
5
5
  throw redirect(`/${requestUrl.search}`);
6
6
  };
7
7
 
8
- export { Login as default, meta } from './login';
8
+ export { Login as default } from './login';