mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 23:21:08 +02:00
feat(auth): add session expired banner when token refresh fails
Users now see an amber banner with a re-login button instead of a broken empty page when their session expires. Uses pub/sub events from tokenManager, integrated in todo, calendar, zitare, contacts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
02db49175a
commit
d6440664ac
9 changed files with 376 additions and 0 deletions
|
|
@ -7,6 +7,7 @@ import type {
|
|||
import { TokenState as TokenStateEnum } from '../types';
|
||||
import { isDeviceConnected, hasStableConnection } from '../adapters/network';
|
||||
import type { AuthService } from './authService';
|
||||
import { emitSessionExpired } from '../events/sessionExpired';
|
||||
|
||||
/**
|
||||
* Configuration for the token manager
|
||||
|
|
@ -110,6 +111,7 @@ export function createTokenManager(authService: AuthService, config?: TokenManag
|
|||
try {
|
||||
await authService.clearAuthStorage();
|
||||
setState(TokenStateEnum.EXPIRED);
|
||||
emitSessionExpired();
|
||||
} catch (error) {
|
||||
console.debug('Error in handleRefreshFailure:', error);
|
||||
}
|
||||
|
|
|
|||
68
packages/shared-auth/src/events/sessionExpired.ts
Normal file
68
packages/shared-auth/src/events/sessionExpired.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
/**
|
||||
* Session expired event system
|
||||
*
|
||||
* Provides a simple pub/sub mechanism for notifying UI layers
|
||||
* when a user's session has permanently expired (token refresh failed).
|
||||
*
|
||||
* This is intentionally kept framework-agnostic so it can be consumed
|
||||
* by Svelte, React, or plain JS consumers.
|
||||
*/
|
||||
|
||||
type SessionExpiredListener = () => void;
|
||||
|
||||
const listeners = new Set<SessionExpiredListener>();
|
||||
|
||||
let _sessionExpired = false;
|
||||
|
||||
/**
|
||||
* Subscribe to session expired events.
|
||||
* Returns an unsubscribe function.
|
||||
*/
|
||||
export function onSessionExpired(listener: SessionExpiredListener): () => void {
|
||||
listeners.add(listener);
|
||||
|
||||
// If session is already expired, notify immediately
|
||||
if (_sessionExpired) {
|
||||
try {
|
||||
listener();
|
||||
} catch {
|
||||
// Ignore listener errors
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a session expired event.
|
||||
* Called internally by the token manager when refresh fails permanently.
|
||||
*/
|
||||
export function emitSessionExpired(): void {
|
||||
if (_sessionExpired) return; // Only emit once
|
||||
_sessionExpired = true;
|
||||
|
||||
listeners.forEach((listener) => {
|
||||
try {
|
||||
listener();
|
||||
} catch {
|
||||
// Ignore listener errors
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the session expired state.
|
||||
* Should be called when the user logs in again.
|
||||
*/
|
||||
export function resetSessionExpired(): void {
|
||||
_sessionExpired = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the session is currently marked as expired.
|
||||
*/
|
||||
export function isSessionExpired(): boolean {
|
||||
return _sessionExpired;
|
||||
}
|
||||
|
|
@ -74,6 +74,16 @@ export type { FetchInterceptorConfig } from './interceptors/fetchInterceptor';
|
|||
export { ContactsClient, createContactsClient } from './clients/contactsClient';
|
||||
export type { ContactsClientConfig, ContactSearchOptions } from './clients/contactsClient';
|
||||
|
||||
// Session expired events
|
||||
import { resetSessionExpired as _resetSessionExpired } from './events/sessionExpired';
|
||||
import { TokenState as _TokenStateEnum } from './types';
|
||||
export {
|
||||
onSessionExpired,
|
||||
emitSessionExpired,
|
||||
resetSessionExpired,
|
||||
isSessionExpired,
|
||||
} from './events/sessionExpired';
|
||||
|
||||
/**
|
||||
* Initialize auth service with all adapters for web
|
||||
*
|
||||
|
|
@ -112,5 +122,12 @@ export function initializeWebAuth(config: {
|
|||
if (config.backendUrl) urls.push(config.backendUrl);
|
||||
_setupFetchInterceptor(authService, tokenManager, { urls });
|
||||
|
||||
// Reset session expired state when token becomes valid again (e.g., after re-login)
|
||||
tokenManager.subscribe((state) => {
|
||||
if (state === _TokenStateEnum.VALID) {
|
||||
_resetSessionExpired();
|
||||
}
|
||||
});
|
||||
|
||||
return { authService, tokenManager };
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue