Skip to content

Conversation

@ddeskov-limechain
Copy link
Contributor

Problem
When users tab away from the app and return later, all API requests fail with "Unauthorized" toast errors. The JWT token expires while the app is idle, but there's no mechanism to detect this and trigger re-authentication when the app regains focus.

What Happens:

  1. When you're logged in, your JWT token is stored in sessionStorage
  2. The backend validates the token on every API request via the JwtBlacklistGuard back-end/apps/api/src/guards/jwt-blacklist.guard.ts
  3. If the token is expired or blacklisted, the backend returns a 401 with { message: 'Unauthorized', statusCode: 401 }
  4. The frontend displays this error via toasts (with duration: 0 meaning they persist until dismissed)
    Why It Happens After Tabbing Away:
    The app has no window focus or visibility change handlers. I searched the entire frontend codebase:
  • No visibilitychange event listener
  • No window.focus event handling
  • No logic to check/refresh tokens when the app regains focus
    The only auto-login logic is in AutoLoginInOrganization.vue but it only runs once when organizations are initially loaded - not when the app regains focus.

Solution
Add a visibility change listener that validates tokens when the app becomes visible and triggers re-authentication if needed.

Implementation:

  1. Create New Composable: useAppVisibility.ts (front-end/src/renderer/composables/useAppVisibility.ts)
    Creates a composable that:
  • Listens to document.visibilitychange events
  • Debounces rapid visibility changes (2 seconds)
  • Checks if any organization tokens need refresh via shouldSignInOrganization()
  • Calls a callback when tokens are expired
  1. Modify AutoLoginInOrganization.vue (front-end/src/renderer/components/Organization/AutoLoginInOrganization.vue)
    Changes:
  • Add defineExpose() to expose a triggerReauthentication() method
  • This method resets the checked flag and calls openPasswordModalIfRequired()
  1. Integrate in GlobalAppProcesses.vue (front-end/src/renderer/components/GlobalAppProcesses/GlobalAppProcesses.vue)
    Changes:
  • Import useAppVisibility composable
  • Add a template ref to AutoLoginInOrganization
  • Use the composable with a callback that triggers re-authentication

Signed-off-by: Dian Deskov <[email protected]>
@codecov
Copy link

codecov bot commented Jan 20, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.85%. Comparing base (466ccd2) to head (eba0261).

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main    #2185   +/-   ##
=======================================
  Coverage   99.85%   99.85%           
=======================================
  Files         176      176           
  Lines        6763     6763           
  Branches     1301     1301           
=======================================
  Hits         6753     6753           
  Misses         10       10           

Impacted file tree graph

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request fixes the "Unauthorized" errors that occur when users tab away from the application and return after their JWT tokens have expired. The solution adds automatic token validation and re-authentication when the app regains focus.

Changes:

  • New composable useAppVisibility that listens for visibility changes and validates organization tokens
  • Exposed triggerReauthentication method in AutoLoginInOrganization component for programmatic re-authentication
  • Integration of the composable in GlobalAppProcesses to handle token expiration on app focus

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
front-end/src/renderer/composables/useAppVisibility.ts New composable that monitors document visibility changes, checks token validity when app becomes visible, and triggers re-authentication callbacks when tokens are expired
front-end/src/renderer/components/Organization/AutoLoginInOrganization.vue Exposed triggerReauthentication method to allow external triggering of the password modal for organization re-authentication
front-end/src/renderer/components/GlobalAppProcesses/GlobalAppProcesses.vue Integrated the useAppVisibility composable with a callback that triggers re-authentication via the AutoLoginInOrganization component reference

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +57 to +64
await user.refetchOrganizationTokens();

if (!tokensValid) {
await user.refetchOrganizations();

if (onTokenExpired) {
await onTokenExpired();
}
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The call to refetchOrganizationTokens is redundant when tokens are invalid. The refetchOrganizations function on line 60 already calls refetchOrganizationTokens internally, so calling it on line 57 before checking tokensValid results in fetching tokens twice when they are invalid. Consider moving the refetchOrganizationTokens call into an else block to only execute when tokens are valid.

Suggested change
await user.refetchOrganizationTokens();
if (!tokensValid) {
await user.refetchOrganizations();
if (onTokenExpired) {
await onTokenExpired();
}
if (!tokensValid) {
await user.refetchOrganizations();
if (onTokenExpired) {
await onTokenExpired();
}
} else {
await user.refetchOrganizationTokens();

Copilot uses AI. Check for mistakes.
Comment on lines +60 to +63
await user.refetchOrganizations();

if (onTokenExpired) {
await onTokenExpired();
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a potential race condition where openPasswordModalIfRequired could be called twice when tokens expire. The sequence is: refetchOrganizations triggers the watcher which sets checked to true and calls openPasswordModalIfRequired, then onTokenExpired is called which triggers triggerReauthentication, setting checked to false and calling openPasswordModalIfRequired again. This could result in duplicate password prompts or authentication attempts.

Suggested change
await user.refetchOrganizations();
if (onTokenExpired) {
await onTokenExpired();
if (onTokenExpired) {
// Delegate token expiry handling to the provided callback to avoid
// triggering re-authentication flows twice (e.g., via refetchOrganizations watcher)
await onTokenExpired();
} else {
await user.refetchOrganizations();

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants