@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,14 +1,49 @@
|
|
|
1
|
-
import { User } from 'firebase/auth';
|
|
2
|
-
import { CaseExportData, CaseImportPreview } from '~/types';
|
|
1
|
+
import type { User } from 'firebase/auth';
|
|
2
|
+
import { type CaseExportData, type CaseImportPreview } from '~/types';
|
|
3
3
|
import { validateCaseNumber } from '../case-manage';
|
|
4
4
|
import {
|
|
5
5
|
extractForensicManifestData,
|
|
6
|
-
SignedForensicManifest,
|
|
6
|
+
type SignedForensicManifest,
|
|
7
7
|
validateCaseIntegritySecure as validateForensicIntegrity,
|
|
8
8
|
verifyForensicManifestSignature
|
|
9
9
|
} from '~/utils/SHA256';
|
|
10
10
|
import { validateExporterUid, removeForensicWarning } from './validation';
|
|
11
11
|
|
|
12
|
+
function getLeafFileName(path: string): string {
|
|
13
|
+
const segments = path.split('/').filter(Boolean);
|
|
14
|
+
return segments.length > 0 ? segments[segments.length - 1] : path;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function selectPreferredPemPath(pemPaths: string[]): string | undefined {
|
|
18
|
+
if (pemPaths.length === 0) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const sortedPaths = [...pemPaths].sort((left, right) => left.localeCompare(right));
|
|
23
|
+
const preferred = sortedPaths.find((path) =>
|
|
24
|
+
/^striae-public-signing-key.*\.pem$/i.test(getLeafFileName(path))
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
return preferred ?? sortedPaths[0];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function extractVerificationPublicKeyFromZip(
|
|
31
|
+
zip: {
|
|
32
|
+
files: Record<string, { dir: boolean }>;
|
|
33
|
+
file: (path: string) => { async: (type: 'text') => Promise<string> } | null;
|
|
34
|
+
}
|
|
35
|
+
): Promise<string | undefined> {
|
|
36
|
+
const filePaths = Object.keys(zip.files).filter((path) => !zip.files[path].dir);
|
|
37
|
+
const pemPaths = filePaths.filter((path) => getLeafFileName(path).toLowerCase().endsWith('.pem'));
|
|
38
|
+
const preferredPemPath = selectPreferredPemPath(pemPaths);
|
|
39
|
+
|
|
40
|
+
if (!preferredPemPath) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return zip.file(preferredPemPath)?.async('text');
|
|
45
|
+
}
|
|
46
|
+
|
|
12
47
|
/**
|
|
13
48
|
* Extract original image ID from export filename format
|
|
14
49
|
* Format: {originalFilename}-{id}.{extension}
|
|
@@ -51,6 +86,7 @@ export async function previewCaseImport(zipFile: File, currentUser: User): Promi
|
|
|
51
86
|
|
|
52
87
|
try {
|
|
53
88
|
const zip = await JSZip.loadAsync(zipFile);
|
|
89
|
+
const verificationPublicKeyPem = await extractVerificationPublicKeyFromZip(zip);
|
|
54
90
|
|
|
55
91
|
// First, validate hash if forensic metadata exists
|
|
56
92
|
let hashValid: boolean | undefined = undefined;
|
|
@@ -128,7 +164,10 @@ export async function previewCaseImport(zipFile: File, currentUser: User): Promi
|
|
|
128
164
|
}));
|
|
129
165
|
}
|
|
130
166
|
|
|
131
|
-
const signatureResult = await verifyForensicManifestSignature(
|
|
167
|
+
const signatureResult = await verifyForensicManifestSignature(
|
|
168
|
+
forensicManifest,
|
|
169
|
+
verificationPublicKeyPem
|
|
170
|
+
);
|
|
132
171
|
|
|
133
172
|
// Perform comprehensive validation
|
|
134
173
|
const validation = await validateForensicIntegrity(
|
|
@@ -267,12 +306,14 @@ export async function parseImportZip(zipFile: File, currentUser: User): Promise<
|
|
|
267
306
|
imageIdMapping: { [exportFilename: string]: string }; // exportFilename -> originalImageId
|
|
268
307
|
metadata?: Record<string, unknown>;
|
|
269
308
|
cleanedContent?: string; // Add cleaned content for hash validation
|
|
309
|
+
verificationPublicKeyPem?: string;
|
|
270
310
|
}> {
|
|
271
311
|
// Dynamic import of JSZip to avoid bundle size issues
|
|
272
312
|
const JSZip = (await import('jszip')).default;
|
|
273
313
|
|
|
274
314
|
try {
|
|
275
315
|
const zip = await JSZip.loadAsync(zipFile);
|
|
316
|
+
const verificationPublicKeyPem = await extractVerificationPublicKeyFromZip(zip);
|
|
276
317
|
|
|
277
318
|
// Find the main data file (JSON or CSV)
|
|
278
319
|
const dataFiles = Object.keys(zip.files).filter(name =>
|
|
@@ -367,7 +408,8 @@ export async function parseImportZip(zipFile: File, currentUser: User): Promise<
|
|
|
367
408
|
imageFiles,
|
|
368
409
|
imageIdMapping,
|
|
369
410
|
metadata,
|
|
370
|
-
cleanedContent
|
|
411
|
+
cleanedContent,
|
|
412
|
+
verificationPublicKeyPem
|
|
371
413
|
};
|
|
372
414
|
|
|
373
415
|
} catch (error) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { User } from 'firebase/auth';
|
|
1
|
+
import type { User } from 'firebase/auth';
|
|
2
2
|
import {
|
|
3
3
|
canCreateCase,
|
|
4
4
|
getUserCases,
|
|
@@ -13,8 +13,8 @@ import {
|
|
|
13
13
|
duplicateCaseData,
|
|
14
14
|
deleteFileAnnotations
|
|
15
15
|
} from '~/utils/data-operations';
|
|
16
|
-
import { CaseData, ReadOnlyCaseData, FileData } from '~/types';
|
|
17
|
-
import { auditService } from '~/services/audit
|
|
16
|
+
import { type CaseData, type ReadOnlyCaseData, type FileData } from '~/types';
|
|
17
|
+
import { auditService } from '~/services/audit';
|
|
18
18
|
import { getImageApiKey } from '~/utils/auth';
|
|
19
19
|
import paths from '~/config/config.json';
|
|
20
20
|
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import { User } from 'firebase/auth';
|
|
1
|
+
import type { User } from 'firebase/auth';
|
|
2
2
|
import { calculateSHA256Secure } from '~/utils/SHA256';
|
|
3
3
|
import { getUserData } from '~/utils/permissions';
|
|
4
4
|
import { getCaseData, updateCaseData, signConfirmationData } from '~/utils/data-operations';
|
|
5
|
-
import { ConfirmationData, CaseConfirmations, CaseDataWithConfirmations, ConfirmationImportData } from '~/types';
|
|
6
|
-
import {
|
|
5
|
+
import { type ConfirmationData, type CaseConfirmations, type CaseDataWithConfirmations, type ConfirmationImportData } from '~/types';
|
|
6
|
+
import {
|
|
7
|
+
createPublicSigningKeyFileName,
|
|
8
|
+
getCurrentPublicSigningKeyDetails,
|
|
9
|
+
getVerificationPublicKey
|
|
10
|
+
} from '~/utils/signature-utils';
|
|
11
|
+
import { auditService } from '~/services/audit';
|
|
7
12
|
|
|
8
13
|
/**
|
|
9
14
|
* Store a confirmation for a specific image, linked to the original image ID
|
|
@@ -267,15 +272,8 @@ export async function exportConfirmationData(
|
|
|
267
272
|
}
|
|
268
273
|
};
|
|
269
274
|
|
|
270
|
-
// Convert final data to JSON blob
|
|
271
275
|
const finalJsonString = JSON.stringify(finalExportData, null, 2);
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
// Create download
|
|
275
|
-
const url = URL.createObjectURL(blob);
|
|
276
|
-
const a = document.createElement('a');
|
|
277
|
-
a.href = url;
|
|
278
|
-
|
|
276
|
+
|
|
279
277
|
// Use local timezone for filename timestamp
|
|
280
278
|
const now = new Date();
|
|
281
279
|
const year = now.getFullYear();
|
|
@@ -285,14 +283,47 @@ export async function exportConfirmationData(
|
|
|
285
283
|
const minutes = String(now.getMinutes()).padStart(2, '0');
|
|
286
284
|
const seconds = String(now.getSeconds()).padStart(2, '0');
|
|
287
285
|
const timestampString = `${year}${month}${day}-${hours}${minutes}${seconds}`;
|
|
286
|
+
|
|
287
|
+
const confirmationFileName = `confirmation-data-${caseNumber}-${timestampString}.json`;
|
|
288
|
+
|
|
289
|
+
const keyFromSignature = getVerificationPublicKey(signingResult.signature.keyId);
|
|
290
|
+
const currentKey = getCurrentPublicSigningKeyDetails();
|
|
291
|
+
const publicKeyPem = keyFromSignature ?? currentKey.publicKeyPem;
|
|
292
|
+
const publicKeyFileName = createPublicSigningKeyFileName(
|
|
293
|
+
keyFromSignature ? signingResult.signature.keyId : currentKey.keyId
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
if (!publicKeyPem || publicKeyPem.trim().length === 0) {
|
|
297
|
+
throw new Error('No public signing key is configured for confirmation export packaging.');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const JSZip = (await import('jszip')).default;
|
|
301
|
+
const zip = new JSZip();
|
|
302
|
+
const normalizedPem = publicKeyPem.endsWith('\n') ? publicKeyPem : `${publicKeyPem}\n`;
|
|
303
|
+
|
|
304
|
+
zip.file(confirmationFileName, finalJsonString);
|
|
305
|
+
zip.file(publicKeyFileName, normalizedPem);
|
|
306
|
+
|
|
307
|
+
const zipBlob = await zip.generateAsync({
|
|
308
|
+
type: 'blob',
|
|
309
|
+
compression: 'DEFLATE',
|
|
310
|
+
compressionOptions: { level: 6 }
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const exportFileName = `confirmation-export-${caseNumber}-${timestampString}.zip`;
|
|
314
|
+
|
|
315
|
+
// Create download
|
|
316
|
+
const url = URL.createObjectURL(zipBlob);
|
|
317
|
+
const a = document.createElement('a');
|
|
318
|
+
a.href = url;
|
|
288
319
|
|
|
289
|
-
a.download =
|
|
320
|
+
a.download = exportFileName;
|
|
290
321
|
document.body.appendChild(a);
|
|
291
322
|
a.click();
|
|
292
323
|
document.body.removeChild(a);
|
|
293
324
|
URL.revokeObjectURL(url);
|
|
294
325
|
|
|
295
|
-
console.log(`Confirmation
|
|
326
|
+
console.log(`Confirmation export ZIP generated for case ${caseNumber}`);
|
|
296
327
|
|
|
297
328
|
// Log successful confirmation export
|
|
298
329
|
const endTime = Date.now();
|
|
@@ -300,14 +331,14 @@ export async function exportConfirmationData(
|
|
|
300
331
|
await auditService.logConfirmationExport(
|
|
301
332
|
user,
|
|
302
333
|
caseNumber,
|
|
303
|
-
|
|
334
|
+
exportFileName,
|
|
304
335
|
confirmationCount,
|
|
305
336
|
'success',
|
|
306
337
|
[],
|
|
307
338
|
undefined, // Original examiner UID not available here
|
|
308
339
|
{
|
|
309
340
|
processingTimeMs: endTime - startTime,
|
|
310
|
-
fileSizeBytes:
|
|
341
|
+
fileSizeBytes: zipBlob.size,
|
|
311
342
|
validationStepsCompleted: confirmationCount,
|
|
312
343
|
validationStepsFailed: 0
|
|
313
344
|
},
|
|
@@ -328,7 +359,7 @@ export async function exportConfirmationData(
|
|
|
328
359
|
await auditService.logConfirmationExport(
|
|
329
360
|
user,
|
|
330
361
|
caseNumber,
|
|
331
|
-
`confirmation-
|
|
362
|
+
`confirmation-export-${caseNumber}-error.zip`,
|
|
332
363
|
0,
|
|
333
364
|
'failure',
|
|
334
365
|
[error instanceof Error ? error.message : 'Unknown error'],
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import paths from '~/config/config.json';
|
|
2
|
-
import { AnnotationData } from '~/types/annotations';
|
|
3
|
-
import { auditService } from '~/services/audit
|
|
2
|
+
import { type AnnotationData } from '~/types/annotations';
|
|
3
|
+
import { auditService } from '~/services/audit';
|
|
4
4
|
import { getPdfApiKey } from '~/utils/auth';
|
|
5
|
-
import { User } from 'firebase/auth';
|
|
5
|
+
import type { User } from 'firebase/auth';
|
|
6
6
|
|
|
7
7
|
interface GeneratePDFParams {
|
|
8
8
|
user: User;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { User } from 'firebase/auth';
|
|
1
|
+
import type { User } from 'firebase/auth';
|
|
2
2
|
import paths from '~/config/config.json';
|
|
3
3
|
import {
|
|
4
4
|
getImageApiKey,
|
|
@@ -6,8 +6,8 @@ import {
|
|
|
6
6
|
} from '~/utils/auth';
|
|
7
7
|
import { canUploadFile } from '~/utils/permissions';
|
|
8
8
|
import { getCaseData, updateCaseData, deleteFileAnnotations } from '~/utils/data-operations';
|
|
9
|
-
import { CaseData, FileData, ImageUploadResponse } from '~/types';
|
|
10
|
-
import { auditService } from '~/services/audit
|
|
9
|
+
import type { CaseData, FileData, ImageUploadResponse } from '~/types';
|
|
10
|
+
import { auditService } from '~/services/audit';
|
|
11
11
|
|
|
12
12
|
const IMAGE_URL = paths.image_worker_url;
|
|
13
13
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { User } from 'firebase/auth';
|
|
2
|
-
import { AnnotationData } from '~/types/annotations';
|
|
3
|
-
import { saveFileAnnotations, getFileAnnotations, DataOperationOptions } from '~/utils/data-operations';
|
|
1
|
+
import type { User } from 'firebase/auth';
|
|
2
|
+
import { type AnnotationData } from '~/types/annotations';
|
|
3
|
+
import { saveFileAnnotations, getFileAnnotations, type DataOperationOptions } from '~/utils/data-operations';
|
|
4
4
|
|
|
5
5
|
export const saveNotes = async (
|
|
6
6
|
user: User,
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { useState, useEffect, useContext, useCallback } from 'react';
|
|
2
2
|
import { AuthContext } from '~/contexts/auth.context';
|
|
3
|
-
import { auditService } from '~/services/audit
|
|
4
|
-
import {
|
|
5
|
-
import { ValidationAuditEntry, AuditAction, AuditResult, AuditTrail, UserData, WorkflowPhase } from '~/types';
|
|
3
|
+
import { auditService, auditExportService } from '~/services/audit';
|
|
4
|
+
import { type ValidationAuditEntry, type AuditAction, type AuditResult, type AuditTrail, type UserData, type WorkflowPhase } from '~/types';
|
|
6
5
|
import { getUserData } from '~/utils/permissions';
|
|
7
6
|
import styles from './user-audit.module.css';
|
|
8
7
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { User } from 'firebase/auth';
|
|
1
|
+
import type { User } from 'firebase/auth';
|
|
2
2
|
import { useEffect, useState } from 'react';
|
|
3
3
|
import { auth } from '~/services/firebase';
|
|
4
4
|
import { useInactivityTimeout } from '~/hooks/useInactivityTimeout';
|
|
5
5
|
import { INACTIVITY_CONFIG } from '~/config/inactivity';
|
|
6
6
|
import { AuthContext } from '~/contexts/auth.context';
|
|
7
7
|
import { InactivityWarning } from '~/components/user/inactivity-warning';
|
|
8
|
-
import { auditService } from '~/services/audit
|
|
8
|
+
import { auditService } from '~/services/audit';
|
|
9
9
|
import { generateUniqueId } from '~/utils/id-generator';
|
|
10
10
|
|
|
11
11
|
interface AuthProviderProps {
|
|
@@ -6,11 +6,11 @@ import {
|
|
|
6
6
|
PhoneMultiFactorGenerator,
|
|
7
7
|
RecaptchaVerifier,
|
|
8
8
|
multiFactor,
|
|
9
|
-
User
|
|
9
|
+
type User
|
|
10
10
|
} from 'firebase/auth';
|
|
11
|
-
import { handleAuthError, getValidationError } from '~/services/firebase
|
|
11
|
+
import { handleAuthError, getValidationError } from '~/services/firebase/errors';
|
|
12
12
|
import { SignOut } from '~/components/actions/signout';
|
|
13
|
-
import { auditService } from '~/services/audit
|
|
13
|
+
import { auditService } from '~/services/audit';
|
|
14
14
|
import styles from './mfa-enrollment.module.css';
|
|
15
15
|
|
|
16
16
|
interface MFAEnrollmentProps {
|
|
@@ -3,13 +3,13 @@ import {
|
|
|
3
3
|
PhoneAuthProvider,
|
|
4
4
|
PhoneMultiFactorGenerator,
|
|
5
5
|
RecaptchaVerifier,
|
|
6
|
-
MultiFactorResolver,
|
|
7
|
-
UserCredential
|
|
6
|
+
type MultiFactorResolver,
|
|
7
|
+
type UserCredential
|
|
8
8
|
} from 'firebase/auth';
|
|
9
9
|
import { auth } from '~/services/firebase';
|
|
10
|
-
import { handleAuthError, getValidationError } from '~/services/firebase
|
|
10
|
+
import { handleAuthError, getValidationError } from '~/services/firebase/errors';
|
|
11
11
|
import { SignOut } from '~/components/actions/signout';
|
|
12
|
-
import { auditService } from '~/services/audit
|
|
12
|
+
import { auditService } from '~/services/audit';
|
|
13
13
|
import { generateUniqueId } from '~/utils/id-generator';
|
|
14
14
|
import styles from './mfa-verification.module.css';
|
|
15
15
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useState, useCallback, useMemo, useRef, useEffect, useContext } from 'react';
|
|
2
|
-
import { BoxAnnotation } from '~/types';
|
|
2
|
+
import { type BoxAnnotation } from '~/types';
|
|
3
3
|
import { AuthContext } from '~/contexts/auth.context';
|
|
4
|
-
import { auditService } from '~/services/audit
|
|
4
|
+
import { auditService } from '~/services/audit';
|
|
5
5
|
import { resolveEarliestAnnotationTimestamp } from '~/utils/annotation-timestamp';
|
|
6
6
|
import styles from './box-annotations.module.css';
|
|
7
7
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect, useState, useRef, useContext, useCallback } from 'react';
|
|
2
2
|
import { BoxAnnotations } from './box-annotations/box-annotations';
|
|
3
3
|
import { ConfirmationModal } from './confirmation/confirmation';
|
|
4
|
-
import { AnnotationData, BoxAnnotation, ConfirmationData } from '~/types/annotations';
|
|
4
|
+
import { type AnnotationData, type BoxAnnotation, type ConfirmationData } from '~/types/annotations';
|
|
5
5
|
import { AuthContext } from '~/contexts/auth.context';
|
|
6
6
|
import { storeConfirmation } from '~/components/actions/confirm-export';
|
|
7
7
|
import styles from './canvas.module.css';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useEffect, useContext } from 'react';
|
|
2
|
-
import { ConfirmationData } from '~/types/annotations';
|
|
2
|
+
import { type ConfirmationData } from '~/types/annotations';
|
|
3
3
|
import { AuthContext } from '~/contexts/auth.context';
|
|
4
4
|
import { generateConfirmationId } from '~/utils/id-generator';
|
|
5
5
|
import styles from './confirmation.module.css';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import styles from './form.module.css';
|
|
2
2
|
|
|
3
3
|
interface FormButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
4
|
-
variant?: 'primary' | 'secondary' | 'success' | 'error';
|
|
4
|
+
variant?: 'primary' | 'secondary' | 'success' | 'error' | 'audit';
|
|
5
5
|
isLoading?: boolean;
|
|
6
6
|
loadingText?: string;
|
|
7
7
|
children: React.ReactNode;
|
|
@@ -124,6 +124,15 @@
|
|
|
124
124
|
background-color: color-mix(in lab, var(--error) 85%, var(--black));
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
.buttonAudit {
|
|
128
|
+
background-color: #6f42c1;
|
|
129
|
+
color: var(--white);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.buttonAudit:hover:not(:disabled) {
|
|
133
|
+
background-color: #5a359a;
|
|
134
|
+
}
|
|
135
|
+
|
|
127
136
|
.button:disabled {
|
|
128
137
|
background-color: color-mix(in lab, var(--background) 95%, transparent);
|
|
129
138
|
color: var(--textLight);
|
|
@@ -79,93 +79,207 @@
|
|
|
79
79
|
font-weight: var(--fontWeightMedium);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
.
|
|
82
|
+
.verifierLayout {
|
|
83
|
+
display: flex;
|
|
84
|
+
flex-direction: column;
|
|
85
|
+
gap: var(--spaceL);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.verificationField {
|
|
89
|
+
display: flex;
|
|
90
|
+
flex-direction: column;
|
|
91
|
+
gap: var(--spaceS);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.fieldHeader {
|
|
95
|
+
display: flex;
|
|
96
|
+
align-items: center;
|
|
97
|
+
justify-content: space-between;
|
|
98
|
+
gap: var(--spaceS);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.fieldLabel {
|
|
83
102
|
font-size: var(--fontSizeBodyXS);
|
|
84
103
|
font-weight: var(--fontWeightMedium);
|
|
85
104
|
color: var(--textTitle);
|
|
86
105
|
}
|
|
87
106
|
|
|
88
|
-
.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
border:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
color: var(--textBody);
|
|
107
|
+
.hiddenFileInput {
|
|
108
|
+
display: none;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.clearButton {
|
|
112
|
+
background: none;
|
|
113
|
+
border: none;
|
|
114
|
+
padding: 0;
|
|
115
|
+
color: var(--primary);
|
|
98
116
|
font-size: var(--fontSizeBodyXS);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
resize: vertical;
|
|
117
|
+
font-weight: var(--fontWeightMedium);
|
|
118
|
+
cursor: pointer;
|
|
102
119
|
}
|
|
103
120
|
|
|
104
|
-
.
|
|
121
|
+
.dropZone {
|
|
122
|
+
min-height: 144px;
|
|
123
|
+
margin: 0;
|
|
124
|
+
display: flex;
|
|
125
|
+
flex-direction: column;
|
|
126
|
+
justify-content: center;
|
|
127
|
+
gap: var(--spaceXS);
|
|
128
|
+
padding: var(--spaceL);
|
|
129
|
+
border: 1px dashed color-mix(in lab, var(--text) 18%, transparent);
|
|
130
|
+
border-radius: var(--radiusM);
|
|
131
|
+
background: linear-gradient(
|
|
132
|
+
135deg,
|
|
133
|
+
color-mix(in lab, var(--primary) 4%, var(--backgroundLight)),
|
|
134
|
+
color-mix(in lab, var(--background) 94%, transparent)
|
|
135
|
+
);
|
|
136
|
+
cursor: pointer;
|
|
137
|
+
transition:
|
|
138
|
+
border-color var(--durationS) var(--bezierFastoutSlowin),
|
|
139
|
+
background-color var(--durationS) var(--bezierFastoutSlowin),
|
|
140
|
+
box-shadow var(--durationS) var(--bezierFastoutSlowin);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.dropZone:hover {
|
|
144
|
+
border-color: color-mix(in lab, var(--primary) 35%, transparent);
|
|
145
|
+
background: linear-gradient(
|
|
146
|
+
135deg,
|
|
147
|
+
color-mix(in lab, var(--primary) 7%, var(--backgroundLight)),
|
|
148
|
+
color-mix(in lab, var(--background) 92%, transparent)
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.dropZone:focus-visible {
|
|
153
|
+
outline: none;
|
|
154
|
+
border-color: color-mix(in lab, var(--primary) 48%, transparent);
|
|
155
|
+
box-shadow: 0 0 0 3px color-mix(in lab, var(--primary) 14%, transparent);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.dropZoneActive {
|
|
159
|
+
border-color: color-mix(in lab, var(--primary) 50%, transparent);
|
|
160
|
+
background: linear-gradient(
|
|
161
|
+
135deg,
|
|
162
|
+
color-mix(in lab, var(--primary) 10%, var(--backgroundLight)),
|
|
163
|
+
color-mix(in lab, var(--background) 90%, transparent)
|
|
164
|
+
);
|
|
165
|
+
box-shadow: 0 0 0 3px color-mix(in lab, var(--primary) 12%, transparent);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.dropZoneDisabled {
|
|
169
|
+
opacity: 0.7;
|
|
170
|
+
cursor: not-allowed;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.dropZonePrimary {
|
|
105
174
|
margin: 0;
|
|
106
175
|
font-size: var(--fontSizeBodyS);
|
|
107
176
|
font-weight: var(--fontWeightMedium);
|
|
108
177
|
color: var(--textTitle);
|
|
109
178
|
}
|
|
110
179
|
|
|
111
|
-
.
|
|
180
|
+
.dropZoneSecondary {
|
|
112
181
|
margin: 0;
|
|
113
|
-
|
|
114
|
-
display: flex;
|
|
115
|
-
flex-direction: column;
|
|
116
|
-
gap: var(--spaceXS);
|
|
182
|
+
font-size: var(--fontSizeBodyXS);
|
|
117
183
|
color: var(--textBody);
|
|
118
|
-
font-size: var(--fontSizeBodyS);
|
|
119
184
|
}
|
|
120
185
|
|
|
121
|
-
.
|
|
186
|
+
.fieldActions {
|
|
122
187
|
display: flex;
|
|
123
|
-
|
|
188
|
+
flex-wrap: wrap;
|
|
124
189
|
gap: var(--spaceS);
|
|
125
190
|
}
|
|
126
191
|
|
|
127
|
-
.
|
|
192
|
+
.fieldError {
|
|
128
193
|
margin: 0;
|
|
129
194
|
font-size: var(--fontSizeBodyXS);
|
|
195
|
+
color: var(--error);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.resultCard {
|
|
199
|
+
display: flex;
|
|
200
|
+
flex-direction: column;
|
|
201
|
+
gap: var(--spaceXS);
|
|
202
|
+
padding: var(--spaceM) var(--spaceL);
|
|
203
|
+
border-radius: var(--radiusM);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.resultPass {
|
|
207
|
+
border: 1px solid color-mix(in lab, var(--success) 38%, transparent);
|
|
208
|
+
background: color-mix(in lab, var(--success) 12%, var(--backgroundLight));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.resultFail {
|
|
212
|
+
border: 1px solid color-mix(in lab, var(--error) 32%, transparent);
|
|
213
|
+
background: color-mix(in lab, var(--errorLight) 40%, var(--backgroundLight));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.resultTitle {
|
|
217
|
+
margin: 0;
|
|
218
|
+
font-size: var(--fontSizeBodyM);
|
|
219
|
+
font-weight: var(--fontWeightBold);
|
|
220
|
+
letter-spacing: 0.06em;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.resultPass .resultTitle {
|
|
224
|
+
color: color-mix(in lab, var(--success) 78%, var(--black));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.resultFail .resultTitle {
|
|
228
|
+
color: color-mix(in lab, var(--error) 78%, var(--black));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.resultMessage {
|
|
232
|
+
margin: 0;
|
|
233
|
+
font-size: var(--fontSizeBodyS);
|
|
130
234
|
color: var(--textBody);
|
|
131
235
|
}
|
|
132
236
|
|
|
133
|
-
.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
237
|
+
.actions {
|
|
238
|
+
display: flex;
|
|
239
|
+
justify-content: flex-end;
|
|
240
|
+
gap: var(--spaceS);
|
|
241
|
+
flex-wrap: wrap;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.primaryButton,
|
|
245
|
+
.secondaryButton {
|
|
137
246
|
border-radius: var(--spaceXS);
|
|
138
247
|
padding: var(--spaceS) var(--spaceL);
|
|
139
248
|
font-size: var(--fontSizeBodyS);
|
|
140
249
|
font-weight: var(--fontWeightMedium);
|
|
141
250
|
cursor: pointer;
|
|
142
|
-
transition:
|
|
251
|
+
transition:
|
|
252
|
+
background-color var(--durationS) var(--bezierFastoutSlowin),
|
|
253
|
+
border-color var(--durationS) var(--bezierFastoutSlowin),
|
|
254
|
+
color var(--durationS) var(--bezierFastoutSlowin);
|
|
143
255
|
}
|
|
144
256
|
|
|
145
|
-
.
|
|
146
|
-
background:
|
|
147
|
-
|
|
257
|
+
.primaryButton {
|
|
258
|
+
background: var(--primary);
|
|
259
|
+
color: var(--white);
|
|
260
|
+
border: 1px solid var(--primary);
|
|
148
261
|
}
|
|
149
262
|
|
|
150
|
-
.
|
|
151
|
-
background: color-mix(in lab, var(--
|
|
152
|
-
color: var(--
|
|
153
|
-
border-color: color-mix(in lab, var(--text) 10%, transparent);
|
|
154
|
-
cursor: not-allowed;
|
|
263
|
+
.primaryButton:hover:not(:disabled) {
|
|
264
|
+
background: color-mix(in lab, var(--primary) 84%, var(--black));
|
|
265
|
+
border-color: color-mix(in lab, var(--primary) 84%, var(--black));
|
|
155
266
|
}
|
|
156
267
|
|
|
157
|
-
.
|
|
158
|
-
background:
|
|
159
|
-
color:
|
|
160
|
-
border:
|
|
161
|
-
border-radius: var(--spaceXS);
|
|
162
|
-
padding: var(--spaceS) var(--spaceL);
|
|
163
|
-
font-size: var(--fontSizeBodyS);
|
|
164
|
-
font-weight: var(--fontWeightMedium);
|
|
165
|
-
cursor: pointer;
|
|
166
|
-
transition: all var(--durationS) var(--bezierFastoutSlowin);
|
|
268
|
+
.secondaryButton {
|
|
269
|
+
background: transparent;
|
|
270
|
+
color: var(--textTitle);
|
|
271
|
+
border: 1px solid color-mix(in lab, var(--text) 16%, transparent);
|
|
167
272
|
}
|
|
168
273
|
|
|
169
|
-
.
|
|
170
|
-
background: color-mix(in lab, var(--
|
|
274
|
+
.secondaryButton:hover:not(:disabled) {
|
|
275
|
+
background: color-mix(in lab, var(--text) 5%, transparent);
|
|
276
|
+
border-color: color-mix(in lab, var(--text) 22%, transparent);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.primaryButton:disabled,
|
|
280
|
+
.secondaryButton:disabled {
|
|
281
|
+
background: color-mix(in lab, var(--background) 95%, transparent);
|
|
282
|
+
color: var(--textLight);
|
|
283
|
+
border-color: color-mix(in lab, var(--text) 10%, transparent);
|
|
284
|
+
cursor: not-allowed;
|
|
171
285
|
}
|