@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.
- package/app/components/actions/case-export/core-export.ts +2 -2
- package/app/components/actions/case-export/data-processing.ts +19 -4
- package/app/components/actions/case-export/download-handlers.ts +57 -8
- package/app/components/actions/case-export/metadata-helpers.ts +1 -1
- package/app/components/actions/case-import/annotation-import.ts +2 -2
- package/app/components/actions/case-import/confirmation-import.ts +44 -20
- package/app/components/actions/case-import/confirmation-package.ts +86 -0
- package/app/components/actions/case-import/image-operations.ts +1 -1
- package/app/components/actions/case-import/index.ts +1 -0
- package/app/components/actions/case-import/orchestrator.ts +16 -6
- package/app/components/actions/case-import/storage-operations.ts +7 -7
- package/app/components/actions/case-import/validation.ts +7 -100
- package/app/components/actions/case-import/zip-processing.ts +47 -5
- package/app/components/actions/case-manage.ts +3 -3
- package/app/components/actions/confirm-export.ts +47 -16
- package/app/components/actions/generate-pdf.ts +3 -3
- package/app/components/actions/image-manage.ts +3 -3
- package/app/components/actions/notes-manage.ts +3 -3
- package/app/components/actions/signout.tsx +1 -1
- package/app/components/audit/user-audit-viewer.tsx +2 -3
- package/app/components/auth/auth-provider.tsx +2 -2
- package/app/components/auth/mfa-enrollment.tsx +3 -3
- package/app/components/auth/mfa-verification.tsx +4 -4
- package/app/components/canvas/box-annotations/box-annotations.tsx +2 -2
- package/app/components/canvas/canvas.tsx +1 -1
- package/app/components/canvas/confirmation/confirmation.tsx +1 -1
- package/app/components/form/form-button.tsx +1 -1
- package/app/components/form/form.module.css +9 -0
- package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +163 -49
- package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +365 -88
- package/app/components/sidebar/case-export/case-export.tsx +2 -54
- package/app/components/sidebar/case-import/case-import.tsx +20 -8
- package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +1 -1
- package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +1 -1
- package/app/components/sidebar/case-import/hooks/useFilePreview.ts +9 -7
- package/app/components/sidebar/case-import/hooks/useImportExecution.ts +2 -2
- package/app/components/sidebar/case-import/utils/file-validation.ts +57 -2
- package/app/components/sidebar/cases/case-sidebar.tsx +106 -50
- package/app/components/sidebar/cases/cases-modal.tsx +1 -1
- package/app/components/sidebar/cases/cases.module.css +101 -18
- package/app/components/sidebar/files/files-modal.tsx +3 -2
- package/app/components/sidebar/notes/notes-sidebar.tsx +3 -3
- package/app/components/sidebar/notes/notes.module.css +33 -13
- package/app/components/sidebar/sidebar-container.tsx +4 -3
- package/app/components/sidebar/sidebar.tsx +2 -2
- package/app/components/sidebar/upload/image-upload-zone.tsx +2 -2
- package/app/components/theme-provider/theme-provider.tsx +1 -1
- package/app/components/user/delete-account.tsx +1 -1
- package/app/components/user/manage-profile.tsx +3 -3
- package/app/components/user/mfa-phone-update.tsx +17 -14
- package/app/contexts/auth.context.ts +1 -1
- package/app/root.tsx +2 -2
- package/app/routes/auth/emailActionHandler.tsx +2 -2
- package/app/routes/auth/emailVerification.tsx +2 -2
- package/app/routes/auth/login.tsx +134 -11
- package/app/routes/auth/passwordReset.tsx +2 -2
- package/app/routes/striae/striae.tsx +2 -2
- package/app/services/audit/audit-console-logger.ts +46 -0
- package/app/services/audit/audit-export-csv.ts +126 -0
- package/app/services/audit/audit-export-report.ts +174 -0
- package/app/services/audit/audit-export-signing.ts +85 -0
- package/app/services/audit/audit-export.service.ts +334 -0
- package/app/services/audit/audit-file-type.ts +13 -0
- package/app/services/audit/audit-query-helpers.ts +88 -0
- package/app/services/audit/audit-worker-client.ts +95 -0
- package/app/services/audit/audit.service.ts +990 -0
- package/app/services/audit/builders/audit-entry-builder.ts +32 -0
- package/app/services/audit/builders/audit-event-builders-annotation.ts +150 -0
- package/app/services/audit/builders/audit-event-builders-case-file.ts +249 -0
- package/app/services/audit/builders/audit-event-builders-user-security.ts +449 -0
- package/app/services/audit/builders/audit-event-builders-workflow.ts +272 -0
- package/app/services/audit/builders/index.ts +40 -0
- package/app/services/audit/index.ts +2 -0
- package/app/types/case.ts +2 -2
- package/app/types/exceljs-bare.d.ts +3 -1
- package/app/types/user.ts +1 -1
- package/app/utils/SHA256.ts +5 -1
- package/app/utils/audit-export-signature.ts +2 -2
- package/app/utils/confirmation-signature.ts +8 -4
- package/app/utils/data-operations.ts +5 -5
- package/app/utils/export-verification.ts +353 -0
- package/app/utils/mfa-phone.ts +1 -1
- package/app/utils/mfa.ts +1 -1
- package/app/utils/permissions.ts +2 -2
- package/app/utils/signature-utils.ts +74 -4
- package/package.json +11 -9
- package/public/favicon.ico +0 -0
- package/public/icon-256.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/manifest.json +39 -0
- package/public/shortcut.png +0 -0
- package/public/social-image.png +0 -0
- package/react-router.config.ts +5 -0
- package/worker-configuration.d.ts +4435 -562
- package/workers/data-worker/src/data-worker.example.ts +3 -3
- package/workers/pdf-worker/scripts/generate-assets.js +94 -0
- package/workers/pdf-worker/src/{generated-assets.ts → assets/generated-assets.ts} +117 -117
- package/workers/pdf-worker/src/{format-striae.ts → formats/format-striae.ts} +535 -535
- package/workers/pdf-worker/src/pdf-worker.example.ts +1 -1
- package/app/services/audit-export.service.ts +0 -755
- package/app/services/audit.service.ts +0 -1474
- package/public/favicon.svg +0 -9
- /package/app/services/{firebase-errors.ts → firebase/errors.ts} +0 -0
- /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
|
|
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;
|
|
@@ -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
|
|
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
|
|
9
|
-
import { handleAuthError, ERROR_MESSAGES } from '~/services/firebase
|
|
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="
|
|
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
|
|
15
|
+
import { auditService } from '~/services/audit';
|
|
16
16
|
import { auth } from '~/services/firebase';
|
|
17
|
-
import { ERROR_MESSAGES, getValidationError, handleAuthError } from '~/services/firebase
|
|
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
|
-
|
|
669
|
-
<
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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
|
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' : '#
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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
|
|
6
|
-
import { auditService } from '~/services/audit
|
|
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
|
+
};
|