@striae-org/striae 5.3.1 → 5.4.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 +3 -0
- package/app/components/actions/generate-pdf.ts +22 -0
- package/app/components/auth/auth.module.css +531 -0
- package/app/components/auth/mfa-enrollment.tsx +132 -79
- package/app/components/auth/mfa-totp-enrollment.tsx +231 -0
- package/app/components/auth/mfa-verification.tsx +155 -33
- package/app/components/{sidebar/cases/cases-modal.tsx → navbar/case-modals/all-cases-modal.tsx} +4 -4
- package/app/components/navbar/case-modals/archive-case-modal.tsx +9 -10
- package/app/components/navbar/case-modals/case-modal-shared.module.css +88 -0
- package/app/components/navbar/case-modals/delete-case-modal.tsx +9 -10
- package/app/components/navbar/case-modals/export-case-modal.tsx +9 -10
- package/app/components/navbar/case-modals/export-confirmations-modal.tsx +9 -10
- package/app/components/navbar/case-modals/open-case-modal.tsx +4 -4
- package/app/components/navbar/case-modals/rename-case-modal.tsx +9 -10
- package/app/components/navbar/navbar.tsx +1 -1
- package/app/components/sidebar/files/delete-files-modal.tsx +3 -3
- package/app/components/sidebar/files/files-modal.module.css +29 -0
- package/app/components/sidebar/notes/{class-details-fields.tsx → class-details/class-details-fields.tsx} +1 -1
- package/app/components/sidebar/notes/{class-details-modal.tsx → class-details/class-details-modal.tsx} +1 -1
- package/app/components/sidebar/notes/{class-details-sections.tsx → class-details/class-details-sections.tsx} +1 -1
- package/app/components/sidebar/notes/notes-editor-form.tsx +2 -2
- package/app/components/sidebar/notes/notes-editor-modal.tsx +6 -6
- package/app/components/sidebar/notes/notes.module.css +52 -0
- package/app/components/toolbar/toolbar-color-selector.tsx +8 -8
- package/app/components/toolbar/toolbar.module.css +181 -2
- package/app/components/user/delete-account.tsx +7 -7
- package/app/components/user/inactivity-warning.tsx +6 -6
- package/app/components/user/manage-profile.tsx +18 -1
- package/app/components/user/mfa-enrolled-factors.tsx +117 -0
- package/app/components/user/mfa-phone-update.tsx +8 -4
- package/app/components/user/mfa-totp-section.tsx +446 -0
- package/app/components/user/user.module.css +665 -0
- package/app/routes/striae/striae.tsx +1 -1
- package/app/services/audit/audit.service.ts +1 -1
- package/app/services/audit/builders/audit-event-builders-user-security.ts +4 -2
- package/app/services/firebase/errors.ts +2 -0
- package/app/utils/auth/mfa.ts +35 -1
- package/functions/api/image/[[path]].ts +19 -3
- package/package.json +16 -21
- package/scripts/deploy-all.sh +166 -0
- package/scripts/deploy-config/modules/env-utils.sh +322 -0
- package/scripts/deploy-config/modules/keys.sh +404 -0
- package/scripts/deploy-config/modules/prompt.sh +375 -0
- package/scripts/deploy-config/modules/scaffolding.sh +310 -0
- package/scripts/deploy-config/modules/validation.sh +354 -0
- package/scripts/deploy-config.sh +236 -0
- package/scripts/deploy-pages-secrets.sh +231 -0
- package/scripts/deploy-pages.sh +34 -0
- package/scripts/deploy-primershear-emails.sh +167 -0
- package/scripts/deploy-worker-secrets.sh +385 -0
- package/scripts/dev.cjs +23 -0
- package/scripts/enable-totp-mfa.mjs +57 -0
- package/scripts/install-workers.sh +87 -0
- package/scripts/run-eslint.cjs +43 -0
- package/scripts/update-compatibility-dates.cjs +124 -0
- package/scripts/update-markdown-versions.cjs +43 -0
- package/workers/audit-worker/package.json +1 -1
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/package.json +1 -1
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/package.json +1 -1
- package/workers/image-worker/src/image-worker.example.ts +36 -2
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/package.json +1 -1
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/package.json +1 -1
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
- package/app/components/auth/mfa-enrollment.module.css +0 -276
- package/app/components/auth/mfa-verification.module.css +0 -259
- package/app/components/navbar/case-modals/archive-case-modal.module.css +0 -34
- package/app/components/navbar/case-modals/delete-case-modal.module.css +0 -9
- package/app/components/navbar/case-modals/export-case-modal.module.css +0 -27
- package/app/components/navbar/case-modals/export-confirmations-modal.module.css +0 -24
- package/app/components/navbar/case-modals/open-case-modal.module.css +0 -82
- package/app/components/navbar/case-modals/rename-case-modal.module.css +0 -9
- package/app/components/sidebar/files/delete-files-modal.module.css +0 -26
- package/app/components/sidebar/notes/notes-editor-modal.module.css +0 -49
- package/app/components/toolbar/toolbar-color-selector.module.css +0 -171
- package/app/components/user/delete-account.module.css +0 -277
- package/app/components/user/inactivity-warning.module.css +0 -148
- package/app/components/user/manage-profile.module.css +0 -192
- package/app/routes/auth/login.module.css +0 -523
- package/app/routes/auth/login.tsx +0 -705
- /package/app/components/{sidebar → navbar}/case-import/case-import.module.css +0 -0
- /package/app/components/{sidebar → navbar}/case-import/case-import.tsx +0 -0
- /package/app/components/{sidebar → navbar}/case-import/components/CasePreviewSection.tsx +0 -0
- /package/app/components/{sidebar → navbar}/case-import/components/ConfirmationDialog.tsx +0 -0
- /package/app/components/{sidebar → navbar}/case-import/components/ConfirmationPreviewSection.tsx +0 -0
- /package/app/components/{sidebar → navbar}/case-import/components/ExistingCaseSection.tsx +0 -0
- /package/app/components/{sidebar → navbar}/case-import/components/FileSelector.tsx +0 -0
- /package/app/components/{sidebar → navbar}/case-import/components/ProgressSection.tsx +0 -0
- /package/app/components/{sidebar → navbar}/case-import/hooks/useFilePreview.ts +0 -0
- /package/app/components/{sidebar → navbar}/case-import/hooks/useImportExecution.ts +0 -0
- /package/app/components/{sidebar → navbar}/case-import/hooks/useImportState.ts +0 -0
- /package/app/components/{sidebar → navbar}/case-import/index.ts +0 -0
- /package/app/components/{sidebar → navbar}/case-import/utils/file-validation.ts +0 -0
- /package/app/components/{sidebar/cases/cases-modal.module.css → navbar/case-modals/all-cases-modal.module.css} +0 -0
- /package/app/components/sidebar/notes/{class-details-shared.ts → class-details/class-details-shared.ts} +0 -0
- /package/app/components/sidebar/notes/{use-class-details-state.ts → class-details/use-class-details-state.ts} +0 -0
package/app/utils/auth/mfa.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// MFA Configuration Helper
|
|
2
2
|
// This file contains utilities and documentation for managing MFA in your Firebase project
|
|
3
3
|
|
|
4
|
-
import { multiFactor, type User } from 'firebase/auth';
|
|
4
|
+
import { multiFactor, PhoneMultiFactorGenerator, TotpMultiFactorGenerator, type User } from 'firebase/auth';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Check if a user has MFA enrolled
|
|
@@ -35,6 +35,40 @@ export const getMFAFactors = (user: User) => {
|
|
|
35
35
|
}));
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Get enrolled TOTP factors for a user
|
|
40
|
+
*/
|
|
41
|
+
export const getTotpFactors = (user: User) => {
|
|
42
|
+
return multiFactor(user).enrolledFactors.filter(
|
|
43
|
+
(factor) => factor.factorId === TotpMultiFactorGenerator.FACTOR_ID
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if a user has TOTP MFA enrolled
|
|
49
|
+
*/
|
|
50
|
+
export const hasTotpEnrolled = (user: User): boolean => {
|
|
51
|
+
return getTotpFactors(user).length > 0;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get enrolled Phone/SMS factors for a user
|
|
56
|
+
*/
|
|
57
|
+
export const getPhoneMfaFactors = (user: User) => {
|
|
58
|
+
return multiFactor(user).enrolledFactors.filter(
|
|
59
|
+
(factor) => factor.factorId === PhoneMultiFactorGenerator.FACTOR_ID
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Return a human-readable label for a factorId
|
|
65
|
+
*/
|
|
66
|
+
export const getMfaMethodLabel = (factorId: string): string => {
|
|
67
|
+
if (factorId === TotpMultiFactorGenerator.FACTOR_ID) return 'Authenticator App';
|
|
68
|
+
if (factorId === PhoneMultiFactorGenerator.FACTOR_ID) return 'Phone (SMS)';
|
|
69
|
+
return 'Unknown Method';
|
|
70
|
+
};
|
|
71
|
+
|
|
38
72
|
/*
|
|
39
73
|
FIREBASE CONSOLE CONFIGURATION STEPS:
|
|
40
74
|
|
|
@@ -67,6 +67,14 @@ function resolveImageWorkerToken(env: Env): string {
|
|
|
67
67
|
return typeof env.IMAGES_API_TOKEN === 'string' ? env.IMAGES_API_TOKEN.trim() : '';
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
const BASE64URL_SEGMENT = /^[A-Za-z0-9_-]+$/;
|
|
71
|
+
|
|
72
|
+
function looksLikeSignedToken(value: string): boolean {
|
|
73
|
+
const parts = value.split('.');
|
|
74
|
+
if (parts.length !== 2) return false;
|
|
75
|
+
return parts.every(part => part.length > 0 && BASE64URL_SEGMENT.test(part));
|
|
76
|
+
}
|
|
77
|
+
|
|
70
78
|
export const onRequest = async ({ request, env }: ImageProxyContext): Promise<Response> => {
|
|
71
79
|
if (!SUPPORTED_METHODS.has(request.method)) {
|
|
72
80
|
return textResponse('Method not allowed', 405);
|
|
@@ -84,9 +92,17 @@ export const onRequest = async ({ request, env }: ImageProxyContext): Promise<Re
|
|
|
84
92
|
|
|
85
93
|
const requestUrl = new URL(request.url);
|
|
86
94
|
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
95
|
+
const signedToken = requestUrl.searchParams.get('st');
|
|
96
|
+
const isSignedTokenRequest =
|
|
97
|
+
request.method === 'GET' &&
|
|
98
|
+
signedToken !== null &&
|
|
99
|
+
looksLikeSignedToken(signedToken);
|
|
100
|
+
|
|
101
|
+
if (!isSignedTokenRequest) {
|
|
102
|
+
const identity = await verifyFirebaseIdentityFromRequest(request, env);
|
|
103
|
+
if (!identity) {
|
|
104
|
+
return textResponse('Unauthorized', 401);
|
|
105
|
+
}
|
|
90
106
|
}
|
|
91
107
|
|
|
92
108
|
const proxyPathResult = extractProxyPath(requestUrl);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@striae-org/striae",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Striae is a specialized, cloud-native platform designed to streamline forensic firearms identification by providing an intuitive environment for digital comparison image annotation, authenticated confirmations, and automated report generation.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -31,27 +31,17 @@
|
|
|
31
31
|
"access": "public"
|
|
32
32
|
},
|
|
33
33
|
"files": [
|
|
34
|
-
"app/
|
|
35
|
-
"app/config
|
|
36
|
-
"app/
|
|
37
|
-
"app/
|
|
38
|
-
"app/routes/",
|
|
39
|
-
"app/services/",
|
|
40
|
-
"app/styles/",
|
|
41
|
-
"app/types/",
|
|
42
|
-
"app/utils/",
|
|
43
|
-
"app/entry.client.tsx",
|
|
44
|
-
"app/entry.server.tsx",
|
|
45
|
-
"app/root.tsx",
|
|
46
|
-
"app/routes.ts",
|
|
47
|
-
"app/global.css",
|
|
34
|
+
"app/",
|
|
35
|
+
"!app/config",
|
|
36
|
+
"!app/routes/auth/login.tsx",
|
|
37
|
+
"!app/routes/auth/login.module.css",
|
|
48
38
|
"react-router.config.ts",
|
|
49
39
|
"load-context.ts",
|
|
40
|
+
"scripts/",
|
|
50
41
|
"functions/",
|
|
51
42
|
"public/",
|
|
52
43
|
"workers/*/package.json",
|
|
53
44
|
"workers/*/src/**/*.example.ts",
|
|
54
|
-
"workers/*/src/**/*.example.js",
|
|
55
45
|
"workers/*/src/**/*.ts",
|
|
56
46
|
"workers/pdf-worker/scripts/*.js",
|
|
57
47
|
"!workers/*/src/**/*worker.ts",
|
|
@@ -92,6 +82,7 @@
|
|
|
92
82
|
"typegen": "wrangler types",
|
|
93
83
|
"preview": "npm run build && wrangler pages dev",
|
|
94
84
|
"cf-typegen": "wrangler types",
|
|
85
|
+
"enable-totp-mfa": "node ./scripts/enable-totp-mfa.mjs",
|
|
95
86
|
"update-versions": "node ./scripts/update-markdown-versions.cjs",
|
|
96
87
|
"update-compatibility-dates": "node ./scripts/update-compatibility-dates.cjs",
|
|
97
88
|
"deploy-config": "bash ./scripts/deploy-config.sh",
|
|
@@ -110,9 +101,10 @@
|
|
|
110
101
|
},
|
|
111
102
|
"dependencies": {
|
|
112
103
|
"@react-router/cloudflare": "^7.13.2",
|
|
113
|
-
"firebase": "^12.
|
|
114
|
-
"isbot": "^5.1.
|
|
104
|
+
"firebase": "^12.11.0",
|
|
105
|
+
"isbot": "^5.1.37",
|
|
115
106
|
"jszip": "^3.10.1",
|
|
107
|
+
"qrcode": "^1.5.4",
|
|
116
108
|
"react": "^19.2.4",
|
|
117
109
|
"react-dom": "^19.2.4",
|
|
118
110
|
"react-router": "^7.13.2"
|
|
@@ -120,23 +112,26 @@
|
|
|
120
112
|
"devDependencies": {
|
|
121
113
|
"@react-router/dev": "^7.13.2",
|
|
122
114
|
"@react-router/fs-routes": "^7.13.2",
|
|
115
|
+
"@types/qrcode": "^1.5.6",
|
|
123
116
|
"@types/react": "^19.2.14",
|
|
124
117
|
"@types/react-dom": "^19.2.3",
|
|
125
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
126
|
-
"@typescript-eslint/parser": "^8.
|
|
118
|
+
"@typescript-eslint/eslint-plugin": "^8.58.0",
|
|
119
|
+
"@typescript-eslint/parser": "^8.58.0",
|
|
127
120
|
"eslint": "^9.39.4",
|
|
128
121
|
"eslint-import-resolver-typescript": "^4.4.4",
|
|
129
122
|
"eslint-plugin-import": "^2.32.0",
|
|
130
123
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
131
124
|
"eslint-plugin-react": "^7.37.5",
|
|
132
125
|
"eslint-plugin-react-hooks": "^7.0.1",
|
|
126
|
+
"firebase-admin": "^13.7.0",
|
|
133
127
|
"modern-normalize": "^3.0.1",
|
|
134
128
|
"typescript": "^5.9.3",
|
|
135
129
|
"vite": "^6.4.1",
|
|
136
130
|
"vite-tsconfig-paths": "^6.1.1",
|
|
137
|
-
"wrangler": "^4.
|
|
131
|
+
"wrangler": "^4.79.0"
|
|
138
132
|
},
|
|
139
133
|
"overrides": {
|
|
134
|
+
"@tootallnate/once": "3.0.1",
|
|
140
135
|
"tar": "7.5.11",
|
|
141
136
|
"undici": "7.24.1"
|
|
142
137
|
},
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# ======================================
|
|
4
|
+
# STRIAE COMPLETE DEPLOYMENT SCRIPT
|
|
5
|
+
# ======================================
|
|
6
|
+
# This script deploys the entire Striae application:
|
|
7
|
+
# 1. Configuration setup (copy configs, replace placeholders)
|
|
8
|
+
# 2. Worker dependencies installation
|
|
9
|
+
# 3. Workers (all 5 workers)
|
|
10
|
+
# 4. Worker secrets/environment variables
|
|
11
|
+
# 5. Pages secrets/environment variables
|
|
12
|
+
# 6. Pages (frontend)
|
|
13
|
+
|
|
14
|
+
set -e
|
|
15
|
+
set -o pipefail
|
|
16
|
+
|
|
17
|
+
# Colors for output
|
|
18
|
+
RED='\033[0;31m'
|
|
19
|
+
GREEN='\033[0;32m'
|
|
20
|
+
YELLOW='\033[1;33m'
|
|
21
|
+
BLUE='\033[0;34m'
|
|
22
|
+
PURPLE='\033[0;35m'
|
|
23
|
+
NC='\033[0m' # No Color
|
|
24
|
+
|
|
25
|
+
echo -e "${BLUE}🚀 Striae Complete Deployment Script${NC}"
|
|
26
|
+
echo "======================================"
|
|
27
|
+
echo ""
|
|
28
|
+
|
|
29
|
+
# Get the script directory
|
|
30
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
31
|
+
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
32
|
+
cd "$PROJECT_ROOT"
|
|
33
|
+
|
|
34
|
+
trap 'echo -e "\n${RED}❌ deploy-all.sh failed near line ${LINENO}${NC}"' ERR
|
|
35
|
+
|
|
36
|
+
require_command() {
|
|
37
|
+
local cmd=$1
|
|
38
|
+
if ! command -v "$cmd" > /dev/null 2>&1; then
|
|
39
|
+
echo -e "${RED}❌ Error: required command '$cmd' is not installed or not in PATH${NC}"
|
|
40
|
+
exit 1
|
|
41
|
+
fi
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
assert_file_exists() {
|
|
45
|
+
local file_path=$1
|
|
46
|
+
if [ ! -f "$file_path" ]; then
|
|
47
|
+
echo -e "${RED}❌ Error: required file is missing: $file_path${NC}"
|
|
48
|
+
exit 1
|
|
49
|
+
fi
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
run_config_checkpoint() {
|
|
53
|
+
echo -e "${YELLOW}🧪 Running configuration checkpoint validation...${NC}"
|
|
54
|
+
if ! bash "$SCRIPT_DIR/deploy-config.sh" --validate-only; then
|
|
55
|
+
echo -e "${RED}❌ Configuration checkpoint validation failed!${NC}"
|
|
56
|
+
exit 1
|
|
57
|
+
fi
|
|
58
|
+
echo -e "${GREEN}✅ Configuration checkpoint validation passed${NC}"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
echo -e "${BLUE}🔍 Running deployment preflight checks...${NC}"
|
|
62
|
+
require_command bash
|
|
63
|
+
require_command node
|
|
64
|
+
require_command npm
|
|
65
|
+
require_command wrangler
|
|
66
|
+
|
|
67
|
+
assert_file_exists "$SCRIPT_DIR/deploy-config.sh"
|
|
68
|
+
assert_file_exists "$SCRIPT_DIR/install-workers.sh"
|
|
69
|
+
assert_file_exists "$SCRIPT_DIR/deploy-worker-secrets.sh"
|
|
70
|
+
assert_file_exists "$SCRIPT_DIR/deploy-pages-secrets.sh"
|
|
71
|
+
assert_file_exists "package.json"
|
|
72
|
+
|
|
73
|
+
if [ ! -f ".env" ] && [ ! -f ".env.example" ]; then
|
|
74
|
+
echo -e "${RED}❌ Error: neither .env nor .env.example was found in project root${NC}"
|
|
75
|
+
exit 1
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
echo -e "${GREEN}✅ Preflight checks passed${NC}"
|
|
79
|
+
echo ""
|
|
80
|
+
|
|
81
|
+
# Step 1: Configuration Setup
|
|
82
|
+
echo -e "${PURPLE}Step 1/6: Configuration Setup${NC}"
|
|
83
|
+
echo "------------------------------"
|
|
84
|
+
echo -e "${YELLOW}⚙️ Setting up configuration files and replacing placeholders...${NC}"
|
|
85
|
+
if ! bash "$SCRIPT_DIR/deploy-config.sh"; then
|
|
86
|
+
echo -e "${RED}❌ Configuration setup failed!${NC}"
|
|
87
|
+
echo -e "${YELLOW}Please check your .env file and configuration before proceeding.${NC}"
|
|
88
|
+
exit 1
|
|
89
|
+
fi
|
|
90
|
+
echo -e "${GREEN}✅ Configuration setup completed successfully${NC}"
|
|
91
|
+
run_config_checkpoint
|
|
92
|
+
echo ""
|
|
93
|
+
|
|
94
|
+
# Step 2: Install Worker Dependencies
|
|
95
|
+
echo -e "${PURPLE}Step 2/6: Installing Worker Dependencies${NC}"
|
|
96
|
+
echo "----------------------------------------"
|
|
97
|
+
echo -e "${YELLOW}📦 Installing npm dependencies for all workers...${NC}"
|
|
98
|
+
if ! bash "$SCRIPT_DIR/install-workers.sh"; then
|
|
99
|
+
echo -e "${RED}❌ Worker dependencies installation failed!${NC}"
|
|
100
|
+
exit 1
|
|
101
|
+
fi
|
|
102
|
+
echo -e "${GREEN}✅ All worker dependencies installed successfully${NC}"
|
|
103
|
+
echo ""
|
|
104
|
+
|
|
105
|
+
# Step 3: Deploy Workers
|
|
106
|
+
echo -e "${PURPLE}Step 3/6: Deploying Workers${NC}"
|
|
107
|
+
echo "----------------------------"
|
|
108
|
+
echo -e "${YELLOW}🔧 Deploying all 5 Cloudflare Workers...${NC}"
|
|
109
|
+
if ! npm run deploy-workers; then
|
|
110
|
+
echo -e "${RED}❌ Worker deployment failed!${NC}"
|
|
111
|
+
exit 1
|
|
112
|
+
fi
|
|
113
|
+
echo -e "${GREEN}✅ All workers deployed successfully${NC}"
|
|
114
|
+
echo ""
|
|
115
|
+
|
|
116
|
+
# Step 4: Deploy Worker Secrets
|
|
117
|
+
echo -e "${PURPLE}Step 4/6: Deploying Worker Secrets${NC}"
|
|
118
|
+
echo "-----------------------------------"
|
|
119
|
+
echo -e "${YELLOW}🔐 Deploying worker environment variables...${NC}"
|
|
120
|
+
if ! bash "$SCRIPT_DIR/deploy-worker-secrets.sh"; then
|
|
121
|
+
echo -e "${RED}❌ Worker secrets deployment failed!${NC}"
|
|
122
|
+
exit 1
|
|
123
|
+
fi
|
|
124
|
+
echo -e "${GREEN}✅ Worker secrets deployed successfully${NC}"
|
|
125
|
+
echo ""
|
|
126
|
+
|
|
127
|
+
# Step 5: Deploy Pages Secrets
|
|
128
|
+
echo -e "${PURPLE}Step 5/6: Deploying Pages Secrets${NC}"
|
|
129
|
+
echo "----------------------------------"
|
|
130
|
+
echo -e "${YELLOW}🔐 Deploying Pages environment variables to production only...${NC}"
|
|
131
|
+
if ! bash "$SCRIPT_DIR/deploy-pages-secrets.sh" --production-only; then
|
|
132
|
+
echo -e "${RED}❌ Pages secrets deployment failed!${NC}"
|
|
133
|
+
exit 1
|
|
134
|
+
fi
|
|
135
|
+
echo -e "${GREEN}✅ Pages secrets deployed successfully${NC}"
|
|
136
|
+
echo ""
|
|
137
|
+
|
|
138
|
+
# Step 6: Deploy Pages
|
|
139
|
+
echo -e "${PURPLE}Step 6/6: Deploying Pages${NC}"
|
|
140
|
+
echo "--------------------------"
|
|
141
|
+
echo -e "${YELLOW}🌐 Building and deploying Pages...${NC}"
|
|
142
|
+
if ! npm run deploy-pages; then
|
|
143
|
+
echo -e "${RED}❌ Pages deployment failed!${NC}"
|
|
144
|
+
exit 1
|
|
145
|
+
fi
|
|
146
|
+
echo -e "${GREEN}✅ Pages deployed successfully${NC}"
|
|
147
|
+
echo ""
|
|
148
|
+
|
|
149
|
+
# Success summary
|
|
150
|
+
echo "=========================================="
|
|
151
|
+
echo -e "${GREEN}🎉 COMPLETE DEPLOYMENT SUCCESSFUL! 🎉${NC}"
|
|
152
|
+
echo "=========================================="
|
|
153
|
+
echo ""
|
|
154
|
+
echo -e "${BLUE}Deployed Components:${NC}"
|
|
155
|
+
echo " ✅ Worker dependencies (npm install)"
|
|
156
|
+
echo " ✅ 5 Cloudflare Workers"
|
|
157
|
+
echo " ✅ Worker environment variables"
|
|
158
|
+
echo " ✅ Pages environment variables"
|
|
159
|
+
echo " ✅ Cloudflare Pages frontend"
|
|
160
|
+
echo ""
|
|
161
|
+
echo -e "${BLUE}Next Steps:${NC}"
|
|
162
|
+
echo " 1. Test your application endpoints"
|
|
163
|
+
echo " 2. Verify all services are working"
|
|
164
|
+
echo " 3. Verify worker and Pages secrets are set as expected"
|
|
165
|
+
echo ""
|
|
166
|
+
echo -e "${GREEN}✨ Your Striae application is now fully deployed!${NC}"
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
escape_for_sed_pattern() {
|
|
4
|
+
printf '%s' "$1" | sed -e 's/[][\\.^$*+?{}|()]/\\&/g'
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
dedupe_env_var_entries() {
|
|
8
|
+
local var_name=$1
|
|
9
|
+
local expected_count=1
|
|
10
|
+
local escaped_var_name
|
|
11
|
+
|
|
12
|
+
escaped_var_name=$(escape_for_sed_pattern "$var_name")
|
|
13
|
+
|
|
14
|
+
if [ -f ".env.example" ]; then
|
|
15
|
+
expected_count=$(grep -c "^$escaped_var_name=" .env.example || true)
|
|
16
|
+
|
|
17
|
+
if [ "$expected_count" -lt 1 ]; then
|
|
18
|
+
expected_count=1
|
|
19
|
+
fi
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
awk -v key="$var_name" -v keep="$expected_count" '
|
|
23
|
+
BEGIN { seen = 0 }
|
|
24
|
+
{
|
|
25
|
+
if (index($0, key "=") == 1) {
|
|
26
|
+
seen++
|
|
27
|
+
|
|
28
|
+
if (seen > keep) {
|
|
29
|
+
next
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
print
|
|
33
|
+
}
|
|
34
|
+
' .env > .env.tmp && mv .env.tmp .env
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
normalize_domain_value() {
|
|
38
|
+
local domain="$1"
|
|
39
|
+
|
|
40
|
+
domain=$(printf '%s' "$domain" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
|
|
41
|
+
domain="${domain#http://}"
|
|
42
|
+
domain="${domain#https://}"
|
|
43
|
+
domain="${domain%/}"
|
|
44
|
+
|
|
45
|
+
printf '%s' "$domain"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
normalize_worker_label_value() {
|
|
49
|
+
local label="$1"
|
|
50
|
+
|
|
51
|
+
label=$(normalize_domain_value "$label")
|
|
52
|
+
label="${label#.}"
|
|
53
|
+
label="${label%.}"
|
|
54
|
+
label=$(printf '%s' "$label" | tr '[:upper:]' '[:lower:]')
|
|
55
|
+
|
|
56
|
+
printf '%s' "$label"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
normalize_worker_subdomain_value() {
|
|
60
|
+
local subdomain="$1"
|
|
61
|
+
|
|
62
|
+
subdomain=$(normalize_domain_value "$subdomain")
|
|
63
|
+
subdomain="${subdomain#.}"
|
|
64
|
+
subdomain="${subdomain%.}"
|
|
65
|
+
subdomain=$(printf '%s' "$subdomain" | tr '[:upper:]' '[:lower:]')
|
|
66
|
+
|
|
67
|
+
printf '%s' "$subdomain"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
is_valid_worker_label() {
|
|
71
|
+
local label="$1"
|
|
72
|
+
|
|
73
|
+
[[ "$label" =~ ^[a-z0-9-]+$ ]]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
is_valid_worker_subdomain() {
|
|
77
|
+
local subdomain="$1"
|
|
78
|
+
|
|
79
|
+
[[ "$subdomain" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$ ]]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
strip_carriage_returns() {
|
|
83
|
+
printf '%s' "$1" | tr -d '\r'
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
read_env_var_from_file() {
|
|
87
|
+
local env_file=$1
|
|
88
|
+
local var_name=$2
|
|
89
|
+
|
|
90
|
+
if [ ! -f "$env_file" ]; then
|
|
91
|
+
return 0
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
awk -v key="$var_name" '
|
|
95
|
+
index($0, key "=") == 1 {
|
|
96
|
+
value = substr($0, length(key) + 2)
|
|
97
|
+
}
|
|
98
|
+
END {
|
|
99
|
+
if (value != "") {
|
|
100
|
+
gsub(/\r/, "", value)
|
|
101
|
+
gsub(/^"/, "", value)
|
|
102
|
+
gsub(/"$/, "", value)
|
|
103
|
+
print value
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
' "$env_file"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
resolve_existing_domain_value() {
|
|
110
|
+
local var_name=$1
|
|
111
|
+
local current_value=$2
|
|
112
|
+
local preserved_value=""
|
|
113
|
+
|
|
114
|
+
current_value=$(normalize_domain_value "$current_value")
|
|
115
|
+
|
|
116
|
+
if [ "$current_value" = "$var_name" ]; then
|
|
117
|
+
current_value=""
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
if [ -n "$current_value" ] && ! is_placeholder "$current_value"; then
|
|
121
|
+
printf '%s' "$current_value"
|
|
122
|
+
return 0
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
if [ -n "$preserved_domain_env_file" ] && [ -f "$preserved_domain_env_file" ]; then
|
|
126
|
+
preserved_value=$(read_env_var_from_file "$preserved_domain_env_file" "$var_name")
|
|
127
|
+
preserved_value=$(normalize_domain_value "$preserved_value")
|
|
128
|
+
|
|
129
|
+
if [ "$preserved_value" = "$var_name" ]; then
|
|
130
|
+
preserved_value=""
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
if [ -n "$preserved_value" ] && ! is_placeholder "$preserved_value"; then
|
|
134
|
+
printf '%s' "$preserved_value"
|
|
135
|
+
return 0
|
|
136
|
+
fi
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
printf '%s' "$current_value"
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
restore_env_var_from_backup_if_missing() {
|
|
143
|
+
local var_name=$1
|
|
144
|
+
local current_value="${!var_name}"
|
|
145
|
+
local preserved_value=""
|
|
146
|
+
|
|
147
|
+
if [ "$update_env" != "true" ]; then
|
|
148
|
+
return 0
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
if [ -z "$preserved_domain_env_file" ] || [ ! -f "$preserved_domain_env_file" ]; then
|
|
152
|
+
return 0
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
current_value=$(strip_carriage_returns "$current_value")
|
|
156
|
+
|
|
157
|
+
if [ -n "$current_value" ] && ! is_placeholder "$current_value"; then
|
|
158
|
+
return 0
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
preserved_value=$(read_env_var_from_file "$preserved_domain_env_file" "$var_name")
|
|
162
|
+
preserved_value=$(strip_carriage_returns "$preserved_value")
|
|
163
|
+
|
|
164
|
+
if [ -z "$preserved_value" ] || is_placeholder "$preserved_value"; then
|
|
165
|
+
return 0
|
|
166
|
+
fi
|
|
167
|
+
|
|
168
|
+
printf -v "$var_name" '%s' "$preserved_value"
|
|
169
|
+
export "$var_name"
|
|
170
|
+
write_env_var "$var_name" "$preserved_value"
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
confirm_key_pair_regeneration() {
|
|
174
|
+
local key_pair_label=$1
|
|
175
|
+
local impact_warning=$2
|
|
176
|
+
local regenerate_choice=""
|
|
177
|
+
|
|
178
|
+
if [ "$force_rotate_keys" = "true" ]; then
|
|
179
|
+
echo -e "${YELLOW}⚠️ Auto-confirmed regeneration for $key_pair_label key pair (--force-rotate-keys).${NC}"
|
|
180
|
+
return 0
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
echo -e "${GREEN}Current $key_pair_label key pair: [HIDDEN]${NC}"
|
|
184
|
+
|
|
185
|
+
if [ -n "$impact_warning" ]; then
|
|
186
|
+
echo -e "${YELLOW}⚠️ $impact_warning${NC}"
|
|
187
|
+
fi
|
|
188
|
+
|
|
189
|
+
read -p "Regenerate $key_pair_label key pair? (press Enter to keep current, or type 'y' to regenerate): " regenerate_choice
|
|
190
|
+
regenerate_choice=$(strip_carriage_returns "$regenerate_choice")
|
|
191
|
+
|
|
192
|
+
if [ "$regenerate_choice" = "y" ] || [ "$regenerate_choice" = "Y" ]; then
|
|
193
|
+
return 0
|
|
194
|
+
fi
|
|
195
|
+
|
|
196
|
+
return 1
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
generate_worker_subdomain_label() {
|
|
200
|
+
node -e "const { randomInt } = require('crypto'); const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789'; let value = ''; for (let index = 0; index < 10; index += 1) { value += alphabet[randomInt(alphabet.length)]; } process.stdout.write(value);" 2>/dev/null
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
compose_worker_domain() {
|
|
204
|
+
local worker_name=$1
|
|
205
|
+
local worker_subdomain=$2
|
|
206
|
+
|
|
207
|
+
worker_name=$(normalize_worker_label_value "$worker_name")
|
|
208
|
+
worker_subdomain=$(normalize_worker_subdomain_value "$worker_subdomain")
|
|
209
|
+
|
|
210
|
+
if [ -z "$worker_name" ] || [ -z "$worker_subdomain" ]; then
|
|
211
|
+
return 1
|
|
212
|
+
fi
|
|
213
|
+
|
|
214
|
+
if ! is_valid_worker_label "$worker_name" || ! is_valid_worker_subdomain "$worker_subdomain"; then
|
|
215
|
+
return 1
|
|
216
|
+
fi
|
|
217
|
+
|
|
218
|
+
printf '%s.%s' "$worker_name" "$worker_subdomain"
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
infer_worker_subdomain_from_domain() {
|
|
222
|
+
local worker_name=$1
|
|
223
|
+
local worker_domain=$2
|
|
224
|
+
local worker_subdomain=""
|
|
225
|
+
|
|
226
|
+
worker_name=$(normalize_worker_label_value "$worker_name")
|
|
227
|
+
worker_domain=$(normalize_domain_value "$worker_domain")
|
|
228
|
+
worker_domain=$(printf '%s' "$worker_domain" | tr '[:upper:]' '[:lower:]')
|
|
229
|
+
|
|
230
|
+
if [ -z "$worker_name" ] || [ -z "$worker_domain" ] || is_placeholder "$worker_name" || is_placeholder "$worker_domain"; then
|
|
231
|
+
printf '%s' ""
|
|
232
|
+
return 0
|
|
233
|
+
fi
|
|
234
|
+
|
|
235
|
+
case "$worker_domain" in
|
|
236
|
+
"$worker_name".*)
|
|
237
|
+
worker_subdomain="${worker_domain#${worker_name}.}"
|
|
238
|
+
worker_subdomain=$(normalize_worker_subdomain_value "$worker_subdomain")
|
|
239
|
+
|
|
240
|
+
if is_valid_worker_subdomain "$worker_subdomain"; then
|
|
241
|
+
printf '%s' "$worker_subdomain"
|
|
242
|
+
return 0
|
|
243
|
+
fi
|
|
244
|
+
;;
|
|
245
|
+
esac
|
|
246
|
+
|
|
247
|
+
printf '%s' ""
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
write_env_var() {
|
|
251
|
+
local var_name=$1
|
|
252
|
+
local var_value=$2
|
|
253
|
+
local env_file_value="$var_value"
|
|
254
|
+
|
|
255
|
+
var_value=$(strip_carriage_returns "$var_value")
|
|
256
|
+
env_file_value="$var_value"
|
|
257
|
+
|
|
258
|
+
if [ "$var_name" = "FIREBASE_SERVICE_ACCOUNT_PRIVATE_KEY" ] || [ "$var_name" = "MANIFEST_SIGNING_PRIVATE_KEY" ] || [ "$var_name" = "MANIFEST_SIGNING_PUBLIC_KEY" ] || [ "$var_name" = "EXPORT_ENCRYPTION_PRIVATE_KEY" ] || [ "$var_name" = "EXPORT_ENCRYPTION_PUBLIC_KEY" ] || [ "$var_name" = "DATA_AT_REST_ENCRYPTION_PRIVATE_KEY" ] || [ "$var_name" = "DATA_AT_REST_ENCRYPTION_PUBLIC_KEY" ] || [ "$var_name" = "USER_KV_ENCRYPTION_PRIVATE_KEY" ] || [ "$var_name" = "USER_KV_ENCRYPTION_PUBLIC_KEY" ] || [ "$var_name" = "EXPORT_ENCRYPTION_KEYS_JSON" ] || [ "$var_name" = "DATA_AT_REST_ENCRYPTION_KEYS_JSON" ] || [ "$var_name" = "USER_KV_ENCRYPTION_KEYS_JSON" ]; then
|
|
259
|
+
# Store as a quoted string so sourced .env preserves escaped newline markers (\n)
|
|
260
|
+
env_file_value=${env_file_value//\"/\\\"}
|
|
261
|
+
env_file_value="\"$env_file_value\""
|
|
262
|
+
fi
|
|
263
|
+
|
|
264
|
+
local escaped_var_name
|
|
265
|
+
local replacement_line
|
|
266
|
+
escaped_var_name=$(escape_for_sed_pattern "$var_name")
|
|
267
|
+
replacement_line=$(escape_for_sed_replacement "$var_name=$env_file_value")
|
|
268
|
+
|
|
269
|
+
if grep -q "^$escaped_var_name=" .env; then
|
|
270
|
+
# Replace all occurrences so intentional duplicates in .env.example stay in sync.
|
|
271
|
+
sed -i "s|^$escaped_var_name=.*|$replacement_line|g" .env
|
|
272
|
+
dedupe_env_var_entries "$var_name"
|
|
273
|
+
else
|
|
274
|
+
echo "$var_name=$env_file_value" >> .env
|
|
275
|
+
fi
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
update_private_key_registry() {
|
|
279
|
+
local registry_var_name=$1
|
|
280
|
+
local active_key_var_name=$2
|
|
281
|
+
local current_key_id=$3
|
|
282
|
+
local private_key_value=$4
|
|
283
|
+
local registry_label=$5
|
|
284
|
+
local existing_registry_json=""
|
|
285
|
+
local updated_registry_json=""
|
|
286
|
+
local registry_entry_count=""
|
|
287
|
+
|
|
288
|
+
if [ -z "$registry_var_name" ] || [ -z "$active_key_var_name" ] || [ -z "$current_key_id" ] || [ -z "$private_key_value" ]; then
|
|
289
|
+
echo -e "${YELLOW}⚠️ Skipping $registry_label key registry update due to missing inputs${NC}"
|
|
290
|
+
return 0
|
|
291
|
+
fi
|
|
292
|
+
|
|
293
|
+
existing_registry_json="${!registry_var_name}"
|
|
294
|
+
existing_registry_json=$(strip_carriage_returns "$existing_registry_json")
|
|
295
|
+
|
|
296
|
+
if [ -z "$existing_registry_json" ] || is_placeholder "$existing_registry_json"; then
|
|
297
|
+
existing_registry_json="{}"
|
|
298
|
+
fi
|
|
299
|
+
|
|
300
|
+
updated_registry_json=$(node -e "const raw = process.argv[1] || '{}'; const keyId = process.argv[2] || ''; const privateKey = process.argv[3] || ''; if (!keyId || !privateKey) process.exit(1); const normalized = { activeKeyId: null, keys: {} }; try { const parsed = JSON.parse(raw); if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { if (parsed.keys && typeof parsed.keys === 'object' && !Array.isArray(parsed.keys)) { normalized.keys = Object.fromEntries(Object.entries(parsed.keys).filter(([id, pem]) => typeof id === 'string' && id.trim().length > 0 && typeof pem === 'string' && pem.trim().length > 0)); if (typeof parsed.activeKeyId === 'string' && parsed.activeKeyId.trim().length > 0) normalized.activeKeyId = parsed.activeKeyId.trim(); } else { normalized.keys = Object.fromEntries(Object.entries(parsed).filter(([id, pem]) => id !== 'activeKeyId' && id !== 'keys' && typeof id === 'string' && id.trim().length > 0 && typeof pem === 'string' && pem.trim().length > 0)); if (typeof parsed.activeKeyId === 'string' && parsed.activeKeyId.trim().length > 0) normalized.activeKeyId = parsed.activeKeyId.trim(); } } } catch (_) {} normalized.keys[keyId] = privateKey; normalized.activeKeyId = keyId; process.stdout.write(JSON.stringify(normalized));" "$existing_registry_json" "$current_key_id" "$private_key_value" 2>/dev/null || true)
|
|
301
|
+
|
|
302
|
+
if [ -z "$updated_registry_json" ]; then
|
|
303
|
+
echo -e "${RED}❌ Error: Failed to update $registry_label key registry JSON${NC}"
|
|
304
|
+
exit 1
|
|
305
|
+
fi
|
|
306
|
+
|
|
307
|
+
printf -v "$registry_var_name" '%s' "$updated_registry_json"
|
|
308
|
+
export "$registry_var_name"
|
|
309
|
+
write_env_var "$registry_var_name" "$updated_registry_json"
|
|
310
|
+
|
|
311
|
+
printf -v "$active_key_var_name" '%s' "$current_key_id"
|
|
312
|
+
export "$active_key_var_name"
|
|
313
|
+
write_env_var "$active_key_var_name" "$current_key_id"
|
|
314
|
+
|
|
315
|
+
registry_entry_count=$(node -e "const raw = process.argv[1] || '{}'; try { const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { process.stdout.write('0'); process.exit(0); } const keys = parsed.keys && typeof parsed.keys === 'object' && !Array.isArray(parsed.keys) ? parsed.keys : parsed; const count = Object.entries(keys).filter(([id, pem]) => id !== 'activeKeyId' && id !== 'keys' && typeof id === 'string' && id.trim().length > 0 && typeof pem === 'string' && pem.trim().length > 0).length; process.stdout.write(String(count)); } catch (_) { process.stdout.write('0'); }" "$updated_registry_json")
|
|
316
|
+
echo -e "${GREEN}✅ Updated $registry_label key registry ($registry_entry_count key IDs tracked)${NC}"
|
|
317
|
+
echo -e "${GREEN}✅ $active_key_var_name: $current_key_id${NC}"
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
escape_for_sed_replacement() {
|
|
321
|
+
printf '%s' "$1" | sed -e 's/[&|\\]/\\&/g'
|
|
322
|
+
}
|