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/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: "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",
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(() => {