varminer-app-header 2.1.4 → 2.1.6
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/dist/AppHeader.d.ts.map +1 -1
- package/dist/index.esm.js +234 -2
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +234 -2
- package/dist/index.js.map +1 -1
- package/dist/utils/localStorage.d.ts +26 -0
- package/dist/utils/localStorage.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -340,6 +340,209 @@ const getAllDataFromStorage = () => {
|
|
|
340
340
|
};
|
|
341
341
|
}
|
|
342
342
|
};
|
|
343
|
+
/**
|
|
344
|
+
* Get profile picture URL from object store API using tenant_id and user_id from JWT token
|
|
345
|
+
* @param baseUrl - Base URL for the object store API (default: http://objectstore.impact0mics.local:9012)
|
|
346
|
+
* @returns Profile picture URL or null if tenant_id or user_id is not available
|
|
347
|
+
*/
|
|
348
|
+
const getProfilePictureUrl = (baseUrl = "http://objectstore.impact0mics.local:9012") => {
|
|
349
|
+
try {
|
|
350
|
+
const allData = getAllDataFromStorage();
|
|
351
|
+
// Get tenant_id and user_id from decoded token
|
|
352
|
+
let tenantId = null;
|
|
353
|
+
let userId = null;
|
|
354
|
+
if (allData.decodedToken) {
|
|
355
|
+
tenantId = allData.decodedToken.tenant_id || allData.decodedToken.tenant || null;
|
|
356
|
+
userId = allData.decodedToken.user_id || null;
|
|
357
|
+
}
|
|
358
|
+
// If not found in decoded token, try auth data
|
|
359
|
+
if ((!tenantId || !userId) && allData.auth) {
|
|
360
|
+
tenantId = tenantId || allData.auth.tenant_id || allData.auth.tenant || null;
|
|
361
|
+
userId = userId || allData.auth.user_id || null;
|
|
362
|
+
}
|
|
363
|
+
// Construct URL if we have both tenant_id and user_id
|
|
364
|
+
if (tenantId && userId) {
|
|
365
|
+
// Remove trailing slash from baseUrl if present
|
|
366
|
+
const cleanBaseUrl = baseUrl.replace(/\/$/, '');
|
|
367
|
+
return `${cleanBaseUrl}/v1/objectStore/profilePicture/path/${tenantId}/${userId}`;
|
|
368
|
+
}
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
catch (err) {
|
|
372
|
+
console.error("Error getting profile picture URL:", err);
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
/**
|
|
377
|
+
* Generate AWS S3 presigned URL for accessing S3 object
|
|
378
|
+
* @param s3Url - Full S3 URL (e.g., https://bucket.s3.region.amazonaws.com/key)
|
|
379
|
+
* @param accessKeyId - AWS Access Key ID
|
|
380
|
+
* @param secretAccessKey - AWS Secret Access Key
|
|
381
|
+
* @param region - AWS Region (default: ap-south-2)
|
|
382
|
+
* @param expiresIn - Expiration time in seconds (default: 3600 = 1 hour)
|
|
383
|
+
* @returns Presigned URL string
|
|
384
|
+
*/
|
|
385
|
+
const generateS3PresignedUrl = async (s3Url, accessKeyId, secretAccessKey, region = "ap-south-2", expiresIn = 3600) => {
|
|
386
|
+
try {
|
|
387
|
+
// Parse S3 URL to extract bucket, region, and key
|
|
388
|
+
// Format: https://bucket.s3.region.amazonaws.com/key or https://bucket.s3-region.amazonaws.com/key
|
|
389
|
+
const url = new URL(s3Url);
|
|
390
|
+
const hostnameParts = url.hostname.split('.');
|
|
391
|
+
let bucket = hostnameParts[0];
|
|
392
|
+
let extractedRegion = region;
|
|
393
|
+
// Try to extract region from hostname (format: bucket.s3.region.amazonaws.com)
|
|
394
|
+
if (hostnameParts.length >= 3 && hostnameParts[1] === 's3') {
|
|
395
|
+
extractedRegion = hostnameParts[2] || region;
|
|
396
|
+
}
|
|
397
|
+
else if (hostnameParts.length >= 2 && hostnameParts[1].startsWith('s3-')) {
|
|
398
|
+
// Format: bucket.s3-region.amazonaws.com
|
|
399
|
+
extractedRegion = hostnameParts[1].substring(3) || region;
|
|
400
|
+
}
|
|
401
|
+
const key = url.pathname.substring(1); // Remove leading slash
|
|
402
|
+
// AWS credentials
|
|
403
|
+
const awsAccessKeyId = accessKeyId;
|
|
404
|
+
const awsSecretAccessKey = secretAccessKey;
|
|
405
|
+
const awsRegion = extractedRegion;
|
|
406
|
+
// Current timestamp
|
|
407
|
+
const now = new Date();
|
|
408
|
+
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, '');
|
|
409
|
+
const amzDate = dateStamp + 'T' + now.toISOString().slice(11, 19).replace(/[:-]/g, '') + 'Z';
|
|
410
|
+
// Step 1: Create canonical request
|
|
411
|
+
const canonicalUri = '/' + encodeURIComponent(key).replace(/%2F/g, '/');
|
|
412
|
+
const canonicalQuerystring = `X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=${encodeURIComponent(awsAccessKeyId + '/' + dateStamp + '/' + awsRegion + '/s3/aws4_request')}&X-Amz-Date=${amzDate}&X-Amz-Expires=${expiresIn}&X-Amz-SignedHeaders=host`;
|
|
413
|
+
const canonicalHeaders = `host:${bucket}.s3.${awsRegion}.amazonaws.com\n`;
|
|
414
|
+
const signedHeaders = 'host';
|
|
415
|
+
const payloadHash = 'UNSIGNED-PAYLOAD';
|
|
416
|
+
const canonicalRequest = `GET\n${canonicalUri}\n${canonicalQuerystring}\n${canonicalHeaders}\n${signedHeaders}\n${payloadHash}`;
|
|
417
|
+
// Step 2: Create string to sign
|
|
418
|
+
const algorithm = 'AWS4-HMAC-SHA256';
|
|
419
|
+
const credentialScope = `${dateStamp}/${awsRegion}/s3/aws4_request`;
|
|
420
|
+
const stringToSign = `${algorithm}\n${amzDate}\n${credentialScope}\n${await sha256(canonicalRequest)}`;
|
|
421
|
+
// Step 3: Calculate signature
|
|
422
|
+
const kDate = await hmacSha256('AWS4' + awsSecretAccessKey, dateStamp);
|
|
423
|
+
const kRegion = await hmacSha256(kDate, awsRegion);
|
|
424
|
+
const kService = await hmacSha256(kRegion, 's3');
|
|
425
|
+
const kSigning = await hmacSha256(kService, 'aws4_request');
|
|
426
|
+
const signature = await hmacSha256(kSigning, stringToSign);
|
|
427
|
+
// Convert signature to hex
|
|
428
|
+
const signatureHex = arrayBufferToHex(signature);
|
|
429
|
+
// Step 4: Construct presigned URL
|
|
430
|
+
const presignedUrl = `https://${bucket}.s3.${awsRegion}.amazonaws.com${canonicalUri}?${canonicalQuerystring}&X-Amz-Signature=${signatureHex}`;
|
|
431
|
+
return presignedUrl;
|
|
432
|
+
}
|
|
433
|
+
catch (err) {
|
|
434
|
+
console.error("Error generating S3 presigned URL:", err);
|
|
435
|
+
throw err;
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
// Helper function for SHA-256 hashing
|
|
439
|
+
const sha256 = async (message) => {
|
|
440
|
+
const msgBuffer = new TextEncoder().encode(message);
|
|
441
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
|
|
442
|
+
return Array.from(new Uint8Array(hashBuffer))
|
|
443
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
444
|
+
.join('');
|
|
445
|
+
};
|
|
446
|
+
// Helper function for HMAC-SHA256
|
|
447
|
+
const hmacSha256 = async (key, message) => {
|
|
448
|
+
const encoder = new TextEncoder();
|
|
449
|
+
let keyBuffer;
|
|
450
|
+
if (typeof key === 'string') {
|
|
451
|
+
keyBuffer = encoder.encode(key);
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
keyBuffer = new Uint8Array(key);
|
|
455
|
+
}
|
|
456
|
+
const messageBuffer = encoder.encode(message);
|
|
457
|
+
const cryptoKey = await crypto.subtle.importKey('raw', keyBuffer, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
458
|
+
return await crypto.subtle.sign('HMAC', cryptoKey, messageBuffer);
|
|
459
|
+
};
|
|
460
|
+
// Helper function to convert ArrayBuffer to hex string
|
|
461
|
+
const arrayBufferToHex = (buffer) => {
|
|
462
|
+
return Array.from(new Uint8Array(buffer))
|
|
463
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
464
|
+
.join('');
|
|
465
|
+
};
|
|
466
|
+
/**
|
|
467
|
+
* Fetch profile picture from API with headers, get S3 URL, generate presigned URL, and return as blob URL
|
|
468
|
+
* This function:
|
|
469
|
+
* 1. Fetches the profile picture path from the API
|
|
470
|
+
* 2. Extracts the S3 filePath from the JSON response
|
|
471
|
+
* 3. Generates a presigned URL for the S3 object
|
|
472
|
+
* 4. Fetches the image using the presigned URL
|
|
473
|
+
* 5. Converts it to a blob URL that can be used in img src
|
|
474
|
+
* @param baseUrl - Base URL for the object store API (default: http://objectstore.impact0mics.local:9012)
|
|
475
|
+
* @param messageId - Optional message ID for X-Message-Id header (default: generated UUID)
|
|
476
|
+
* @param correlationId - Optional correlation ID for X-Correlation-Id header (default: generated UUID)
|
|
477
|
+
* @param awsConfig - AWS configuration (accessKeyId, secretAccessKey, region, bucket)
|
|
478
|
+
* @returns Promise that resolves to blob URL string or null if fetch fails
|
|
479
|
+
*/
|
|
480
|
+
const fetchProfilePictureAsBlobUrl = async (baseUrl = "http://objectstore.impact0mics.local:9012", messageId, correlationId, awsConfig) => {
|
|
481
|
+
try {
|
|
482
|
+
const profilePictureUrl = getProfilePictureUrl(baseUrl);
|
|
483
|
+
if (!profilePictureUrl) {
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
// AWS credentials (default values provided by user)
|
|
487
|
+
const defaultAwsConfig = {
|
|
488
|
+
accessKeyId: "AKIAVRUVTJGLBCYZEI5L",
|
|
489
|
+
secretAccessKey: "kbMVqmx6s29njcS5P48qAqpXlb1oir6+b7zu1Qxi",
|
|
490
|
+
region: "ap-south-2",
|
|
491
|
+
bucket: "development-varminer-test"
|
|
492
|
+
};
|
|
493
|
+
const finalAwsConfig = awsConfig || defaultAwsConfig;
|
|
494
|
+
// Generate message ID and correlation ID if not provided
|
|
495
|
+
const msgId = messageId || `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
496
|
+
const corrId = correlationId || `corr-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
497
|
+
// Step 1: Fetch the profile picture path from API (returns JSON with filePath)
|
|
498
|
+
const apiResponse = await fetch(profilePictureUrl, {
|
|
499
|
+
method: 'GET',
|
|
500
|
+
headers: {
|
|
501
|
+
'X-Message-Id': msgId,
|
|
502
|
+
'X-Correlation-Id': corrId,
|
|
503
|
+
},
|
|
504
|
+
});
|
|
505
|
+
// Check if the API response is successful
|
|
506
|
+
if (!apiResponse.ok) {
|
|
507
|
+
console.warn(`Failed to fetch profile picture path: ${apiResponse.status} ${apiResponse.statusText}`);
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
// Parse JSON response
|
|
511
|
+
const apiData = await apiResponse.json();
|
|
512
|
+
// Extract filePath from response
|
|
513
|
+
const s3Url = apiData?.filePath;
|
|
514
|
+
if (!s3Url || typeof s3Url !== 'string') {
|
|
515
|
+
console.warn('Profile picture API response does not contain valid filePath');
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
// Step 2: Generate presigned URL for S3 object
|
|
519
|
+
const presignedUrl = await generateS3PresignedUrl(s3Url, finalAwsConfig.accessKeyId, finalAwsConfig.secretAccessKey, finalAwsConfig.region);
|
|
520
|
+
// Step 3: Fetch the image using presigned URL
|
|
521
|
+
const imageResponse = await fetch(presignedUrl, {
|
|
522
|
+
method: 'GET',
|
|
523
|
+
});
|
|
524
|
+
// Check if the image response is successful
|
|
525
|
+
if (!imageResponse.ok) {
|
|
526
|
+
console.warn(`Failed to fetch profile picture image: ${imageResponse.status} ${imageResponse.statusText}`);
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
// Check if the response is an image
|
|
530
|
+
const contentType = imageResponse.headers.get('content-type');
|
|
531
|
+
if (!contentType || !contentType.startsWith('image/')) {
|
|
532
|
+
console.warn(`Profile picture response is not an image: ${contentType}`);
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
// Step 4: Convert response to blob
|
|
536
|
+
const blob = await imageResponse.blob();
|
|
537
|
+
// Step 5: Create blob URL
|
|
538
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
539
|
+
return blobUrl;
|
|
540
|
+
}
|
|
541
|
+
catch (err) {
|
|
542
|
+
console.error("Error fetching profile picture:", err);
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
};
|
|
343
546
|
|
|
344
547
|
const translations = {
|
|
345
548
|
en: {
|
|
@@ -547,6 +750,34 @@ const AppHeader = ({ language: languageProp }) => {
|
|
|
547
750
|
const [messageCount, setMessageCount] = React.useState(() => {
|
|
548
751
|
return getMessageCountFromStorage() ?? undefined;
|
|
549
752
|
});
|
|
753
|
+
// State for profile picture blob URL
|
|
754
|
+
const [profilePictureBlobUrl, setProfilePictureBlobUrl] = React.useState(null);
|
|
755
|
+
// Fetch profile picture from API when component mounts or user data changes
|
|
756
|
+
React.useEffect(() => {
|
|
757
|
+
const fetchProfilePicture = async () => {
|
|
758
|
+
// Try to fetch profile picture from API
|
|
759
|
+
const blobUrl = await fetchProfilePictureAsBlobUrl();
|
|
760
|
+
if (blobUrl) {
|
|
761
|
+
// Clean up previous blob URL if it exists
|
|
762
|
+
setProfilePictureBlobUrl((prevUrl) => {
|
|
763
|
+
if (prevUrl) {
|
|
764
|
+
URL.revokeObjectURL(prevUrl);
|
|
765
|
+
}
|
|
766
|
+
return blobUrl;
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
fetchProfilePicture();
|
|
771
|
+
// Cleanup function to revoke blob URL when component unmounts or effect re-runs
|
|
772
|
+
return () => {
|
|
773
|
+
setProfilePictureBlobUrl((prevUrl) => {
|
|
774
|
+
if (prevUrl) {
|
|
775
|
+
URL.revokeObjectURL(prevUrl);
|
|
776
|
+
}
|
|
777
|
+
return null;
|
|
778
|
+
});
|
|
779
|
+
};
|
|
780
|
+
}, []); // Only run once on mount - fetch when component loads
|
|
550
781
|
React.useEffect(() => {
|
|
551
782
|
const allData = getAllDataFromStorage();
|
|
552
783
|
// Try to get user data from various sources
|
|
@@ -609,11 +840,12 @@ const AppHeader = ({ language: languageProp }) => {
|
|
|
609
840
|
userInitials = userInitials || profileData.initials || undefined;
|
|
610
841
|
}
|
|
611
842
|
}
|
|
843
|
+
// Use fetched blob URL if available, otherwise fall back to other sources
|
|
612
844
|
setUser({
|
|
613
845
|
name: userName || "",
|
|
614
846
|
email: userEmail || "",
|
|
615
847
|
role: userRole || "",
|
|
616
|
-
avatar: userAvatar,
|
|
848
|
+
avatar: profilePictureBlobUrl || userAvatar,
|
|
617
849
|
initials: userInitials,
|
|
618
850
|
});
|
|
619
851
|
// Update online status
|
|
@@ -638,7 +870,7 @@ const AppHeader = ({ language: languageProp }) => {
|
|
|
638
870
|
setCurrentLanguage(languageProp);
|
|
639
871
|
setI18nLocaleToStorage(languageProp);
|
|
640
872
|
}
|
|
641
|
-
}, [languageProp]);
|
|
873
|
+
}, [languageProp, profilePictureBlobUrl]); // Also update when profile picture is fetched
|
|
642
874
|
const finalRoutes = DEFAULT_ROUTES;
|
|
643
875
|
// Get online status from localStorage dynamically
|
|
644
876
|
const [isOnlineStatus, setIsOnlineStatus] = React.useState(() => {
|