@striae-org/striae 3.2.1 → 3.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.
Files changed (104) hide show
  1. package/app/components/actions/case-export/core-export.ts +2 -2
  2. package/app/components/actions/case-export/data-processing.ts +19 -4
  3. package/app/components/actions/case-export/download-handlers.ts +57 -8
  4. package/app/components/actions/case-export/metadata-helpers.ts +1 -1
  5. package/app/components/actions/case-import/annotation-import.ts +2 -2
  6. package/app/components/actions/case-import/confirmation-import.ts +44 -20
  7. package/app/components/actions/case-import/confirmation-package.ts +86 -0
  8. package/app/components/actions/case-import/image-operations.ts +1 -1
  9. package/app/components/actions/case-import/index.ts +1 -0
  10. package/app/components/actions/case-import/orchestrator.ts +16 -6
  11. package/app/components/actions/case-import/storage-operations.ts +7 -7
  12. package/app/components/actions/case-import/validation.ts +7 -100
  13. package/app/components/actions/case-import/zip-processing.ts +47 -5
  14. package/app/components/actions/case-manage.ts +3 -3
  15. package/app/components/actions/confirm-export.ts +47 -16
  16. package/app/components/actions/generate-pdf.ts +3 -3
  17. package/app/components/actions/image-manage.ts +3 -3
  18. package/app/components/actions/notes-manage.ts +3 -3
  19. package/app/components/actions/signout.tsx +1 -1
  20. package/app/components/audit/user-audit-viewer.tsx +2 -3
  21. package/app/components/auth/auth-provider.tsx +2 -2
  22. package/app/components/auth/mfa-enrollment.tsx +3 -3
  23. package/app/components/auth/mfa-verification.tsx +4 -4
  24. package/app/components/canvas/box-annotations/box-annotations.tsx +2 -2
  25. package/app/components/canvas/canvas.tsx +1 -1
  26. package/app/components/canvas/confirmation/confirmation.tsx +1 -1
  27. package/app/components/form/form-button.tsx +1 -1
  28. package/app/components/form/form.module.css +9 -0
  29. package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +163 -49
  30. package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +365 -88
  31. package/app/components/sidebar/case-export/case-export.tsx +2 -54
  32. package/app/components/sidebar/case-import/case-import.tsx +20 -8
  33. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +1 -1
  34. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +1 -1
  35. package/app/components/sidebar/case-import/hooks/useFilePreview.ts +9 -7
  36. package/app/components/sidebar/case-import/hooks/useImportExecution.ts +2 -2
  37. package/app/components/sidebar/case-import/utils/file-validation.ts +57 -2
  38. package/app/components/sidebar/cases/case-sidebar.tsx +106 -50
  39. package/app/components/sidebar/cases/cases-modal.tsx +1 -1
  40. package/app/components/sidebar/cases/cases.module.css +101 -18
  41. package/app/components/sidebar/files/files-modal.tsx +3 -2
  42. package/app/components/sidebar/notes/notes-sidebar.tsx +3 -3
  43. package/app/components/sidebar/notes/notes.module.css +33 -13
  44. package/app/components/sidebar/sidebar-container.tsx +4 -3
  45. package/app/components/sidebar/sidebar.tsx +2 -2
  46. package/app/components/sidebar/upload/image-upload-zone.tsx +2 -2
  47. package/app/components/theme-provider/theme-provider.tsx +1 -1
  48. package/app/components/user/delete-account.tsx +1 -1
  49. package/app/components/user/manage-profile.tsx +3 -3
  50. package/app/components/user/mfa-phone-update.tsx +17 -14
  51. package/app/contexts/auth.context.ts +1 -1
  52. package/app/root.tsx +2 -2
  53. package/app/routes/auth/emailActionHandler.tsx +2 -2
  54. package/app/routes/auth/emailVerification.tsx +2 -2
  55. package/app/routes/auth/login.tsx +134 -11
  56. package/app/routes/auth/passwordReset.tsx +2 -2
  57. package/app/routes/striae/striae.tsx +2 -2
  58. package/app/services/audit/audit-console-logger.ts +46 -0
  59. package/app/services/audit/audit-export-csv.ts +126 -0
  60. package/app/services/audit/audit-export-report.ts +174 -0
  61. package/app/services/audit/audit-export-signing.ts +85 -0
  62. package/app/services/audit/audit-export.service.ts +334 -0
  63. package/app/services/audit/audit-file-type.ts +13 -0
  64. package/app/services/audit/audit-query-helpers.ts +88 -0
  65. package/app/services/audit/audit-worker-client.ts +95 -0
  66. package/app/services/audit/audit.service.ts +990 -0
  67. package/app/services/audit/builders/audit-entry-builder.ts +32 -0
  68. package/app/services/audit/builders/audit-event-builders-annotation.ts +150 -0
  69. package/app/services/audit/builders/audit-event-builders-case-file.ts +249 -0
  70. package/app/services/audit/builders/audit-event-builders-user-security.ts +449 -0
  71. package/app/services/audit/builders/audit-event-builders-workflow.ts +272 -0
  72. package/app/services/audit/builders/index.ts +40 -0
  73. package/app/services/audit/index.ts +2 -0
  74. package/app/types/case.ts +2 -2
  75. package/app/types/exceljs-bare.d.ts +3 -1
  76. package/app/types/user.ts +1 -1
  77. package/app/utils/SHA256.ts +5 -1
  78. package/app/utils/audit-export-signature.ts +2 -2
  79. package/app/utils/confirmation-signature.ts +8 -4
  80. package/app/utils/data-operations.ts +5 -5
  81. package/app/utils/export-verification.ts +353 -0
  82. package/app/utils/mfa-phone.ts +1 -1
  83. package/app/utils/mfa.ts +1 -1
  84. package/app/utils/permissions.ts +2 -2
  85. package/app/utils/signature-utils.ts +74 -4
  86. package/package.json +11 -9
  87. package/public/favicon.ico +0 -0
  88. package/public/icon-256.png +0 -0
  89. package/public/icon-512.png +0 -0
  90. package/public/manifest.json +39 -0
  91. package/public/shortcut.png +0 -0
  92. package/public/social-image.png +0 -0
  93. package/react-router.config.ts +5 -0
  94. package/worker-configuration.d.ts +4435 -562
  95. package/workers/data-worker/src/data-worker.example.ts +3 -3
  96. package/workers/pdf-worker/scripts/generate-assets.js +94 -0
  97. package/workers/pdf-worker/src/{generated-assets.ts → assets/generated-assets.ts} +117 -117
  98. package/workers/pdf-worker/src/{format-striae.ts → formats/format-striae.ts} +535 -535
  99. package/workers/pdf-worker/src/pdf-worker.example.ts +1 -1
  100. package/app/services/audit-export.service.ts +0 -755
  101. package/app/services/audit.service.ts +0 -1474
  102. package/public/favicon.svg +0 -9
  103. /package/app/services/{firebase-errors.ts → firebase/errors.ts} +0 -0
  104. /package/app/services/{firebase.ts → firebase/index.ts} +0 -0
@@ -1,10 +1,11 @@
1
1
  /* eslint-disable jsx-a11y/no-static-element-interactions */
2
2
  /* eslint-disable jsx-a11y/click-events-have-key-events */
3
- import React, { useState, useEffect } from 'react';
3
+ import type React from 'react';
4
+ import { useState, useEffect } from 'react';
4
5
  import { Link } from 'react-router';
5
6
  import { Sidebar } from './sidebar';
6
- import { User } from 'firebase/auth';
7
- import { FileData } from '~/types';
7
+ import type { User } from 'firebase/auth';
8
+ import { type FileData } from '~/types';
8
9
  import styles from './sidebar.module.css';
9
10
  import { getAppVersion } from '../../utils/version';
10
11
 
@@ -1,4 +1,4 @@
1
- import { User } from 'firebase/auth';
1
+ import type { User } from 'firebase/auth';
2
2
  import { useState, useCallback } from 'react';
3
3
  import styles from './sidebar.module.css';
4
4
  import { ManageProfile } from '../user/manage-profile';
@@ -7,7 +7,7 @@ import { CaseSidebar } from './cases/case-sidebar';
7
7
  import { NotesSidebar } from './notes/notes-sidebar';
8
8
  import { CaseImport } from './case-import/case-import';
9
9
  import { Toast } from '../toast/toast';
10
- import { FileData, ImportResult, ConfirmationImportResult } from '~/types';
10
+ import { type FileData, type ImportResult, type ConfirmationImportResult } from '~/types';
11
11
 
12
12
  interface SidebarProps {
13
13
  user: User;
@@ -1,8 +1,8 @@
1
- import { User } from 'firebase/auth';
1
+ import type { User } from 'firebase/auth';
2
2
  import { useState, useRef, useEffect, useCallback } from 'react';
3
3
  import styles from './image-upload-zone.module.css';
4
4
  import { uploadFile } from '~/components/actions/image-manage';
5
- import { FileData } from '~/types';
5
+ import { type FileData } from '~/types';
6
6
 
7
7
  interface ImageUploadZoneProps {
8
8
  user: User;
@@ -1,4 +1,4 @@
1
- import { JSX, createContext, useContext } from 'react';
1
+ import { type JSX, createContext, useContext } from 'react';
2
2
  import { classes, media } from '~/utils/style';
3
3
  import { themes, tokens } from './theme';
4
4
 
@@ -3,7 +3,7 @@ import { signOut } from 'firebase/auth';
3
3
  import { auth } from '~/services/firebase';
4
4
  import paths from '~/config/config.json';
5
5
  import { getUserApiKey } from '~/utils/auth';
6
- import { auditService } from '~/services/audit.service';
6
+ import { auditService } from '~/services/audit';
7
7
  import styles from './delete-account.module.css';
8
8
 
9
9
  interface DeletionProgress {
@@ -5,8 +5,8 @@ import { DeleteAccount } from './delete-account';
5
5
  import { UserAuditViewer } from '../audit/user-audit-viewer';
6
6
  import { AuthContext } from '~/contexts/auth.context';
7
7
  import { getUserData, updateUserData } from '~/utils/permissions';
8
- import { auditService } from '~/services/audit.service';
9
- import { handleAuthError, ERROR_MESSAGES } from '~/services/firebase-errors';
8
+ import { auditService } from '~/services/audit';
9
+ import { handleAuthError, ERROR_MESSAGES } from '~/services/firebase/errors';
10
10
  import { FormField, FormButton, FormMessage } from '../form';
11
11
  import { MfaPhoneUpdateSection } from './mfa-phone-update';
12
12
  import styles from './manage-profile.module.css';
@@ -234,7 +234,7 @@ export const ManageProfile = ({ isOpen, onClose }: ManageProfileProps) => {
234
234
  <FormButton variant="primary" type="submit" isLoading={isLoading} loadingText="Updating...">
235
235
  Update Profile
236
236
  </FormButton>
237
- <FormButton variant="secondary" type="button" onClick={() => setShowAuditViewer(true)}>
237
+ <FormButton variant="audit" type="button" onClick={() => setShowAuditViewer(true)}>
238
238
  View My Audit Trail
239
239
  </FormButton>
240
240
  <FormButton variant="secondary" type="button" onClick={() => setShowResetForm(true)}>
@@ -12,9 +12,9 @@ import {
12
12
  type MultiFactorResolver,
13
13
  type User,
14
14
  } from 'firebase/auth';
15
- import { auditService } from '~/services/audit.service';
15
+ import { auditService } from '~/services/audit';
16
16
  import { auth } from '~/services/firebase';
17
- import { ERROR_MESSAGES, getValidationError, handleAuthError } from '~/services/firebase-errors';
17
+ import { ERROR_MESSAGES, getValidationError, handleAuthError } from '~/services/firebase/errors';
18
18
  import {
19
19
  formatPhoneNumberForMfa,
20
20
  getMaskedFactorDisplay,
@@ -58,6 +58,7 @@ export const MfaPhoneUpdateSection = ({
58
58
  const [recaptchaVerifier, setRecaptchaVerifier] = useState<RecaptchaVerifier | null>(null);
59
59
 
60
60
  const isMfaBusy = isMfaLoading || isMfaReauthLoading;
61
+ const hasMfaPhoneInput = mfaPhoneInput.trim().length > 0;
61
62
 
62
63
  const resetMfaReauthFlow = useCallback(() => {
63
64
  setShowMfaReauthPrompt(false);
@@ -665,18 +666,20 @@ export const MfaPhoneUpdateSection = ({
665
666
  )}
666
667
  </div>
667
668
  ) : !isMfaCodeSent ? (
668
- <div className={styles.mfaButtonGroup}>
669
- <FormButton
670
- variant="secondary"
671
- type="button"
672
- onClick={handleSendMfaVerificationCode}
673
- isLoading={isMfaLoading}
674
- loadingText="Sending Code..."
675
- disabled={!mfaPhoneInput.trim()}
676
- >
677
- Send Verification Code
678
- </FormButton>
679
- </div>
669
+ hasMfaPhoneInput ? (
670
+ <div className={styles.mfaButtonGroup}>
671
+ <FormButton
672
+ variant="secondary"
673
+ type="button"
674
+ onClick={handleSendMfaVerificationCode}
675
+ isLoading={isMfaLoading}
676
+ loadingText="Sending Code..."
677
+ disabled={!hasMfaPhoneInput}
678
+ >
679
+ Send Verification Code
680
+ </FormButton>
681
+ </div>
682
+ ) : null
680
683
  ) : (
681
684
  <div className={styles.mfaVerificationSection}>
682
685
  <input
@@ -1,5 +1,5 @@
1
1
  import { createContext } from 'react';
2
- import { User } from 'firebase/auth';
2
+ import type { User } from 'firebase/auth';
3
3
 
4
4
  interface AuthContextType {
5
5
  user: User | null;
package/app/root.tsx CHANGED
@@ -30,8 +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' },
33
34
  { rel: 'icon', href: '/favicon.ico' },
34
- { rel: 'icon', href: '/favicon.svg', type: 'image/svg+xml' },
35
35
  ];
36
36
 
37
37
  type AppTheme = 'dark' | 'light';
@@ -61,7 +61,7 @@ const resolveRouteTheme = (matches: ReturnType<typeof useMatches>): AppTheme =>
61
61
  export function Layout({ children }: { children: React.ReactNode }) {
62
62
  const matches = useMatches();
63
63
  const theme = resolveRouteTheme(matches);
64
- const themeColor = theme === 'dark' ? '#000000' : '#f5f5f5';
64
+ const themeColor = theme === 'dark' ? '#000000' : '#377087';
65
65
 
66
66
  return (
67
67
  <html lang="en" data-theme={theme}>
@@ -7,10 +7,10 @@ import {
7
7
  verifyPasswordResetCode,
8
8
  } from 'firebase/auth';
9
9
  import { auth } from '~/services/firebase';
10
- import { handleAuthError } from '~/services/firebase-errors';
10
+ import { handleAuthError } from '~/services/firebase/errors';
11
11
  import { evaluatePasswordPolicy } from '~/utils/password-policy';
12
12
  import { getSafeContinuePath } from '~/utils/auth-action-settings';
13
- import { auditService } from '~/services/audit.service';
13
+ import { auditService } from '~/services/audit';
14
14
  import { Icon } from '~/components/icon/icon';
15
15
  import styles from './emailActionHandler.module.css';
16
16
 
@@ -1,7 +1,7 @@
1
1
  import { useState } from 'react';
2
2
  import { Link } from 'react-router';
3
- import { sendEmailVerification, User } from 'firebase/auth';
4
- import { auditService } from '~/services/audit.service';
3
+ import { sendEmailVerification, type User } from 'firebase/auth';
4
+ import { auditService } from '~/services/audit';
5
5
  import { buildActionCodeSettings } from '~/utils/auth-action-settings';
6
6
  import styles from './login.module.css';
7
7
 
@@ -1,50 +1,151 @@
1
- import { useState, useEffect } from 'react';
2
- import { Link, useSearchParams } from 'react-router';
1
+ import { useState, useEffect, useRef } from 'react';
2
+ import { Link, useSearchParams, type MetaFunction } from 'react-router';
3
3
  import { auth } from '~/services/firebase';
4
4
  import {
5
5
  signInWithEmailAndPassword,
6
6
  createUserWithEmailAndPassword,
7
7
  onAuthStateChanged,
8
8
  sendEmailVerification,
9
- User,
9
+ type User,
10
10
  updateProfile,
11
11
  getMultiFactorResolver,
12
- MultiFactorResolver,
13
- MultiFactorError
12
+ type MultiFactorResolver,
13
+ type MultiFactorError
14
14
  } from 'firebase/auth';
15
15
  import { PasswordReset } from '~/routes/auth/passwordReset';
16
16
  import { EmailVerification } from '~/routes/auth/emailVerification';
17
17
  import { EmailActionHandler } from '~/routes/auth/emailActionHandler';
18
- import { handleAuthError } from '~/services/firebase-errors';
18
+ import { handleAuthError } from '~/services/firebase/errors';
19
19
  import { MFAVerification } from '~/components/auth/mfa-verification';
20
20
  import { MFAEnrollment } from '~/components/auth/mfa-enrollment';
21
+ import { Toast } from '~/components/toast/toast';
21
22
  import { Icon } from '~/components/icon/icon';
22
23
  import styles from './login.module.css';
23
24
  import { Striae } from '~/routes/striae/striae';
24
25
  import { getUserData, createUser } from '~/utils/permissions';
25
- import { auditService } from '~/services/audit.service';
26
+ import { auditService } from '~/services/audit';
26
27
  import { generateUniqueId } from '~/utils/id-generator';
27
28
  import { evaluatePasswordPolicy } from '~/utils/password-policy';
28
29
  import { buildActionCodeSettings } from '~/utils/auth-action-settings';
29
30
  import { userHasMFA } from '~/utils/mfa';
30
31
 
31
- export const meta = () => {
32
- const titleText = 'Striae | Welcome to Striae';
33
- const description = 'Login to your Striae account to access your projects and data';
32
+ const APP_CANONICAL_ORIGIN = 'https://app.striae.org';
33
+ const SOCIAL_IMAGE_PATH = '/social-image.png';
34
+ const SOCIAL_IMAGE_ALT = 'Striae forensic annotation and comparison workspace';
35
+ const LOGIN_PATH_ALIASES = new Set(['/auth', '/auth/', '/auth/login', '/auth/login/']);
36
+
37
+ type AuthMetaContent = {
38
+ title: string;
39
+ description: string;
40
+ robots: string;
41
+ };
42
+
43
+ const getCanonicalPath = (pathname: string): string => {
44
+ if (!pathname || LOGIN_PATH_ALIASES.has(pathname)) {
45
+ return '/';
46
+ }
47
+
48
+ return pathname.startsWith('/') ? pathname : `/${pathname}`;
49
+ };
50
+
51
+ const getAuthMetaContent = (mode: string | null, hasActionCode: boolean): AuthMetaContent => {
52
+ if (!mode && !hasActionCode) {
53
+ return {
54
+ title: 'Striae | Secure Login for Firearms Examiners',
55
+ description: 'Sign in to Striae to access your forensic annotation workspace, case files, and comparison tools.',
56
+ robots: 'index,follow,max-image-preview:large,max-snippet:-1,max-video-preview:-1',
57
+ };
58
+ }
59
+
60
+ if (mode === 'resetPassword') {
61
+ return {
62
+ title: 'Striae | Reset Your Password',
63
+ description: 'Use this secure page to reset your Striae account password and restore access to your workspace.',
64
+ robots: 'noindex,nofollow,noarchive',
65
+ };
66
+ }
67
+
68
+ if (mode === 'verifyEmail') {
69
+ return {
70
+ title: 'Striae | Verify Your Email Address',
71
+ description: 'Confirm your email address to complete Striae account activation and continue securely.',
72
+ robots: 'noindex,nofollow,noarchive',
73
+ };
74
+ }
75
+
76
+ if (mode === 'recoverEmail') {
77
+ return {
78
+ title: 'Striae | Recover Email Access',
79
+ description: 'Complete your Striae account email recovery steps securely.',
80
+ robots: 'noindex,nofollow,noarchive',
81
+ };
82
+ }
83
+
84
+ return {
85
+ title: 'Striae | Account Action',
86
+ description: 'Complete your Striae account action securely.',
87
+ robots: 'noindex,nofollow,noarchive',
88
+ };
89
+ };
90
+
91
+ export const meta: MetaFunction = ({ location }) => {
92
+ const searchParams = new URLSearchParams(location.search);
93
+ const mode = searchParams.get('mode');
94
+ const hasActionCode = Boolean(searchParams.get('oobCode'));
95
+
96
+ const canonicalPath = getCanonicalPath(location.pathname);
97
+ const canonicalHref = `${APP_CANONICAL_ORIGIN}${canonicalPath}`;
98
+ const socialImageHref = `${APP_CANONICAL_ORIGIN}${SOCIAL_IMAGE_PATH}`;
99
+ const { title, description, robots } = getAuthMetaContent(mode, hasActionCode);
34
100
 
35
101
  return [
36
- { title: titleText },
102
+ { title },
37
103
  { name: 'description', content: description },
104
+ { name: 'robots', content: robots },
105
+ { property: 'og:site_name', content: 'Striae' },
106
+ { property: 'og:type', content: 'website' },
107
+ { property: 'og:url', content: canonicalHref },
108
+ { property: 'og:title', content: title },
109
+ { property: 'og:description', content: description },
110
+ { property: 'og:image', content: socialImageHref },
111
+ { property: 'og:image:secure_url', content: socialImageHref },
112
+ { property: 'og:image:alt', content: SOCIAL_IMAGE_ALT },
113
+ { name: 'twitter:card', content: 'summary_large_image' },
114
+ { name: 'twitter:title', content: title },
115
+ { name: 'twitter:description', content: description },
116
+ { name: 'twitter:image', content: socialImageHref },
117
+ { name: 'twitter:image:alt', content: SOCIAL_IMAGE_ALT },
118
+ { tagName: 'link', rel: 'canonical', href: canonicalHref },
38
119
  ];
39
120
  };
40
121
 
41
122
  const SUPPORTED_EMAIL_ACTION_MODES = new Set(['resetPassword', 'verifyEmail', 'recoverEmail']);
42
123
 
124
+ const getUserFirstName = (user: User): string => {
125
+ const displayName = user.displayName?.trim();
126
+ if (displayName) {
127
+ const [firstName] = displayName.split(/\s+/);
128
+ if (firstName) {
129
+ return firstName;
130
+ }
131
+ }
132
+
133
+ const emailPrefix = user.email?.split('@')[0]?.trim();
134
+ if (emailPrefix) {
135
+ return emailPrefix;
136
+ }
137
+
138
+ return 'User';
139
+ };
140
+
43
141
  export const Login = () => {
44
142
  const [searchParams] = useSearchParams();
143
+ const shouldShowWelcomeToastRef = useRef(false);
45
144
 
46
145
  const [error, setError] = useState('');
47
146
  const [success, setSuccess] = useState('');
147
+ const [welcomeToastMessage, setWelcomeToastMessage] = useState('');
148
+ const [isWelcomeToastVisible, setIsWelcomeToastVisible] = useState(false);
48
149
  const [isLogin, setIsLogin] = useState(true);
49
150
  const [isLoading, setIsLoading] = useState(false);
50
151
  const [isCheckingUser, setIsCheckingUser] = useState(false);
@@ -180,6 +281,12 @@ export const Login = () => {
180
281
 
181
282
  console.log("User signed in:", currentUser.email);
182
283
  setShowMfaEnrollment(false);
284
+
285
+ if (shouldShowWelcomeToastRef.current) {
286
+ setWelcomeToastMessage(`Welcome to Striae, ${getUserFirstName(currentUser)}!`);
287
+ setIsWelcomeToastVisible(true);
288
+ shouldShowWelcomeToastRef.current = false;
289
+ }
183
290
 
184
291
  // Log successful login audit
185
292
  try {
@@ -198,6 +305,8 @@ export const Login = () => {
198
305
  setUser(null);
199
306
  setShowMfaEnrollment(false);
200
307
  setIsCheckingUser(false);
308
+ setIsWelcomeToastVisible(false);
309
+ shouldShowWelcomeToastRef.current = false;
201
310
  }
202
311
  });
203
312
 
@@ -339,6 +448,7 @@ export const Login = () => {
339
448
  // Don't sign out - let user stay logged in but unverified to see verification screen
340
449
  } else {
341
450
  // Login
451
+ shouldShowWelcomeToastRef.current = true;
342
452
  try {
343
453
  await signInWithEmailAndPassword(auth, email, password);
344
454
  } catch (loginError: unknown) {
@@ -356,10 +466,12 @@ export const Login = () => {
356
466
  setIsLoading(false);
357
467
  return;
358
468
  }
469
+ shouldShowWelcomeToastRef.current = false;
359
470
  throw loginError; // Re-throw non-MFA errors
360
471
  }
361
472
  }
362
473
  } catch (err) {
474
+ shouldShowWelcomeToastRef.current = false;
363
475
  const { message } = handleAuthError(err);
364
476
  setError(message);
365
477
 
@@ -408,6 +520,8 @@ export const Login = () => {
408
520
  setShowMfaEnrollment(false);
409
521
  setShowMfaVerification(false);
410
522
  setMfaResolver(null);
523
+ setIsWelcomeToastVisible(false);
524
+ shouldShowWelcomeToastRef.current = false;
411
525
  } catch (err) {
412
526
  console.error('Sign out error:', err);
413
527
  }
@@ -648,6 +762,15 @@ export const Login = () => {
648
762
  mandatory={true}
649
763
  />
650
764
  )}
765
+
766
+ {!shouldHandleEmailAction && (
767
+ <Toast
768
+ message={welcomeToastMessage}
769
+ type="success"
770
+ isVisible={isWelcomeToastVisible}
771
+ onClose={() => setIsWelcomeToastVisible(false)}
772
+ />
773
+ )}
651
774
 
652
775
  </>
653
776
  );
@@ -2,8 +2,8 @@ import { useRef, useState } from 'react';
2
2
  import { Link } from 'react-router';
3
3
  import { sendPasswordResetEmail, signOut } from 'firebase/auth';
4
4
  import { auth } from '~/services/firebase';
5
- import { handleAuthError, ERROR_MESSAGES } from '~/services/firebase-errors';
6
- import { auditService } from '~/services/audit.service';
5
+ import { handleAuthError, ERROR_MESSAGES } from '~/services/firebase/errors';
6
+ import { auditService } from '~/services/audit';
7
7
  import { buildActionCodeSettings } from '~/utils/auth-action-settings';
8
8
  import styles from './passwordReset.module.css';
9
9
 
@@ -1,4 +1,4 @@
1
- import { User } from 'firebase/auth';
1
+ import type { User } from 'firebase/auth';
2
2
  import { useState, useEffect } from 'react';
3
3
  import { SidebarContainer } from '~/components/sidebar/sidebar-container';
4
4
  import { Toolbar } from '~/components/toolbar/toolbar';
@@ -9,7 +9,7 @@ import { getNotes, saveNotes } from '~/components/actions/notes-manage';
9
9
  import { generatePDF } from '~/components/actions/generate-pdf';
10
10
  import { getUserApiKey } from '~/utils/auth';
11
11
  import { resolveEarliestAnnotationTimestamp } from '~/utils/annotation-timestamp';
12
- import { AnnotationData, FileData } from '~/types';
12
+ import { type AnnotationData, type FileData } from '~/types';
13
13
  import { checkCaseIsReadOnly } from '~/components/actions/case-manage';
14
14
  import paths from '~/config/config.json';
15
15
  import styles from './striae.module.css';
@@ -0,0 +1,46 @@
1
+ import { type ValidationAuditEntry } from '~/types';
2
+
3
+ export const getAuditSecurityIssuesForConsole = (
4
+ entry: ValidationAuditEntry
5
+ ): string[] => {
6
+ const checks = entry.details.securityChecks;
7
+ if (!checks) {
8
+ return [];
9
+ }
10
+
11
+ const securityIssues = [];
12
+
13
+ // For console diagnostics, self-confirmation is relevant for import actions only.
14
+ if (entry.action === 'import' && checks.selfConfirmationPrevented === true) {
15
+ securityIssues.push('selfConfirmationPrevented');
16
+ }
17
+
18
+ if (checks.fileIntegrityValid === false) {
19
+ securityIssues.push('fileIntegrityValid');
20
+ }
21
+
22
+ if (checks.exporterUidValidated === false) {
23
+ securityIssues.push('exporterUidValidated');
24
+ }
25
+
26
+ return securityIssues;
27
+ };
28
+
29
+ export const logAuditEntryToConsole = (entry: ValidationAuditEntry): void => {
30
+ const icon = entry.result === 'success' ? '✅' :
31
+ entry.result === 'failure' ? '❌' : '⚠️';
32
+
33
+ console.log(
34
+ `${icon} Audit [${entry.action.toUpperCase()}]: ${entry.details.fileName} ` +
35
+ `(Case: ${entry.details.caseNumber || 'N/A'}) - ${entry.result.toUpperCase()}`
36
+ );
37
+
38
+ if (entry.details.validationErrors.length > 0) {
39
+ console.log(' Errors:', entry.details.validationErrors);
40
+ }
41
+
42
+ const securityIssues = getAuditSecurityIssuesForConsole(entry);
43
+ if (securityIssues.length > 0) {
44
+ console.warn(' Security Issues:', securityIssues);
45
+ }
46
+ };
@@ -0,0 +1,126 @@
1
+ import { type ValidationAuditEntry } from '~/types';
2
+
3
+ export const AUDIT_CSV_ENTRY_HEADERS = [
4
+ 'Timestamp',
5
+ 'User Email',
6
+ 'Action',
7
+ 'Result',
8
+ 'File Name',
9
+ 'File Type',
10
+ 'Case Number',
11
+ 'Confirmation ID',
12
+ 'Original Examiner UID',
13
+ 'Reviewing Examiner UID',
14
+ 'File ID',
15
+ 'Original Filename',
16
+ 'File Size (MB)',
17
+ 'MIME Type',
18
+ 'Upload Method',
19
+ 'Delete Reason',
20
+ 'Annotation ID',
21
+ 'Annotation Type',
22
+ 'Annotation Tool',
23
+ 'Session ID',
24
+ 'User Agent',
25
+ 'Processing Time (ms)',
26
+ 'Hash Valid',
27
+ 'Validation Errors',
28
+ 'Security Issues',
29
+ 'Workflow Phase',
30
+ 'Profile Field',
31
+ 'Old Value',
32
+ 'New Value',
33
+ 'Total Confirmations In File',
34
+ 'Confirmations Successfully Imported',
35
+ 'Validation Steps Failed',
36
+ 'Case Name',
37
+ 'Total Files',
38
+ 'MFA Method',
39
+ 'Security Incident Type',
40
+ 'Security Severity'
41
+ ];
42
+
43
+ export const formatForCSV = (value?: string | number | null): string => {
44
+ if (value === undefined || value === null) return '';
45
+ const str = String(value);
46
+ if (str.includes(',') || str.includes('"') || str.includes('\n')) {
47
+ return `"${str.replace(/"/g, '""')}"`;
48
+ }
49
+ return str;
50
+ };
51
+
52
+ const getSecurityIssues = (entry: ValidationAuditEntry): string => {
53
+ const securityChecks = entry.details.securityChecks;
54
+ if (!securityChecks) {
55
+ return '';
56
+ }
57
+
58
+ const issues = [];
59
+
60
+ if (securityChecks.selfConfirmationPrevented === true) {
61
+ issues.push('selfConfirmationPrevented');
62
+ }
63
+
64
+ if (securityChecks.fileIntegrityValid === false) {
65
+ issues.push('fileIntegrityValid');
66
+ }
67
+
68
+ if (securityChecks.exporterUidValidated === false) {
69
+ issues.push('exporterUidValidated');
70
+ }
71
+
72
+ return issues.join('; ');
73
+ };
74
+
75
+ export const entryToCSVRow = (entry: ValidationAuditEntry): string => {
76
+ const fileDetails = entry.details.fileDetails;
77
+ const annotationDetails = entry.details.annotationDetails;
78
+ const sessionDetails = entry.details.sessionDetails;
79
+ const userProfileDetails = entry.details.userProfileDetails;
80
+ const caseDetails = entry.details.caseDetails;
81
+ const performanceMetrics = entry.details.performanceMetrics;
82
+ const securityDetails = entry.details.securityDetails;
83
+ const securityIssues = getSecurityIssues(entry);
84
+
85
+ const values = [
86
+ formatForCSV(entry.timestamp),
87
+ formatForCSV(entry.userEmail),
88
+ formatForCSV(entry.action),
89
+ formatForCSV(entry.result),
90
+ formatForCSV(entry.details.fileName),
91
+ formatForCSV(entry.details.fileType),
92
+ formatForCSV(entry.details.caseNumber),
93
+ formatForCSV(entry.details.confirmationId),
94
+ formatForCSV(entry.details.originalExaminerUid),
95
+ formatForCSV(entry.details.reviewingExaminerUid),
96
+ formatForCSV(fileDetails?.fileId),
97
+ formatForCSV(fileDetails?.originalFileName),
98
+ fileDetails?.fileSize ? (fileDetails.fileSize / 1024 / 1024).toFixed(2) : '',
99
+ formatForCSV(fileDetails?.mimeType),
100
+ formatForCSV(fileDetails?.uploadMethod),
101
+ formatForCSV(fileDetails?.deleteReason),
102
+ formatForCSV(annotationDetails?.annotationId),
103
+ formatForCSV(annotationDetails?.annotationType),
104
+ formatForCSV(annotationDetails?.tool),
105
+ formatForCSV(sessionDetails?.sessionId),
106
+ formatForCSV(sessionDetails?.userAgent),
107
+ performanceMetrics?.processingTimeMs || '',
108
+ entry.details.hashValid !== undefined ? (entry.details.hashValid ? 'Yes' : 'No') : '',
109
+ formatForCSV(entry.details.validationErrors?.join('; ')),
110
+ formatForCSV(securityIssues),
111
+ formatForCSV(entry.details.workflowPhase),
112
+ formatForCSV(userProfileDetails?.profileField),
113
+ formatForCSV(userProfileDetails?.oldValue),
114
+ formatForCSV(userProfileDetails?.newValue),
115
+ caseDetails?.totalAnnotations?.toString() || '',
116
+ performanceMetrics?.validationStepsCompleted?.toString() || '',
117
+ performanceMetrics?.validationStepsFailed?.toString() || '',
118
+ formatForCSV(caseDetails?.newCaseName || caseDetails?.oldCaseName),
119
+ caseDetails?.totalFiles?.toString() || '',
120
+ formatForCSV(securityDetails?.mfaMethod),
121
+ formatForCSV(securityDetails?.incidentType),
122
+ formatForCSV(securityDetails?.severity)
123
+ ];
124
+
125
+ return values.join(',');
126
+ };