@striae-org/striae 3.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +100 -0
- package/LICENSE +190 -0
- package/NOTICE +18 -0
- package/README.md +133 -0
- package/app/components/actions/case-export/core-export.ts +328 -0
- package/app/components/actions/case-export/data-processing.ts +167 -0
- package/app/components/actions/case-export/download-handlers.ts +900 -0
- package/app/components/actions/case-export/index.ts +41 -0
- package/app/components/actions/case-export/metadata-helpers.ts +107 -0
- package/app/components/actions/case-export/types-constants.ts +56 -0
- package/app/components/actions/case-export/validation-utils.ts +25 -0
- package/app/components/actions/case-export.ts +4 -0
- package/app/components/actions/case-import/annotation-import.ts +35 -0
- package/app/components/actions/case-import/confirmation-import.ts +363 -0
- package/app/components/actions/case-import/image-operations.ts +61 -0
- package/app/components/actions/case-import/index.ts +39 -0
- package/app/components/actions/case-import/orchestrator.ts +420 -0
- package/app/components/actions/case-import/storage-operations.ts +270 -0
- package/app/components/actions/case-import/validation.ts +189 -0
- package/app/components/actions/case-import/zip-processing.ts +413 -0
- package/app/components/actions/case-manage.ts +524 -0
- package/app/components/actions/case-review.ts +4 -0
- package/app/components/actions/confirm-export.ts +351 -0
- package/app/components/actions/generate-pdf.ts +210 -0
- package/app/components/actions/image-manage.ts +385 -0
- package/app/components/actions/notes-manage.ts +33 -0
- package/app/components/actions/signout.module.css +15 -0
- package/app/components/actions/signout.tsx +50 -0
- package/app/components/audit/user-audit-viewer.tsx +975 -0
- package/app/components/audit/user-audit.module.css +568 -0
- package/app/components/auth/auth-provider.tsx +78 -0
- package/app/components/auth/mfa-enrollment.module.css +268 -0
- package/app/components/auth/mfa-enrollment.tsx +398 -0
- package/app/components/auth/mfa-verification.module.css +251 -0
- package/app/components/auth/mfa-verification.tsx +295 -0
- package/app/components/button/button.module.css +63 -0
- package/app/components/button/button.tsx +46 -0
- package/app/components/canvas/box-annotations/box-annotations.module.css +170 -0
- package/app/components/canvas/box-annotations/box-annotations.tsx +634 -0
- package/app/components/canvas/canvas.module.css +314 -0
- package/app/components/canvas/canvas.tsx +449 -0
- package/app/components/canvas/confirmation/confirmation.module.css +187 -0
- package/app/components/canvas/confirmation/confirmation.tsx +214 -0
- package/app/components/colors/colors.module.css +59 -0
- package/app/components/colors/colors.tsx +68 -0
- package/app/components/form/base-form.tsx +21 -0
- package/app/components/form/form-button.tsx +28 -0
- package/app/components/form/form-field.tsx +53 -0
- package/app/components/form/form-message.tsx +17 -0
- package/app/components/form/form-toggle.tsx +23 -0
- package/app/components/form/form.module.css +427 -0
- package/app/components/form/index.ts +6 -0
- package/app/components/icon/icon.module.css +3 -0
- package/app/components/icon/icon.tsx +27 -0
- package/app/components/icon/icons.svg +102 -0
- package/app/components/icon/manifest.json +110 -0
- package/app/components/sidebar/case-export/case-export.module.css +386 -0
- package/app/components/sidebar/case-export/case-export.tsx +317 -0
- package/app/components/sidebar/case-import/case-import.module.css +626 -0
- package/app/components/sidebar/case-import/case-import.tsx +404 -0
- package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +72 -0
- package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +72 -0
- package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +71 -0
- package/app/components/sidebar/case-import/components/ExistingCaseSection.tsx +40 -0
- package/app/components/sidebar/case-import/components/FileSelector.tsx +161 -0
- package/app/components/sidebar/case-import/components/ProgressSection.tsx +46 -0
- package/app/components/sidebar/case-import/hooks/useFilePreview.ts +101 -0
- package/app/components/sidebar/case-import/hooks/useImportExecution.ts +152 -0
- package/app/components/sidebar/case-import/hooks/useImportState.ts +88 -0
- package/app/components/sidebar/case-import/index.ts +18 -0
- package/app/components/sidebar/case-import/utils/file-validation.ts +43 -0
- package/app/components/sidebar/cases/case-sidebar.tsx +827 -0
- package/app/components/sidebar/cases/cases-modal.module.css +166 -0
- package/app/components/sidebar/cases/cases-modal.tsx +201 -0
- package/app/components/sidebar/cases/cases.module.css +713 -0
- package/app/components/sidebar/files/files-modal.module.css +209 -0
- package/app/components/sidebar/files/files-modal.tsx +239 -0
- package/app/components/sidebar/hash/hash-utility.module.css +366 -0
- package/app/components/sidebar/hash/hash-utility.tsx +982 -0
- package/app/components/sidebar/notes/notes-modal.tsx +51 -0
- package/app/components/sidebar/notes/notes-sidebar.tsx +491 -0
- package/app/components/sidebar/notes/notes.module.css +360 -0
- package/app/components/sidebar/sidebar-container.tsx +149 -0
- package/app/components/sidebar/sidebar.module.css +321 -0
- package/app/components/sidebar/sidebar.tsx +215 -0
- package/app/components/sidebar/upload/image-upload-zone.module.css +123 -0
- package/app/components/sidebar/upload/image-upload-zone.tsx +330 -0
- package/app/components/theme-provider/theme-provider.tsx +131 -0
- package/app/components/theme-provider/theme.ts +155 -0
- package/app/components/toast/toast.module.css +137 -0
- package/app/components/toast/toast.tsx +56 -0
- package/app/components/toolbar/toolbar-color-selector.module.css +171 -0
- package/app/components/toolbar/toolbar-color-selector.tsx +129 -0
- package/app/components/toolbar/toolbar.module.css +42 -0
- package/app/components/toolbar/toolbar.tsx +167 -0
- package/app/components/user/delete-account.module.css +274 -0
- package/app/components/user/delete-account.tsx +471 -0
- package/app/components/user/inactivity-warning.module.css +145 -0
- package/app/components/user/inactivity-warning.tsx +84 -0
- package/app/components/user/manage-profile.module.css +190 -0
- package/app/components/user/manage-profile.tsx +253 -0
- package/app/components/user/mfa-phone-update.tsx +739 -0
- package/app/config-example/admin-service.json +13 -0
- package/app/config-example/config.json +17 -0
- package/app/config-example/firebase.ts +21 -0
- package/app/config-example/inactivity.ts +13 -0
- package/app/config-example/meta-config.json +6 -0
- package/app/contexts/auth.context.ts +12 -0
- package/app/entry.client.tsx +12 -0
- package/app/entry.server.tsx +44 -0
- package/app/hooks/useInactivityTimeout.ts +110 -0
- package/app/root.tsx +170 -0
- package/app/routes/_index.tsx +16 -0
- package/app/routes/auth/emailActionHandler.module.css +232 -0
- package/app/routes/auth/emailActionHandler.tsx +405 -0
- package/app/routes/auth/emailVerification.tsx +120 -0
- package/app/routes/auth/login.module.css +523 -0
- package/app/routes/auth/login.tsx +654 -0
- package/app/routes/auth/passwordReset.module.css +274 -0
- package/app/routes/auth/passwordReset.tsx +154 -0
- package/app/routes/auth/route.ts +16 -0
- package/app/routes/mobile-prevented/mobilePrevented.module.css +47 -0
- package/app/routes/mobile-prevented/mobilePrevented.tsx +26 -0
- package/app/routes/mobile-prevented/route.ts +14 -0
- package/app/routes/striae/striae.module.css +30 -0
- package/app/routes/striae/striae.tsx +417 -0
- package/app/services/audit-export.service.ts +755 -0
- package/app/services/audit.service.ts +1454 -0
- package/app/services/firebase-errors.ts +106 -0
- package/app/services/firebase.ts +15 -0
- package/app/styles/legal-pages.module.css +113 -0
- package/app/styles/root.module.css +146 -0
- package/app/tailwind.css +225 -0
- package/app/types/annotations.ts +45 -0
- package/app/types/audit.ts +301 -0
- package/app/types/case.ts +90 -0
- package/app/types/export.ts +8 -0
- package/app/types/file.ts +30 -0
- package/app/types/import.ts +107 -0
- package/app/types/index.ts +24 -0
- package/app/types/user.ts +38 -0
- package/app/utils/SHA256.ts +461 -0
- package/app/utils/annotation-timestamp.ts +25 -0
- package/app/utils/audit-export-signature.ts +117 -0
- package/app/utils/auth-action-settings.ts +48 -0
- package/app/utils/auth.ts +34 -0
- package/app/utils/batch-operations.ts +135 -0
- package/app/utils/confirmation-signature.ts +193 -0
- package/app/utils/data-operations.ts +871 -0
- package/app/utils/device-detection.ts +5 -0
- package/app/utils/html-sanitizer.ts +80 -0
- package/app/utils/id-generator.ts +36 -0
- package/app/utils/meta.ts +48 -0
- package/app/utils/mfa-phone.ts +97 -0
- package/app/utils/mfa.ts +79 -0
- package/app/utils/password-policy.ts +28 -0
- package/app/utils/permissions.ts +562 -0
- package/app/utils/signature-utils.ts +160 -0
- package/app/utils/style.ts +83 -0
- package/app/utils/version.ts +5 -0
- package/firebase.json +11 -0
- package/functions/[[path]].ts +10 -0
- package/package.json +138 -0
- package/postcss.config.js +6 -0
- package/public/.well-known/publickey.info@striae.org.asc +17 -0
- package/public/.well-known/security.txt +7 -0
- package/public/_headers +28 -0
- package/public/_routes.json +13 -0
- package/public/assets/striae.jpg +0 -0
- package/public/clear.jpg +0 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +9 -0
- package/public/icon-256.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/logo-dark.png +0 -0
- package/public/manifest.json +25 -0
- package/public/oin-badge.png +0 -0
- package/public/shortcut.png +0 -0
- package/public/social-image.png +0 -0
- package/public/striae-ascii.txt +10 -0
- package/scripts/deploy-all.sh +100 -0
- package/scripts/deploy-config.sh +940 -0
- package/scripts/deploy-pages.sh +34 -0
- package/scripts/deploy-worker-secrets.sh +215 -0
- package/scripts/dev.cjs +23 -0
- package/scripts/install-workers.sh +88 -0
- package/scripts/run-eslint.cjs +35 -0
- package/scripts/update-compatibility-dates.cjs +124 -0
- package/scripts/update-markdown-versions.cjs +43 -0
- package/tailwind.config.ts +22 -0
- package/tsconfig.json +33 -0
- package/vite.config.ts +35 -0
- package/worker-configuration.d.ts +7490 -0
- package/workers/audit-worker/package.json +17 -0
- package/workers/audit-worker/src/audit-worker.example.ts +195 -0
- package/workers/audit-worker/worker-configuration.d.ts +7448 -0
- package/workers/audit-worker/wrangler.jsonc.example +29 -0
- package/workers/data-worker/package.json +17 -0
- package/workers/data-worker/src/data-worker.example.ts +267 -0
- package/workers/data-worker/src/signature-utils.ts +79 -0
- package/workers/data-worker/src/signing-payload-utils.ts +290 -0
- package/workers/data-worker/worker-configuration.d.ts +7448 -0
- package/workers/data-worker/wrangler.jsonc.example +30 -0
- package/workers/image-worker/package.json +17 -0
- package/workers/image-worker/src/image-worker.example.ts +180 -0
- package/workers/image-worker/worker-configuration.d.ts +7447 -0
- package/workers/image-worker/wrangler.jsonc.example +22 -0
- package/workers/keys-worker/package.json +17 -0
- package/workers/keys-worker/src/keys.example.ts +66 -0
- package/workers/keys-worker/src/keys.ts +66 -0
- package/workers/keys-worker/worker-configuration.d.ts +7447 -0
- package/workers/keys-worker/wrangler.jsonc.example +22 -0
- package/workers/pdf-worker/package.json +17 -0
- package/workers/pdf-worker/src/format-striae.ts +534 -0
- package/workers/pdf-worker/src/pdf-worker.example.ts +119 -0
- package/workers/pdf-worker/src/report-types.ts +69 -0
- package/workers/pdf-worker/worker-configuration.d.ts +7448 -0
- package/workers/pdf-worker/wrangler.jsonc.example +26 -0
- package/workers/user-worker/package.json +17 -0
- package/workers/user-worker/src/user-worker.example.ts +636 -0
- package/workers/user-worker/worker-configuration.d.ts +7448 -0
- package/workers/user-worker/wrangler.jsonc.example +29 -0
- package/wrangler.toml.example +8 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { signOut } from 'firebase/auth';
|
|
3
|
+
import { auth } from '~/services/firebase';
|
|
4
|
+
import paths from '~/config/config.json';
|
|
5
|
+
import { getUserApiKey } from '~/utils/auth';
|
|
6
|
+
import { auditService } from '~/services/audit.service';
|
|
7
|
+
import styles from './delete-account.module.css';
|
|
8
|
+
|
|
9
|
+
interface DeletionProgress {
|
|
10
|
+
totalCases: number;
|
|
11
|
+
completedCases: number;
|
|
12
|
+
currentCaseNumber: string;
|
|
13
|
+
percent: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const initialDeletionProgress: DeletionProgress = {
|
|
17
|
+
totalCases: 0,
|
|
18
|
+
completedCases: 0,
|
|
19
|
+
currentCaseNumber: '',
|
|
20
|
+
percent: 0
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
interface DeleteAccountProps {
|
|
24
|
+
isOpen: boolean;
|
|
25
|
+
onClose: () => void;
|
|
26
|
+
user: {
|
|
27
|
+
uid: string;
|
|
28
|
+
displayName: string | null;
|
|
29
|
+
email: string | null;
|
|
30
|
+
};
|
|
31
|
+
company: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const DeleteAccount = ({ isOpen, onClose, user, company }: DeleteAccountProps) => {
|
|
35
|
+
const [uidConfirmation, setUidConfirmation] = useState('');
|
|
36
|
+
const [emailConfirmation, setEmailConfirmation] = useState('');
|
|
37
|
+
const [isDeleting, setIsDeleting] = useState(false);
|
|
38
|
+
const [error, setError] = useState('');
|
|
39
|
+
const [success, setSuccess] = useState(false);
|
|
40
|
+
const [deletionProgress, setDeletionProgress] = useState<DeletionProgress>(initialDeletionProgress);
|
|
41
|
+
|
|
42
|
+
// Extract first and last name from display name
|
|
43
|
+
const [firstName, lastName] = (user.displayName || '').split(' ');
|
|
44
|
+
const fullName = `${firstName || ''} ${lastName || ''}`.trim();
|
|
45
|
+
|
|
46
|
+
// Check if confirmations match user data
|
|
47
|
+
const isConfirmationValid = uidConfirmation === user.uid && emailConfirmation === user.email;
|
|
48
|
+
|
|
49
|
+
const scheduleLogout = () => {
|
|
50
|
+
setTimeout(async () => {
|
|
51
|
+
try {
|
|
52
|
+
await signOut(auth);
|
|
53
|
+
onClose();
|
|
54
|
+
} catch (logoutError) {
|
|
55
|
+
console.error('Error during logout:', logoutError);
|
|
56
|
+
onClose();
|
|
57
|
+
}
|
|
58
|
+
}, 3000);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const updateProgress = (totalCases: number, completedCases: number, currentCaseNumber = '', forceComplete = false) => {
|
|
62
|
+
const safeTotal = totalCases > 0 ? totalCases : 0;
|
|
63
|
+
const safeCompleted = completedCases > safeTotal ? safeTotal : completedCases;
|
|
64
|
+
const percent = safeTotal > 0
|
|
65
|
+
? Math.round((safeCompleted / safeTotal) * 100)
|
|
66
|
+
: (forceComplete ? 100 : 0);
|
|
67
|
+
|
|
68
|
+
setDeletionProgress({
|
|
69
|
+
totalCases: safeTotal,
|
|
70
|
+
completedCases: safeCompleted,
|
|
71
|
+
currentCaseNumber,
|
|
72
|
+
percent
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const handleProgressStream = async (response: Response) => {
|
|
77
|
+
if (!response.body) {
|
|
78
|
+
throw new Error('No progress stream available from account deletion service.');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const reader = response.body.getReader();
|
|
82
|
+
const decoder = new TextDecoder();
|
|
83
|
+
let buffer = '';
|
|
84
|
+
let streamError = '';
|
|
85
|
+
let streamCompleted = false;
|
|
86
|
+
let latestTotalCases = 0;
|
|
87
|
+
let latestCompletedCases = 0;
|
|
88
|
+
|
|
89
|
+
while (true) {
|
|
90
|
+
const { value, done } = await reader.read();
|
|
91
|
+
if (done) break;
|
|
92
|
+
|
|
93
|
+
buffer += decoder.decode(value, { stream: true });
|
|
94
|
+
|
|
95
|
+
let eventBoundary = buffer.indexOf('\n\n');
|
|
96
|
+
while (eventBoundary !== -1) {
|
|
97
|
+
const rawEvent = buffer.slice(0, eventBoundary);
|
|
98
|
+
buffer = buffer.slice(eventBoundary + 2);
|
|
99
|
+
|
|
100
|
+
const lines = rawEvent.split('\n');
|
|
101
|
+
const eventTypeLine = lines.find((line) => line.startsWith('event:'));
|
|
102
|
+
const dataLine = lines.find((line) => line.startsWith('data:'));
|
|
103
|
+
|
|
104
|
+
if (eventTypeLine && dataLine) {
|
|
105
|
+
const eventType = eventTypeLine.replace('event:', '').trim();
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const data = JSON.parse(dataLine.replace('data:', '').trim()) as {
|
|
109
|
+
totalCases?: number;
|
|
110
|
+
completedCases?: number;
|
|
111
|
+
currentCaseNumber?: string;
|
|
112
|
+
success?: boolean;
|
|
113
|
+
message?: string;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
if (eventType === 'start') {
|
|
117
|
+
latestTotalCases = data.totalCases ?? 0;
|
|
118
|
+
latestCompletedCases = data.completedCases ?? 0;
|
|
119
|
+
updateProgress(latestTotalCases, latestCompletedCases, '');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (eventType === 'case-start') {
|
|
123
|
+
latestTotalCases = data.totalCases ?? latestTotalCases;
|
|
124
|
+
latestCompletedCases = data.completedCases ?? latestCompletedCases;
|
|
125
|
+
updateProgress(
|
|
126
|
+
latestTotalCases,
|
|
127
|
+
latestCompletedCases,
|
|
128
|
+
data.currentCaseNumber ?? ''
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (eventType === 'case-complete') {
|
|
133
|
+
latestTotalCases = data.totalCases ?? latestTotalCases;
|
|
134
|
+
latestCompletedCases = data.completedCases ?? latestCompletedCases;
|
|
135
|
+
updateProgress(
|
|
136
|
+
latestTotalCases,
|
|
137
|
+
latestCompletedCases,
|
|
138
|
+
data.currentCaseNumber ?? ''
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (eventType === 'complete') {
|
|
143
|
+
latestTotalCases = data.totalCases ?? latestTotalCases;
|
|
144
|
+
latestCompletedCases = data.completedCases ?? latestCompletedCases;
|
|
145
|
+
streamCompleted = data.success === true;
|
|
146
|
+
updateProgress(latestTotalCases, latestCompletedCases, '', streamCompleted && latestTotalCases === 0);
|
|
147
|
+
if (!streamCompleted) {
|
|
148
|
+
streamError = data.message || 'Account deletion failed.';
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (eventType === 'error') {
|
|
153
|
+
streamError = data.message || 'Account deletion failed.';
|
|
154
|
+
}
|
|
155
|
+
} catch {
|
|
156
|
+
streamError = 'Failed to parse account deletion progress.';
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
eventBoundary = buffer.indexOf('\n\n');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (streamError) {
|
|
165
|
+
throw new Error(streamError);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!streamCompleted) {
|
|
169
|
+
throw new Error('Account deletion did not complete successfully.');
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
175
|
+
if (e.key === 'Escape' && isOpen) {
|
|
176
|
+
onClose();
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
if (isOpen) {
|
|
181
|
+
document.addEventListener('keydown', handleEscape);
|
|
182
|
+
// Reset form when modal opens
|
|
183
|
+
setUidConfirmation('');
|
|
184
|
+
setEmailConfirmation('');
|
|
185
|
+
setError('');
|
|
186
|
+
setSuccess(false);
|
|
187
|
+
setDeletionProgress(initialDeletionProgress);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return () => {
|
|
191
|
+
document.removeEventListener('keydown', handleEscape);
|
|
192
|
+
};
|
|
193
|
+
}, [isOpen, onClose]);
|
|
194
|
+
|
|
195
|
+
const handleDeleteAccount = async () => {
|
|
196
|
+
if (!isConfirmationValid) return;
|
|
197
|
+
|
|
198
|
+
// Additional confirmation dialog similar to case-sidebar patterns
|
|
199
|
+
const confirmed = window.confirm(
|
|
200
|
+
`Are you sure you want to permanently delete your Striae account? This action cannot be undone and will delete all your data, cases, and files. Your email address will be permanently disabled.`
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
if (!confirmed) return;
|
|
204
|
+
|
|
205
|
+
setIsDeleting(true);
|
|
206
|
+
setError('');
|
|
207
|
+
updateProgress(0, 0, '');
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
// Log account deletion attempt
|
|
211
|
+
await auditService.logAccountDeletionSimple(
|
|
212
|
+
user.uid,
|
|
213
|
+
user.email || '',
|
|
214
|
+
'pending',
|
|
215
|
+
'user-requested',
|
|
216
|
+
'uid-email',
|
|
217
|
+
undefined, // casesCount - to be filled after deletion
|
|
218
|
+
undefined, // filesCount - to be filled after deletion
|
|
219
|
+
undefined, // dataRetentionPeriod
|
|
220
|
+
false // emailNotificationSent - deletion emails disabled
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// Get API key for user-worker authentication
|
|
224
|
+
const apiKey = await getUserApiKey();
|
|
225
|
+
|
|
226
|
+
// Delete the user account via user-worker
|
|
227
|
+
const deleteResponse = await fetch(`${paths.user_worker_url}/${user.uid}?stream=true`, {
|
|
228
|
+
method: 'DELETE',
|
|
229
|
+
headers: {
|
|
230
|
+
'X-Custom-Auth-Key': apiKey,
|
|
231
|
+
'Accept': 'text/event-stream'
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
if (!deleteResponse.ok) {
|
|
236
|
+
const errorData = await deleteResponse.json().catch(() => ({})) as { message?: string };
|
|
237
|
+
throw new Error(errorData.message || 'Failed to delete account');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const contentType = deleteResponse.headers.get('content-type') || '';
|
|
241
|
+
|
|
242
|
+
if (contentType.includes('text/event-stream')) {
|
|
243
|
+
await handleProgressStream(deleteResponse);
|
|
244
|
+
|
|
245
|
+
// Log successful account deletion
|
|
246
|
+
await auditService.logAccountDeletionSimple(
|
|
247
|
+
user.uid,
|
|
248
|
+
user.email || '',
|
|
249
|
+
'success',
|
|
250
|
+
'user-requested',
|
|
251
|
+
'uid-email',
|
|
252
|
+
undefined, // casesCount - not available from response
|
|
253
|
+
undefined, // filesCount - not available from response
|
|
254
|
+
undefined, // dataRetentionPeriod
|
|
255
|
+
false // emailNotificationSent - deletion emails disabled
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
setSuccess(true);
|
|
259
|
+
scheduleLogout();
|
|
260
|
+
} else {
|
|
261
|
+
const result = await deleteResponse.json() as { success: boolean; message?: string };
|
|
262
|
+
if (result.success) {
|
|
263
|
+
updateProgress(1, 1, '');
|
|
264
|
+
|
|
265
|
+
await auditService.logAccountDeletionSimple(
|
|
266
|
+
user.uid,
|
|
267
|
+
user.email || '',
|
|
268
|
+
'success',
|
|
269
|
+
'user-requested',
|
|
270
|
+
'uid-email',
|
|
271
|
+
undefined,
|
|
272
|
+
undefined,
|
|
273
|
+
undefined,
|
|
274
|
+
false
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
setSuccess(true);
|
|
278
|
+
scheduleLogout();
|
|
279
|
+
} else {
|
|
280
|
+
throw new Error(result.message || 'Account deletion failed');
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
} catch (err) {
|
|
285
|
+
// Log failed account deletion
|
|
286
|
+
await auditService.logAccountDeletionSimple(
|
|
287
|
+
user.uid,
|
|
288
|
+
user.email || '',
|
|
289
|
+
'failure',
|
|
290
|
+
'user-requested',
|
|
291
|
+
'uid-email',
|
|
292
|
+
undefined, // casesCount
|
|
293
|
+
undefined, // filesCount
|
|
294
|
+
undefined, // dataRetentionPeriod
|
|
295
|
+
false, // emailNotificationSent
|
|
296
|
+
undefined, // sessionId
|
|
297
|
+
[err instanceof Error ? err.message : 'Unknown error during account deletion']
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
console.error('Delete account error:', err);
|
|
301
|
+
setError(err instanceof Error ? err.message : 'Failed to delete account. Please try again or contact support.');
|
|
302
|
+
} finally {
|
|
303
|
+
setIsDeleting(false);
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
if (!isOpen) return null;
|
|
308
|
+
|
|
309
|
+
const showProgress = isDeleting || success || (Boolean(error) && deletionProgress.totalCases > 0);
|
|
310
|
+
const progressStatus = success
|
|
311
|
+
? 'All cases have been deleted.'
|
|
312
|
+
: error
|
|
313
|
+
? (deletionProgress.currentCaseNumber
|
|
314
|
+
? `Deletion stopped while processing case ${deletionProgress.currentCaseNumber}.`
|
|
315
|
+
: 'Deletion stopped before completion.')
|
|
316
|
+
: (deletionProgress.currentCaseNumber
|
|
317
|
+
? `Deleting case ${deletionProgress.currentCaseNumber}...`
|
|
318
|
+
: 'Preparing account deletion...');
|
|
319
|
+
|
|
320
|
+
return (
|
|
321
|
+
<div
|
|
322
|
+
className={styles.modalOverlay}
|
|
323
|
+
onClick={onClose}
|
|
324
|
+
role="presentation"
|
|
325
|
+
>
|
|
326
|
+
<div
|
|
327
|
+
className={styles.modal}
|
|
328
|
+
role="dialog"
|
|
329
|
+
aria-modal="true"
|
|
330
|
+
aria-labelledby="modal-title"
|
|
331
|
+
onClick={(e) => e.stopPropagation()}
|
|
332
|
+
>
|
|
333
|
+
{/* Header */}
|
|
334
|
+
<header className={styles.modalHeader}>
|
|
335
|
+
<h1 id="modal-title" className={styles.dangerTitle}>Delete Striae Account</h1>
|
|
336
|
+
<button
|
|
337
|
+
onClick={onClose}
|
|
338
|
+
className={styles.closeButton}
|
|
339
|
+
aria-label="Close modal"
|
|
340
|
+
>
|
|
341
|
+
×
|
|
342
|
+
</button>
|
|
343
|
+
</header>
|
|
344
|
+
|
|
345
|
+
<div className={styles.modalContent}>
|
|
346
|
+
{/* User Information */}
|
|
347
|
+
<div className={styles.userInfo}>
|
|
348
|
+
<div className={styles.infoRow}>
|
|
349
|
+
<span className={styles.label}>UID:</span>
|
|
350
|
+
<span className={styles.value}>{user.uid}</span>
|
|
351
|
+
</div>
|
|
352
|
+
<div className={styles.infoRow}>
|
|
353
|
+
<span className={styles.label}>Name:</span>
|
|
354
|
+
<span className={styles.value}>{fullName || 'Not provided'}</span>
|
|
355
|
+
</div>
|
|
356
|
+
<div className={styles.infoRow}>
|
|
357
|
+
<span className={styles.label}>Email:</span>
|
|
358
|
+
<span className={styles.value}>{user.email}</span>
|
|
359
|
+
</div>
|
|
360
|
+
<div className={styles.infoRow}>
|
|
361
|
+
<span className={styles.label}>Lab/Company:</span>
|
|
362
|
+
<span className={styles.value}>{company || 'Not provided'}</span>
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
|
|
366
|
+
{/* Divider */}
|
|
367
|
+
<div className={styles.divider}></div>
|
|
368
|
+
|
|
369
|
+
{/* Warning Message */}
|
|
370
|
+
<div className={styles.warningSection}>
|
|
371
|
+
<p className={styles.warningText}>
|
|
372
|
+
{isDeleting
|
|
373
|
+
? 'Deleting your account now. If you have a lot of data, this may take a while...'
|
|
374
|
+
: <>
|
|
375
|
+
Deleting your account is irreversible! All account information and data will be deleted from Striae. The email address associated with this account will be permanently disabled. <strong><em>Please be certain you want to take this action!</em></strong>
|
|
376
|
+
</>
|
|
377
|
+
}
|
|
378
|
+
</p>
|
|
379
|
+
</div>
|
|
380
|
+
|
|
381
|
+
{/* Divider */}
|
|
382
|
+
<div className={styles.divider}></div>
|
|
383
|
+
|
|
384
|
+
{showProgress && (
|
|
385
|
+
<div className={styles.progressSection} aria-live="polite">
|
|
386
|
+
<div className={styles.progressHeader}>
|
|
387
|
+
<p className={styles.progressTitle}>Deletion Progress</p>
|
|
388
|
+
<p className={styles.progressMeta}>
|
|
389
|
+
{deletionProgress.completedCases}/{deletionProgress.totalCases} cases
|
|
390
|
+
</p>
|
|
391
|
+
</div>
|
|
392
|
+
<div
|
|
393
|
+
className={styles.progressTrack}
|
|
394
|
+
role="progressbar"
|
|
395
|
+
aria-label="Account deletion progress"
|
|
396
|
+
aria-valuemin={0}
|
|
397
|
+
aria-valuemax={100}
|
|
398
|
+
aria-valuenow={deletionProgress.percent}
|
|
399
|
+
>
|
|
400
|
+
<div
|
|
401
|
+
className={styles.progressFill}
|
|
402
|
+
style={{ width: `${deletionProgress.percent}%` }}
|
|
403
|
+
/>
|
|
404
|
+
</div>
|
|
405
|
+
<p className={styles.progressStatus}>{progressStatus}</p>
|
|
406
|
+
</div>
|
|
407
|
+
)}
|
|
408
|
+
|
|
409
|
+
{/* Success/Error Messages */}
|
|
410
|
+
{error && (
|
|
411
|
+
<div className={styles.errorMessage}>
|
|
412
|
+
<p>{error}</p>
|
|
413
|
+
</div>
|
|
414
|
+
)}
|
|
415
|
+
|
|
416
|
+
{success && (
|
|
417
|
+
<div className={styles.successMessage}>
|
|
418
|
+
<p>✓ Account deletion successful!</p>
|
|
419
|
+
<p>You will be logged out automatically in 3 seconds...</p>
|
|
420
|
+
</div>
|
|
421
|
+
)}
|
|
422
|
+
|
|
423
|
+
{/* Confirmation Form */}
|
|
424
|
+
{!success && (
|
|
425
|
+
<form className={styles.confirmationForm}>
|
|
426
|
+
<div className={styles.formGroup}>
|
|
427
|
+
<label htmlFor="uid-confirmation" className={styles.formLabel}>
|
|
428
|
+
Enter UID to confirm account deletion:
|
|
429
|
+
</label>
|
|
430
|
+
<input
|
|
431
|
+
id="uid-confirmation"
|
|
432
|
+
type="text"
|
|
433
|
+
value={uidConfirmation}
|
|
434
|
+
onChange={(e) => setUidConfirmation(e.target.value)}
|
|
435
|
+
className={styles.confirmationInput}
|
|
436
|
+
placeholder="Enter your User ID"
|
|
437
|
+
autoComplete="off"
|
|
438
|
+
/>
|
|
439
|
+
</div>
|
|
440
|
+
|
|
441
|
+
<div className={styles.formGroup}>
|
|
442
|
+
<label htmlFor="email-confirmation" className={styles.formLabel}>
|
|
443
|
+
Enter your email address to confirm account deletion:
|
|
444
|
+
</label>
|
|
445
|
+
<input
|
|
446
|
+
id="email-confirmation"
|
|
447
|
+
type="email"
|
|
448
|
+
value={emailConfirmation}
|
|
449
|
+
onChange={(e) => setEmailConfirmation(e.target.value)}
|
|
450
|
+
onPaste={(e) => e.preventDefault()}
|
|
451
|
+
className={styles.confirmationInput}
|
|
452
|
+
placeholder="Enter your email address"
|
|
453
|
+
autoComplete="off"
|
|
454
|
+
/>
|
|
455
|
+
</div>
|
|
456
|
+
|
|
457
|
+
<button
|
|
458
|
+
type="button"
|
|
459
|
+
onClick={handleDeleteAccount}
|
|
460
|
+
className={styles.deleteButton}
|
|
461
|
+
disabled={!isConfirmationValid || isDeleting}
|
|
462
|
+
>
|
|
463
|
+
{isDeleting ? 'Deleting Account...' : 'Delete Striae Account'}
|
|
464
|
+
</button>
|
|
465
|
+
</form>
|
|
466
|
+
)}
|
|
467
|
+
</div>
|
|
468
|
+
</div>
|
|
469
|
+
</div>
|
|
470
|
+
);
|
|
471
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
.overlay {
|
|
2
|
+
position: fixed;
|
|
3
|
+
top: 0;
|
|
4
|
+
left: 0;
|
|
5
|
+
right: 0;
|
|
6
|
+
bottom: 0;
|
|
7
|
+
background-color: rgba(0, 0, 0, 0.8);
|
|
8
|
+
display: flex;
|
|
9
|
+
justify-content: center;
|
|
10
|
+
align-items: center;
|
|
11
|
+
z-index: 9999;
|
|
12
|
+
backdrop-filter: blur(2px);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.modal {
|
|
16
|
+
background: #ffffff;
|
|
17
|
+
border-radius: 12px;
|
|
18
|
+
padding: 2rem;
|
|
19
|
+
max-width: 400px;
|
|
20
|
+
width: 90%;
|
|
21
|
+
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
|
|
22
|
+
border: 1px solid #e0e0e0;
|
|
23
|
+
animation: slideIn 0.3s ease-out;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@keyframes slideIn {
|
|
27
|
+
from {
|
|
28
|
+
opacity: 0;
|
|
29
|
+
transform: translateY(-20px) scale(0.95);
|
|
30
|
+
}
|
|
31
|
+
to {
|
|
32
|
+
opacity: 1;
|
|
33
|
+
transform: translateY(0) scale(1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.header {
|
|
38
|
+
text-align: center;
|
|
39
|
+
margin-bottom: 1.5rem;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.header h3 {
|
|
43
|
+
color: #1a1a1a;
|
|
44
|
+
font-size: 1.25rem;
|
|
45
|
+
font-weight: 600;
|
|
46
|
+
margin: 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.content {
|
|
50
|
+
text-align: center;
|
|
51
|
+
margin-bottom: 2rem;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.content p {
|
|
55
|
+
color: #4a4a4a;
|
|
56
|
+
margin: 0 0 1rem 0;
|
|
57
|
+
line-height: 1.5;
|
|
58
|
+
font-size: 1rem;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.countdown {
|
|
62
|
+
font-size: 2rem;
|
|
63
|
+
font-weight: bold;
|
|
64
|
+
color: #dc3545;
|
|
65
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
66
|
+
background: rgba(220, 53, 69, 0.1);
|
|
67
|
+
padding: 1rem;
|
|
68
|
+
border-radius: 8px;
|
|
69
|
+
margin: 1rem 0;
|
|
70
|
+
border: 2px solid rgba(220, 53, 69, 0.2);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.actions {
|
|
74
|
+
display: flex;
|
|
75
|
+
gap: 1rem;
|
|
76
|
+
justify-content: center;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.extendButton,
|
|
80
|
+
.signOutButton {
|
|
81
|
+
padding: 0.75rem 1.5rem;
|
|
82
|
+
border-radius: 6px;
|
|
83
|
+
border: none;
|
|
84
|
+
font-weight: 500;
|
|
85
|
+
cursor: pointer;
|
|
86
|
+
transition: all 0.2s ease;
|
|
87
|
+
font-size: 0.9rem;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.extendButton {
|
|
91
|
+
background: #007bff;
|
|
92
|
+
color: white;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.extendButton:hover {
|
|
96
|
+
background: #0056b3;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.signOutButton {
|
|
100
|
+
background: transparent;
|
|
101
|
+
color: #6c757d;
|
|
102
|
+
border: 1px solid #dee2e6;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.signOutButton:hover {
|
|
106
|
+
background: #f8f9fa;
|
|
107
|
+
color: #495057;
|
|
108
|
+
border-color: #adb5bd;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.extendButton:focus,
|
|
112
|
+
.signOutButton:focus {
|
|
113
|
+
outline: 2px solid #007bff;
|
|
114
|
+
outline-offset: 2px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@media (prefers-color-scheme: dark) {
|
|
118
|
+
.overlay {
|
|
119
|
+
background-color: rgba(0, 0, 0, 0.9);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.modal {
|
|
123
|
+
background: #2d2d2d;
|
|
124
|
+
border-color: #404040;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.header h3 {
|
|
128
|
+
color: #ffffff;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.content p {
|
|
132
|
+
color: #cccccc;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.signOutButton {
|
|
136
|
+
color: #adb5bd;
|
|
137
|
+
border-color: #6c757d;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.signOutButton:hover {
|
|
141
|
+
background: #404040;
|
|
142
|
+
color: #ffffff;
|
|
143
|
+
border-color: #adb5bd;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import styles from './inactivity-warning.module.css';
|
|
3
|
+
|
|
4
|
+
interface InactivityWarningProps {
|
|
5
|
+
isOpen: boolean;
|
|
6
|
+
remainingSeconds: number;
|
|
7
|
+
onExtendSession: () => void;
|
|
8
|
+
onSignOut: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const InactivityWarning = ({
|
|
12
|
+
isOpen,
|
|
13
|
+
remainingSeconds,
|
|
14
|
+
onExtendSession,
|
|
15
|
+
onSignOut
|
|
16
|
+
}: InactivityWarningProps) => {
|
|
17
|
+
const [countdown, setCountdown] = useState(remainingSeconds);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
setCountdown(remainingSeconds);
|
|
21
|
+
}, [remainingSeconds]);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!isOpen) {
|
|
25
|
+
setCountdown(0);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const interval = setInterval(() => {
|
|
30
|
+
setCountdown(prev => {
|
|
31
|
+
if (prev <= 1) {
|
|
32
|
+
clearInterval(interval);
|
|
33
|
+
onSignOut();
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
return prev - 1;
|
|
37
|
+
});
|
|
38
|
+
}, 1000);
|
|
39
|
+
|
|
40
|
+
return () => clearInterval(interval);
|
|
41
|
+
}, [isOpen, onSignOut]);
|
|
42
|
+
|
|
43
|
+
if (!isOpen) return null;
|
|
44
|
+
|
|
45
|
+
const minutes = Math.floor(countdown / 60);
|
|
46
|
+
const seconds = countdown % 60;
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className={styles.overlay}>
|
|
50
|
+
<div className={styles.modal}>
|
|
51
|
+
<div className={styles.header}>
|
|
52
|
+
<h3>Session Timeout Warning</h3>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div className={styles.content}>
|
|
56
|
+
<p>
|
|
57
|
+
Your session will expire due to inactivity in:
|
|
58
|
+
</p>
|
|
59
|
+
<div className={styles.countdown}>
|
|
60
|
+
{minutes}:{seconds.toString().padStart(2, '0')}
|
|
61
|
+
</div>
|
|
62
|
+
<p>
|
|
63
|
+
Would you like to extend your session?
|
|
64
|
+
</p>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div className={styles.actions}>
|
|
68
|
+
<button
|
|
69
|
+
onClick={onExtendSession}
|
|
70
|
+
className={styles.extendButton}
|
|
71
|
+
>
|
|
72
|
+
Extend Session
|
|
73
|
+
</button>
|
|
74
|
+
<button
|
|
75
|
+
onClick={onSignOut}
|
|
76
|
+
className={styles.signOutButton}
|
|
77
|
+
>
|
|
78
|
+
Sign Out Now
|
|
79
|
+
</button>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
};
|