@striae-org/striae 3.3.0 → 4.0.1
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 +1 -1
- package/README.md +1 -1
- package/app/components/actions/case-export/core-export.ts +5 -2
- package/app/components/actions/case-export/download-handlers.ts +1 -1
- package/app/components/actions/case-import/confirmation-import.ts +24 -23
- package/app/components/actions/case-import/image-operations.ts +20 -49
- package/app/components/actions/case-import/orchestrator.ts +1 -1
- package/app/components/actions/case-import/storage-operations.ts +54 -89
- package/app/components/actions/case-import/validation.ts +2 -13
- package/app/components/actions/case-manage.ts +15 -27
- package/app/components/actions/generate-pdf.ts +3 -7
- package/app/components/actions/image-manage.ts +64 -129
- package/app/components/button/button.module.css +12 -8
- package/app/components/sidebar/case-export/case-export.tsx +11 -6
- package/app/components/sidebar/cases/case-sidebar.tsx +21 -6
- package/app/components/sidebar/sidebar.module.css +0 -2
- package/app/components/user/delete-account.tsx +7 -7
- package/app/config-example/config.json +2 -8
- package/app/hooks/useInactivityTimeout.ts +2 -5
- package/app/root.tsx +94 -63
- package/app/routes/auth/emailActionHandler.tsx +1 -1
- package/app/routes/auth/emailVerification.tsx +1 -1
- package/app/routes/auth/login.tsx +7 -9
- package/app/routes/auth/passwordReset.tsx +1 -1
- package/app/routes/auth/route.ts +4 -3
- package/app/routes/striae/striae.tsx +4 -8
- package/app/services/audit/audit-api-client.ts +40 -0
- package/app/services/audit/audit-worker-client.ts +14 -17
- package/app/styles/root.module.css +13 -101
- package/app/tailwind.css +9 -2
- package/app/utils/auth.ts +5 -32
- package/app/utils/data-api-client.ts +43 -0
- package/app/utils/data-operations.ts +59 -75
- package/app/utils/image-api-client.ts +130 -0
- package/app/utils/pdf-api-client.ts +43 -0
- package/app/utils/permissions.ts +10 -23
- package/app/utils/user-api-client.ts +90 -0
- package/functions/api/_shared/firebase-auth.ts +255 -0
- package/functions/api/audit/[[path]].ts +150 -0
- package/functions/api/data/[[path]].ts +141 -0
- package/functions/api/image/[[path]].ts +143 -0
- package/functions/api/pdf/[[path]].ts +110 -0
- package/functions/api/user/[[path]].ts +196 -0
- package/package.json +3 -2
- package/public/.well-known/security.txt +3 -4
- package/scripts/deploy-all.sh +22 -8
- package/scripts/deploy-config.sh +194 -165
- package/scripts/deploy-pages-secrets.sh +231 -0
- package/scripts/deploy-worker-secrets.sh +1 -1
- package/worker-configuration.d.ts +7491 -11363
- package/workers/audit-worker/worker-configuration.d.ts +11323 -7448
- package/workers/audit-worker/wrangler.jsonc.example +1 -8
- package/workers/data-worker/worker-configuration.d.ts +11323 -7448
- package/workers/data-worker/wrangler.jsonc.example +1 -8
- package/workers/image-worker/src/image-worker.example.ts +10 -2
- package/workers/image-worker/worker-configuration.d.ts +11322 -7447
- package/workers/image-worker/wrangler.jsonc.example +1 -8
- package/workers/keys-worker/src/keys.ts +2 -1
- package/workers/keys-worker/worker-configuration.d.ts +11322 -7447
- package/workers/keys-worker/wrangler.jsonc.example +2 -9
- package/workers/pdf-worker/src/assets/icon-256.png +0 -0
- package/workers/pdf-worker/worker-configuration.d.ts +11323 -7448
- package/workers/pdf-worker/wrangler.jsonc.example +1 -8
- package/workers/user-worker/src/user-worker.example.ts +121 -41
- package/workers/user-worker/worker-configuration.d.ts +11323 -7448
- package/workers/user-worker/wrangler.jsonc.example +1 -8
- package/wrangler.toml.example +1 -1
- package/app/styles/legal-pages.module.css +0 -113
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "PDF_WORKER_NAME",
|
|
3
3
|
"account_id": "ACCOUNT_ID",
|
|
4
4
|
"main": "src/pdf-worker.ts",
|
|
5
|
-
"compatibility_date": "2026-03-
|
|
5
|
+
"compatibility_date": "2026-03-15",
|
|
6
6
|
"compatibility_flags": [
|
|
7
7
|
"nodejs_compat"
|
|
8
8
|
],
|
|
@@ -15,12 +15,5 @@
|
|
|
15
15
|
"enabled": true
|
|
16
16
|
},
|
|
17
17
|
|
|
18
|
-
"routes": [
|
|
19
|
-
{
|
|
20
|
-
"pattern": "PDF_WORKER_DOMAIN",
|
|
21
|
-
"custom_domain": true
|
|
22
|
-
}
|
|
23
|
-
],
|
|
24
|
-
|
|
25
18
|
"placement": { "mode": "smart" }
|
|
26
19
|
}
|
|
@@ -3,6 +3,8 @@ interface Env {
|
|
|
3
3
|
USER_DB: KVNamespace;
|
|
4
4
|
R2_KEY_SECRET: string;
|
|
5
5
|
IMAGES_API_TOKEN: string;
|
|
6
|
+
DATA_WORKER_DOMAIN?: string;
|
|
7
|
+
IMAGES_WORKER_DOMAIN?: string;
|
|
6
8
|
PROJECT_ID: string;
|
|
7
9
|
FIREBASE_SERVICE_ACCOUNT_EMAIL: string;
|
|
8
10
|
FIREBASE_SERVICE_ACCOUNT_PRIVATE_KEY: string;
|
|
@@ -84,9 +86,8 @@ const corsHeaders: Record<string, string> = {
|
|
|
84
86
|
};
|
|
85
87
|
|
|
86
88
|
// Worker URLs - configure these for deployment
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
const IMAGE_WORKER_URL = 'IMAGES_WORKER_DOMAIN';
|
|
89
|
+
const DEFAULT_DATA_WORKER_BASE_URL = 'DATA_WORKER_DOMAIN';
|
|
90
|
+
const DEFAULT_IMAGE_WORKER_BASE_URL = 'IMAGES_WORKER_DOMAIN';
|
|
90
91
|
|
|
91
92
|
const GOOGLE_OAUTH_TOKEN_URL = 'https://oauth2.googleapis.com/token';
|
|
92
93
|
const FIREBASE_IDENTITY_TOOLKIT_BASE_URL = 'https://identitytoolkit.googleapis.com/v1/projects';
|
|
@@ -98,6 +99,33 @@ async function authenticate(request: Request, env: Env): Promise<void> {
|
|
|
98
99
|
if (authKey !== env.USER_DB_AUTH) throw new Error('Unauthorized');
|
|
99
100
|
}
|
|
100
101
|
|
|
102
|
+
function normalizeWorkerBaseUrl(workerDomain: string): string {
|
|
103
|
+
const trimmedDomain = workerDomain.trim().replace(/\/+$/, '');
|
|
104
|
+
if (trimmedDomain.startsWith('http://') || trimmedDomain.startsWith('https://')) {
|
|
105
|
+
return trimmedDomain;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return `https://${trimmedDomain}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function resolveDataWorkerBaseUrl(env: Env): string {
|
|
112
|
+
const configuredDomain = typeof env.DATA_WORKER_DOMAIN === 'string' ? env.DATA_WORKER_DOMAIN.trim() : '';
|
|
113
|
+
if (configuredDomain.length > 0) {
|
|
114
|
+
return normalizeWorkerBaseUrl(configuredDomain);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return normalizeWorkerBaseUrl(DEFAULT_DATA_WORKER_BASE_URL);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function resolveImageWorkerBaseUrl(env: Env): string {
|
|
121
|
+
const configuredDomain = typeof env.IMAGES_WORKER_DOMAIN === 'string' ? env.IMAGES_WORKER_DOMAIN.trim() : '';
|
|
122
|
+
if (configuredDomain.length > 0) {
|
|
123
|
+
return normalizeWorkerBaseUrl(configuredDomain);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return normalizeWorkerBaseUrl(DEFAULT_IMAGE_WORKER_BASE_URL);
|
|
127
|
+
}
|
|
128
|
+
|
|
101
129
|
function base64UrlEncode(value: string | Uint8Array): string {
|
|
102
130
|
const bytes = typeof value === 'string' ? textEncoder.encode(value) : value;
|
|
103
131
|
let binary = '';
|
|
@@ -317,49 +345,86 @@ async function deleteSingleCase(env: Env, userUid: string, caseNumber: string):
|
|
|
317
345
|
const dataApiKey = env.R2_KEY_SECRET;
|
|
318
346
|
const imageApiKey = env.IMAGES_API_TOKEN;
|
|
319
347
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
});
|
|
348
|
+
const dataWorkerBaseUrl = resolveDataWorkerBaseUrl(env);
|
|
349
|
+
const imageWorkerBaseUrl = resolveImageWorkerBaseUrl(env);
|
|
350
|
+
const encodedUserId = encodeURIComponent(userUid);
|
|
351
|
+
const encodedCaseNumber = encodeURIComponent(caseNumber);
|
|
325
352
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
353
|
+
const caseResponse = await fetch(`${dataWorkerBaseUrl}/${encodedUserId}/${encodedCaseNumber}/data.json`, {
|
|
354
|
+
headers: { 'X-Custom-Auth-Key': dataApiKey }
|
|
355
|
+
});
|
|
329
356
|
|
|
330
|
-
|
|
357
|
+
if (caseResponse.status === 404) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
331
360
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
361
|
+
if (!caseResponse.ok) {
|
|
362
|
+
throw new Error(`Failed to load case data for deletion (${caseNumber}): ${caseResponse.status}`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const caseData = await caseResponse.json() as CaseData;
|
|
366
|
+
const deletionErrors: string[] = [];
|
|
367
|
+
|
|
368
|
+
// Delete all files associated with this case
|
|
369
|
+
if (caseData.files && caseData.files.length > 0) {
|
|
370
|
+
for (const file of caseData.files) {
|
|
371
|
+
const encodedFileId = encodeURIComponent(file.id);
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
const imageDeleteResponse = await fetch(`${imageWorkerBaseUrl}/${encodedFileId}`, {
|
|
375
|
+
method: 'DELETE',
|
|
376
|
+
headers: {
|
|
377
|
+
'Authorization': `Bearer ${imageApiKey}`
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
if (!imageDeleteResponse.ok && imageDeleteResponse.status !== 404) {
|
|
382
|
+
deletionErrors.push(`image ${file.id} delete failed (${imageDeleteResponse.status})`);
|
|
383
|
+
}
|
|
384
|
+
} catch (error) {
|
|
385
|
+
const message = error instanceof Error ? error.message : 'unknown image delete error';
|
|
386
|
+
deletionErrors.push(`image ${file.id} delete threw (${message})`);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
const notesDeleteResponse = await fetch(
|
|
391
|
+
`${dataWorkerBaseUrl}/${encodedUserId}/${encodedCaseNumber}/${encodedFileId}/data.json`,
|
|
392
|
+
{
|
|
346
393
|
method: 'DELETE',
|
|
347
394
|
headers: { 'X-Custom-Auth-Key': dataApiKey }
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
|
|
395
|
+
}
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
if (!notesDeleteResponse.ok && notesDeleteResponse.status !== 404) {
|
|
399
|
+
deletionErrors.push(`annotation ${file.id} delete failed (${notesDeleteResponse.status})`);
|
|
351
400
|
}
|
|
401
|
+
} catch (error) {
|
|
402
|
+
const message = error instanceof Error ? error.message : 'unknown annotation delete error';
|
|
403
|
+
deletionErrors.push(`annotation ${file.id} delete threw (${message})`);
|
|
352
404
|
}
|
|
353
405
|
}
|
|
406
|
+
}
|
|
354
407
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
408
|
+
// Delete case data file
|
|
409
|
+
try {
|
|
410
|
+
const caseDeleteResponse = await fetch(
|
|
411
|
+
`${dataWorkerBaseUrl}/${encodedUserId}/${encodedCaseNumber}/data.json`,
|
|
412
|
+
{
|
|
413
|
+
method: 'DELETE',
|
|
414
|
+
headers: { 'X-Custom-Auth-Key': dataApiKey }
|
|
415
|
+
}
|
|
416
|
+
);
|
|
360
417
|
|
|
361
|
-
|
|
362
|
-
|
|
418
|
+
if (!caseDeleteResponse.ok && caseDeleteResponse.status !== 404) {
|
|
419
|
+
deletionErrors.push(`case ${caseNumber} delete failed (${caseDeleteResponse.status})`);
|
|
420
|
+
}
|
|
421
|
+
} catch (error) {
|
|
422
|
+
const message = error instanceof Error ? error.message : 'unknown case delete error';
|
|
423
|
+
deletionErrors.push(`case ${caseNumber} delete threw (${message})`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (deletionErrors.length > 0) {
|
|
427
|
+
throw new Error(`Case cleanup incomplete for ${caseNumber}: ${deletionErrors.join('; ')}`);
|
|
363
428
|
}
|
|
364
429
|
}
|
|
365
430
|
|
|
@@ -376,11 +441,10 @@ async function executeUserDeletion(
|
|
|
376
441
|
const userObject: UserData = JSON.parse(userData);
|
|
377
442
|
const ownedCases = (userObject.cases || []).map((caseItem) => caseItem.caseNumber);
|
|
378
443
|
const readOnlyCases = (userObject.readOnlyCases || []).map((caseItem) => caseItem.caseNumber);
|
|
379
|
-
const allCaseNumbers = [...ownedCases, ...readOnlyCases];
|
|
444
|
+
const allCaseNumbers = Array.from(new Set([...ownedCases, ...readOnlyCases]));
|
|
380
445
|
const totalCases = allCaseNumbers.length;
|
|
381
446
|
let completedCases = 0;
|
|
382
|
-
|
|
383
|
-
await deleteFirebaseAuthUser(env, userUid);
|
|
447
|
+
const caseCleanupErrors: string[] = [];
|
|
384
448
|
|
|
385
449
|
reportProgress?.({
|
|
386
450
|
event: 'start',
|
|
@@ -396,17 +460,33 @@ async function executeUserDeletion(
|
|
|
396
460
|
currentCaseNumber: caseNumber
|
|
397
461
|
});
|
|
398
462
|
|
|
399
|
-
|
|
463
|
+
let caseDeletionError: string | null = null;
|
|
464
|
+
try {
|
|
465
|
+
await deleteSingleCase(env, userUid, caseNumber);
|
|
466
|
+
} catch (error) {
|
|
467
|
+
caseDeletionError = error instanceof Error ? error.message : `Case cleanup failed for ${caseNumber}`;
|
|
468
|
+
caseCleanupErrors.push(caseDeletionError);
|
|
469
|
+
console.error(`Case cleanup error for ${caseNumber}:`, error);
|
|
470
|
+
}
|
|
471
|
+
|
|
400
472
|
completedCases += 1;
|
|
401
473
|
|
|
402
474
|
reportProgress?.({
|
|
403
475
|
event: 'case-complete',
|
|
404
476
|
totalCases,
|
|
405
477
|
completedCases,
|
|
406
|
-
currentCaseNumber: caseNumber
|
|
478
|
+
currentCaseNumber: caseNumber,
|
|
479
|
+
success: caseDeletionError === null,
|
|
480
|
+
message: caseDeletionError || undefined
|
|
407
481
|
});
|
|
408
482
|
}
|
|
409
483
|
|
|
484
|
+
if (caseCleanupErrors.length > 0) {
|
|
485
|
+
throw new Error(`Failed to fully delete all case data: ${caseCleanupErrors.join(' | ')}`);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
await deleteFirebaseAuthUser(env, userUid);
|
|
489
|
+
|
|
410
490
|
// Delete the user account from the database
|
|
411
491
|
await env.USER_DB.delete(userUid);
|
|
412
492
|
|