varminer-app-header 2.1.5 → 2.1.7

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
@@ -55,7 +55,42 @@ const DrawerProvider = ({ children }) => {
55
55
  };
56
56
  const useDrawer = () => React__namespace.useContext(DrawerContext);
57
57
 
58
+ // --- IAM app user details (shared from IAM after verify-OTP, same origin) ---
59
+ /** localStorage key used by IAM app for user details (GET /v1/userManagement/userDetails response). */
60
+ const USER_DETAILS_STORAGE_KEY = "linn-i-am-userDetails";
61
+ /**
62
+ * Read user details from IAM app localStorage (set after verify-OTP on same origin).
63
+ * Does not write or overwrite this key; IAM app is the source of truth.
64
+ */
65
+ function getStoredUserDetails() {
66
+ try {
67
+ const raw = localStorage.getItem(USER_DETAILS_STORAGE_KEY);
68
+ if (!raw)
69
+ return null;
70
+ const parsed = JSON.parse(raw);
71
+ if (parsed.status !== "SUCCESS" || !parsed.data?.usersDetails?.length) {
72
+ return null;
73
+ }
74
+ return parsed.data.usersDetails[0];
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ }
58
80
  const getUserDataFromStorage = () => {
81
+ // Prefer IAM user details when available (same origin, after verify-OTP)
82
+ const iamUser = getStoredUserDetails();
83
+ if (iamUser) {
84
+ const name = [iamUser.firstName, iamUser.lastName].filter(Boolean).join(" ").trim();
85
+ const role = iamUser.roles?.length ? iamUser.roles[0] : iamUser.workInfo?.jobTitle ?? "";
86
+ return {
87
+ name: name || "",
88
+ email: iamUser.email || "",
89
+ role: role || "",
90
+ avatar: undefined,
91
+ initials: name ? name.split(/\s+/).map((s) => s[0]).join("").slice(0, 2).toUpperCase() : undefined,
92
+ };
93
+ }
59
94
  const userDbString = localStorage.getItem("persist:userdb");
60
95
  if (userDbString) {
61
96
  try {
@@ -374,45 +409,167 @@ const getProfilePictureUrl = (baseUrl = "http://objectstore.impact0mics.local:90
374
409
  }
375
410
  };
376
411
  /**
377
- * Fetch profile picture from API with headers and return as blob URL
378
- * This function fetches the image with required headers (X-Message-Id, X-Correlation-Id)
379
- * and converts it to a blob URL that can be used in img src
412
+ * Generate AWS S3 presigned URL for accessing S3 object
413
+ * @param s3Url - Full S3 URL (e.g., https://bucket.s3.region.amazonaws.com/key)
414
+ * @param accessKeyId - AWS Access Key ID
415
+ * @param secretAccessKey - AWS Secret Access Key
416
+ * @param region - AWS Region (default: ap-south-2)
417
+ * @param expiresIn - Expiration time in seconds (default: 3600 = 1 hour)
418
+ * @returns Presigned URL string
419
+ */
420
+ const generateS3PresignedUrl = async (s3Url, accessKeyId, secretAccessKey, region = "ap-south-2", expiresIn = 3600) => {
421
+ try {
422
+ // Parse S3 URL to extract bucket, region, and key
423
+ // Format: https://bucket.s3.region.amazonaws.com/key or https://bucket.s3-region.amazonaws.com/key
424
+ const url = new URL(s3Url);
425
+ const hostnameParts = url.hostname.split('.');
426
+ let bucket = hostnameParts[0];
427
+ let extractedRegion = region;
428
+ // Try to extract region from hostname (format: bucket.s3.region.amazonaws.com)
429
+ if (hostnameParts.length >= 3 && hostnameParts[1] === 's3') {
430
+ extractedRegion = hostnameParts[2] || region;
431
+ }
432
+ else if (hostnameParts.length >= 2 && hostnameParts[1].startsWith('s3-')) {
433
+ // Format: bucket.s3-region.amazonaws.com
434
+ extractedRegion = hostnameParts[1].substring(3) || region;
435
+ }
436
+ const key = url.pathname.substring(1); // Remove leading slash
437
+ // AWS credentials
438
+ const awsAccessKeyId = accessKeyId;
439
+ const awsSecretAccessKey = secretAccessKey;
440
+ const awsRegion = extractedRegion;
441
+ // Current timestamp
442
+ const now = new Date();
443
+ const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, '');
444
+ const amzDate = dateStamp + 'T' + now.toISOString().slice(11, 19).replace(/[:-]/g, '') + 'Z';
445
+ // Step 1: Create canonical request
446
+ const canonicalUri = '/' + encodeURIComponent(key).replace(/%2F/g, '/');
447
+ 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`;
448
+ const canonicalHeaders = `host:${bucket}.s3.${awsRegion}.amazonaws.com\n`;
449
+ const signedHeaders = 'host';
450
+ const payloadHash = 'UNSIGNED-PAYLOAD';
451
+ const canonicalRequest = `GET\n${canonicalUri}\n${canonicalQuerystring}\n${canonicalHeaders}\n${signedHeaders}\n${payloadHash}`;
452
+ // Step 2: Create string to sign
453
+ const algorithm = 'AWS4-HMAC-SHA256';
454
+ const credentialScope = `${dateStamp}/${awsRegion}/s3/aws4_request`;
455
+ const stringToSign = `${algorithm}\n${amzDate}\n${credentialScope}\n${await sha256(canonicalRequest)}`;
456
+ // Step 3: Calculate signature
457
+ const kDate = await hmacSha256('AWS4' + awsSecretAccessKey, dateStamp);
458
+ const kRegion = await hmacSha256(kDate, awsRegion);
459
+ const kService = await hmacSha256(kRegion, 's3');
460
+ const kSigning = await hmacSha256(kService, 'aws4_request');
461
+ const signature = await hmacSha256(kSigning, stringToSign);
462
+ // Convert signature to hex
463
+ const signatureHex = arrayBufferToHex(signature);
464
+ // Step 4: Construct presigned URL
465
+ const presignedUrl = `https://${bucket}.s3.${awsRegion}.amazonaws.com${canonicalUri}?${canonicalQuerystring}&X-Amz-Signature=${signatureHex}`;
466
+ return presignedUrl;
467
+ }
468
+ catch (err) {
469
+ console.error("Error generating S3 presigned URL:", err);
470
+ throw err;
471
+ }
472
+ };
473
+ // Helper function for SHA-256 hashing
474
+ const sha256 = async (message) => {
475
+ const msgBuffer = new TextEncoder().encode(message);
476
+ const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
477
+ return Array.from(new Uint8Array(hashBuffer))
478
+ .map(b => b.toString(16).padStart(2, '0'))
479
+ .join('');
480
+ };
481
+ // Helper function for HMAC-SHA256
482
+ const hmacSha256 = async (key, message) => {
483
+ const encoder = new TextEncoder();
484
+ let keyBuffer;
485
+ if (typeof key === 'string') {
486
+ keyBuffer = encoder.encode(key);
487
+ }
488
+ else {
489
+ keyBuffer = new Uint8Array(key);
490
+ }
491
+ const messageBuffer = encoder.encode(message);
492
+ const cryptoKey = await crypto.subtle.importKey('raw', keyBuffer, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
493
+ return await crypto.subtle.sign('HMAC', cryptoKey, messageBuffer);
494
+ };
495
+ // Helper function to convert ArrayBuffer to hex string
496
+ const arrayBufferToHex = (buffer) => {
497
+ return Array.from(new Uint8Array(buffer))
498
+ .map(b => b.toString(16).padStart(2, '0'))
499
+ .join('');
500
+ };
501
+ /**
502
+ * Fetch profile picture from API with headers, get S3 URL, generate presigned URL, and return as blob URL
503
+ * This function:
504
+ * 1. Fetches the profile picture path from the API
505
+ * 2. Extracts the S3 filePath from the JSON response
506
+ * 3. Generates a presigned URL for the S3 object
507
+ * 4. Fetches the image using the presigned URL
508
+ * 5. Converts it to a blob URL that can be used in img src
380
509
  * @param baseUrl - Base URL for the object store API (default: http://objectstore.impact0mics.local:9012)
381
510
  * @param messageId - Optional message ID for X-Message-Id header (default: generated UUID)
382
511
  * @param correlationId - Optional correlation ID for X-Correlation-Id header (default: generated UUID)
512
+ * @param awsConfig - AWS configuration (accessKeyId, secretAccessKey, region, bucket)
383
513
  * @returns Promise that resolves to blob URL string or null if fetch fails
384
514
  */
385
- const fetchProfilePictureAsBlobUrl = async (baseUrl = "http://objectstore.impact0mics.local:9012", messageId, correlationId) => {
515
+ const fetchProfilePictureAsBlobUrl = async (baseUrl = "http://objectstore.impact0mics.local:9012", messageId, correlationId, awsConfig) => {
386
516
  try {
387
517
  const profilePictureUrl = getProfilePictureUrl(baseUrl);
388
518
  if (!profilePictureUrl) {
389
519
  return null;
390
520
  }
521
+ // AWS credentials (default values provided by user)
522
+ const defaultAwsConfig = {
523
+ accessKeyId: "AKIAVRUVTJGLBCYZEI5L",
524
+ secretAccessKey: "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",
525
+ region: "ap-south-2",
526
+ bucket: "development-varminer-test"
527
+ };
528
+ const finalAwsConfig = awsConfig || defaultAwsConfig;
391
529
  // Generate message ID and correlation ID if not provided
392
530
  const msgId = messageId || `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
393
531
  const corrId = correlationId || `corr-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
394
- // Fetch the image with required headers
395
- const response = await fetch(profilePictureUrl, {
532
+ // Step 1: Fetch the profile picture path from API (returns JSON with filePath)
533
+ const apiResponse = await fetch(profilePictureUrl, {
396
534
  method: 'GET',
397
535
  headers: {
398
536
  'X-Message-Id': msgId,
399
537
  'X-Correlation-Id': corrId,
400
538
  },
401
539
  });
402
- // Check if the response is successful
403
- if (!response.ok) {
404
- console.warn(`Failed to fetch profile picture: ${response.status} ${response.statusText}`);
540
+ // Check if the API response is successful
541
+ if (!apiResponse.ok) {
542
+ console.warn(`Failed to fetch profile picture path: ${apiResponse.status} ${apiResponse.statusText}`);
543
+ return null;
544
+ }
545
+ // Parse JSON response
546
+ const apiData = await apiResponse.json();
547
+ // Extract filePath from response
548
+ const s3Url = apiData?.filePath;
549
+ if (!s3Url || typeof s3Url !== 'string') {
550
+ console.warn('Profile picture API response does not contain valid filePath');
551
+ return null;
552
+ }
553
+ // Step 2: Generate presigned URL for S3 object
554
+ const presignedUrl = await generateS3PresignedUrl(s3Url, finalAwsConfig.accessKeyId, finalAwsConfig.secretAccessKey, finalAwsConfig.region);
555
+ // Step 3: Fetch the image using presigned URL
556
+ const imageResponse = await fetch(presignedUrl, {
557
+ method: 'GET',
558
+ });
559
+ // Check if the image response is successful
560
+ if (!imageResponse.ok) {
561
+ console.warn(`Failed to fetch profile picture image: ${imageResponse.status} ${imageResponse.statusText}`);
405
562
  return null;
406
563
  }
407
564
  // Check if the response is an image
408
- const contentType = response.headers.get('content-type');
565
+ const contentType = imageResponse.headers.get('content-type');
409
566
  if (!contentType || !contentType.startsWith('image/')) {
410
567
  console.warn(`Profile picture response is not an image: ${contentType}`);
411
568
  return null;
412
569
  }
413
- // Convert response to blob
414
- const blob = await response.blob();
415
- // Create blob URL
570
+ // Step 4: Convert response to blob
571
+ const blob = await imageResponse.blob();
572
+ // Step 5: Create blob URL
416
573
  const blobUrl = URL.createObjectURL(blob);
417
574
  return blobUrl;
418
575
  }
@@ -458,7 +615,8 @@ const languages = [
458
615
  { code: "en", name: "English", flag: GBFlag },
459
616
  { code: "es", name: "Spanish", flag: ESFlag },
460
617
  ];
461
- const LanguageSelector = ({ currentLanguage, onLanguageChange, }) => {
618
+ const LanguageSelector = ({ currentLanguage, onLanguageChange: _onLanguageChange, // reserved for parent callback; we reload the page on select
619
+ }) => {
462
620
  const [anchorEl, setAnchorEl] = React.useState(null);
463
621
  const open = Boolean(anchorEl);
464
622
  const currentLang = languages.find((lang) => lang.code === currentLanguage) || languages[0];
@@ -819,7 +977,13 @@ const AppHeader = ({ language: languageProp }) => {
819
977
  };
820
978
  const handleSignOutClick = () => {
821
979
  handleClose();
822
- localStorage.clear();
980
+ try {
981
+ localStorage.clear();
982
+ sessionStorage.clear();
983
+ }
984
+ catch (e) {
985
+ console.warn("Clear storage on logout:", e);
986
+ }
823
987
  const path = finalRoutes.logout.startsWith('/')
824
988
  ? finalRoutes.logout
825
989
  : `/${finalRoutes.logout}`;
@@ -969,7 +1133,17 @@ const AppHeader = ({ language: languageProp }) => {
969
1133
 
970
1134
  exports.AppHeader = AppHeader;
971
1135
  exports.DrawerProvider = DrawerProvider;
1136
+ exports.USER_DETAILS_STORAGE_KEY = USER_DETAILS_STORAGE_KEY;
1137
+ exports.fetchProfilePictureAsBlobUrl = fetchProfilePictureAsBlobUrl;
1138
+ exports.getAllDataFromStorage = getAllDataFromStorage;
1139
+ exports.getI18nLocaleFromStorage = getI18nLocaleFromStorage;
1140
+ exports.getMessageCountFromStorage = getMessageCountFromStorage;
1141
+ exports.getNotificationCountFromStorage = getNotificationCountFromStorage;
1142
+ exports.getProfilePictureUrl = getProfilePictureUrl;
1143
+ exports.getStoredUserDetails = getStoredUserDetails;
972
1144
  exports.getTranslations = getTranslations;
1145
+ exports.getUserDataFromStorage = getUserDataFromStorage;
1146
+ exports.setI18nLocaleToStorage = setI18nLocaleToStorage;
973
1147
  exports.translations = translations;
974
1148
  exports.useDrawer = useDrawer;
975
1149
  //# sourceMappingURL=index.js.map