Repository: weni-ai/weni-integrations-webapp
Pull request #511: [HS-984] Fix: Refactor token retrieval
Author: login: acnormun
Created: 2025-04-03T19:44:21Z | Merged: 2025-04-08T21:48:38Z

## Description
### Type of Change
<!-- Remove the space between "[" and "]", enter an x in the category(ies) that fits the pull request and do not exclude the others -->
* * [ ] Bugfix
* * [ ] Feature
* * [ ] Code style update (formatting, local variables)
* * [x] Refactoring (no functional changes, no api changes)
* * [ ] Tests
* * [ ] Other

### Motivation and Context
Refactor token retrieval from route-based call to event-based handling


Diff:
diff --git a/index.html b/index.html
index 85c7efa5..4be386df 100644
--- a/index.html
+++ b/index.html
@@ -6,7 +6,7 @@
   <link rel="icon" href="/favicon.ico">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Integrations</title>
-  <script>
+  <script nonce="env">
     //CONFIGURATIONS_PLACEHOLDER
   </script>
 </head>
diff --git a/src/main.js b/src/main.js
index 7efa9bd1..5632f580 100644
--- a/src/main.js
+++ b/src/main.js
@@ -11,32 +11,41 @@ import './utils/plugins/Hotjar';
 import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
 import App from './App.vue';
 import router from './router';
+import { getJwtToken } from './utils/jwt';
+import { auth_store } from './stores/modules/auth.store';
 
-const app = createApp(App);
-app.config.productionTip = false;
+getJwtToken().then(() => {
+  const app = createApp(App);
+  app.config.productionTip = false;
 
-if (getEnv('NODE_ENV') === 'development') {
-  makeServer();
-}
+  if (getEnv('NODE_ENV') === 'development') {
+    makeServer();
+  }
 
-if (getEnv('VITE_APP_USE_SENTRY') && getEnv('VITE_APP_SENTRY_DSN')) {
-  Sentry.init({
-    dsn: getEnv('VITE_APP_SENTRY_DSN'),
-    integrations: [Sentry.browserTracingIntegration({ router }), Sentry.replayIntegration()],
-    logErrors: true,
+  if (getEnv('VITE_APP_USE_SENTRY') && getEnv('VITE_APP_SENTRY_DSN')) {
+    Sentry.init({
+      dsn: getEnv('VITE_APP_SENTRY_DSN'),
+      integrations: [Sentry.browserTracingIntegration({ router }), Sentry.replayIntegration()],
+      logErrors: true,
+    });
+  }
+
+  const pinia = createPinia();
+  pinia.use(piniaPluginPersistedstate);
+  pinia.use(({ store }) => {
+    store.router = markRaw(router);
   });
-}
 
-const pinia = createPinia();
-pinia.use(piniaPluginPersistedstate);
-pinia.use(({ store }) => {
-  store.router = markRaw(router);
-});
+  app.use(Unnnic);
+  app.use(pinia);
+  app.use(router);
+  app.use(i18n);
+  app.use(vueUse);
 
-app.use(Unnnic);
-app.use(pinia);
-app.use(router);
-app.use(i18n);
-app.use(vueUse);
+  const token = localStorage.getItem('authToken');
+  if (token) {
+    auth_store().externalLogin({ token });
+  }
 
-app.mount('#app');
+  app.mount('#app');
+});
diff --git a/src/router/index.js b/src/router/index.js
index 513e4827..22438a64 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -108,12 +108,11 @@ const router = createRouter({
       ],
     },
     {
-      path: '/loginexternal/:token/:project/:flowOrg',
+      path: '/:project/:flowOrg',
       name: 'externalLogin',
       component: null,
       beforeEnter: (to, from, next) => {
-        const { token, project, flowOrg } = to.params;
-        auth_store().externalLogin({ token: token.replace('+', ' ') });
+        const { project, flowOrg } = to.params;
         auth_store().selectedProject({ project });
         flowOrg ? auth_store().selectedFlowOrg({ flowOrg }) : auth_store().getFlowOrganization();
         if (to.query.next) {
diff --git a/src/tests/components/config/ecommerce/vtex/ConnectCatalogModalContent.spec.js b/src/tests/components/config/ecommerce/vtex/ConnectCatalogModalContent.spec.js
index 78582a56..99750e09 100644
--- a/src/tests/components/config/ecommerce/vtex/ConnectCatalogModalContent.spec.js
+++ b/src/tests/components/config/ecommerce/vtex/ConnectCatalogModalContent.spec.js
@@ -21,8 +21,8 @@ describe('ConnectCatalogModalContent.vue', () => {
 
     const input = wrapper.findComponent({ name: 'unnnic-input' });
     expect(input.exists()).toBe(true);
-    expect(input.props().label).toBe('Catalog name');
-    expect(input.props().placeholder).toBe('Ex: Weekly offers');
+    expect(input.props().label).toBe('Catalog ID');
+    expect(input.props().placeholder).toBe('');
 
     const footer = wrapper.find('.modal__content__form__footer');
     expect(footer.exists()).toBe(true);
diff --git a/src/utils/jwt.js b/src/utils/jwt.js
new file mode 100644
index 00000000..1b56f824
--- /dev/null
+++ b/src/utils/jwt.js
@@ -0,0 +1,17 @@
+export function getJwtToken() {
+  return new Promise((resolve) => {
+    const isInIframe = window.self !== window.top;
+
+    if (!isInIframe) resolve();
+
+    const eventHandler = (event) => {
+      if (event.data.event === 'updateToken') {
+        localStorage.setItem('authToken', `Bearer ${event.data.token}`);
+        window.removeEventListener('message', eventHandler);
+        return resolve();
+      }
+    };
+    window.addEventListener('message', eventHandler);
+    window.parent.postMessage({ event: 'getToken' }, '*');
+  });
+}


Reviews:
user: login: gemini-code-assist[bot]
body: Hello @acnormun, I'm Gemini Code Assist[^1]! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

## Summary of Changes
This pull request focuses on refactoring the token retrieval process within the application. Instead of relying on route-based calls, it shifts to an event-based handling mechanism. This involves changes to `index.html`, `src/main.js`, `src/router/index.js`, and the addition of a new file `src/utils/jwt.js` to manage JWT token retrieval. The goal is to improve the way the application handles authentication tokens, particularly when running within an iframe.

### Highlights
* **Token Retrieval Refactor**: The primary change is moving from route-based token retrieval to an event-based system for better handling of authentication, especially in iframe scenarios.
* **Event-Based Communication**: A new `getJwtToken` function in `src/utils/jwt.js` listens for 'updateToken' events to retrieve and store the authentication token.
* **Route Modification**: The `/loginexternal/:token/:project/:flowOrg` route in `src/router/index.js` has been modified to `/:project/:flowOrg`, removing the direct token parameter from the route.
* **Initial Token Load**: The `src/main.js` now attempts to load the token from local storage and use it to perform an external login via the auth store.

### Changelog
<details>
<summary>Click here to see the changelog</summary>

* **index.html**
  * Added a `nonce="env"` attribute to the `<script>` tag. This is likely for security purposes, possibly related to Content Security Policy (CSP).
* **src/main.js**
  * Imported `getJwtToken` from `src/utils/jwt.js` and `auth_store` from `src/stores/modules/auth.store.js`.
  * Wrapped the entire application initialization within a `getJwtToken().then()` block to ensure the token is retrieved before the app mounts.
  * After mounting, the code checks for an existing 'authToken' in localStorage and attempts to log in using that token.
* **src/router/index.js**
  * The `/loginexternal/:token/:project/:flowOrg` route was changed to `/:project/:flowOrg`, removing the token parameter from the route definition.
  * The `beforeEnter` guard now only extracts `project` and `flowOrg` from the route parameters.
* **src/utils/jwt.js**
  * A new `getJwtToken` function was added to handle token retrieval via message events.
  * The function checks if the application is running inside an iframe.
  * If it's in an iframe, it listens for 'updateToken' messages, stores the token in localStorage, and resolves the promise.
  * It also sends a 'getToken' message to the parent window to request the token.

</details>


<details>

<summary><b>Using Gemini Code Assist</b></summary>
<br>

The full guide for Gemini Code Assist can be found on our [documentation page](https://developers.google.com/gemini-code-assist/docs/review-github-code), here are some quick tips.

<b>Invoking Gemini</b>

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either `/gemini <command>` or `@gemini-code-assist <command>`. Below is a summary of the supported commands.

Feature | Command | Description
--- | --- | ---
Code Review | `/gemini review` | Performs a code review for the current pull request in its current state.
Pull Request Summary | `/gemini summary` | Provides a summary of the current pull request in its current state.
Comment | @gemini-code-assist | Responds in comments when explicitly tagged, both in issue comments and review comments.
Help | `/gemini help` | Displays a list of available commands.

<b>Customization</b>

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a `.gemini/` folder in the base of the repository. Detailed instructions can be found [here](https://developers.google.com/gemini-code-assist/docs/customize-gemini-behavior-github).

<b>Limitations & Feedback</b>

Gemini Code Assist is currently in preview and may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with :thumbsup: and :thumbsdown: on @gemini-code-assist comments to provide feedback.

</details>

<b>You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the [Gemini Code Assist IDE Extension](https://cloud.google.com/products/gemini/code-assist).</b>


[^1]: Review the [Privacy Notices](https://policies.google.com/privacy), [Generative AI Prohibited Use Policy](https://policies.google.com/terms/generative-ai/use-policy), [Terms of Service](https://policies.google.com/terms), and learn how to configure Gemini Code Assist in GitHub [here](https://developers.google.com/gemini-code-assist/docs/review-github-code). Gemini can make mistakes, so double check it and [use code with caution](https://support.google.com/legal/answer/13505487).

---

> A token's quest,
> From route to event's behest,
> Iframe's call rings true.


state: COMMENTED
author_association: NONE
submitted_at: 2025-04-03T19:44:30Z
user: login: gemini-code-assist[bot]
body: ## Code Review
The pull request refactors token retrieval to be event-based, which is a good approach for handling tokens in an iframe environment. The changes look good overall, but there are a few points that could be improved for clarity and maintainability.

### Summary of Findings
* **Token Storage**: Storing the token directly in localStorage without proper encoding or security considerations can be a potential vulnerability. Consider using more secure storage mechanisms or encoding techniques.
* **Event Listener Management**: The event listener added in `getJwtToken` is removed only when the 'updateToken' event is received. If the event is never received, the listener remains active, potentially leading to memory leaks. Ensure proper cleanup of the event listener in all scenarios.
* **Route Parameter Handling**: The route parameter handling in `src/router/index.js` has been simplified, which is good. However, ensure that the new route structure is well-documented and communicated to other developers to avoid confusion.

### Merge Readiness
The changes are a good step towards improving token retrieval. However, the security concern regarding token storage should be addressed before merging. Additionally, ensure that the event listener is properly cleaned up to prevent potential memory leaks. I am unable to directly approve this pull request, and recommend that others review and approve this code before merging.
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-04-03T19:44:42Z
user: login: Aldemylla
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-08T21:17:16Z

Review comments:
diff_hunk: @@ -0,0 +1,17 @@
+export function getJwtToken() {
+  return new Promise((resolve) => {
+    const isInIframe = window.self !== window.top;
+
+    if (!isInIframe) resolve();
+
+    const eventHandler = (event) => {
+      if (event.data.event === 'updateToken') {
+        localStorage.setItem('authToken', `Bearer ${event.data.token}`);
+        window.removeEventListener('message', eventHandler);
+        return resolve();
+      }
+    };
path: src/utils/jwt.js
user: login: gemini-code-assist[bot]
body: ![high](https://img.shields.io/badge/priority-high-orange)

The event listener is only removed when the 'updateToken' event is received. If the event is never received, the listener will remain active. Consider adding a timeout to remove the listener after a certain period to prevent potential memory leaks.

```javascript
    const eventHandler = (event) => {
      if (event.data.event === 'updateToken') {
        localStorage.setItem('authToken', `Bearer ${event.data.token}`);
        window.removeEventListener('message', eventHandler);
        clearTimeout(timeout);
        return resolve();
      }
    };
    const timeout = setTimeout(() => {
      window.removeEventListener('message', eventHandler);
      resolve();
    }, 5000); // Timeout after 5 seconds
```
created_at: 2025-04-03T19:44:41Z
author_association: NONE
diff_hunk: @@ -0,0 +1,17 @@
+export function getJwtToken() {
+  return new Promise((resolve) => {
+    const isInIframe = window.self !== window.top;
+
+    if (!isInIframe) resolve();
+
+    const eventHandler = (event) => {
+      if (event.data.event === 'updateToken') {
+        localStorage.setItem('authToken', `Bearer ${event.data.token}`);
path: src/utils/jwt.js
user: login: gemini-code-assist[bot]
body: ![high](https://img.shields.io/badge/priority-high-orange)

Storing the token directly in `localStorage` can be a security risk. Consider encoding the token or using a more secure storage mechanism like `IndexedDB` or `cookies` with appropriate security flags.

```javascript
localStorage.setItem('authToken', btoa(`Bearer ${event.data.token}`)); // Encode the token
```
created_at: 2025-04-03T19:44:42Z
author_association: NONE
diff_hunk: @@ -6,7 +6,7 @@
   <link rel="icon" href="/favicon.ico">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Integrations</title>
-  <script>
+  <script nonce="env">
path: index.html
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Adding `nonce="env"` to the script tag is a good security practice to allow only specific scripts to run. However, ensure that the server is properly configured to generate and manage the nonce value to prevent potential bypasses.
created_at: 2025-04-03T19:44:42Z
author_association: NONE
diff_hunk: @@ -11,32 +11,44 @@
 import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
 import App from './App.vue';
 import router from './router';
+import { getJwtToken } from './utils/jwt';
+import { auth_store } from './stores/modules/auth.store';
 
-const app = createApp(App);
-app.config.productionTip = false;
+getJwtToken().then(() => {
+  const app = createApp(App);
+  app.config.productionTip = false;
 
-if (getEnv('NODE_ENV') === 'development') {
-  makeServer();
-}
+  if (getEnv('NODE_ENV') === 'development') {
+    makeServer();
+  }
 
-if (getEnv('VITE_APP_USE_SENTRY') && getEnv('VITE_APP_SENTRY_DSN')) {
-  Sentry.init({
-    dsn: getEnv('VITE_APP_SENTRY_DSN'),
-    integrations: [Sentry.browserTracingIntegration({ router }), Sentry.replayIntegration()],
-    logErrors: true,
+  if (getEnv('VITE_APP_USE_SENTRY') && getEnv('VITE_APP_SENTRY_DSN')) {
+    Sentry.init({
+      dsn: getEnv('VITE_APP_SENTRY_DSN'),
+      integrations: [Sentry.browserTracingIntegration({ router }), Sentry.replayIntegration()],
+      logErrors: true,
+    });
+  }
+
+  const pinia = createPinia();
+  pinia.use(piniaPluginPersistedstate);
+  pinia.use(({ store }) => {
+    store.router = markRaw(router);
   });
-}
 
-const pinia = createPinia();
-pinia.use(piniaPluginPersistedstate);
-pinia.use(({ store }) => {
-  store.router = markRaw(router);
-});
+  app.use(Unnnic);
+  app.use(pinia);
+  app.use(router);
+  app.use(i18n);
+  app.use(vueUse);
 
-app.use(Unnnic);
-app.use(pinia);
-app.use(router);
-app.use(i18n);
-app.use(vueUse);
+  const token = localStorage.getItem('authToken');
+  if (token) {
+    console.log('token: ', token);
+    auth_store().externalLogin({ token });
+  } else {
+    console.log('no token');
+  }
path: src/main.js
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Consider adding a check to ensure that `localStorage` is available before attempting to access it. This can prevent errors in environments where `localStorage` is disabled or unavailable.

```suggestion
  if (typeof localStorage !== 'undefined') {
    const token = localStorage.getItem('authToken');
    if (token) {
      console.log('token: ', token);
      auth_store().externalLogin({ token });
    } else {
      console.log('no token');
    }
  }
```
created_at: 2025-04-03T19:44:42Z
author_association: NONE
diff_hunk: @@ -108,12 +108,11 @@ const router = createRouter({
       ],
     },
     {
-      path: '/loginexternal/:token/:project/:flowOrg',
+      path: '/:project/:flowOrg',
       name: 'externalLogin',
       component: null,
path: src/router/index.js
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

The route path has been changed from `/loginexternal/:token/:project/:flowOrg` to `/:project/:flowOrg`. Ensure that all references to the old route are updated and that the new route structure is well-documented.
created_at: 2025-04-03T19:44:42Z
author_association: NONE

PR comments:
user: login: codecov[bot]
created_at: 2025-04-03T19:44:48Z
body: ## [Codecov](https://app.codecov.io/gh/weni-ai/weni-integrations-webapp/pull/511?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=weni-ai) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 86.96%. Comparing base [(`e573d12`)](https://app.codecov.io/gh/weni-ai/weni-integrations-webapp/commit/e573d12859e9f78d4b77ac3416f603c9aba3e840?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=weni-ai) to head [(`0e719eb`)](https://app.codecov.io/gh/weni-ai/weni-integrations-webapp/commit/0e719eb88eba39166c1a8e61a25605eaae1880ea?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=weni-ai).
> Report is 15 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main     #511   +/-   ##
=======================================
  Coverage   86.96%   86.96%           
=======================================
  Files         186      186           
  Lines       21239    21239           
  Branches     1766     1768    +2     
=======================================
  Hits        18471    18471           
  Misses       2768     2768           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/weni-ai/weni-integrations-webapp/pull/511?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=weni-ai).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=weni-ai).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: gemini-code-assist[bot]
created_at: 2025-04-08T21:48:20Z
body: > [!WARNING]
> You have reached your daily quota limit. As a reminder, free tier users are limited to 5 requests per day. Please wait up to 24 hours and I will start processing your requests again!
author_association: NONE
user: login: gemini-code-assist[bot]
created_at: 2025-04-08T21:48:22Z
body: > [!WARNING]
> You have reached your daily quota limit. As a reminder, free tier users are limited to 5 requests per day. Please wait up to 24 hours and I will start processing your requests again!
author_association: NONE
user: login: gemini-code-assist[bot]
created_at: 2025-04-08T21:48:25Z
body: > [!WARNING]
> You have reached your daily quota limit. As a reminder, free tier users are limited to 5 requests per day. Please wait up to 24 hours and I will start processing your requests again!
author_association: NONE
user: login: gemini-code-assist[bot]
created_at: 2025-04-08T21:48:27Z
body: > [!WARNING]
> You have reached your daily quota limit. As a reminder, free tier users are limited to 5 requests per day. Please wait up to 24 hours and I will start processing your requests again!
author_association: NONE
user: login: gemini-code-assist[bot]
created_at: 2025-04-08T21:48:29Z
body: > [!WARNING]
> You have reached your daily quota limit. As a reminder, free tier users are limited to 5 requests per day. Please wait up to 24 hours and I will start processing your requests again!
author_association: NONE