@striae-org/striae 4.0.3 → 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 +19 -8
- 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-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
|
@@ -26,6 +26,7 @@ import { getUserData, createUser } from '~/utils/data';
|
|
|
26
26
|
import { auditService } from '~/services/audit';
|
|
27
27
|
import { generateUniqueId } from '~/utils/common';
|
|
28
28
|
import { evaluatePasswordPolicy, buildActionCodeSettings, userHasMFA } from '~/utils/auth';
|
|
29
|
+
import type { UserData } from '~/types';
|
|
29
30
|
|
|
30
31
|
const APP_CANONICAL_ORIGIN = 'PAGES_CUSTOM_DOMAIN';
|
|
31
32
|
const SOCIAL_IMAGE_PATH = '/social-image.png';
|
|
@@ -143,6 +144,7 @@ export const Login = () => {
|
|
|
143
144
|
const [error, setError] = useState('');
|
|
144
145
|
const [success, setSuccess] = useState('');
|
|
145
146
|
const [welcomeToastMessage, setWelcomeToastMessage] = useState('');
|
|
147
|
+
const [welcomeToastType, setWelcomeToastType] = useState<'success' | 'warning'>('success');
|
|
146
148
|
const [isWelcomeToastVisible, setIsWelcomeToastVisible] = useState(false);
|
|
147
149
|
const [isLogin, setIsLogin] = useState(true);
|
|
148
150
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -212,11 +214,9 @@ export const Login = () => {
|
|
|
212
214
|
};
|
|
213
215
|
|
|
214
216
|
// Check if user exists in the USER_DB using centralized function
|
|
215
|
-
const checkUserExists = async (currentUser: User): Promise<
|
|
217
|
+
const checkUserExists = async (currentUser: User): Promise<UserData | null> => {
|
|
216
218
|
try {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
return userData !== null;
|
|
219
|
+
return await getUserData(currentUser);
|
|
220
220
|
} catch (error) {
|
|
221
221
|
console.error('Error checking user existence:', error);
|
|
222
222
|
// On network/API errors, throw error to prevent login
|
|
@@ -251,16 +251,19 @@ export const Login = () => {
|
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
// Check if user exists in the USER_DB
|
|
254
|
+
let hasBadgeId = true;
|
|
254
255
|
setIsCheckingUser(true);
|
|
255
256
|
try {
|
|
256
|
-
const
|
|
257
|
+
const userData = await checkUserExists(currentUser);
|
|
257
258
|
setIsCheckingUser(false);
|
|
258
259
|
|
|
259
|
-
if (!
|
|
260
|
+
if (!userData) {
|
|
260
261
|
handleSignOut();
|
|
261
262
|
setError('This account does not exist or has been deleted');
|
|
262
263
|
return;
|
|
263
264
|
}
|
|
265
|
+
|
|
266
|
+
hasBadgeId = Boolean(userData.badgeId?.trim());
|
|
264
267
|
} catch (error) {
|
|
265
268
|
setIsCheckingUser(false);
|
|
266
269
|
handleSignOut();
|
|
@@ -279,7 +282,13 @@ export const Login = () => {
|
|
|
279
282
|
setShowMfaEnrollment(false);
|
|
280
283
|
|
|
281
284
|
if (shouldShowWelcomeToastRef.current) {
|
|
282
|
-
|
|
285
|
+
if (hasBadgeId) {
|
|
286
|
+
setWelcomeToastType('success');
|
|
287
|
+
setWelcomeToastMessage(`Welcome to Striae, ${getUserFirstName(currentUser)}!`);
|
|
288
|
+
} else {
|
|
289
|
+
setWelcomeToastType('warning');
|
|
290
|
+
setWelcomeToastMessage('Your badge or ID number is not set. You can set one in Manage Profile.');
|
|
291
|
+
}
|
|
283
292
|
setIsWelcomeToastVisible(true);
|
|
284
293
|
shouldShowWelcomeToastRef.current = false;
|
|
285
294
|
}
|
|
@@ -302,6 +311,7 @@ export const Login = () => {
|
|
|
302
311
|
setShowMfaEnrollment(false);
|
|
303
312
|
setIsCheckingUser(false);
|
|
304
313
|
setIsWelcomeToastVisible(false);
|
|
314
|
+
setWelcomeToastType('success');
|
|
305
315
|
shouldShowWelcomeToastRef.current = false;
|
|
306
316
|
}
|
|
307
317
|
});
|
|
@@ -517,6 +527,7 @@ export const Login = () => {
|
|
|
517
527
|
setShowMfaVerification(false);
|
|
518
528
|
setMfaResolver(null);
|
|
519
529
|
setIsWelcomeToastVisible(false);
|
|
530
|
+
setWelcomeToastType('success');
|
|
520
531
|
shouldShowWelcomeToastRef.current = false;
|
|
521
532
|
} catch (err) {
|
|
522
533
|
console.error('Sign out error:', err);
|
|
@@ -762,7 +773,7 @@ export const Login = () => {
|
|
|
762
773
|
{!shouldHandleEmailAction && (
|
|
763
774
|
<Toast
|
|
764
775
|
message={welcomeToastMessage}
|
|
765
|
-
type=
|
|
776
|
+
type={welcomeToastType}
|
|
766
777
|
isVisible={isWelcomeToastVisible}
|
|
767
778
|
onClose={() => setIsWelcomeToastVisible(false)}
|
|
768
779
|
/>
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
background-color: color-mix(in lab, var(--backgroundLight) 95%, transparent);
|
|
13
13
|
padding: var(--space2XL);
|
|
14
14
|
border-radius: var(--spaceXS);
|
|
15
|
-
box-shadow: 0 var(--spaceXS) var(--spaceM)
|
|
15
|
+
box-shadow: 0 var(--spaceXS) var(--spaceM)
|
|
16
|
+
color-mix(in lab, var(--black) 10%, transparent);
|
|
16
17
|
width: 100%;
|
|
17
18
|
max-width: var(--maxWidthS);
|
|
18
19
|
}
|
|
@@ -29,7 +30,7 @@
|
|
|
29
30
|
left: var(--spaceL);
|
|
30
31
|
width: 150px;
|
|
31
32
|
height: 150px;
|
|
32
|
-
background-image: url(
|
|
33
|
+
background-image: url("/logo-dark.png");
|
|
33
34
|
background-size: contain;
|
|
34
35
|
background-repeat: no-repeat;
|
|
35
36
|
background-position: center;
|
|
@@ -114,7 +115,8 @@
|
|
|
114
115
|
margin: var(--spaceXS) 0 var(--spaceS) 0;
|
|
115
116
|
font-size: var(--fontSizeBodyXS);
|
|
116
117
|
padding: var(--spaceS) var(--spaceM);
|
|
117
|
-
background: linear-gradient(
|
|
118
|
+
background: linear-gradient(
|
|
119
|
+
135deg,
|
|
118
120
|
color-mix(in lab, var(--error) 12%, transparent),
|
|
119
121
|
color-mix(in lab, var(--error) 8%, transparent)
|
|
120
122
|
);
|
|
@@ -128,7 +130,7 @@
|
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
.error::before {
|
|
131
|
-
content:
|
|
133
|
+
content: "";
|
|
132
134
|
position: absolute;
|
|
133
135
|
top: 0;
|
|
134
136
|
left: 0;
|
|
@@ -144,7 +146,8 @@
|
|
|
144
146
|
margin: var(--spaceXS) 0 var(--spaceS) 0;
|
|
145
147
|
font-size: var(--fontSizeBodyXS);
|
|
146
148
|
padding: var(--spaceS) var(--spaceM);
|
|
147
|
-
background: linear-gradient(
|
|
149
|
+
background: linear-gradient(
|
|
150
|
+
135deg,
|
|
148
151
|
color-mix(in lab, var(--success) 12%, transparent),
|
|
149
152
|
color-mix(in lab, var(--success) 8%, transparent)
|
|
150
153
|
);
|
|
@@ -158,7 +161,7 @@
|
|
|
158
161
|
}
|
|
159
162
|
|
|
160
163
|
.success::before {
|
|
161
|
-
content:
|
|
164
|
+
content: "";
|
|
162
165
|
position: absolute;
|
|
163
166
|
top: 0;
|
|
164
167
|
left: 0;
|
|
@@ -176,6 +179,7 @@
|
|
|
176
179
|
justify-content: center;
|
|
177
180
|
align-items: center;
|
|
178
181
|
z-index: var(--zIndex5);
|
|
182
|
+
cursor: default;
|
|
179
183
|
}
|
|
180
184
|
|
|
181
185
|
.modal {
|
|
@@ -183,9 +187,11 @@
|
|
|
183
187
|
border-radius: var(--spaceXS);
|
|
184
188
|
width: 90%;
|
|
185
189
|
max-width: 500px;
|
|
186
|
-
box-shadow: 0 var(--spaceXS) var(--spaceL)
|
|
190
|
+
box-shadow: 0 var(--spaceXS) var(--spaceL)
|
|
191
|
+
color-mix(in lab, var(--black) 10%, transparent);
|
|
187
192
|
overflow: hidden;
|
|
188
193
|
padding: var(--space2XL);
|
|
194
|
+
cursor: default;
|
|
189
195
|
}
|
|
190
196
|
|
|
191
197
|
.modalHeader {
|
|
@@ -220,7 +226,8 @@
|
|
|
220
226
|
}
|
|
221
227
|
|
|
222
228
|
@keyframes errorPulse {
|
|
223
|
-
0%,
|
|
229
|
+
0%,
|
|
230
|
+
100% {
|
|
224
231
|
opacity: 1;
|
|
225
232
|
box-shadow: 0 0 0 0 color-mix(in lab, var(--error) 40%, transparent);
|
|
226
233
|
}
|
|
@@ -252,7 +259,8 @@
|
|
|
252
259
|
}
|
|
253
260
|
|
|
254
261
|
@keyframes successPulse {
|
|
255
|
-
0%,
|
|
262
|
+
0%,
|
|
263
|
+
100% {
|
|
256
264
|
opacity: 1;
|
|
257
265
|
box-shadow: 0 0 0 0 color-mix(in lab, var(--success) 40%, transparent);
|
|
258
266
|
}
|
|
@@ -264,11 +272,13 @@
|
|
|
264
272
|
|
|
265
273
|
/* Reduce motion for accessibility */
|
|
266
274
|
@media (prefers-reduced-motion: reduce) {
|
|
267
|
-
.error,
|
|
275
|
+
.error,
|
|
276
|
+
.success {
|
|
268
277
|
animation: none;
|
|
269
278
|
}
|
|
270
|
-
|
|
271
|
-
.error::before,
|
|
279
|
+
|
|
280
|
+
.error::before,
|
|
281
|
+
.success::before {
|
|
272
282
|
animation: none;
|
|
273
283
|
}
|
|
274
|
-
}
|
|
284
|
+
}
|
|
@@ -28,6 +28,8 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
28
28
|
// User states
|
|
29
29
|
const [userCompany, setUserCompany] = useState<string>('');
|
|
30
30
|
const [userFirstName, setUserFirstName] = useState<string>('');
|
|
31
|
+
const [userLastName, setUserLastName] = useState<string>('');
|
|
32
|
+
const [userBadgeId, setUserBadgeId] = useState<string>('');
|
|
31
33
|
|
|
32
34
|
// Case management states - All managed here
|
|
33
35
|
const [currentCase, setCurrentCase] = useState<string>('');
|
|
@@ -80,9 +82,11 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
80
82
|
});
|
|
81
83
|
|
|
82
84
|
if (response.ok) {
|
|
83
|
-
const userData = await response.json() as { company?: string; firstName?: string };
|
|
85
|
+
const userData = await response.json() as { company?: string; firstName?: string; lastName?: string; badgeId?: string };
|
|
84
86
|
setUserCompany(userData.company || '');
|
|
85
87
|
setUserFirstName(userData.firstName || '');
|
|
88
|
+
setUserLastName(userData.lastName || '');
|
|
89
|
+
setUserBadgeId(userData.badgeId || '');
|
|
86
90
|
}
|
|
87
91
|
} catch (err) {
|
|
88
92
|
console.error('Failed to load user company:', err);
|
|
@@ -167,6 +171,8 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
167
171
|
selectedFilename,
|
|
168
172
|
userCompany,
|
|
169
173
|
userFirstName,
|
|
174
|
+
userLastName,
|
|
175
|
+
userBadgeId,
|
|
170
176
|
currentCase,
|
|
171
177
|
annotationData,
|
|
172
178
|
activeAnnotations,
|
|
@@ -389,6 +395,7 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
389
395
|
imageUrl={selectedImage}
|
|
390
396
|
filename={selectedFilename}
|
|
391
397
|
company={userCompany}
|
|
398
|
+
badgeId={userBadgeId}
|
|
392
399
|
firstName={userFirstName}
|
|
393
400
|
error={error ?? ''}
|
|
394
401
|
activeAnnotations={activeAnnotations}
|
package/app/routes.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type RouteConfig, index, route } from "@react-router/dev/routes";
|
|
2
|
+
|
|
3
|
+
export default [
|
|
4
|
+
index("routes/_index.tsx"),
|
|
5
|
+
route("auth", "routes/auth/route.ts"),
|
|
6
|
+
route("auth/login", "routes/auth/route.ts", { id: "routes/auth/login-alias" }),
|
|
7
|
+
] satisfies RouteConfig;
|
|
@@ -30,6 +30,7 @@ export const AUDIT_CSV_ENTRY_HEADERS = [
|
|
|
30
30
|
'Profile Field',
|
|
31
31
|
'Old Value',
|
|
32
32
|
'New Value',
|
|
33
|
+
'Badge/ID',
|
|
33
34
|
'Total Confirmations In File',
|
|
34
35
|
'Confirmations Successfully Imported',
|
|
35
36
|
'Validation Steps Failed',
|
|
@@ -112,6 +113,7 @@ export const entryToCSVRow = (entry: ValidationAuditEntry): string => {
|
|
|
112
113
|
formatForCSV(userProfileDetails?.profileField),
|
|
113
114
|
formatForCSV(userProfileDetails?.oldValue),
|
|
114
115
|
formatForCSV(userProfileDetails?.newValue),
|
|
116
|
+
formatForCSV(userProfileDetails?.badgeId),
|
|
115
117
|
caseDetails?.totalAnnotations?.toString() || '',
|
|
116
118
|
performanceMetrics?.validationStepsCompleted?.toString() || '',
|
|
117
119
|
performanceMetrics?.validationStepsFailed?.toString() || '',
|
|
@@ -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",
|
|
@@ -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
|
|