@striae-org/striae 3.2.2 → 4.0.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/.env.example +1 -1
- package/app/components/actions/case-export/core-export.ts +5 -2
- package/app/components/actions/case-export/download-handlers.ts +51 -3
- package/app/components/actions/case-import/confirmation-import.ts +65 -40
- package/app/components/actions/case-import/confirmation-package.ts +86 -0
- package/app/components/actions/case-import/image-operations.ts +20 -49
- package/app/components/actions/case-import/index.ts +1 -0
- package/app/components/actions/case-import/orchestrator.ts +13 -3
- package/app/components/actions/case-import/storage-operations.ts +54 -89
- package/app/components/actions/case-import/validation.ts +7 -111
- package/app/components/actions/case-import/zip-processing.ts +44 -2
- package/app/components/actions/case-manage.ts +15 -27
- package/app/components/actions/confirm-export.ts +44 -13
- package/app/components/actions/generate-pdf.ts +3 -7
- package/app/components/actions/image-manage.ts +63 -129
- package/app/components/button/button.module.css +12 -8
- 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 +13 -60
- package/app/components/sidebar/case-import/case-import.tsx +18 -6
- package/app/components/sidebar/case-import/hooks/useFilePreview.ts +6 -4
- package/app/components/sidebar/case-import/utils/file-validation.ts +57 -2
- package/app/components/sidebar/cases/case-sidebar.tsx +122 -52
- package/app/components/sidebar/cases/cases.module.css +101 -18
- package/app/components/sidebar/notes/notes.module.css +33 -13
- package/app/components/sidebar/sidebar.module.css +0 -2
- package/app/components/user/delete-account.tsx +7 -7
- package/app/components/user/manage-profile.tsx +1 -1
- package/app/components/user/mfa-phone-update.tsx +15 -12
- package/app/config-example/config.json +2 -8
- package/app/hooks/useInactivityTimeout.ts +2 -5
- package/app/root.tsx +96 -65
- package/app/routes/auth/login.tsx +132 -11
- 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/SHA256.ts +5 -1
- package/app/utils/auth.ts +5 -32
- package/app/utils/confirmation-signature.ts +5 -1
- package/app/utils/data-api-client.ts +43 -0
- package/app/utils/data-operations.ts +59 -75
- package/app/utils/export-verification.ts +353 -0
- 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/signature-utils.ts +74 -4
- 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 +127 -0
- package/functions/api/pdf/[[path]].ts +110 -0
- package/functions/api/user/[[path]].ts +196 -0
- package/package.json +8 -4
- 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/scripts/deploy-all.sh +22 -8
- package/scripts/deploy-config.sh +143 -148
- package/scripts/deploy-pages-secrets.sh +231 -0
- package/scripts/deploy-worker-secrets.sh +1 -1
- package/workers/audit-worker/wrangler.jsonc.example +1 -8
- package/workers/data-worker/wrangler.jsonc.example +1 -8
- package/workers/image-worker/wrangler.jsonc.example +1 -8
- package/workers/keys-worker/wrangler.jsonc.example +2 -9
- package/workers/pdf-worker/scripts/generate-assets.js +94 -0
- package/workers/pdf-worker/src/assets/icon-256.png +0 -0
- package/workers/pdf-worker/wrangler.jsonc.example +1 -8
- package/workers/user-worker/src/user-worker.example.ts +121 -41
- package/workers/user-worker/wrangler.jsonc.example +1 -8
- package/wrangler.toml.example +1 -1
- package/app/styles/legal-pages.module.css +0 -113
- package/public/favicon.svg +0 -9
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# ======================================
|
|
4
|
+
# STRIAE PAGES SECRETS DEPLOYMENT SCRIPT
|
|
5
|
+
# ======================================
|
|
6
|
+
# This script deploys required secrets to Cloudflare Pages environments.
|
|
7
|
+
|
|
8
|
+
set -e
|
|
9
|
+
set -o pipefail
|
|
10
|
+
|
|
11
|
+
# Colors for output
|
|
12
|
+
RED='\033[0;31m'
|
|
13
|
+
GREEN='\033[0;32m'
|
|
14
|
+
YELLOW='\033[1;33m'
|
|
15
|
+
BLUE='\033[0;34m'
|
|
16
|
+
NC='\033[0m' # No Color
|
|
17
|
+
|
|
18
|
+
echo -e "${BLUE}🔐 Striae Pages Secrets Deployment Script${NC}"
|
|
19
|
+
echo "=========================================="
|
|
20
|
+
|
|
21
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
22
|
+
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
23
|
+
cd "$PROJECT_ROOT"
|
|
24
|
+
|
|
25
|
+
trap 'echo -e "\n${RED}❌ deploy-pages-secrets.sh failed near line ${LINENO}${NC}"' ERR
|
|
26
|
+
|
|
27
|
+
show_help=false
|
|
28
|
+
deploy_production=true
|
|
29
|
+
deploy_preview=true
|
|
30
|
+
|
|
31
|
+
for arg in "$@"; do
|
|
32
|
+
case "$arg" in
|
|
33
|
+
-h|--help)
|
|
34
|
+
show_help=true
|
|
35
|
+
;;
|
|
36
|
+
--production-only)
|
|
37
|
+
deploy_production=true
|
|
38
|
+
deploy_preview=false
|
|
39
|
+
;;
|
|
40
|
+
--preview-only)
|
|
41
|
+
deploy_production=false
|
|
42
|
+
deploy_preview=true
|
|
43
|
+
;;
|
|
44
|
+
*)
|
|
45
|
+
echo -e "${RED}❌ Unknown option: $arg${NC}"
|
|
46
|
+
echo "Use --help to see supported options."
|
|
47
|
+
exit 1
|
|
48
|
+
;;
|
|
49
|
+
esac
|
|
50
|
+
done
|
|
51
|
+
|
|
52
|
+
if [ "$show_help" = "true" ]; then
|
|
53
|
+
echo "Usage: bash ./scripts/deploy-pages-secrets.sh [--production-only|--preview-only]"
|
|
54
|
+
echo ""
|
|
55
|
+
echo "Options:"
|
|
56
|
+
echo " --production-only Deploy secrets only to the production Pages environment"
|
|
57
|
+
echo " --preview-only Deploy secrets only to the preview Pages environment"
|
|
58
|
+
echo " -h, --help Show this help message"
|
|
59
|
+
exit 0
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
if [ "$deploy_production" != "true" ] && [ "$deploy_preview" != "true" ]; then
|
|
63
|
+
echo -e "${RED}❌ No target environment selected${NC}"
|
|
64
|
+
exit 1
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
require_command() {
|
|
68
|
+
local cmd=$1
|
|
69
|
+
if ! command -v "$cmd" > /dev/null 2>&1; then
|
|
70
|
+
echo -e "${RED}❌ Error: required command '$cmd' is not installed or not in PATH${NC}"
|
|
71
|
+
exit 1
|
|
72
|
+
fi
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
strip_carriage_returns() {
|
|
76
|
+
printf '%s' "$1" | tr -d '\r'
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
is_placeholder() {
|
|
80
|
+
local value="$1"
|
|
81
|
+
local normalized
|
|
82
|
+
|
|
83
|
+
normalized=$(echo "$value" | tr '[:upper:]' '[:lower:]')
|
|
84
|
+
|
|
85
|
+
if [ -z "$normalized" ]; then
|
|
86
|
+
return 0
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
[[ "$normalized" == your_*_here ]]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
load_required_project_id() {
|
|
93
|
+
local admin_service_path="app/config/admin-service.json"
|
|
94
|
+
local service_project_id
|
|
95
|
+
|
|
96
|
+
if [ ! -f "$admin_service_path" ]; then
|
|
97
|
+
echo -e "${RED}❌ Error: Required Firebase admin service file not found: $admin_service_path${NC}"
|
|
98
|
+
echo -e "${YELLOW} Create app/config/admin-service.json before deploying Pages secrets.${NC}"
|
|
99
|
+
exit 1
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
if ! service_project_id=$(node -e "const fs=require('fs'); const data=JSON.parse(fs.readFileSync(process.argv[1], 'utf8')); process.stdout.write(data.project_id || '');" "$admin_service_path"); then
|
|
103
|
+
echo -e "${RED}❌ Error: Could not parse project_id from $admin_service_path${NC}"
|
|
104
|
+
exit 1
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
service_project_id=$(strip_carriage_returns "$service_project_id")
|
|
108
|
+
|
|
109
|
+
if [ -z "$service_project_id" ] || is_placeholder "$service_project_id"; then
|
|
110
|
+
echo -e "${RED}❌ Error: project_id in $admin_service_path is missing or placeholder${NC}"
|
|
111
|
+
exit 1
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
PROJECT_ID="$service_project_id"
|
|
115
|
+
export PROJECT_ID
|
|
116
|
+
|
|
117
|
+
echo -e "${GREEN}✅ Loaded PROJECT_ID from $admin_service_path${NC}"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
get_required_value() {
|
|
121
|
+
local var_name=$1
|
|
122
|
+
local value="${!var_name}"
|
|
123
|
+
|
|
124
|
+
value=$(strip_carriage_returns "$value")
|
|
125
|
+
|
|
126
|
+
if [ -z "$value" ] || is_placeholder "$value"; then
|
|
127
|
+
echo -e "${RED}❌ Error: required value for $var_name is missing or placeholder${NC}" >&2
|
|
128
|
+
exit 1
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
printf '%s' "$value"
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
get_optional_value() {
|
|
135
|
+
local var_name=$1
|
|
136
|
+
local value="${!var_name}"
|
|
137
|
+
|
|
138
|
+
value=$(strip_carriage_returns "$value")
|
|
139
|
+
|
|
140
|
+
if [ -z "$value" ] || is_placeholder "$value"; then
|
|
141
|
+
printf ''
|
|
142
|
+
return 0
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
printf '%s' "$value"
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
set_pages_secret() {
|
|
149
|
+
local secret_name=$1
|
|
150
|
+
local secret_value=$2
|
|
151
|
+
local pages_env=$3
|
|
152
|
+
|
|
153
|
+
echo -e "${YELLOW} Setting $secret_name for $pages_env...${NC}"
|
|
154
|
+
|
|
155
|
+
if [ "$pages_env" = "production" ]; then
|
|
156
|
+
printf '%s' "$secret_value" | wrangler pages secret put "$secret_name" --project-name "$PAGES_PROJECT_NAME"
|
|
157
|
+
return 0
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
printf '%s' "$secret_value" | wrangler pages secret put "$secret_name" --project-name "$PAGES_PROJECT_NAME" --env "$pages_env"
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
deploy_pages_environment_secrets() {
|
|
164
|
+
local pages_env=$1
|
|
165
|
+
local secret
|
|
166
|
+
local secret_value
|
|
167
|
+
|
|
168
|
+
echo -e "\n${BLUE}🔧 Deploying Pages secrets to $pages_env...${NC}"
|
|
169
|
+
|
|
170
|
+
for secret in "${required_pages_secrets[@]}"; do
|
|
171
|
+
secret_value=$(get_required_value "$secret")
|
|
172
|
+
set_pages_secret "$secret" "$secret_value" "$pages_env"
|
|
173
|
+
done
|
|
174
|
+
|
|
175
|
+
local optional_api_token
|
|
176
|
+
optional_api_token=$(get_optional_value "API_TOKEN")
|
|
177
|
+
if [ -n "$optional_api_token" ]; then
|
|
178
|
+
set_pages_secret "API_TOKEN" "$optional_api_token" "$pages_env"
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
echo -e "${GREEN}✅ Pages secrets deployed to $pages_env${NC}"
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
require_command wrangler
|
|
185
|
+
require_command node
|
|
186
|
+
|
|
187
|
+
if [ ! -f ".env" ]; then
|
|
188
|
+
echo -e "${RED}❌ Error: .env file not found${NC}"
|
|
189
|
+
echo -e "${YELLOW} Run deploy-config first to generate and populate .env.${NC}"
|
|
190
|
+
exit 1
|
|
191
|
+
fi
|
|
192
|
+
|
|
193
|
+
echo -e "${YELLOW}📖 Loading environment variables from .env...${NC}"
|
|
194
|
+
source .env
|
|
195
|
+
|
|
196
|
+
load_required_project_id
|
|
197
|
+
|
|
198
|
+
PAGES_PROJECT_NAME=$(strip_carriage_returns "$PAGES_PROJECT_NAME")
|
|
199
|
+
if [ -z "$PAGES_PROJECT_NAME" ] || is_placeholder "$PAGES_PROJECT_NAME"; then
|
|
200
|
+
echo -e "${RED}❌ Error: PAGES_PROJECT_NAME is missing or placeholder in .env${NC}"
|
|
201
|
+
exit 1
|
|
202
|
+
fi
|
|
203
|
+
|
|
204
|
+
required_pages_secrets=(
|
|
205
|
+
"AUDIT_WORKER_DOMAIN"
|
|
206
|
+
"DATA_WORKER_DOMAIN"
|
|
207
|
+
"IMAGES_API_TOKEN"
|
|
208
|
+
"IMAGES_WORKER_DOMAIN"
|
|
209
|
+
"PDF_WORKER_AUTH"
|
|
210
|
+
"PDF_WORKER_DOMAIN"
|
|
211
|
+
"PROJECT_ID"
|
|
212
|
+
"R2_KEY_SECRET"
|
|
213
|
+
"USER_DB_AUTH"
|
|
214
|
+
"USER_WORKER_DOMAIN"
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
echo -e "${YELLOW}🔍 Validating required Pages secret values...${NC}"
|
|
218
|
+
for secret in "${required_pages_secrets[@]}"; do
|
|
219
|
+
get_required_value "$secret" > /dev/null
|
|
220
|
+
done
|
|
221
|
+
echo -e "${GREEN}✅ Required Pages secret values found${NC}"
|
|
222
|
+
|
|
223
|
+
if [ "$deploy_production" = "true" ]; then
|
|
224
|
+
deploy_pages_environment_secrets "production"
|
|
225
|
+
fi
|
|
226
|
+
|
|
227
|
+
if [ "$deploy_preview" = "true" ]; then
|
|
228
|
+
deploy_pages_environment_secrets "preview"
|
|
229
|
+
fi
|
|
230
|
+
|
|
231
|
+
echo -e "\n${GREEN}🎉 Pages secrets deployment completed!${NC}"
|
|
@@ -181,7 +181,7 @@ fi
|
|
|
181
181
|
|
|
182
182
|
# User Worker
|
|
183
183
|
if ! set_worker_secrets "User Worker" "workers/user-worker" \
|
|
184
|
-
"USER_DB_AUTH" "R2_KEY_SECRET" "IMAGES_API_TOKEN" "PROJECT_ID" "FIREBASE_SERVICE_ACCOUNT_EMAIL" "FIREBASE_SERVICE_ACCOUNT_PRIVATE_KEY"; then
|
|
184
|
+
"USER_DB_AUTH" "R2_KEY_SECRET" "IMAGES_API_TOKEN" "DATA_WORKER_DOMAIN" "IMAGES_WORKER_DOMAIN" "PROJECT_ID" "FIREBASE_SERVICE_ACCOUNT_EMAIL" "FIREBASE_SERVICE_ACCOUNT_PRIVATE_KEY"; then
|
|
185
185
|
echo -e "${YELLOW}⚠️ Skipping User Worker (not configured)${NC}"
|
|
186
186
|
fi
|
|
187
187
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "AUDIT_WORKER_NAME",
|
|
3
3
|
"account_id": "ACCOUNT_ID",
|
|
4
4
|
"main": "src/audit-worker.ts",
|
|
5
|
-
"compatibility_date": "2026-03-
|
|
5
|
+
"compatibility_date": "2026-03-15",
|
|
6
6
|
"compatibility_flags": [
|
|
7
7
|
"nodejs_compat"
|
|
8
8
|
],
|
|
@@ -18,12 +18,5 @@
|
|
|
18
18
|
}
|
|
19
19
|
],
|
|
20
20
|
|
|
21
|
-
"routes": [
|
|
22
|
-
{
|
|
23
|
-
"pattern": "AUDIT_WORKER_DOMAIN",
|
|
24
|
-
"custom_domain": true
|
|
25
|
-
}
|
|
26
|
-
],
|
|
27
|
-
|
|
28
21
|
"placement": { "mode": "smart" }
|
|
29
22
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "DATA_WORKER_NAME",
|
|
4
4
|
"account_id": "ACCOUNT_ID",
|
|
5
5
|
"main": "src/data-worker.ts",
|
|
6
|
-
"compatibility_date": "2026-03-
|
|
6
|
+
"compatibility_date": "2026-03-15",
|
|
7
7
|
"compatibility_flags": [
|
|
8
8
|
"nodejs_compat"
|
|
9
9
|
],
|
|
@@ -19,12 +19,5 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
|
|
22
|
-
"routes": [
|
|
23
|
-
{
|
|
24
|
-
"pattern": "DATA_WORKER_DOMAIN",
|
|
25
|
-
"custom_domain": true
|
|
26
|
-
}
|
|
27
|
-
],
|
|
28
|
-
|
|
29
22
|
"placement": { "mode": "smart" }
|
|
30
23
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "IMAGES_WORKER_NAME",
|
|
3
3
|
"account_id": "ACCOUNT_ID",
|
|
4
4
|
"main": "src/image-worker.ts",
|
|
5
|
-
"compatibility_date": "2026-03-
|
|
5
|
+
"compatibility_date": "2026-03-15",
|
|
6
6
|
"compatibility_flags": [
|
|
7
7
|
"nodejs_compat"
|
|
8
8
|
],
|
|
@@ -10,13 +10,6 @@
|
|
|
10
10
|
"observability": {
|
|
11
11
|
"enabled": true
|
|
12
12
|
},
|
|
13
|
-
|
|
14
|
-
"routes": [
|
|
15
|
-
{
|
|
16
|
-
"pattern": "IMAGES_WORKER_DOMAIN",
|
|
17
|
-
"custom_domain": true
|
|
18
|
-
}
|
|
19
|
-
],
|
|
20
13
|
|
|
21
14
|
"placement": { "mode": "smart" }
|
|
22
15
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "KEYS_WORKER_NAME",
|
|
3
3
|
"account_id": "ACCOUNT_ID",
|
|
4
4
|
"main": "src/keys.ts",
|
|
5
|
-
"compatibility_date": "2026-03-
|
|
5
|
+
"compatibility_date": "2026-03-15",
|
|
6
6
|
"compatibility_flags": [
|
|
7
7
|
"nodejs_compat"
|
|
8
8
|
],
|
|
@@ -10,13 +10,6 @@
|
|
|
10
10
|
"observability": {
|
|
11
11
|
"enabled": true
|
|
12
12
|
},
|
|
13
|
-
|
|
14
|
-
"routes": [
|
|
15
|
-
{
|
|
16
|
-
"pattern": "KEYS_WORKER_DOMAIN",
|
|
17
|
-
"custom_domain": true
|
|
18
|
-
}
|
|
19
|
-
],
|
|
20
|
-
|
|
13
|
+
|
|
21
14
|
"placement": { "mode": "smart" }
|
|
22
15
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Scans src/assets/ and generates src/assets/generated-assets.ts with one named
|
|
4
|
+
* export per file. Run via: npm run generate:assets
|
|
5
|
+
*
|
|
6
|
+
* Naming rule: filename → remove extension → uppercase → non-alphanumeric → _
|
|
7
|
+
* icon-256.png → ICON_256
|
|
8
|
+
* logo.svg → LOGO
|
|
9
|
+
* brand-mark.webp → BRAND_MARK
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
const ASSETS_DIR = path.join(__dirname, '../src/assets');
|
|
16
|
+
const OUTPUT_FILE = path.join(__dirname, '../src/assets/generated-assets.ts');
|
|
17
|
+
|
|
18
|
+
const MIME_TYPES = {
|
|
19
|
+
'.png': 'image/png',
|
|
20
|
+
'.jpg': 'image/jpeg',
|
|
21
|
+
'.jpeg': 'image/jpeg',
|
|
22
|
+
'.svg': 'image/svg+xml',
|
|
23
|
+
'.gif': 'image/gif',
|
|
24
|
+
'.webp': 'image/webp',
|
|
25
|
+
'.ico': 'image/x-icon',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function toConstName(filename) {
|
|
29
|
+
return filename
|
|
30
|
+
.replace(/\.[^.]+$/, '') // strip extension
|
|
31
|
+
.toUpperCase()
|
|
32
|
+
.replace(/[^A-Z0-9]+/g, '_') // non-alphanumeric → underscore
|
|
33
|
+
.replace(/^_+|_+$/g, ''); // trim leading/trailing underscores
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function toDataUri(filepath) {
|
|
37
|
+
const ext = path.extname(filepath).toLowerCase();
|
|
38
|
+
const mime = MIME_TYPES[ext];
|
|
39
|
+
if (!mime) return null;
|
|
40
|
+
|
|
41
|
+
const data = fs.readFileSync(filepath);
|
|
42
|
+
const b64 = data.toString('base64');
|
|
43
|
+
const chunks = b64.match(/.{1,120}/g) || [];
|
|
44
|
+
|
|
45
|
+
const lines = [];
|
|
46
|
+
lines.push(` "data:${mime};base64," +`);
|
|
47
|
+
chunks.forEach((chunk, i) => {
|
|
48
|
+
lines.push(` "${chunk}"${i === chunks.length - 1 ? ';' : ' +'}`);
|
|
49
|
+
});
|
|
50
|
+
return lines.join('\n');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!fs.existsSync(ASSETS_DIR)) {
|
|
54
|
+
console.error(`assets directory not found: ${ASSETS_DIR}`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const files = fs
|
|
59
|
+
.readdirSync(ASSETS_DIR)
|
|
60
|
+
.filter((f) => Object.keys(MIME_TYPES).includes(path.extname(f).toLowerCase()))
|
|
61
|
+
.sort();
|
|
62
|
+
|
|
63
|
+
if (files.length === 0) {
|
|
64
|
+
console.warn('No supported asset files found in src/assets/.');
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const sections = [
|
|
69
|
+
'// Auto-generated by scripts/generate-assets.js',
|
|
70
|
+
'// Do not edit manually — run `npm run generate:assets` to regenerate.',
|
|
71
|
+
'',
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const exported = [];
|
|
75
|
+
|
|
76
|
+
for (const file of files) {
|
|
77
|
+
const uri = toDataUri(path.join(ASSETS_DIR, file));
|
|
78
|
+
if (!uri) continue;
|
|
79
|
+
|
|
80
|
+
const constName = toConstName(file);
|
|
81
|
+
exported.push({ file, constName });
|
|
82
|
+
|
|
83
|
+
sections.push(`// Source: src/assets/${file}`);
|
|
84
|
+
sections.push(`export const ${constName} =`);
|
|
85
|
+
sections.push(uri);
|
|
86
|
+
sections.push('');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
fs.writeFileSync(OUTPUT_FILE, sections.join('\n'));
|
|
90
|
+
|
|
91
|
+
console.log(`Generated ${OUTPUT_FILE}`);
|
|
92
|
+
exported.forEach(({ file, constName }) =>
|
|
93
|
+
console.log(` ${constName.padEnd(30)} ← src/assets/${file}`)
|
|
94
|
+
);
|
|
Binary file
|
|
@@ -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
|
|