From 5215a0bdb8c3a33387296ce111bee25c8dfbb1db Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Fri, 26 Sep 2025 17:48:41 +0200 Subject: [PATCH] Add logout event emitter (#1274) --- .../entrypoints/popup/context/AppContext.tsx | 17 ++++- .../entrypoints/popup/context/AuthContext.tsx | 2 +- .../popup/context/NavigationContext.tsx | 4 - .../entrypoints/popup/pages/auth/Login.tsx | 2 +- .../src/events/LogoutEventEmitter.ts | 38 ++++++++++ .../src/i18n/locales/ca.json | 3 +- .../src/i18n/locales/de.json | 3 +- .../src/i18n/locales/en.json | 2 +- .../src/i18n/locales/es.json | 3 +- .../src/i18n/locales/fi.json | 3 +- .../src/i18n/locales/fr.json | 3 +- .../src/i18n/locales/he.json | 3 +- .../src/i18n/locales/it.json | 3 +- .../src/i18n/locales/nl.json | 3 +- .../src/i18n/locales/pt.json | 3 +- .../src/i18n/locales/ru.json | 3 +- .../src/i18n/locales/sv.json | 3 +- .../src/i18n/locales/tr.json | 3 +- .../src/i18n/locales/uk.json | 3 +- .../src/i18n/locales/zh.json | 3 +- .../src/utils/WebApiService.ts | 74 ++++++++++--------- 21 files changed, 108 insertions(+), 73 deletions(-) create mode 100644 apps/browser-extension/src/events/LogoutEventEmitter.ts diff --git a/apps/browser-extension/src/entrypoints/popup/context/AppContext.tsx b/apps/browser-extension/src/entrypoints/popup/context/AppContext.tsx index 0288b2b2f..2845ed368 100644 --- a/apps/browser-extension/src/entrypoints/popup/context/AppContext.tsx +++ b/apps/browser-extension/src/entrypoints/popup/context/AppContext.tsx @@ -1,8 +1,11 @@ import React, { createContext, useContext, useMemo, useCallback, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useAuth } from '@/entrypoints/popup/context/AuthContext'; import { useWebApi } from '@/entrypoints/popup/context/WebApiContext'; +import { logoutEventEmitter } from '@/events/LogoutEventEmitter'; + type AppContextType = { isLoggedIn: boolean; isInitialized: boolean; @@ -23,6 +26,7 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children const auth = useAuth(); const webApi = useWebApi(); const [isLoggedIn, setIsLoggedIn] = useState(false); + const { t } = useTranslation(); /** * Logout the user by revoking tokens and clearing the auth tokens from storage. @@ -38,13 +42,22 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children * @returns boolean indicating whether the user is logged in. */ const initializeAuth = useCallback(async () : Promise => { - console.log('initializeAuth'); const isLoggedIn = await auth.initializeAuth(); - console.log('isLoggedIn', isLoggedIn); setIsLoggedIn(isLoggedIn); return isLoggedIn; }, [auth]); + /** + * Subscribe to logout events from WebApiService. + */ + useEffect(() => { + const unsubscribe = logoutEventEmitter.subscribe(async (errorKey: string) => { + await logout(t(errorKey)); + }); + + return unsubscribe; + }, [logout, t]); + /** * Check for tokens in browser local storage on initial load when this context is mounted. */ diff --git a/apps/browser-extension/src/entrypoints/popup/context/AuthContext.tsx b/apps/browser-extension/src/entrypoints/popup/context/AuthContext.tsx index 46a256606..c64326fa6 100644 --- a/apps/browser-extension/src/entrypoints/popup/context/AuthContext.tsx +++ b/apps/browser-extension/src/entrypoints/popup/context/AuthContext.tsx @@ -1,4 +1,4 @@ -import React, { createContext, useContext, useState, useEffect, useMemo, useCallback } from 'react'; +import React, { createContext, useContext, useState, useMemo, useCallback } from 'react'; import { sendMessage } from 'webext-bridge/popup'; import { useDb } from '@/entrypoints/popup/context/DbContext'; diff --git a/apps/browser-extension/src/entrypoints/popup/context/NavigationContext.tsx b/apps/browser-extension/src/entrypoints/popup/context/NavigationContext.tsx index e63e8e029..27f881bcd 100644 --- a/apps/browser-extension/src/entrypoints/popup/context/NavigationContext.tsx +++ b/apps/browser-extension/src/entrypoints/popup/context/NavigationContext.tsx @@ -85,10 +85,6 @@ export const NavigationProvider: React.FC<{ children: React.ReactNode }> = ({ ch // Listen on isloggedin state to redirect to login page if not logged in useEffect(() => { - console.log('authInitialized', authInitialized); - console.log('dbInitialized', dbInitialized); - console.log('isFullyInitialized', isFullyInitialized); - console.log('isLoggedIn', isLoggedIn); if (isFullyInitialized && !isLoggedIn) { navigate('/login', { replace: true }); } diff --git a/apps/browser-extension/src/entrypoints/popup/pages/auth/Login.tsx b/apps/browser-extension/src/entrypoints/popup/pages/auth/Login.tsx index bbec4367f..90941bf05 100644 --- a/apps/browser-extension/src/entrypoints/popup/pages/auth/Login.tsx +++ b/apps/browser-extension/src/entrypoints/popup/pages/auth/Login.tsx @@ -223,7 +223,7 @@ const Login: React.FC = () => { showLoading(); if (!passwordHashString || !passwordHashBase64 || !loginResponse) { - throw new Error(t('auth.errors.loginDataMissing')); + throw new Error(t('common.errors.unknownError')); } // Validate that 2FA code is a 6-digit number diff --git a/apps/browser-extension/src/events/LogoutEventEmitter.ts b/apps/browser-extension/src/events/LogoutEventEmitter.ts new file mode 100644 index 000000000..3150a36da --- /dev/null +++ b/apps/browser-extension/src/events/LogoutEventEmitter.ts @@ -0,0 +1,38 @@ +type LogoutListener = (errorMessage: string) => void | Promise; + +/** + * Simple event emitter for logout events to avoid circular dependencies + * between WebApiService and Auth contexts. + */ +class LogoutEventEmitter { + private listeners: Set = new Set(); + + /** + * Subscribe to logout events. + * Returns an unsubscribe function. + */ + public subscribe(listener: LogoutListener): () => void { + this.listeners.add(listener); + return () => { + this.listeners.delete(listener); + }; + } + + /** + * Emit a logout event to all listeners. + * + * @param errorKey - The translation key of the error message to emit. + */ + public emit(errorTranslationKey: string): void { + this.listeners.forEach(listener => { + try { + listener(errorTranslationKey); + } catch (error) { + console.error('Error in logout listener:', error); + } + }); + } +} + +// Export singleton instance +export const logoutEventEmitter = new LogoutEventEmitter(); diff --git a/apps/browser-extension/src/i18n/locales/ca.json b/apps/browser-extension/src/i18n/locales/ca.json index 44cb1f6b0..18f2a3375 100644 --- a/apps/browser-extension/src/i18n/locales/ca.json +++ b/apps/browser-extension/src/i18n/locales/ca.json @@ -37,8 +37,7 @@ "migrationError": "An error occurred while checking for pending migrations.", "wrongPassword": "Incorrect password. Please try again.", "accountLocked": "Account temporarily locked due to too many failed attempts.", - "networkError": "Network error. Please check your connection and try again.", - "loginDataMissing": "Login session expired. Please try again." + "networkError": "Network error. Please check your connection and try again." } }, "menu": { diff --git a/apps/browser-extension/src/i18n/locales/de.json b/apps/browser-extension/src/i18n/locales/de.json index b9419575b..2fabe666b 100644 --- a/apps/browser-extension/src/i18n/locales/de.json +++ b/apps/browser-extension/src/i18n/locales/de.json @@ -37,8 +37,7 @@ "migrationError": "Beim Prüfen auf ausstehende Migrationen ist ein Fehler aufgetreten.", "wrongPassword": "Falsches Passwort. Bitte versuche es erneut.", "accountLocked": "Das Konto wurde wegen zu vieler fehlgeschlagener Anmeldeversuche vorübergehend gesperrt.", - "networkError": "Netzwerkfehler. Bitte überprüfe Deine Verbindung und versuche es erneut.", - "loginDataMissing": "Deine Anmelde-Sitzung ist abgelaufen. Bitte versuche es erneut." + "networkError": "Netzwerkfehler. Bitte überprüfe Deine Verbindung und versuche es erneut." } }, "menu": { diff --git a/apps/browser-extension/src/i18n/locales/en.json b/apps/browser-extension/src/i18n/locales/en.json index 86732e6b9..a3a3370b2 100644 --- a/apps/browser-extension/src/i18n/locales/en.json +++ b/apps/browser-extension/src/i18n/locales/en.json @@ -38,7 +38,7 @@ "wrongPassword": "Incorrect password. Please try again.", "accountLocked": "Account temporarily locked due to too many failed attempts.", "networkError": "Network error. Please check your connection and try again.", - "loginDataMissing": "Login session expired. Please try again." + "sessionExpired": "Your session has expired. Please log in again." } }, "menu": { diff --git a/apps/browser-extension/src/i18n/locales/es.json b/apps/browser-extension/src/i18n/locales/es.json index 38d57da6b..4ab9bc14a 100644 --- a/apps/browser-extension/src/i18n/locales/es.json +++ b/apps/browser-extension/src/i18n/locales/es.json @@ -37,8 +37,7 @@ "migrationError": "An error occurred while checking for pending migrations.", "wrongPassword": "Incorrect password. Please try again.", "accountLocked": "Account temporarily locked due to too many failed attempts.", - "networkError": "Network error. Please check your connection and try again.", - "loginDataMissing": "Login session expired. Please try again." + "networkError": "Network error. Please check your connection and try again." } }, "menu": { diff --git a/apps/browser-extension/src/i18n/locales/fi.json b/apps/browser-extension/src/i18n/locales/fi.json index ee7865132..136982584 100644 --- a/apps/browser-extension/src/i18n/locales/fi.json +++ b/apps/browser-extension/src/i18n/locales/fi.json @@ -37,8 +37,7 @@ "migrationError": "Tapahtui virhe tarkistettaessa odottavia siirtoja.", "wrongPassword": "Virheellinen salasana. Yritä uudelleen.", "accountLocked": "Tili on tilapäisesti lukittu liian monen epäonnistuneen yrityksen vuoksi. Yritä myöhemmin uudelleen.", - "networkError": "Verkkovirhe: tarkista yhteytesi ja yritä uudelleen.", - "loginDataMissing": "Kirjautumisistunto on vanhentunut. Yritä uudelleen." + "networkError": "Verkkovirhe: tarkista yhteytesi ja yritä uudelleen." } }, "menu": { diff --git a/apps/browser-extension/src/i18n/locales/fr.json b/apps/browser-extension/src/i18n/locales/fr.json index 069c0fe95..891e4ba96 100644 --- a/apps/browser-extension/src/i18n/locales/fr.json +++ b/apps/browser-extension/src/i18n/locales/fr.json @@ -37,8 +37,7 @@ "migrationError": "Une erreur s'est produite lors de la vérification des migrations en attente.", "wrongPassword": "Mot de passe incorrect, veuillez réessayer.", "accountLocked": "Compte temporairement verrouillé en raison d'un trop grand nombre de tentatives échouées.", - "networkError": "Erreur réseau. Vérifiez votre connexion et réessayez.", - "loginDataMissing": "La session a expiré. Veuillez réessayer." + "networkError": "Erreur réseau. Vérifiez votre connexion et réessayez." } }, "menu": { diff --git a/apps/browser-extension/src/i18n/locales/he.json b/apps/browser-extension/src/i18n/locales/he.json index 8abb0b834..133e2ee83 100644 --- a/apps/browser-extension/src/i18n/locales/he.json +++ b/apps/browser-extension/src/i18n/locales/he.json @@ -37,8 +37,7 @@ "migrationError": "אירעה שגיאה בעת בדיקה לאיתור הסבות ממתינות.", "wrongPassword": "סיסמה שגויה. נא לנסות שוב.", "accountLocked": "החשבון נעול זמנית עקב ריבוי ניסיונות כושלים.", - "networkError": "שגיאת רשת. נא לבדוק את החיבור ולנסות שוב.", - "loginDataMissing": "תוקף ההפעלה שלך פג. נא לנסות שוב." + "networkError": "שגיאת רשת. נא לבדוק את החיבור ולנסות שוב." } }, "menu": { diff --git a/apps/browser-extension/src/i18n/locales/it.json b/apps/browser-extension/src/i18n/locales/it.json index eb24ca8c2..d48ab0736 100644 --- a/apps/browser-extension/src/i18n/locales/it.json +++ b/apps/browser-extension/src/i18n/locales/it.json @@ -37,8 +37,7 @@ "migrationError": "Si è verificato un errore nel controllo delle migrazioni pendenti.", "wrongPassword": "Password non corretta. Riprova nuovamente.", "accountLocked": "Account temporaneamente bloccato a causa di troppi tentativi falliti.", - "networkError": "Errore di rete: Controlla la tua connessione e riprova.", - "loginDataMissing": "Sessione di accesso scaduta. Effettua nuovamente l'accesso." + "networkError": "Errore di rete: Controlla la tua connessione e riprova." } }, "menu": { diff --git a/apps/browser-extension/src/i18n/locales/nl.json b/apps/browser-extension/src/i18n/locales/nl.json index 47b77f3ff..232257bfc 100644 --- a/apps/browser-extension/src/i18n/locales/nl.json +++ b/apps/browser-extension/src/i18n/locales/nl.json @@ -37,8 +37,7 @@ "migrationError": "Er is een fout opgetreden bij het controleren op updates.", "wrongPassword": "Onjuist wachtwoord. Probeer het opnieuw.", "accountLocked": "Account tijdelijk vergrendeld vanwege te veel mislukte pogingen.", - "networkError": "Netwerkfout. Controleer de verbinding en probeer het opnieuw.", - "loginDataMissing": "Sessie verlopen. Probeer het opnieuw." + "networkError": "Netwerkfout. Controleer de verbinding en probeer het opnieuw." } }, "menu": { diff --git a/apps/browser-extension/src/i18n/locales/pt.json b/apps/browser-extension/src/i18n/locales/pt.json index 397811595..ddb727155 100644 --- a/apps/browser-extension/src/i18n/locales/pt.json +++ b/apps/browser-extension/src/i18n/locales/pt.json @@ -37,8 +37,7 @@ "migrationError": "An error occurred while checking for pending migrations.", "wrongPassword": "Incorrect password. Please try again.", "accountLocked": "Account temporarily locked due to too many failed attempts.", - "networkError": "Network error. Please check your connection and try again.", - "loginDataMissing": "Login session expired. Please try again." + "networkError": "Network error. Please check your connection and try again." } }, "menu": { diff --git a/apps/browser-extension/src/i18n/locales/ru.json b/apps/browser-extension/src/i18n/locales/ru.json index d24dec108..0fa7e23a1 100644 --- a/apps/browser-extension/src/i18n/locales/ru.json +++ b/apps/browser-extension/src/i18n/locales/ru.json @@ -37,8 +37,7 @@ "migrationError": "Возникла ошибка при проверке ожидающих перемещений.", "wrongPassword": "Неверный пароль. Пожалуйста, повторите попытку.", "accountLocked": "Аккаунт временно заблокирован из-за слишком большого числа неудачных попыток.", - "networkError": "Ошибка сети. Пожалуйста, проверьте соединение и повторите еще раз.", - "loginDataMissing": "Время входа истекло. Пожалуйста, повторите попытку." + "networkError": "Ошибка сети. Пожалуйста, проверьте соединение и повторите еще раз." } }, "menu": { diff --git a/apps/browser-extension/src/i18n/locales/sv.json b/apps/browser-extension/src/i18n/locales/sv.json index 397811595..ddb727155 100644 --- a/apps/browser-extension/src/i18n/locales/sv.json +++ b/apps/browser-extension/src/i18n/locales/sv.json @@ -37,8 +37,7 @@ "migrationError": "An error occurred while checking for pending migrations.", "wrongPassword": "Incorrect password. Please try again.", "accountLocked": "Account temporarily locked due to too many failed attempts.", - "networkError": "Network error. Please check your connection and try again.", - "loginDataMissing": "Login session expired. Please try again." + "networkError": "Network error. Please check your connection and try again." } }, "menu": { diff --git a/apps/browser-extension/src/i18n/locales/tr.json b/apps/browser-extension/src/i18n/locales/tr.json index 73472c5b5..024e6fae9 100644 --- a/apps/browser-extension/src/i18n/locales/tr.json +++ b/apps/browser-extension/src/i18n/locales/tr.json @@ -37,8 +37,7 @@ "migrationError": "An error occurred while checking for pending migrations.", "wrongPassword": "Incorrect password. Please try again.", "accountLocked": "Account temporarily locked due to too many failed attempts.", - "networkError": "Network error. Please check your connection and try again.", - "loginDataMissing": "Login session expired. Please try again." + "networkError": "Network error. Please check your connection and try again." } }, "menu": { diff --git a/apps/browser-extension/src/i18n/locales/uk.json b/apps/browser-extension/src/i18n/locales/uk.json index 2b78750b1..16ec9d1d1 100644 --- a/apps/browser-extension/src/i18n/locales/uk.json +++ b/apps/browser-extension/src/i18n/locales/uk.json @@ -37,8 +37,7 @@ "migrationError": "Під час перевірки незавершених перенесень сталася помилка.", "wrongPassword": "Невірний пароль. Будь ласка, спробуйте ще раз.", "accountLocked": "Обліковий запис тимчасово заблоковано через занадто багато невдалих спроб.", - "networkError": "Помилка мережі. Будь ласка, перевірте з’єднання та спробуйте ще раз.", - "loginDataMissing": "Термін дії сеансу закінчився. Будь ласка, спробуйте ще раз." + "networkError": "Помилка мережі. Будь ласка, перевірте з’єднання та спробуйте ще раз." } }, "menu": { diff --git a/apps/browser-extension/src/i18n/locales/zh.json b/apps/browser-extension/src/i18n/locales/zh.json index 64feccef8..ae9d482e5 100644 --- a/apps/browser-extension/src/i18n/locales/zh.json +++ b/apps/browser-extension/src/i18n/locales/zh.json @@ -37,8 +37,7 @@ "migrationError": "检查待处理迁移时发生错误。", "wrongPassword": "密码不正确。请重试。", "accountLocked": "由于多次尝试失败,账户已暂时锁定。", - "networkError": "网络错误。请检查你的连接后重试。", - "loginDataMissing": "登录会话已过期。请重试。" + "networkError": "网络错误。请检查你的连接后重试。" } }, "menu": { diff --git a/apps/browser-extension/src/utils/WebApiService.ts b/apps/browser-extension/src/utils/WebApiService.ts index 3fb2857ab..bc7ef24ab 100644 --- a/apps/browser-extension/src/utils/WebApiService.ts +++ b/apps/browser-extension/src/utils/WebApiService.ts @@ -1,5 +1,7 @@ import type { StatusResponse } from '@/utils/dist/shared/models/webapi'; +import { logoutEventEmitter } from '@/events/LogoutEventEmitter'; + import { AppInfo } from "./AppInfo"; import { storage } from '#imports'; @@ -74,7 +76,7 @@ export class WebApiService { return parseJson ? retryResponse.json() : retryResponse as unknown as T; } else { - this.logout('Your session has expired. Please login again.'); + logoutEventEmitter.emit('auth.errors.sessionExpired'); throw new Error('Session expired'); } } @@ -118,41 +120,6 @@ export class WebApiService { } } - /** - * Refresh the access token. - */ - private async refreshAccessToken(): Promise { - const refreshToken = await this.getRefreshToken(); - if (!refreshToken) { - return null; - } - - try { - const response = await this.rawFetch('Auth/refresh', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Ignore-Failure': 'true', - }, - body: JSON.stringify({ - token: await this.getAccessToken(), - refreshToken: refreshToken, - }), - }); - - if (!response.ok) { - throw new Error('Failed to refresh token'); - } - - const tokenResponse: TokenResponse = await response.json(); - this.updateTokens(tokenResponse.token, tokenResponse.refreshToken); - return tokenResponse.token; - } catch { - this.logout('Your session has expired. Please login again.'); - return null; - } - } - /** * Issue GET request to the API. */ @@ -271,6 +238,41 @@ export class WebApiService { return null; } + /** + * Refresh the access token. + */ + private async refreshAccessToken(): Promise { + const refreshToken = await this.getRefreshToken(); + if (!refreshToken) { + return null; + } + + try { + const response = await this.rawFetch('Auth/refresh', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Ignore-Failure': 'true', + }, + body: JSON.stringify({ + token: await this.getAccessToken(), + refreshToken: refreshToken, + }), + }); + + if (!response.ok) { + throw new Error('Failed to refresh token'); + } + + const tokenResponse: TokenResponse = await response.json(); + this.updateTokens(tokenResponse.token, tokenResponse.refreshToken); + return tokenResponse.token; + } catch { + logoutEventEmitter.emit('auth.errors.sessionExpired'); + return null; + } + } + /** * Get the current access token from storage. */