@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.
Files changed (82) hide show
  1. package/.env.example +1 -1
  2. package/app/components/actions/case-export/core-export.ts +5 -2
  3. package/app/components/actions/case-export/download-handlers.ts +51 -3
  4. package/app/components/actions/case-import/confirmation-import.ts +65 -40
  5. package/app/components/actions/case-import/confirmation-package.ts +86 -0
  6. package/app/components/actions/case-import/image-operations.ts +20 -49
  7. package/app/components/actions/case-import/index.ts +1 -0
  8. package/app/components/actions/case-import/orchestrator.ts +13 -3
  9. package/app/components/actions/case-import/storage-operations.ts +54 -89
  10. package/app/components/actions/case-import/validation.ts +7 -111
  11. package/app/components/actions/case-import/zip-processing.ts +44 -2
  12. package/app/components/actions/case-manage.ts +15 -27
  13. package/app/components/actions/confirm-export.ts +44 -13
  14. package/app/components/actions/generate-pdf.ts +3 -7
  15. package/app/components/actions/image-manage.ts +63 -129
  16. package/app/components/button/button.module.css +12 -8
  17. package/app/components/form/form-button.tsx +1 -1
  18. package/app/components/form/form.module.css +9 -0
  19. package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +163 -49
  20. package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +365 -88
  21. package/app/components/sidebar/case-export/case-export.tsx +13 -60
  22. package/app/components/sidebar/case-import/case-import.tsx +18 -6
  23. package/app/components/sidebar/case-import/hooks/useFilePreview.ts +6 -4
  24. package/app/components/sidebar/case-import/utils/file-validation.ts +57 -2
  25. package/app/components/sidebar/cases/case-sidebar.tsx +122 -52
  26. package/app/components/sidebar/cases/cases.module.css +101 -18
  27. package/app/components/sidebar/notes/notes.module.css +33 -13
  28. package/app/components/sidebar/sidebar.module.css +0 -2
  29. package/app/components/user/delete-account.tsx +7 -7
  30. package/app/components/user/manage-profile.tsx +1 -1
  31. package/app/components/user/mfa-phone-update.tsx +15 -12
  32. package/app/config-example/config.json +2 -8
  33. package/app/hooks/useInactivityTimeout.ts +2 -5
  34. package/app/root.tsx +96 -65
  35. package/app/routes/auth/login.tsx +132 -11
  36. package/app/routes/auth/route.ts +4 -3
  37. package/app/routes/striae/striae.tsx +4 -8
  38. package/app/services/audit/audit-api-client.ts +40 -0
  39. package/app/services/audit/audit-worker-client.ts +14 -17
  40. package/app/styles/root.module.css +13 -101
  41. package/app/tailwind.css +9 -2
  42. package/app/utils/SHA256.ts +5 -1
  43. package/app/utils/auth.ts +5 -32
  44. package/app/utils/confirmation-signature.ts +5 -1
  45. package/app/utils/data-api-client.ts +43 -0
  46. package/app/utils/data-operations.ts +59 -75
  47. package/app/utils/export-verification.ts +353 -0
  48. package/app/utils/image-api-client.ts +130 -0
  49. package/app/utils/pdf-api-client.ts +43 -0
  50. package/app/utils/permissions.ts +10 -23
  51. package/app/utils/signature-utils.ts +74 -4
  52. package/app/utils/user-api-client.ts +90 -0
  53. package/functions/api/_shared/firebase-auth.ts +255 -0
  54. package/functions/api/audit/[[path]].ts +150 -0
  55. package/functions/api/data/[[path]].ts +141 -0
  56. package/functions/api/image/[[path]].ts +127 -0
  57. package/functions/api/pdf/[[path]].ts +110 -0
  58. package/functions/api/user/[[path]].ts +196 -0
  59. package/package.json +8 -4
  60. package/public/favicon.ico +0 -0
  61. package/public/icon-256.png +0 -0
  62. package/public/icon-512.png +0 -0
  63. package/public/manifest.json +39 -0
  64. package/public/shortcut.png +0 -0
  65. package/public/social-image.png +0 -0
  66. package/react-router.config.ts +5 -0
  67. package/scripts/deploy-all.sh +22 -8
  68. package/scripts/deploy-config.sh +143 -148
  69. package/scripts/deploy-pages-secrets.sh +231 -0
  70. package/scripts/deploy-worker-secrets.sh +1 -1
  71. package/workers/audit-worker/wrangler.jsonc.example +1 -8
  72. package/workers/data-worker/wrangler.jsonc.example +1 -8
  73. package/workers/image-worker/wrangler.jsonc.example +1 -8
  74. package/workers/keys-worker/wrangler.jsonc.example +2 -9
  75. package/workers/pdf-worker/scripts/generate-assets.js +94 -0
  76. package/workers/pdf-worker/src/assets/icon-256.png +0 -0
  77. package/workers/pdf-worker/wrangler.jsonc.example +1 -8
  78. package/workers/user-worker/src/user-worker.example.ts +121 -41
  79. package/workers/user-worker/wrangler.jsonc.example +1 -8
  80. package/wrangler.toml.example +1 -1
  81. package/app/styles/legal-pages.module.css +0 -113
  82. package/public/favicon.svg +0 -9
@@ -10,12 +10,12 @@ hr {
10
10
  }
11
11
 
12
12
  .section {
13
- margin-bottom: 2rem;
13
+ margin-bottom: 2rem;
14
14
  }
15
15
 
16
16
  .sectionTitle {
17
17
  font-size: 1.3rem;
18
- font-weight: 600;
18
+ font-weight: 600;
19
19
  color: #495057;
20
20
  margin-bottom: 1rem;
21
21
  }
@@ -50,7 +50,7 @@ input[type="color"]:focus,
50
50
  textarea:focus {
51
51
  outline: none;
52
52
  border-color: #0d6efd;
53
- box-shadow: 0 0 0 2px rgba(13,110,253,.25);
53
+ box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.25);
54
54
  }
55
55
 
56
56
  .caseNumbers {
@@ -74,10 +74,14 @@ textarea:focus {
74
74
  box-sizing: border-box;
75
75
  }
76
76
 
77
+ .caseInput input:not(:disabled) {
78
+ background-color: #ffffff;
79
+ }
80
+
77
81
  .caseInput input:focus {
78
82
  outline: none;
79
83
  border-color: #0d6efd;
80
- box-shadow: 0 0 0 2px rgba(13,110,253,.25);
84
+ box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.25);
81
85
  }
82
86
 
83
87
  .checkboxLabel {
@@ -108,6 +112,11 @@ textarea:focus {
108
112
  transition: border-color 0.2s;
109
113
  }
110
114
 
115
+ .classCharacteristics input:not(:disabled),
116
+ .classCharacteristics textarea:not(:disabled) {
117
+ background-color: #ffffff;
118
+ }
119
+
111
120
  .classCharacteristics select,
112
121
  .support select {
113
122
  width: 100%;
@@ -124,7 +133,7 @@ textarea:focus {
124
133
  .support select:focus {
125
134
  outline: none;
126
135
  border-color: #0d6efd;
127
- box-shadow: 0 0 0 2px rgba(13,110,253,.25);
136
+ box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.25);
128
137
  }
129
138
 
130
139
  .classCharacteristics textarea {
@@ -138,11 +147,14 @@ textarea:focus {
138
147
  box-sizing: border-box;
139
148
  }
140
149
 
150
+ .classCharacteristics input + textarea {
151
+ margin-top: 1rem;
152
+ }
153
+
141
154
  .classCharacteristics textarea:focus {
142
155
  outline: none;
143
156
  border-color: #0d6efd;
144
- box-shadow: 0 0 0 2px rgba(13,110,253,.25);
145
- margin-top: 1rem;
157
+ box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.25);
146
158
  resize: vertical;
147
159
  }
148
160
 
@@ -188,13 +200,17 @@ textarea:focus {
188
200
  box-sizing: border-box;
189
201
  }
190
202
 
203
+ .indexing input[type="text"]:not(:disabled) {
204
+ background-color: #ffffff;
205
+ }
206
+
191
207
  .indexing input[type="text"]:focus {
192
208
  outline: none;
193
209
  border-color: #0d6efd;
194
- box-shadow: 0 0 0 2px rgba(13,110,253,.25);
210
+ box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.25);
195
211
  }
196
212
 
197
- .confirmation {
213
+ .confirmation {
198
214
  padding: 0.75rem;
199
215
  background-color: #f8f9fa;
200
216
  border-radius: 6px;
@@ -232,7 +248,7 @@ textarea:focus {
232
248
  }
233
249
 
234
250
  .notesButton:hover {
235
- background-color: color-mix(in lab, var(--primary) 95%, transparent);
251
+ background-color: color-mix(in lab, var(--primary) 95%, transparent);
236
252
  }
237
253
 
238
254
  .modalOverlay {
@@ -355,6 +371,10 @@ textarea:focus {
355
371
  }
356
372
 
357
373
  @keyframes fadeIn {
358
- from { opacity: 0; }
359
- to { opacity: 1; }
360
- }
374
+ from {
375
+ opacity: 0;
376
+ }
377
+ to {
378
+ opacity: 1;
379
+ }
380
+ }
@@ -109,7 +109,6 @@
109
109
  .footerSectionButton:hover {
110
110
  background-color: #5c636a;
111
111
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
112
- transform: translateY(-1px);
113
112
  }
114
113
 
115
114
  /* Footer Modal */
@@ -163,7 +162,6 @@
163
162
 
164
163
  .footerModalClose:hover {
165
164
  background-color: #f8f9fa;
166
- transform: translateY(-1px);
167
165
  }
168
166
 
169
167
  .footerModalContent {
@@ -1,8 +1,7 @@
1
1
  import { useState, useEffect } from 'react';
2
2
  import { signOut } from 'firebase/auth';
3
3
  import { auth } from '~/services/firebase';
4
- import paths from '~/config/config.json';
5
- import { getUserApiKey } from '~/utils/auth';
4
+ import { fetchUserApi } from '~/utils/user-api-client';
6
5
  import { auditService } from '~/services/audit';
7
6
  import styles from './delete-account.module.css';
8
7
 
@@ -220,14 +219,15 @@ export const DeleteAccount = ({ isOpen, onClose, user, company }: DeleteAccountP
220
219
  false // emailNotificationSent - deletion emails disabled
221
220
  );
222
221
 
223
- // Get API key for user-worker authentication
224
- const apiKey = await getUserApiKey();
222
+ const currentUser = auth.currentUser;
223
+ if (!currentUser || currentUser.uid !== user.uid) {
224
+ throw new Error('User session mismatch. Please sign in again.');
225
+ }
225
226
 
226
- // Delete the user account via user-worker
227
- const deleteResponse = await fetch(`${paths.user_worker_url}/${user.uid}?stream=true`, {
227
+ // Delete the user account via user proxy
228
+ const deleteResponse = await fetchUserApi(currentUser, `/${encodeURIComponent(user.uid)}?stream=true`, {
228
229
  method: 'DELETE',
229
230
  headers: {
230
- 'X-Custom-Auth-Key': apiKey,
231
231
  'Accept': 'text/event-stream'
232
232
  }
233
233
  });
@@ -234,7 +234,7 @@ export const ManageProfile = ({ isOpen, onClose }: ManageProfileProps) => {
234
234
  <FormButton variant="primary" type="submit" isLoading={isLoading} loadingText="Updating...">
235
235
  Update Profile
236
236
  </FormButton>
237
- <FormButton variant="secondary" type="button" onClick={() => setShowAuditViewer(true)}>
237
+ <FormButton variant="audit" type="button" onClick={() => setShowAuditViewer(true)}>
238
238
  View My Audit Trail
239
239
  </FormButton>
240
240
  <FormButton variant="secondary" type="button" onClick={() => setShowResetForm(true)}>
@@ -58,6 +58,7 @@ export const MfaPhoneUpdateSection = ({
58
58
  const [recaptchaVerifier, setRecaptchaVerifier] = useState<RecaptchaVerifier | null>(null);
59
59
 
60
60
  const isMfaBusy = isMfaLoading || isMfaReauthLoading;
61
+ const hasMfaPhoneInput = mfaPhoneInput.trim().length > 0;
61
62
 
62
63
  const resetMfaReauthFlow = useCallback(() => {
63
64
  setShowMfaReauthPrompt(false);
@@ -665,18 +666,20 @@ export const MfaPhoneUpdateSection = ({
665
666
  )}
666
667
  </div>
667
668
  ) : !isMfaCodeSent ? (
668
- <div className={styles.mfaButtonGroup}>
669
- <FormButton
670
- variant="secondary"
671
- type="button"
672
- onClick={handleSendMfaVerificationCode}
673
- isLoading={isMfaLoading}
674
- loadingText="Sending Code..."
675
- disabled={!mfaPhoneInput.trim()}
676
- >
677
- Send Verification Code
678
- </FormButton>
679
- </div>
669
+ hasMfaPhoneInput ? (
670
+ <div className={styles.mfaButtonGroup}>
671
+ <FormButton
672
+ variant="secondary"
673
+ type="button"
674
+ onClick={handleSendMfaVerificationCode}
675
+ isLoading={isMfaLoading}
676
+ loadingText="Sending Code..."
677
+ disabled={!hasMfaPhoneInput}
678
+ >
679
+ Send Verification Code
680
+ </FormButton>
681
+ </div>
682
+ ) : null
680
683
  ) : (
681
684
  <div className={styles.mfaVerificationSection}>
682
685
  <input
@@ -1,12 +1,6 @@
1
1
  {
2
- "url": "PAGES_CUSTOM_DOMAIN",
3
- "data_worker_url": "DATA_WORKER_CUSTOM_DOMAIN",
4
- "keys_url": "KEYS_WORKER_CUSTOM_DOMAIN",
5
- "image_worker_url": "IMAGE_WORKER_CUSTOM_DOMAIN",
6
- "user_worker_url": "USER_WORKER_CUSTOM_DOMAIN",
7
- "pdf_worker_url": "PDF_WORKER_CUSTOM_DOMAIN",
8
- "audit_worker_url": "AUDIT_WORKER_CUSTOM_DOMAIN",
9
- "keys_auth": "YOUR_KEYS_AUTH_TOKEN",
2
+ "url": "PAGES_CUSTOM_DOMAIN",
3
+ "account_hash": "ACCOUNT_HASH",
10
4
  "manifest_signing_key_id": "MANIFEST_SIGNING_KEY_ID",
11
5
  "manifest_signing_public_key": "MANIFEST_SIGNING_PUBLIC_KEY",
12
6
  "manifest_signing_public_keys": {
@@ -1,5 +1,4 @@
1
1
  import { useEffect, useRef, useCallback } from 'react';
2
- import { useLocation } from 'react-router';
3
2
  import { signOut } from 'firebase/auth';
4
3
  import { auth } from '~/services/firebase';
5
4
  import { INACTIVITY_CONFIG } from '~/config/inactivity';
@@ -19,13 +18,11 @@ export const useInactivityTimeout = ({
19
18
  onTimeout,
20
19
  enabled = true
21
20
  }: UseInactivityTimeoutOptions = {}) => {
22
- const location = useLocation();
23
21
  const timeoutRef = useRef<NodeJS.Timeout | null>(null);
24
22
  const warningTimeoutRef = useRef<NodeJS.Timeout | null>(null);
25
23
  const lastActivityRef = useRef<number>(0);
26
24
 
27
- const isAuthRoute = location.pathname.startsWith('/auth');
28
- const shouldEnable = enabled && isAuthRoute;
25
+ const shouldEnable = enabled;
29
26
 
30
27
  useEffect(() => {
31
28
  lastActivityRef.current = Date.now();
@@ -104,7 +101,7 @@ export const useInactivityTimeout = ({
104
101
  });
105
102
  clearTimeouts();
106
103
  };
107
- }, [shouldEnable, resetTimer, clearTimeouts, location.pathname]);
104
+ }, [shouldEnable, resetTimer, clearTimeouts]);
108
105
 
109
106
  return {
110
107
  extendSession,
package/app/root.tsx CHANGED
@@ -7,8 +7,6 @@ import {
7
7
  ScrollRestoration,
8
8
  isRouteErrorResponse,
9
9
  useRouteError,
10
- Link,
11
- useLocation,
12
10
  useMatches,
13
11
  } from 'react-router';
14
12
  import {
@@ -16,6 +14,7 @@ import {
16
14
  themeStyles
17
15
  } from '~/components/theme-provider/theme-provider';
18
16
  import { AuthProvider } from '~/components/auth/auth-provider';
17
+ import { auth } from '~/services/firebase';
19
18
  import styles from '~/styles/root.module.css';
20
19
  import './tailwind.css';
21
20
 
@@ -30,8 +29,8 @@ export const links: LinksFunction = () => [
30
29
  rel: "stylesheet",
31
30
  href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
32
31
  },
32
+ { rel: 'manifest', href: '/manifest.json' },
33
33
  { rel: 'icon', href: '/favicon.ico' },
34
- { rel: 'icon', href: '/favicon.svg', type: 'image/svg+xml' },
35
34
  ];
36
35
 
37
36
  type AppTheme = 'dark' | 'light';
@@ -61,7 +60,7 @@ const resolveRouteTheme = (matches: ReturnType<typeof useMatches>): AppTheme =>
61
60
  export function Layout({ children }: { children: React.ReactNode }) {
62
61
  const matches = useMatches();
63
62
  const theme = resolveRouteTheme(matches);
64
- const themeColor = theme === 'dark' ? '#000000' : '#f5f5f5';
63
+ const themeColor = theme === 'dark' ? '#000000' : '#377087';
65
64
 
66
65
  return (
67
66
  <html lang="en" data-theme={theme}>
@@ -88,80 +87,112 @@ export function Layout({ children }: { children: React.ReactNode }) {
88
87
  }
89
88
 
90
89
  export default function App() {
91
- const matches = useMatches();
92
- const location = useLocation();
93
- const isAuthRoute = matches.some(match =>
94
- match.id.includes('auth') ||
95
- match.pathname?.includes('/auth')
96
- ) || location.pathname === '/';
90
+ return (
91
+ <AuthProvider>
92
+ <Outlet />
93
+ </AuthProvider>
94
+ );
95
+ }
97
96
 
98
- if (isAuthRoute) {
99
- return (
100
- <AuthProvider>
101
- <Outlet />
102
- </AuthProvider>
103
- );
97
+ interface ErrorBoundaryShellProps {
98
+ title: string;
99
+ children: React.ReactNode;
100
+ }
101
+
102
+ const LOGIN_REDIRECT_PATH = '/';
103
+
104
+ const errorActionStyle = {
105
+ alignItems: 'center',
106
+ appearance: 'none',
107
+ backgroundColor: '#0d6efd',
108
+ border: '1px solid #0b5ed7',
109
+ borderRadius: '8px',
110
+ color: '#ffffff',
111
+ cursor: 'pointer',
112
+ display: 'inline-flex',
113
+ fontSize: '1rem',
114
+ fontWeight: 600,
115
+ justifyContent: 'center',
116
+ lineHeight: 1,
117
+ marginTop: '1rem',
118
+ minWidth: '220px',
119
+ padding: '0.9rem 1.6rem',
120
+ textDecoration: 'none',
121
+ } as const;
122
+
123
+ async function returnToLogin() {
124
+ try {
125
+ await auth.signOut();
126
+ } catch (error) {
127
+ console.error('Error boundary sign out failed:', error);
128
+ } finally {
129
+ if (typeof window !== 'undefined') {
130
+ localStorage.clear();
131
+ window.location.href = LOGIN_REDIRECT_PATH;
132
+ }
104
133
  }
134
+ }
105
135
 
106
- return <Outlet />;
136
+ function ErrorBoundaryShell({ title, children }: ErrorBoundaryShellProps) {
137
+ return (
138
+ <html lang="en" data-theme="light">
139
+ <head>
140
+ <meta charSet="utf-8" />
141
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
142
+ <meta name="theme-color" content="#377087" />
143
+ <meta name="color-scheme" content="light" />
144
+ <style dangerouslySetInnerHTML={{ __html: themeStyles }} />
145
+ <title>{title}</title>
146
+ <Meta />
147
+ <Links />
148
+ </head>
149
+ <body className="flex flex-col h-screen w-full overflow-x-hidden">
150
+ <ThemeProvider theme="light" className="">
151
+ <main>{children}</main>
152
+ </ThemeProvider>
153
+ <ScrollRestoration />
154
+ <Scripts />
155
+ </body>
156
+ </html>
157
+ );
107
158
  }
108
159
 
109
160
  export function ErrorBoundary() {
110
161
  const error = useRouteError();
111
162
 
112
163
  if (isRouteErrorResponse(error)) {
164
+ const statusText = error.statusText || 'Unexpected error';
165
+
113
166
  return (
114
- <html lang="en">
115
- <head>
116
- <title>{`${error.status} ${error.statusText}`}</title>
117
- </head>
118
- <body className="flex flex-col h-screen">
119
- <ThemeProvider theme="light" className="">
120
- <main>
121
- <div className={styles.errorContainer}>
122
- <div className={styles.errorTitle}>{error.status}</div>
123
- <p className={styles.errorMessage}>{error.statusText}</p>
124
- <Link
125
- viewTransition
126
- prefetch="intent"
127
- to="https://striae.org"
128
- className={styles.errorLink}>
129
- Return Home
130
- </Link>
131
- </div>
132
- </main>
133
- </ThemeProvider>
134
- <ScrollRestoration />
135
- <Scripts />
136
- </body>
137
- </html>
167
+ <ErrorBoundaryShell title={`${error.status} ${statusText}`}>
168
+ <div className={styles.errorContainer}>
169
+ <div className={styles.errorTitle}>{error.status}</div>
170
+ <p className={styles.errorMessage}>{statusText}</p>
171
+ <button
172
+ type="button"
173
+ onClick={() => void returnToLogin()}
174
+ style={errorActionStyle}
175
+ className={styles.errorLink}>
176
+ Return to Login
177
+ </button>
178
+ </div>
179
+ </ErrorBoundaryShell>
138
180
  );
139
181
  }
140
182
 
141
183
  return (
142
- <html lang="en">
143
- <head>
144
- <title>Oops! Something went wrong</title>
145
- </head>
146
- <body className="flex flex-col h-screen">
147
- <ThemeProvider theme="light" className="">
148
- <main>
149
- <div className={styles.errorContainer}>
150
- <div className={styles.errorTitle}>500</div>
151
- <p className={styles.errorMessage}>Something went wrong. Please try again later.</p>
152
- <Link
153
- viewTransition
154
- prefetch="intent"
155
- to="https://striae.org"
156
- className={styles.errorLink}>
157
- Return Home
158
- </Link>
159
- </div>
160
- </main>
161
- </ThemeProvider>
162
- <ScrollRestoration />
163
- <Scripts />
164
- </body>
165
- </html>
184
+ <ErrorBoundaryShell title="Oops! Something went wrong">
185
+ <div className={styles.errorContainer}>
186
+ <div className={styles.errorTitle}>500</div>
187
+ <p className={styles.errorMessage}>Something went wrong. Please try again later.</p>
188
+ <button
189
+ type="button"
190
+ onClick={() => void returnToLogin()}
191
+ style={errorActionStyle}
192
+ className={styles.errorLink}>
193
+ Return to Login
194
+ </button>
195
+ </div>
196
+ </ErrorBoundaryShell>
166
197
  );
167
198
  }