@striae-org/striae 3.2.2 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +1 -1
- package/app/components/actions/case-export/core-export.ts +5 -2
- package/app/components/actions/case-export/download-handlers.ts +51 -3
- package/app/components/actions/case-import/confirmation-import.ts +65 -40
- package/app/components/actions/case-import/confirmation-package.ts +86 -0
- package/app/components/actions/case-import/image-operations.ts +20 -49
- package/app/components/actions/case-import/index.ts +1 -0
- package/app/components/actions/case-import/orchestrator.ts +13 -3
- package/app/components/actions/case-import/storage-operations.ts +54 -89
- package/app/components/actions/case-import/validation.ts +7 -111
- package/app/components/actions/case-import/zip-processing.ts +44 -2
- package/app/components/actions/case-manage.ts +15 -27
- package/app/components/actions/confirm-export.ts +44 -13
- package/app/components/actions/generate-pdf.ts +3 -7
- package/app/components/actions/image-manage.ts +63 -129
- package/app/components/button/button.module.css +12 -8
- package/app/components/form/form-button.tsx +1 -1
- package/app/components/form/form.module.css +9 -0
- package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +163 -49
- package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +365 -88
- package/app/components/sidebar/case-export/case-export.tsx +13 -60
- package/app/components/sidebar/case-import/case-import.tsx +18 -6
- package/app/components/sidebar/case-import/hooks/useFilePreview.ts +6 -4
- package/app/components/sidebar/case-import/utils/file-validation.ts +57 -2
- package/app/components/sidebar/cases/case-sidebar.tsx +122 -52
- package/app/components/sidebar/cases/cases.module.css +101 -18
- package/app/components/sidebar/notes/notes.module.css +33 -13
- package/app/components/sidebar/sidebar.module.css +0 -2
- package/app/components/user/delete-account.tsx +7 -7
- package/app/components/user/manage-profile.tsx +1 -1
- package/app/components/user/mfa-phone-update.tsx +15 -12
- package/app/config-example/config.json +2 -8
- package/app/hooks/useInactivityTimeout.ts +2 -5
- package/app/root.tsx +96 -65
- package/app/routes/auth/login.tsx +132 -11
- package/app/routes/auth/route.ts +4 -3
- package/app/routes/striae/striae.tsx +4 -8
- package/app/services/audit/audit-api-client.ts +40 -0
- package/app/services/audit/audit-worker-client.ts +14 -17
- package/app/styles/root.module.css +13 -101
- package/app/tailwind.css +9 -2
- package/app/utils/SHA256.ts +5 -1
- package/app/utils/auth.ts +5 -32
- package/app/utils/confirmation-signature.ts +5 -1
- package/app/utils/data-api-client.ts +43 -0
- package/app/utils/data-operations.ts +59 -75
- package/app/utils/export-verification.ts +353 -0
- package/app/utils/image-api-client.ts +130 -0
- package/app/utils/pdf-api-client.ts +43 -0
- package/app/utils/permissions.ts +10 -23
- package/app/utils/signature-utils.ts +74 -4
- package/app/utils/user-api-client.ts +90 -0
- package/functions/api/_shared/firebase-auth.ts +255 -0
- package/functions/api/audit/[[path]].ts +150 -0
- package/functions/api/data/[[path]].ts +141 -0
- package/functions/api/image/[[path]].ts +127 -0
- package/functions/api/pdf/[[path]].ts +110 -0
- package/functions/api/user/[[path]].ts +196 -0
- package/package.json +8 -4
- package/public/favicon.ico +0 -0
- package/public/icon-256.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/manifest.json +39 -0
- package/public/shortcut.png +0 -0
- package/public/social-image.png +0 -0
- package/react-router.config.ts +5 -0
- package/scripts/deploy-all.sh +22 -8
- package/scripts/deploy-config.sh +143 -148
- package/scripts/deploy-pages-secrets.sh +231 -0
- package/scripts/deploy-worker-secrets.sh +1 -1
- package/workers/audit-worker/wrangler.jsonc.example +1 -8
- package/workers/data-worker/wrangler.jsonc.example +1 -8
- package/workers/image-worker/wrangler.jsonc.example +1 -8
- package/workers/keys-worker/wrangler.jsonc.example +2 -9
- package/workers/pdf-worker/scripts/generate-assets.js +94 -0
- package/workers/pdf-worker/src/assets/icon-256.png +0 -0
- package/workers/pdf-worker/wrangler.jsonc.example +1 -8
- package/workers/user-worker/src/user-worker.example.ts +121 -41
- package/workers/user-worker/wrangler.jsonc.example +1 -8
- package/wrangler.toml.example +1 -1
- package/app/styles/legal-pages.module.css +0 -113
- package/public/favicon.svg +0 -9
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { User } from 'firebase/auth';
|
|
2
|
+
import { type ImageUploadResponse } from '~/types';
|
|
3
|
+
|
|
4
|
+
const IMAGE_API_BASE = '/api/image';
|
|
5
|
+
|
|
6
|
+
function normalizePath(path: string): string {
|
|
7
|
+
if (!path) {
|
|
8
|
+
return '/';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return path.startsWith('/') ? path : `/${path}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function fetchImageApi(
|
|
15
|
+
user: User,
|
|
16
|
+
path: string,
|
|
17
|
+
init: RequestInit = {}
|
|
18
|
+
): Promise<Response> {
|
|
19
|
+
const normalizedPath = normalizePath(path);
|
|
20
|
+
const userWithOptionalToken = user as User & { getIdToken?: () => Promise<string> };
|
|
21
|
+
|
|
22
|
+
if (typeof userWithOptionalToken.getIdToken !== 'function') {
|
|
23
|
+
throw new Error('Unable to authenticate request: missing Firebase token provider');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let idToken: string;
|
|
27
|
+
try {
|
|
28
|
+
idToken = await userWithOptionalToken.getIdToken();
|
|
29
|
+
} catch {
|
|
30
|
+
throw new Error('Unable to authenticate request: failed to retrieve Firebase token');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!idToken) {
|
|
34
|
+
throw new Error('Unable to authenticate request: empty Firebase token');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const headers = new Headers(init.headers);
|
|
38
|
+
headers.set('Authorization', `Bearer ${idToken}`);
|
|
39
|
+
|
|
40
|
+
return fetch(`${IMAGE_API_BASE}${normalizedPath}`, {
|
|
41
|
+
...init,
|
|
42
|
+
headers
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface XhrUploadResult {
|
|
47
|
+
status: number;
|
|
48
|
+
responseText: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function uploadWithXhr(
|
|
52
|
+
targetUrl: string,
|
|
53
|
+
authorizationValue: string,
|
|
54
|
+
file: File,
|
|
55
|
+
onProgress?: (progress: number) => void
|
|
56
|
+
): Promise<XhrUploadResult> {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const xhr = new XMLHttpRequest();
|
|
59
|
+
const formData = new FormData();
|
|
60
|
+
formData.append('file', file);
|
|
61
|
+
|
|
62
|
+
xhr.upload.addEventListener('progress', (event) => {
|
|
63
|
+
if (event.lengthComputable && onProgress) {
|
|
64
|
+
const progress = Math.round((event.loaded / event.total) * 100);
|
|
65
|
+
onProgress(progress);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
xhr.addEventListener('load', () => {
|
|
70
|
+
resolve({
|
|
71
|
+
status: xhr.status,
|
|
72
|
+
responseText: xhr.responseText
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
xhr.addEventListener('error', () => {
|
|
77
|
+
reject(new Error('Upload failed'));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
xhr.open('POST', targetUrl);
|
|
81
|
+
xhr.setRequestHeader('Authorization', authorizationValue);
|
|
82
|
+
xhr.send(formData);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function parseUploadResponse(payload: string): ImageUploadResponse {
|
|
87
|
+
const parsed = JSON.parse(payload) as ImageUploadResponse;
|
|
88
|
+
if (!parsed.success || !parsed.result?.id) {
|
|
89
|
+
const errorMessage = parsed.errors?.map((entry) => entry.message).join(', ') || 'Upload failed';
|
|
90
|
+
throw new Error(errorMessage);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return parsed;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function uploadImageApi(
|
|
97
|
+
user: User,
|
|
98
|
+
file: File,
|
|
99
|
+
onProgress?: (progress: number) => void
|
|
100
|
+
): Promise<ImageUploadResponse> {
|
|
101
|
+
const userWithOptionalToken = user as User & { getIdToken?: () => Promise<string> };
|
|
102
|
+
|
|
103
|
+
if (typeof userWithOptionalToken.getIdToken !== 'function') {
|
|
104
|
+
throw new Error('Unable to authenticate upload: missing Firebase token provider');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let idToken: string;
|
|
108
|
+
try {
|
|
109
|
+
idToken = await userWithOptionalToken.getIdToken();
|
|
110
|
+
} catch {
|
|
111
|
+
throw new Error('Unable to authenticate upload: failed to retrieve Firebase token');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!idToken) {
|
|
115
|
+
throw new Error('Unable to authenticate upload: empty Firebase token');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const proxyUploadResult = await uploadWithXhr(
|
|
119
|
+
`${IMAGE_API_BASE}/`,
|
|
120
|
+
`Bearer ${idToken}`,
|
|
121
|
+
file,
|
|
122
|
+
onProgress
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
if (proxyUploadResult.status < 200 || proxyUploadResult.status >= 300) {
|
|
126
|
+
throw new Error(`Upload failed with status ${proxyUploadResult.status}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return parseUploadResponse(proxyUploadResult.responseText);
|
|
130
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { User } from 'firebase/auth';
|
|
2
|
+
|
|
3
|
+
const PDF_API_BASE = '/api/pdf';
|
|
4
|
+
|
|
5
|
+
function normalizePath(path: string): string {
|
|
6
|
+
if (!path) {
|
|
7
|
+
return '/';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return path.startsWith('/') ? path : `/${path}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function fetchPdfApi(
|
|
14
|
+
user: User,
|
|
15
|
+
path: string,
|
|
16
|
+
init: RequestInit = {}
|
|
17
|
+
): Promise<Response> {
|
|
18
|
+
const normalizedPath = normalizePath(path);
|
|
19
|
+
const userWithOptionalToken = user as User & { getIdToken?: () => Promise<string> };
|
|
20
|
+
|
|
21
|
+
if (typeof userWithOptionalToken.getIdToken !== 'function') {
|
|
22
|
+
throw new Error('Unable to authenticate request: missing Firebase token provider');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let idToken: string;
|
|
26
|
+
try {
|
|
27
|
+
idToken = await userWithOptionalToken.getIdToken();
|
|
28
|
+
} catch {
|
|
29
|
+
throw new Error('Unable to authenticate request: failed to retrieve Firebase token');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!idToken) {
|
|
33
|
+
throw new Error('Unable to authenticate request: empty Firebase token');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const headers = new Headers(init.headers);
|
|
37
|
+
headers.set('Authorization', `Bearer ${idToken}`);
|
|
38
|
+
|
|
39
|
+
return fetch(`${PDF_API_BASE}${normalizedPath}`, {
|
|
40
|
+
...init,
|
|
41
|
+
headers
|
|
42
|
+
});
|
|
43
|
+
}
|
package/app/utils/permissions.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { User } from 'firebase/auth';
|
|
2
2
|
import type { UserData, ExtendedUserData, UserLimits, ReadOnlyCaseMetadata } from '~/types';
|
|
3
3
|
import paths from '~/config/config.json';
|
|
4
|
-
import {
|
|
4
|
+
import { fetchUserApi } from './user-api-client';
|
|
5
5
|
|
|
6
|
-
const USER_WORKER_URL = paths.user_worker_url;
|
|
7
6
|
const MAX_CASES_REVIEW = paths.max_cases_review;
|
|
8
7
|
const MAX_FILES_PER_CASE_REVIEW = paths.max_files_per_case_review;
|
|
9
8
|
|
|
@@ -32,12 +31,8 @@ export interface CaseMetadata {
|
|
|
32
31
|
*/
|
|
33
32
|
export const getUserData = async (user: User): Promise<UserData | null> => {
|
|
34
33
|
try {
|
|
35
|
-
const
|
|
36
|
-
const response = await fetch(`${USER_WORKER_URL}/${encodeURIComponent(user.uid)}`, {
|
|
34
|
+
const response = await fetchUserApi(user, `/${encodeURIComponent(user.uid)}`, {
|
|
37
35
|
method: 'GET',
|
|
38
|
-
headers: {
|
|
39
|
-
'X-Custom-Auth-Key': apiKey
|
|
40
|
-
}
|
|
41
36
|
});
|
|
42
37
|
|
|
43
38
|
if (response.ok) {
|
|
@@ -121,12 +116,10 @@ export const createUser = async (
|
|
|
121
116
|
createdAt: new Date().toISOString()
|
|
122
117
|
};
|
|
123
118
|
|
|
124
|
-
const
|
|
125
|
-
const response = await fetch(`${USER_WORKER_URL}/${encodeURIComponent(user.uid)}`, {
|
|
119
|
+
const response = await fetchUserApi(user, `/${encodeURIComponent(user.uid)}`, {
|
|
126
120
|
method: 'PUT',
|
|
127
121
|
headers: {
|
|
128
|
-
'Content-Type': 'application/json'
|
|
129
|
-
'X-Custom-Auth-Key': apiKey
|
|
122
|
+
'Content-Type': 'application/json'
|
|
130
123
|
},
|
|
131
124
|
body: JSON.stringify(userData)
|
|
132
125
|
});
|
|
@@ -280,12 +273,10 @@ export const updateUserData = async (user: User, updates: Partial<UserData>): Pr
|
|
|
280
273
|
};
|
|
281
274
|
|
|
282
275
|
// Perform the update with API key management
|
|
283
|
-
const
|
|
284
|
-
const response = await fetch(`${USER_WORKER_URL}/${encodeURIComponent(user.uid)}`, {
|
|
276
|
+
const response = await fetchUserApi(user, `/${encodeURIComponent(user.uid)}`, {
|
|
285
277
|
method: 'PUT',
|
|
286
278
|
headers: {
|
|
287
|
-
'Content-Type': 'application/json'
|
|
288
|
-
'X-Custom-Auth-Key': apiKey
|
|
279
|
+
'Content-Type': 'application/json'
|
|
289
280
|
},
|
|
290
281
|
body: JSON.stringify(updatedUserData)
|
|
291
282
|
});
|
|
@@ -489,12 +480,10 @@ export const addUserCase = async (user: User, caseData: CaseMetadata): Promise<v
|
|
|
489
480
|
}
|
|
490
481
|
|
|
491
482
|
// Use the dedicated /cases endpoint to add the case
|
|
492
|
-
const
|
|
493
|
-
const response = await fetch(`${USER_WORKER_URL}/${encodeURIComponent(user.uid)}/cases`, {
|
|
483
|
+
const response = await fetchUserApi(user, `/${encodeURIComponent(user.uid)}/cases`, {
|
|
494
484
|
method: 'PUT',
|
|
495
485
|
headers: {
|
|
496
|
-
'Content-Type': 'application/json'
|
|
497
|
-
'X-Custom-Auth-Key': apiKey
|
|
486
|
+
'Content-Type': 'application/json'
|
|
498
487
|
},
|
|
499
488
|
body: JSON.stringify({
|
|
500
489
|
cases: [caseData]
|
|
@@ -536,12 +525,10 @@ export const removeUserCase = async (user: User, caseNumber: string): Promise<vo
|
|
|
536
525
|
}
|
|
537
526
|
|
|
538
527
|
// Use the dedicated /cases DELETE endpoint to remove the case
|
|
539
|
-
const
|
|
540
|
-
const response = await fetch(`${USER_WORKER_URL}/${encodeURIComponent(user.uid)}/cases`, {
|
|
528
|
+
const response = await fetchUserApi(user, `/${encodeURIComponent(user.uid)}/cases`, {
|
|
541
529
|
method: 'DELETE',
|
|
542
530
|
headers: {
|
|
543
|
-
'Content-Type': 'application/json'
|
|
544
|
-
'X-Custom-Auth-Key': apiKey
|
|
531
|
+
'Content-Type': 'application/json'
|
|
545
532
|
},
|
|
546
533
|
body: JSON.stringify({
|
|
547
534
|
casesToDelete: [caseNumber]
|
|
@@ -20,6 +20,15 @@ export interface SignatureVerificationMessages {
|
|
|
20
20
|
verificationFailedError?: string;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
export interface SignatureVerificationOptions {
|
|
24
|
+
verificationPublicKeyPem?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface PublicSigningKeyDetails {
|
|
28
|
+
keyId: string | null;
|
|
29
|
+
publicKeyPem: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
23
32
|
type ManifestSigningConfig = {
|
|
24
33
|
manifest_signing_public_keys?: Record<string, string>;
|
|
25
34
|
manifest_signing_public_key?: string;
|
|
@@ -30,6 +39,63 @@ function normalizePemPublicKey(pem: string): string {
|
|
|
30
39
|
return pem.replace(/\\n/g, '\n').trim();
|
|
31
40
|
}
|
|
32
41
|
|
|
42
|
+
function normalizePemOrNull(pem: unknown): string | null {
|
|
43
|
+
if (typeof pem !== 'string' || pem.trim().length === 0) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return normalizePemPublicKey(pem);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sanitizeKeyIdForFileName(keyId: string): string {
|
|
51
|
+
return keyId.trim().replace(/[^a-z0-9_-]+/gi, '-');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function createPublicSigningKeyFileName(keyId?: string | null): string {
|
|
55
|
+
if (typeof keyId === 'string' && keyId.trim().length > 0) {
|
|
56
|
+
return `striae-public-signing-key-${sanitizeKeyIdForFileName(keyId)}.pem`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return 'striae-public-signing-key.pem';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getCurrentPublicSigningKeyDetails(): PublicSigningKeyDetails {
|
|
63
|
+
const config = paths as unknown as ManifestSigningConfig;
|
|
64
|
+
const configuredKeyId =
|
|
65
|
+
typeof config.manifest_signing_key_id === 'string' && config.manifest_signing_key_id.trim().length > 0
|
|
66
|
+
? config.manifest_signing_key_id
|
|
67
|
+
: null;
|
|
68
|
+
|
|
69
|
+
if (configuredKeyId) {
|
|
70
|
+
const configuredKey = getVerificationPublicKey(configuredKeyId);
|
|
71
|
+
if (configuredKey) {
|
|
72
|
+
return {
|
|
73
|
+
keyId: configuredKeyId,
|
|
74
|
+
publicKeyPem: configuredKey
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const keyMap = config.manifest_signing_public_keys;
|
|
80
|
+
if (keyMap && typeof keyMap === 'object') {
|
|
81
|
+
const firstConfiguredEntry = Object.entries(keyMap).find(
|
|
82
|
+
([, value]) => typeof value === 'string' && value.trim().length > 0
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (firstConfiguredEntry) {
|
|
86
|
+
return {
|
|
87
|
+
keyId: firstConfiguredEntry[0],
|
|
88
|
+
publicKeyPem: normalizePemPublicKey(firstConfiguredEntry[1])
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
keyId: null,
|
|
95
|
+
publicKeyPem: normalizePemOrNull(config.manifest_signing_public_key)
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
33
99
|
function publicKeyPemToArrayBuffer(publicKeyPem: string, invalidPublicKeyError: string): ArrayBuffer {
|
|
34
100
|
const normalized = normalizePemPublicKey(publicKeyPem);
|
|
35
101
|
const pemBody = normalized
|
|
@@ -71,7 +137,7 @@ export function getVerificationPublicKey(keyId: string): string | null {
|
|
|
71
137
|
if (keyMap && typeof keyMap === 'object') {
|
|
72
138
|
const mappedKey = keyMap[keyId];
|
|
73
139
|
if (typeof mappedKey === 'string' && mappedKey.trim().length > 0) {
|
|
74
|
-
return mappedKey;
|
|
140
|
+
return normalizePemPublicKey(mappedKey);
|
|
75
141
|
}
|
|
76
142
|
}
|
|
77
143
|
|
|
@@ -81,7 +147,7 @@ export function getVerificationPublicKey(keyId: string): string | null {
|
|
|
81
147
|
typeof config.manifest_signing_public_key === 'string' &&
|
|
82
148
|
config.manifest_signing_public_key.trim().length > 0
|
|
83
149
|
) {
|
|
84
|
-
return config.manifest_signing_public_key;
|
|
150
|
+
return normalizePemPublicKey(config.manifest_signing_public_key);
|
|
85
151
|
}
|
|
86
152
|
|
|
87
153
|
return null;
|
|
@@ -91,7 +157,8 @@ export async function verifySignaturePayload(
|
|
|
91
157
|
payload: string,
|
|
92
158
|
signature: SignatureEnvelope,
|
|
93
159
|
expectedAlgorithm: string,
|
|
94
|
-
messages: SignatureVerificationMessages = {}
|
|
160
|
+
messages: SignatureVerificationMessages = {},
|
|
161
|
+
options: SignatureVerificationOptions = {}
|
|
95
162
|
): Promise<SignatureVerificationResult> {
|
|
96
163
|
if (signature.algorithm !== expectedAlgorithm) {
|
|
97
164
|
return {
|
|
@@ -108,7 +175,10 @@ export async function verifySignaturePayload(
|
|
|
108
175
|
};
|
|
109
176
|
}
|
|
110
177
|
|
|
111
|
-
const publicKeyPem =
|
|
178
|
+
const publicKeyPem =
|
|
179
|
+
typeof options.verificationPublicKeyPem === 'string' && options.verificationPublicKeyPem.trim().length > 0
|
|
180
|
+
? options.verificationPublicKeyPem
|
|
181
|
+
: getVerificationPublicKey(signature.keyId);
|
|
112
182
|
if (!publicKeyPem) {
|
|
113
183
|
return {
|
|
114
184
|
isValid: false,
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { User } from 'firebase/auth';
|
|
2
|
+
|
|
3
|
+
const USER_API_BASE = '/api/user';
|
|
4
|
+
const USER_EXISTS_API_BASE = '/api/user/exists';
|
|
5
|
+
|
|
6
|
+
function normalizePath(path: string): string {
|
|
7
|
+
if (!path) {
|
|
8
|
+
return '/';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return path.startsWith('/') ? path : `/${path}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function fetchUserApi(
|
|
15
|
+
user: User,
|
|
16
|
+
path: string,
|
|
17
|
+
init: RequestInit = {}
|
|
18
|
+
): Promise<Response> {
|
|
19
|
+
const normalizedPath = normalizePath(path);
|
|
20
|
+
const userWithOptionalToken = user as User & { getIdToken?: () => Promise<string> };
|
|
21
|
+
|
|
22
|
+
if (typeof userWithOptionalToken.getIdToken !== 'function') {
|
|
23
|
+
throw new Error('Unable to authenticate request: missing Firebase token provider');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let idToken: string;
|
|
27
|
+
try {
|
|
28
|
+
idToken = await userWithOptionalToken.getIdToken();
|
|
29
|
+
} catch {
|
|
30
|
+
throw new Error('Unable to authenticate request: failed to retrieve Firebase token');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!idToken) {
|
|
34
|
+
throw new Error('Unable to authenticate request: empty Firebase token');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const headers = new Headers(init.headers);
|
|
38
|
+
headers.set('Authorization', `Bearer ${idToken}`);
|
|
39
|
+
|
|
40
|
+
return fetch(`${USER_API_BASE}${normalizedPath}`, {
|
|
41
|
+
...init,
|
|
42
|
+
headers
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function checkUserExistsApi(user: User, targetUid: string): Promise<boolean> {
|
|
47
|
+
const encodedTargetUid = encodeURIComponent(targetUid);
|
|
48
|
+
const userWithOptionalToken = user as User & { getIdToken?: () => Promise<string> };
|
|
49
|
+
|
|
50
|
+
if (typeof userWithOptionalToken.getIdToken !== 'function') {
|
|
51
|
+
throw new Error('Unable to authenticate request: missing Firebase token provider');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let idToken: string;
|
|
55
|
+
try {
|
|
56
|
+
idToken = await userWithOptionalToken.getIdToken();
|
|
57
|
+
} catch {
|
|
58
|
+
throw new Error('Unable to authenticate request: failed to retrieve Firebase token');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!idToken) {
|
|
62
|
+
throw new Error('Unable to authenticate request: empty Firebase token');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const proxyResponse = await fetch(`${USER_EXISTS_API_BASE}/${encodedTargetUid}`, {
|
|
66
|
+
method: 'GET',
|
|
67
|
+
headers: {
|
|
68
|
+
'Authorization': `Bearer ${idToken}`,
|
|
69
|
+
'Accept': 'application/json'
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (proxyResponse.status === 404) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!proxyResponse.ok) {
|
|
78
|
+
throw new Error(`Failed to verify user existence: ${proxyResponse.status}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const responseData = await proxyResponse.json().catch(() => null) as {
|
|
82
|
+
exists?: boolean;
|
|
83
|
+
} | null;
|
|
84
|
+
|
|
85
|
+
if (typeof responseData?.exists === 'boolean') {
|
|
86
|
+
return responseData.exists;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return true;
|
|
90
|
+
}
|