@striae-org/striae 4.0.2 → 4.1.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/app/components/actions/confirm-export.ts +4 -2
- package/app/components/actions/generate-pdf.ts +10 -2
- package/app/components/audit/user-audit-viewer.tsx +121 -940
- package/app/components/audit/user-audit.module.css +20 -0
- package/app/components/audit/viewer/audit-activity-summary.tsx +52 -0
- package/app/components/audit/viewer/audit-entries-list.tsx +200 -0
- package/app/components/audit/viewer/audit-filters-panel.tsx +306 -0
- package/app/components/audit/viewer/audit-user-info-card.tsx +44 -0
- package/app/components/audit/viewer/audit-viewer-header.tsx +55 -0
- package/app/components/audit/viewer/audit-viewer-utils.ts +121 -0
- package/app/components/audit/viewer/types.ts +1 -0
- package/app/components/audit/viewer/use-audit-viewer-data.ts +166 -0
- package/app/components/audit/viewer/use-audit-viewer-export.ts +176 -0
- package/app/components/audit/viewer/use-audit-viewer-filters.ts +141 -0
- package/app/components/auth/mfa-enrollment.module.css +13 -5
- package/app/components/auth/mfa-verification.module.css +13 -5
- package/app/components/canvas/canvas.tsx +3 -0
- package/app/components/canvas/confirmation/confirmation.tsx +13 -37
- package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +1 -0
- package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +8 -37
- package/app/components/sidebar/case-export/case-export.tsx +9 -34
- package/app/components/sidebar/case-import/case-import.module.css +2 -0
- package/app/components/sidebar/case-import/case-import.tsx +10 -34
- package/app/components/sidebar/cases/cases-modal.module.css +44 -9
- package/app/components/sidebar/cases/cases-modal.tsx +16 -14
- package/app/components/sidebar/files/files-modal.module.css +45 -10
- package/app/components/sidebar/files/files-modal.tsx +16 -16
- package/app/components/sidebar/notes/notes-modal.tsx +17 -15
- package/app/components/sidebar/notes/notes.module.css +2 -0
- package/app/components/sidebar/sidebar.module.css +2 -2
- package/app/components/toast/toast.module.css +2 -1
- package/app/components/toast/toast.tsx +16 -11
- package/app/components/user/delete-account.tsx +10 -31
- package/app/components/user/inactivity-warning.module.css +8 -6
- package/app/components/user/manage-profile.module.css +2 -0
- package/app/components/user/manage-profile.tsx +85 -30
- package/app/hooks/useOverlayDismiss.ts +68 -0
- package/app/routes/auth/login.example.tsx +786 -0
- package/app/routes/auth/login.module.example.css +523 -0
- package/app/routes/auth/login.tsx +1 -1
- package/app/routes/auth/passwordReset.module.css +23 -13
- package/app/routes/striae/striae.tsx +8 -1
- package/app/routes.ts +7 -0
- package/app/services/audit/audit-export-csv.ts +2 -0
- package/app/services/audit/audit.service.ts +29 -5
- package/app/services/audit/builders/audit-entry-builder.ts +2 -1
- package/app/services/audit/builders/audit-event-builders-user-security.ts +4 -2
- package/app/services/audit/builders/audit-event-builders-workflow.ts +6 -0
- package/app/types/audit.ts +2 -1
- package/app/types/user.ts +1 -0
- package/app/utils/data/permissions.ts +1 -0
- package/functions/api/pdf/[[path]].ts +32 -1
- package/load-context.ts +9 -0
- package/package.json +5 -1
- package/primershear.emails.example +6 -0
- package/scripts/deploy-config.sh +27 -0
- package/scripts/deploy-pages-secrets.sh +6 -0
- package/scripts/deploy-primershear-emails.sh +166 -0
- package/worker-configuration.d.ts +7493 -7491
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/keys-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/src/pdf-worker.example.ts +3 -0
- package/workers/pdf-worker/src/report-types.ts +3 -0
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/src/user-worker.example.ts +6 -1
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
|
@@ -60,6 +60,7 @@ export class AuditService {
|
|
|
60
60
|
private static instance: AuditService;
|
|
61
61
|
private auditBuffer: ValidationAuditEntry[] = [];
|
|
62
62
|
private workflowId: string | null = null;
|
|
63
|
+
private userBadgeIdByUserId = new Map<string, string>();
|
|
63
64
|
|
|
64
65
|
private constructor() {}
|
|
65
66
|
|
|
@@ -97,7 +98,26 @@ export class AuditService {
|
|
|
97
98
|
const startTime = Date.now();
|
|
98
99
|
|
|
99
100
|
try {
|
|
100
|
-
const
|
|
101
|
+
const providedBadgeId = params.userProfileDetails?.badgeId?.trim();
|
|
102
|
+
if (providedBadgeId && params.userId) {
|
|
103
|
+
this.userBadgeIdByUserId.set(params.userId, providedBadgeId);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const resolvedBadgeId =
|
|
107
|
+
providedBadgeId ||
|
|
108
|
+
(params.userId ? this.userBadgeIdByUserId.get(params.userId) : undefined);
|
|
109
|
+
|
|
110
|
+
const paramsWithBadgeId: CreateAuditEntryParams = resolvedBadgeId
|
|
111
|
+
? {
|
|
112
|
+
...params,
|
|
113
|
+
userProfileDetails: {
|
|
114
|
+
...(params.userProfileDetails || {}),
|
|
115
|
+
badgeId: resolvedBadgeId
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
: params;
|
|
119
|
+
|
|
120
|
+
const auditEntry = buildValidationAuditEntry(paramsWithBadgeId);
|
|
101
121
|
|
|
102
122
|
// Add to buffer for batch processing
|
|
103
123
|
this.auditBuffer.push(auditEntry);
|
|
@@ -196,13 +216,15 @@ export class AuditService {
|
|
|
196
216
|
originalExaminerUid?: string,
|
|
197
217
|
performanceMetrics?: PerformanceMetrics,
|
|
198
218
|
imageFileId?: string,
|
|
199
|
-
originalImageFileName?: string
|
|
219
|
+
originalImageFileName?: string,
|
|
220
|
+
badgeId?: string
|
|
200
221
|
): Promise<void> {
|
|
201
222
|
await this.logEvent(
|
|
202
223
|
buildConfirmationCreationAuditParams({
|
|
203
224
|
user,
|
|
204
225
|
caseNumber,
|
|
205
226
|
confirmationId,
|
|
227
|
+
badgeId,
|
|
206
228
|
result,
|
|
207
229
|
errors,
|
|
208
230
|
originalExaminerUid,
|
|
@@ -550,12 +572,13 @@ export class AuditService {
|
|
|
550
572
|
*/
|
|
551
573
|
public async logUserProfileUpdate(
|
|
552
574
|
user: User,
|
|
553
|
-
profileField: 'displayName' | 'email' | 'organization' | 'role' | 'preferences' | 'avatar',
|
|
575
|
+
profileField: 'displayName' | 'email' | 'organization' | 'role' | 'preferences' | 'avatar' | 'badgeId',
|
|
554
576
|
oldValue: string,
|
|
555
577
|
newValue: string,
|
|
556
578
|
result: AuditResult,
|
|
557
579
|
sessionId?: string,
|
|
558
|
-
errors: string[] = []
|
|
580
|
+
errors: string[] = [],
|
|
581
|
+
badgeId?: string
|
|
559
582
|
): Promise<void> {
|
|
560
583
|
await this.logEvent(
|
|
561
584
|
buildUserProfileUpdateAuditParams({
|
|
@@ -565,7 +588,8 @@ export class AuditService {
|
|
|
565
588
|
newValue,
|
|
566
589
|
result,
|
|
567
590
|
sessionId,
|
|
568
|
-
errors
|
|
591
|
+
errors,
|
|
592
|
+
badgeId
|
|
569
593
|
})
|
|
570
594
|
);
|
|
571
595
|
}
|
|
@@ -26,7 +26,8 @@ export const buildValidationAuditEntry = (
|
|
|
26
26
|
fileDetails: params.fileDetails,
|
|
27
27
|
annotationDetails: params.annotationDetails,
|
|
28
28
|
sessionDetails: params.sessionDetails,
|
|
29
|
-
securityDetails: params.securityDetails
|
|
29
|
+
securityDetails: params.securityDetails,
|
|
30
|
+
userProfileDetails: params.userProfileDetails
|
|
30
31
|
}
|
|
31
32
|
};
|
|
32
33
|
};
|
|
@@ -57,9 +57,10 @@ export const buildUserLogoutAuditParams = (
|
|
|
57
57
|
|
|
58
58
|
interface BuildUserProfileUpdateAuditParamsInput {
|
|
59
59
|
user: User;
|
|
60
|
-
profileField: 'displayName' | 'email' | 'organization' | 'role' | 'preferences' | 'avatar';
|
|
60
|
+
profileField: 'displayName' | 'email' | 'organization' | 'role' | 'preferences' | 'avatar' | 'badgeId';
|
|
61
61
|
oldValue: string;
|
|
62
62
|
newValue: string;
|
|
63
|
+
badgeId?: string;
|
|
63
64
|
result: AuditResult;
|
|
64
65
|
sessionId?: string;
|
|
65
66
|
errors?: string[];
|
|
@@ -85,7 +86,8 @@ export const buildUserProfileUpdateAuditParams = (
|
|
|
85
86
|
userProfileDetails: {
|
|
86
87
|
profileField: input.profileField,
|
|
87
88
|
oldValue: input.oldValue,
|
|
88
|
-
newValue: input.newValue
|
|
89
|
+
newValue: input.newValue,
|
|
90
|
+
badgeId: input.badgeId
|
|
89
91
|
}
|
|
90
92
|
};
|
|
91
93
|
};
|
|
@@ -125,6 +125,7 @@ interface BuildConfirmationCreationAuditParamsInput {
|
|
|
125
125
|
user: User;
|
|
126
126
|
caseNumber: string;
|
|
127
127
|
confirmationId: string;
|
|
128
|
+
badgeId?: string;
|
|
128
129
|
result: AuditResult;
|
|
129
130
|
errors?: string[];
|
|
130
131
|
originalExaminerUid?: string;
|
|
@@ -156,6 +157,11 @@ export const buildConfirmationCreationAuditParams = (
|
|
|
156
157
|
performanceMetrics: input.performanceMetrics,
|
|
157
158
|
originalExaminerUid: input.originalExaminerUid,
|
|
158
159
|
reviewingExaminerUid: input.user.uid,
|
|
160
|
+
userProfileDetails: input.badgeId
|
|
161
|
+
? {
|
|
162
|
+
badgeId: input.badgeId
|
|
163
|
+
}
|
|
164
|
+
: undefined,
|
|
159
165
|
fileDetails: input.imageFileId && input.originalImageFileName
|
|
160
166
|
? {
|
|
161
167
|
fileId: input.imageFileId,
|
package/app/types/audit.ts
CHANGED
|
@@ -269,9 +269,10 @@ export interface SecurityAuditDetails {
|
|
|
269
269
|
* User profile and authentication specific audit details
|
|
270
270
|
*/
|
|
271
271
|
export interface UserProfileAuditDetails {
|
|
272
|
-
profileField?: 'displayName' | 'email' | 'organization' | 'role' | 'preferences' | 'avatar';
|
|
272
|
+
profileField?: 'displayName' | 'email' | 'organization' | 'role' | 'preferences' | 'avatar' | 'badgeId';
|
|
273
273
|
oldValue?: string;
|
|
274
274
|
newValue?: string;
|
|
275
|
+
badgeId?: string;
|
|
275
276
|
resetMethod?: 'email' | 'sms' | 'security-questions' | 'admin-reset';
|
|
276
277
|
resetToken?: string; // Partial token for tracking (last 4 chars)
|
|
277
278
|
verificationMethod?: 'email-link' | 'sms-code' | 'totp' | 'backup-codes' | 'admin-verification';
|
package/app/types/user.ts
CHANGED
|
@@ -6,6 +6,8 @@ interface PdfProxyContext {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
const SUPPORTED_METHODS = new Set(['POST', 'OPTIONS']);
|
|
9
|
+
const PRIMERSHEAR_FORMAT = 'primershear';
|
|
10
|
+
const DEFAULT_FORMAT = 'striae';
|
|
9
11
|
|
|
10
12
|
function textResponse(message: string, status: number): Response {
|
|
11
13
|
return new Response(message, {
|
|
@@ -40,6 +42,12 @@ function extractProxyPath(url: URL): string | null {
|
|
|
40
42
|
return remainder.length > 0 ? remainder : '/';
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
function resolveReportFormat(email: string | null, primershearEmails: string): string {
|
|
46
|
+
if (!email) return DEFAULT_FORMAT;
|
|
47
|
+
const allowed = primershearEmails.split(',').map(e => e.trim().toLowerCase()).filter(Boolean);
|
|
48
|
+
return allowed.includes(email.toLowerCase()) ? PRIMERSHEAR_FORMAT : DEFAULT_FORMAT;
|
|
49
|
+
}
|
|
50
|
+
|
|
43
51
|
export const onRequest = async ({ request, env }: PdfProxyContext): Promise<Response> => {
|
|
44
52
|
if (!SUPPORTED_METHODS.has(request.method)) {
|
|
45
53
|
return textResponse('Method not allowed', 405);
|
|
@@ -86,12 +94,35 @@ export const onRequest = async ({ request, env }: PdfProxyContext): Promise<Resp
|
|
|
86
94
|
|
|
87
95
|
upstreamHeaders.set('X-Custom-Auth-Key', env.PDF_WORKER_AUTH);
|
|
88
96
|
|
|
97
|
+
// Resolve the report format server-side based on the verified user email.
|
|
98
|
+
// This prevents email lists from ever being exposed in the client bundle.
|
|
99
|
+
const reportFormat = resolveReportFormat(
|
|
100
|
+
identity.email,
|
|
101
|
+
env.PRIMERSHEAR_EMAILS ?? ''
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
let upstreamBody: BodyInit;
|
|
105
|
+
try {
|
|
106
|
+
const payload = await request.json() as Record<string, unknown>;
|
|
107
|
+
// Inject the server-resolved format, overriding any client-supplied value.
|
|
108
|
+
if (payload.data && typeof payload.data === 'object') {
|
|
109
|
+
payload.reportFormat = reportFormat;
|
|
110
|
+
} else {
|
|
111
|
+
// Legacy flat payload shape
|
|
112
|
+
payload.reportFormat = reportFormat;
|
|
113
|
+
}
|
|
114
|
+
upstreamBody = JSON.stringify(payload);
|
|
115
|
+
upstreamHeaders.set('Content-Type', 'application/json');
|
|
116
|
+
} catch {
|
|
117
|
+
return textResponse('Invalid request body', 400);
|
|
118
|
+
}
|
|
119
|
+
|
|
89
120
|
let upstreamResponse: Response;
|
|
90
121
|
try {
|
|
91
122
|
upstreamResponse = await fetch(upstreamUrl, {
|
|
92
123
|
method: request.method,
|
|
93
124
|
headers: upstreamHeaders,
|
|
94
|
-
body:
|
|
125
|
+
body: upstreamBody
|
|
95
126
|
});
|
|
96
127
|
} catch {
|
|
97
128
|
return textResponse('Upstream PDF service unavailable', 502);
|
package/load-context.ts
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@striae-org/striae",
|
|
3
|
-
"version": "4.0
|
|
3
|
+
"version": "4.1.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",
|
|
@@ -43,8 +43,10 @@
|
|
|
43
43
|
"app/entry.client.tsx",
|
|
44
44
|
"app/entry.server.tsx",
|
|
45
45
|
"app/root.tsx",
|
|
46
|
+
"app/routes.ts",
|
|
46
47
|
"app/tailwind.css",
|
|
47
48
|
"react-router.config.ts",
|
|
49
|
+
"load-context.ts",
|
|
48
50
|
"functions/",
|
|
49
51
|
"public/",
|
|
50
52
|
"scripts/",
|
|
@@ -60,6 +62,7 @@
|
|
|
60
62
|
"workers/pdf-worker/src/report-types.ts",
|
|
61
63
|
"workers/*/wrangler.jsonc.example",
|
|
62
64
|
".env.example",
|
|
65
|
+
"primershear.emails.example",
|
|
63
66
|
"firebase.json",
|
|
64
67
|
"postcss.config.js",
|
|
65
68
|
"tailwind.config.ts",
|
|
@@ -101,6 +104,7 @@
|
|
|
101
104
|
"deploy-workers:secrets": "bash ./scripts/deploy-worker-secrets.sh",
|
|
102
105
|
"deploy-pages:secrets": "bash ./scripts/deploy-pages-secrets.sh",
|
|
103
106
|
"deploy-pages": "bash ./scripts/deploy-pages.sh",
|
|
107
|
+
"deploy-primershear": "bash ./scripts/deploy-primershear-emails.sh",
|
|
104
108
|
"deploy-workers:audit": "cd workers/audit-worker && npm run deploy",
|
|
105
109
|
"deploy-workers:data": "cd workers/data-worker && npm run deploy",
|
|
106
110
|
"deploy-workers:image": "cd workers/image-worker && npm run deploy",
|
package/scripts/deploy-config.sh
CHANGED
|
@@ -697,6 +697,8 @@ validate_generated_configs() {
|
|
|
697
697
|
"app/config/config.json"
|
|
698
698
|
"app/config/firebase.ts"
|
|
699
699
|
"app/config/admin-service.json"
|
|
700
|
+
"app/routes/auth/login.tsx"
|
|
701
|
+
"app/routes/auth/login.module.css"
|
|
700
702
|
"workers/audit-worker/wrangler.jsonc"
|
|
701
703
|
"workers/data-worker/wrangler.jsonc"
|
|
702
704
|
"workers/image-worker/wrangler.jsonc"
|
|
@@ -741,6 +743,7 @@ validate_generated_configs() {
|
|
|
741
743
|
|
|
742
744
|
assert_contains_literal "app/config/config.json" "https://$PAGES_CUSTOM_DOMAIN" "PAGES_CUSTOM_DOMAIN missing in app/config/config.json"
|
|
743
745
|
assert_contains_literal "app/config/config.json" "$ACCOUNT_HASH" "ACCOUNT_HASH missing in app/config/config.json"
|
|
746
|
+
assert_contains_literal "app/routes/auth/login.tsx" "const APP_CANONICAL_ORIGIN = 'https://$PAGES_CUSTOM_DOMAIN';" "PAGES_CUSTOM_DOMAIN missing in app/routes/auth/login.tsx canonical origin"
|
|
744
747
|
|
|
745
748
|
assert_contains_literal "app/config/firebase.ts" "$API_KEY" "API_KEY missing in app/config/firebase.ts"
|
|
746
749
|
assert_contains_literal "app/config/firebase.ts" "$AUTH_DOMAIN" "AUTH_DOMAIN missing in app/config/firebase.ts"
|
|
@@ -776,6 +779,7 @@ validate_generated_configs() {
|
|
|
776
779
|
"workers/user-worker/src/user-worker.ts"
|
|
777
780
|
"app/config/config.json"
|
|
778
781
|
"app/config/firebase.ts"
|
|
782
|
+
"app/routes/auth/login.tsx"
|
|
779
783
|
)
|
|
780
784
|
|
|
781
785
|
for file_path in "${files_to_scan[@]}"; do
|
|
@@ -862,6 +866,23 @@ copy_example_configs() {
|
|
|
862
866
|
|
|
863
867
|
echo -e "${GREEN} ✅ app: copied $copied_config_files config file(s) from config-example${NC}"
|
|
864
868
|
fi
|
|
869
|
+
|
|
870
|
+
# Copy auth route template files
|
|
871
|
+
echo -e "${YELLOW} Copying auth route template files...${NC}"
|
|
872
|
+
|
|
873
|
+
if [ -f "app/routes/auth/login.example.tsx" ] && { [ "$update_env" = "true" ] || [ ! -f "app/routes/auth/login.tsx" ]; }; then
|
|
874
|
+
cp app/routes/auth/login.example.tsx app/routes/auth/login.tsx
|
|
875
|
+
echo -e "${GREEN} ✅ auth: login.tsx created from example${NC}"
|
|
876
|
+
elif [ -f "app/routes/auth/login.tsx" ]; then
|
|
877
|
+
echo -e "${YELLOW} ⚠️ auth: login.tsx already exists, skipping copy${NC}"
|
|
878
|
+
fi
|
|
879
|
+
|
|
880
|
+
if [ -f "app/routes/auth/login.module.example.css" ] && { [ "$update_env" = "true" ] || [ ! -f "app/routes/auth/login.module.css" ]; }; then
|
|
881
|
+
cp app/routes/auth/login.module.example.css app/routes/auth/login.module.css
|
|
882
|
+
echo -e "${GREEN} ✅ auth: login.module.css created from example${NC}"
|
|
883
|
+
elif [ -f "app/routes/auth/login.module.css" ]; then
|
|
884
|
+
echo -e "${YELLOW} ⚠️ auth: login.module.css already exists, skipping copy${NC}"
|
|
885
|
+
fi
|
|
865
886
|
|
|
866
887
|
# Navigate to each worker directory and copy the example file
|
|
867
888
|
echo -e "${YELLOW} Copying worker configuration files...${NC}"
|
|
@@ -1450,6 +1471,12 @@ update_wrangler_configs() {
|
|
|
1450
1471
|
sed -i "s|\"YOUR_FIREBASE_MEASUREMENT_ID\"|\"$MEASUREMENT_ID\"|g" app/config/firebase.ts
|
|
1451
1472
|
echo -e "${GREEN} ✅ app firebase.ts updated${NC}"
|
|
1452
1473
|
fi
|
|
1474
|
+
|
|
1475
|
+
if [ -f "app/routes/auth/login.tsx" ]; then
|
|
1476
|
+
echo -e "${YELLOW} Updating app/routes/auth/login.tsx...${NC}"
|
|
1477
|
+
sed -i "s|^const APP_CANONICAL_ORIGIN = .*;|const APP_CANONICAL_ORIGIN = 'https://$escaped_pages_custom_domain';|g" app/routes/auth/login.tsx
|
|
1478
|
+
echo -e "${GREEN} ✅ app login.tsx canonical origin updated${NC}"
|
|
1479
|
+
fi
|
|
1453
1480
|
|
|
1454
1481
|
echo -e "${GREEN}✅ All configuration files updated${NC}"
|
|
1455
1482
|
}
|
|
@@ -178,6 +178,12 @@ deploy_pages_environment_secrets() {
|
|
|
178
178
|
set_pages_secret "API_TOKEN" "$optional_api_token" "$pages_env"
|
|
179
179
|
fi
|
|
180
180
|
|
|
181
|
+
local optional_primershear_emails
|
|
182
|
+
optional_primershear_emails=$(get_optional_value "PRIMERSHEAR_EMAILS")
|
|
183
|
+
if [ -n "$optional_primershear_emails" ]; then
|
|
184
|
+
set_pages_secret "PRIMERSHEAR_EMAILS" "$optional_primershear_emails" "$pages_env"
|
|
185
|
+
fi
|
|
186
|
+
|
|
181
187
|
echo -e "${GREEN}✅ Pages secrets deployed to $pages_env${NC}"
|
|
182
188
|
}
|
|
183
189
|
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# ============================================
|
|
4
|
+
# PRIMERSHEAR EMAIL LIST DEPLOYMENT SCRIPT
|
|
5
|
+
# ============================================
|
|
6
|
+
# Reads primershear.emails, updates PRIMERSHEAR_EMAILS in .env,
|
|
7
|
+
# then deploys that secret directly to Cloudflare Pages.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# bash ./scripts/deploy-primershear-emails.sh [--production-only|--preview-only|--env-only]
|
|
11
|
+
#
|
|
12
|
+
# Options:
|
|
13
|
+
# --production-only Deploy to production Pages environment only
|
|
14
|
+
# --preview-only Deploy to preview Pages environment only
|
|
15
|
+
# --env-only Update .env only; do not deploy to Cloudflare
|
|
16
|
+
# -h, --help Show this help message
|
|
17
|
+
|
|
18
|
+
set -e
|
|
19
|
+
set -o pipefail
|
|
20
|
+
|
|
21
|
+
RED='\033[0;31m'
|
|
22
|
+
GREEN='\033[0;32m'
|
|
23
|
+
YELLOW='\033[1;33m'
|
|
24
|
+
BLUE='\033[0;34m'
|
|
25
|
+
NC='\033[0m'
|
|
26
|
+
|
|
27
|
+
echo -e "${BLUE}📧 PrimerShear Email List Deployment${NC}"
|
|
28
|
+
echo "======================================"
|
|
29
|
+
|
|
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-primershear-emails.sh failed near line ${LINENO}${NC}"' ERR
|
|
35
|
+
|
|
36
|
+
# ── Argument parsing ─────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
deploy_production=true
|
|
39
|
+
deploy_preview=true
|
|
40
|
+
env_only=false
|
|
41
|
+
|
|
42
|
+
for arg in "$@"; do
|
|
43
|
+
case "$arg" in
|
|
44
|
+
-h|--help)
|
|
45
|
+
echo "Usage: bash ./scripts/deploy-primershear-emails.sh [--production-only|--preview-only|--env-only]"
|
|
46
|
+
echo ""
|
|
47
|
+
echo "Options:"
|
|
48
|
+
echo " --production-only Deploy to production Pages environment only"
|
|
49
|
+
echo " --preview-only Deploy to preview Pages environment only"
|
|
50
|
+
echo " --env-only Update .env only; do not deploy to Cloudflare"
|
|
51
|
+
echo " -h, --help Show this help message"
|
|
52
|
+
exit 0
|
|
53
|
+
;;
|
|
54
|
+
--production-only)
|
|
55
|
+
deploy_production=true
|
|
56
|
+
deploy_preview=false
|
|
57
|
+
;;
|
|
58
|
+
--preview-only)
|
|
59
|
+
deploy_production=false
|
|
60
|
+
deploy_preview=true
|
|
61
|
+
;;
|
|
62
|
+
--env-only)
|
|
63
|
+
env_only=true
|
|
64
|
+
;;
|
|
65
|
+
*)
|
|
66
|
+
echo -e "${RED}❌ Unknown option: $arg${NC}"
|
|
67
|
+
echo "Use --help to see supported options."
|
|
68
|
+
exit 1
|
|
69
|
+
;;
|
|
70
|
+
esac
|
|
71
|
+
done
|
|
72
|
+
|
|
73
|
+
# ── Read emails file ──────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
EMAILS_FILE="$PROJECT_ROOT/primershear.emails"
|
|
76
|
+
|
|
77
|
+
if [ ! -f "$EMAILS_FILE" ]; then
|
|
78
|
+
echo -e "${RED}❌ primershear.emails not found at: $EMAILS_FILE${NC}"
|
|
79
|
+
echo -e "${YELLOW} Create it with one email address per line.${NC}"
|
|
80
|
+
exit 1
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
# Strip comment lines and blank lines, then join with commas
|
|
84
|
+
PRIMERSHEAR_EMAILS=$(grep -v '^\s*#' "$EMAILS_FILE" | grep -v '^\s*$' | paste -sd ',' -)
|
|
85
|
+
|
|
86
|
+
if [ -z "$PRIMERSHEAR_EMAILS" ]; then
|
|
87
|
+
echo -e "${YELLOW}⚠️ primershear.emails contains no active email addresses.${NC}"
|
|
88
|
+
echo -e "${YELLOW} The secret will be set to an empty string, disabling the feature.${NC}"
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
EMAIL_COUNT=$(echo "$PRIMERSHEAR_EMAILS" | tr ',' '\n' | grep -c '[^[:space:]]' || true)
|
|
92
|
+
echo -e "${GREEN}✅ Loaded $EMAIL_COUNT email address(es) from primershear.emails${NC}"
|
|
93
|
+
|
|
94
|
+
# ── Update .env ───────────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
ENV_FILE="$PROJECT_ROOT/.env"
|
|
97
|
+
|
|
98
|
+
if [ ! -f "$ENV_FILE" ]; then
|
|
99
|
+
echo -e "${RED}❌ .env not found. Run deploy-config first.${NC}"
|
|
100
|
+
exit 1
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# Replace the PRIMERSHEAR_EMAILS= line in .env (handles both empty and populated values)
|
|
104
|
+
if grep -q '^PRIMERSHEAR_EMAILS=' "$ENV_FILE"; then
|
|
105
|
+
# Use a temp file to avoid sed -i portability issues across macOS/Linux
|
|
106
|
+
local_tmp=$(mktemp)
|
|
107
|
+
sed "s|^PRIMERSHEAR_EMAILS=.*|PRIMERSHEAR_EMAILS=${PRIMERSHEAR_EMAILS}|" "$ENV_FILE" > "$local_tmp"
|
|
108
|
+
mv "$local_tmp" "$ENV_FILE"
|
|
109
|
+
echo -e "${GREEN}✅ Updated PRIMERSHEAR_EMAILS in .env${NC}"
|
|
110
|
+
else
|
|
111
|
+
echo "" >> "$ENV_FILE"
|
|
112
|
+
echo "PRIMERSHEAR_EMAILS=${PRIMERSHEAR_EMAILS}" >> "$ENV_FILE"
|
|
113
|
+
echo -e "${GREEN}✅ Appended PRIMERSHEAR_EMAILS to .env${NC}"
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
if [ "$env_only" = "true" ]; then
|
|
117
|
+
echo -e "\n${GREEN}🎉 .env updated. Skipping Cloudflare deployment (--env-only).${NC}"
|
|
118
|
+
exit 0
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
# ── Deploy to Cloudflare Pages ────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
if ! command -v wrangler > /dev/null 2>&1; then
|
|
124
|
+
echo -e "${RED}❌ wrangler is not installed or not in PATH${NC}"
|
|
125
|
+
exit 1
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
source "$ENV_FILE"
|
|
129
|
+
|
|
130
|
+
PAGES_PROJECT_NAME=$(echo "$PAGES_PROJECT_NAME" | tr -d '\r')
|
|
131
|
+
if [ -z "$PAGES_PROJECT_NAME" ]; then
|
|
132
|
+
echo -e "${RED}❌ PAGES_PROJECT_NAME is missing from .env${NC}"
|
|
133
|
+
exit 1
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
set_secret() {
|
|
137
|
+
local pages_env=$1
|
|
138
|
+
echo -e "${YELLOW} Setting PRIMERSHEAR_EMAILS for $pages_env...${NC}"
|
|
139
|
+
if [ "$pages_env" = "production" ]; then
|
|
140
|
+
printf '%s' "$PRIMERSHEAR_EMAILS" | wrangler pages secret put PRIMERSHEAR_EMAILS \
|
|
141
|
+
--project-name "$PAGES_PROJECT_NAME"
|
|
142
|
+
else
|
|
143
|
+
printf '%s' "$PRIMERSHEAR_EMAILS" | wrangler pages secret put PRIMERSHEAR_EMAILS \
|
|
144
|
+
--project-name "$PAGES_PROJECT_NAME" --env "$pages_env"
|
|
145
|
+
fi
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if [ "$deploy_production" = "true" ]; then
|
|
149
|
+
set_secret "production"
|
|
150
|
+
echo -e "${GREEN}✅ PRIMERSHEAR_EMAILS deployed to production${NC}"
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
if [ "$deploy_preview" = "true" ]; then
|
|
154
|
+
set_secret "preview"
|
|
155
|
+
echo -e "${GREEN}✅ PRIMERSHEAR_EMAILS deployed to preview${NC}"
|
|
156
|
+
fi
|
|
157
|
+
|
|
158
|
+
# Deploy Pages so the new secret takes effect immediately
|
|
159
|
+
echo -e "\n${YELLOW}🚀 Building and deploying Pages to activate new secret...${NC}"
|
|
160
|
+
if ! npm run deploy; then
|
|
161
|
+
echo -e "${RED}❌ Pages deployment failed${NC}"
|
|
162
|
+
exit 1
|
|
163
|
+
fi
|
|
164
|
+
echo -e "${GREEN}✅ Pages deployment complete${NC}"
|
|
165
|
+
|
|
166
|
+
echo -e "\n${GREEN}🎉 PrimerShear email list deployment complete!${NC}"
|