@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.
Files changed (223) hide show
  1. package/.env.example +100 -0
  2. package/LICENSE +190 -0
  3. package/NOTICE +18 -0
  4. package/README.md +133 -0
  5. package/app/components/actions/case-export/core-export.ts +328 -0
  6. package/app/components/actions/case-export/data-processing.ts +167 -0
  7. package/app/components/actions/case-export/download-handlers.ts +900 -0
  8. package/app/components/actions/case-export/index.ts +41 -0
  9. package/app/components/actions/case-export/metadata-helpers.ts +107 -0
  10. package/app/components/actions/case-export/types-constants.ts +56 -0
  11. package/app/components/actions/case-export/validation-utils.ts +25 -0
  12. package/app/components/actions/case-export.ts +4 -0
  13. package/app/components/actions/case-import/annotation-import.ts +35 -0
  14. package/app/components/actions/case-import/confirmation-import.ts +363 -0
  15. package/app/components/actions/case-import/image-operations.ts +61 -0
  16. package/app/components/actions/case-import/index.ts +39 -0
  17. package/app/components/actions/case-import/orchestrator.ts +420 -0
  18. package/app/components/actions/case-import/storage-operations.ts +270 -0
  19. package/app/components/actions/case-import/validation.ts +189 -0
  20. package/app/components/actions/case-import/zip-processing.ts +413 -0
  21. package/app/components/actions/case-manage.ts +524 -0
  22. package/app/components/actions/case-review.ts +4 -0
  23. package/app/components/actions/confirm-export.ts +351 -0
  24. package/app/components/actions/generate-pdf.ts +210 -0
  25. package/app/components/actions/image-manage.ts +385 -0
  26. package/app/components/actions/notes-manage.ts +33 -0
  27. package/app/components/actions/signout.module.css +15 -0
  28. package/app/components/actions/signout.tsx +50 -0
  29. package/app/components/audit/user-audit-viewer.tsx +975 -0
  30. package/app/components/audit/user-audit.module.css +568 -0
  31. package/app/components/auth/auth-provider.tsx +78 -0
  32. package/app/components/auth/mfa-enrollment.module.css +268 -0
  33. package/app/components/auth/mfa-enrollment.tsx +398 -0
  34. package/app/components/auth/mfa-verification.module.css +251 -0
  35. package/app/components/auth/mfa-verification.tsx +295 -0
  36. package/app/components/button/button.module.css +63 -0
  37. package/app/components/button/button.tsx +46 -0
  38. package/app/components/canvas/box-annotations/box-annotations.module.css +170 -0
  39. package/app/components/canvas/box-annotations/box-annotations.tsx +634 -0
  40. package/app/components/canvas/canvas.module.css +314 -0
  41. package/app/components/canvas/canvas.tsx +449 -0
  42. package/app/components/canvas/confirmation/confirmation.module.css +187 -0
  43. package/app/components/canvas/confirmation/confirmation.tsx +214 -0
  44. package/app/components/colors/colors.module.css +59 -0
  45. package/app/components/colors/colors.tsx +68 -0
  46. package/app/components/form/base-form.tsx +21 -0
  47. package/app/components/form/form-button.tsx +28 -0
  48. package/app/components/form/form-field.tsx +53 -0
  49. package/app/components/form/form-message.tsx +17 -0
  50. package/app/components/form/form-toggle.tsx +23 -0
  51. package/app/components/form/form.module.css +427 -0
  52. package/app/components/form/index.ts +6 -0
  53. package/app/components/icon/icon.module.css +3 -0
  54. package/app/components/icon/icon.tsx +27 -0
  55. package/app/components/icon/icons.svg +102 -0
  56. package/app/components/icon/manifest.json +110 -0
  57. package/app/components/sidebar/case-export/case-export.module.css +386 -0
  58. package/app/components/sidebar/case-export/case-export.tsx +317 -0
  59. package/app/components/sidebar/case-import/case-import.module.css +626 -0
  60. package/app/components/sidebar/case-import/case-import.tsx +404 -0
  61. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +72 -0
  62. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +72 -0
  63. package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +71 -0
  64. package/app/components/sidebar/case-import/components/ExistingCaseSection.tsx +40 -0
  65. package/app/components/sidebar/case-import/components/FileSelector.tsx +161 -0
  66. package/app/components/sidebar/case-import/components/ProgressSection.tsx +46 -0
  67. package/app/components/sidebar/case-import/hooks/useFilePreview.ts +101 -0
  68. package/app/components/sidebar/case-import/hooks/useImportExecution.ts +152 -0
  69. package/app/components/sidebar/case-import/hooks/useImportState.ts +88 -0
  70. package/app/components/sidebar/case-import/index.ts +18 -0
  71. package/app/components/sidebar/case-import/utils/file-validation.ts +43 -0
  72. package/app/components/sidebar/cases/case-sidebar.tsx +827 -0
  73. package/app/components/sidebar/cases/cases-modal.module.css +166 -0
  74. package/app/components/sidebar/cases/cases-modal.tsx +201 -0
  75. package/app/components/sidebar/cases/cases.module.css +713 -0
  76. package/app/components/sidebar/files/files-modal.module.css +209 -0
  77. package/app/components/sidebar/files/files-modal.tsx +239 -0
  78. package/app/components/sidebar/hash/hash-utility.module.css +366 -0
  79. package/app/components/sidebar/hash/hash-utility.tsx +982 -0
  80. package/app/components/sidebar/notes/notes-modal.tsx +51 -0
  81. package/app/components/sidebar/notes/notes-sidebar.tsx +491 -0
  82. package/app/components/sidebar/notes/notes.module.css +360 -0
  83. package/app/components/sidebar/sidebar-container.tsx +149 -0
  84. package/app/components/sidebar/sidebar.module.css +321 -0
  85. package/app/components/sidebar/sidebar.tsx +215 -0
  86. package/app/components/sidebar/upload/image-upload-zone.module.css +123 -0
  87. package/app/components/sidebar/upload/image-upload-zone.tsx +330 -0
  88. package/app/components/theme-provider/theme-provider.tsx +131 -0
  89. package/app/components/theme-provider/theme.ts +155 -0
  90. package/app/components/toast/toast.module.css +137 -0
  91. package/app/components/toast/toast.tsx +56 -0
  92. package/app/components/toolbar/toolbar-color-selector.module.css +171 -0
  93. package/app/components/toolbar/toolbar-color-selector.tsx +129 -0
  94. package/app/components/toolbar/toolbar.module.css +42 -0
  95. package/app/components/toolbar/toolbar.tsx +167 -0
  96. package/app/components/user/delete-account.module.css +274 -0
  97. package/app/components/user/delete-account.tsx +471 -0
  98. package/app/components/user/inactivity-warning.module.css +145 -0
  99. package/app/components/user/inactivity-warning.tsx +84 -0
  100. package/app/components/user/manage-profile.module.css +190 -0
  101. package/app/components/user/manage-profile.tsx +253 -0
  102. package/app/components/user/mfa-phone-update.tsx +739 -0
  103. package/app/config-example/admin-service.json +13 -0
  104. package/app/config-example/config.json +17 -0
  105. package/app/config-example/firebase.ts +21 -0
  106. package/app/config-example/inactivity.ts +13 -0
  107. package/app/config-example/meta-config.json +6 -0
  108. package/app/contexts/auth.context.ts +12 -0
  109. package/app/entry.client.tsx +12 -0
  110. package/app/entry.server.tsx +44 -0
  111. package/app/hooks/useInactivityTimeout.ts +110 -0
  112. package/app/root.tsx +170 -0
  113. package/app/routes/_index.tsx +16 -0
  114. package/app/routes/auth/emailActionHandler.module.css +232 -0
  115. package/app/routes/auth/emailActionHandler.tsx +405 -0
  116. package/app/routes/auth/emailVerification.tsx +120 -0
  117. package/app/routes/auth/login.module.css +523 -0
  118. package/app/routes/auth/login.tsx +654 -0
  119. package/app/routes/auth/passwordReset.module.css +274 -0
  120. package/app/routes/auth/passwordReset.tsx +154 -0
  121. package/app/routes/auth/route.ts +16 -0
  122. package/app/routes/mobile-prevented/mobilePrevented.module.css +47 -0
  123. package/app/routes/mobile-prevented/mobilePrevented.tsx +26 -0
  124. package/app/routes/mobile-prevented/route.ts +14 -0
  125. package/app/routes/striae/striae.module.css +30 -0
  126. package/app/routes/striae/striae.tsx +417 -0
  127. package/app/services/audit-export.service.ts +755 -0
  128. package/app/services/audit.service.ts +1454 -0
  129. package/app/services/firebase-errors.ts +106 -0
  130. package/app/services/firebase.ts +15 -0
  131. package/app/styles/legal-pages.module.css +113 -0
  132. package/app/styles/root.module.css +146 -0
  133. package/app/tailwind.css +225 -0
  134. package/app/types/annotations.ts +45 -0
  135. package/app/types/audit.ts +301 -0
  136. package/app/types/case.ts +90 -0
  137. package/app/types/export.ts +8 -0
  138. package/app/types/file.ts +30 -0
  139. package/app/types/import.ts +107 -0
  140. package/app/types/index.ts +24 -0
  141. package/app/types/user.ts +38 -0
  142. package/app/utils/SHA256.ts +461 -0
  143. package/app/utils/annotation-timestamp.ts +25 -0
  144. package/app/utils/audit-export-signature.ts +117 -0
  145. package/app/utils/auth-action-settings.ts +48 -0
  146. package/app/utils/auth.ts +34 -0
  147. package/app/utils/batch-operations.ts +135 -0
  148. package/app/utils/confirmation-signature.ts +193 -0
  149. package/app/utils/data-operations.ts +871 -0
  150. package/app/utils/device-detection.ts +5 -0
  151. package/app/utils/html-sanitizer.ts +80 -0
  152. package/app/utils/id-generator.ts +36 -0
  153. package/app/utils/meta.ts +48 -0
  154. package/app/utils/mfa-phone.ts +97 -0
  155. package/app/utils/mfa.ts +79 -0
  156. package/app/utils/password-policy.ts +28 -0
  157. package/app/utils/permissions.ts +562 -0
  158. package/app/utils/signature-utils.ts +160 -0
  159. package/app/utils/style.ts +83 -0
  160. package/app/utils/version.ts +5 -0
  161. package/firebase.json +11 -0
  162. package/functions/[[path]].ts +10 -0
  163. package/package.json +138 -0
  164. package/postcss.config.js +6 -0
  165. package/public/.well-known/publickey.info@striae.org.asc +17 -0
  166. package/public/.well-known/security.txt +7 -0
  167. package/public/_headers +28 -0
  168. package/public/_routes.json +13 -0
  169. package/public/assets/striae.jpg +0 -0
  170. package/public/clear.jpg +0 -0
  171. package/public/favicon.ico +0 -0
  172. package/public/favicon.svg +9 -0
  173. package/public/icon-256.png +0 -0
  174. package/public/icon-512.png +0 -0
  175. package/public/logo-dark.png +0 -0
  176. package/public/manifest.json +25 -0
  177. package/public/oin-badge.png +0 -0
  178. package/public/shortcut.png +0 -0
  179. package/public/social-image.png +0 -0
  180. package/public/striae-ascii.txt +10 -0
  181. package/scripts/deploy-all.sh +100 -0
  182. package/scripts/deploy-config.sh +940 -0
  183. package/scripts/deploy-pages.sh +34 -0
  184. package/scripts/deploy-worker-secrets.sh +215 -0
  185. package/scripts/dev.cjs +23 -0
  186. package/scripts/install-workers.sh +88 -0
  187. package/scripts/run-eslint.cjs +35 -0
  188. package/scripts/update-compatibility-dates.cjs +124 -0
  189. package/scripts/update-markdown-versions.cjs +43 -0
  190. package/tailwind.config.ts +22 -0
  191. package/tsconfig.json +33 -0
  192. package/vite.config.ts +35 -0
  193. package/worker-configuration.d.ts +7490 -0
  194. package/workers/audit-worker/package.json +17 -0
  195. package/workers/audit-worker/src/audit-worker.example.ts +195 -0
  196. package/workers/audit-worker/worker-configuration.d.ts +7448 -0
  197. package/workers/audit-worker/wrangler.jsonc.example +29 -0
  198. package/workers/data-worker/package.json +17 -0
  199. package/workers/data-worker/src/data-worker.example.ts +267 -0
  200. package/workers/data-worker/src/signature-utils.ts +79 -0
  201. package/workers/data-worker/src/signing-payload-utils.ts +290 -0
  202. package/workers/data-worker/worker-configuration.d.ts +7448 -0
  203. package/workers/data-worker/wrangler.jsonc.example +30 -0
  204. package/workers/image-worker/package.json +17 -0
  205. package/workers/image-worker/src/image-worker.example.ts +180 -0
  206. package/workers/image-worker/worker-configuration.d.ts +7447 -0
  207. package/workers/image-worker/wrangler.jsonc.example +22 -0
  208. package/workers/keys-worker/package.json +17 -0
  209. package/workers/keys-worker/src/keys.example.ts +66 -0
  210. package/workers/keys-worker/src/keys.ts +66 -0
  211. package/workers/keys-worker/worker-configuration.d.ts +7447 -0
  212. package/workers/keys-worker/wrangler.jsonc.example +22 -0
  213. package/workers/pdf-worker/package.json +17 -0
  214. package/workers/pdf-worker/src/format-striae.ts +534 -0
  215. package/workers/pdf-worker/src/pdf-worker.example.ts +119 -0
  216. package/workers/pdf-worker/src/report-types.ts +69 -0
  217. package/workers/pdf-worker/worker-configuration.d.ts +7448 -0
  218. package/workers/pdf-worker/wrangler.jsonc.example +26 -0
  219. package/workers/user-worker/package.json +17 -0
  220. package/workers/user-worker/src/user-worker.example.ts +636 -0
  221. package/workers/user-worker/worker-configuration.d.ts +7448 -0
  222. package/workers/user-worker/wrangler.jsonc.example +29 -0
  223. 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
+ &times;
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
+ };