All files / src/services sessionManager.ts

80% Statements 56/70
70.58% Branches 12/17
60% Functions 9/15
81.15% Lines 56/69

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150  75x 75x 75x         75x       75x 75x 75x 75x 75x     11x           3x               2x                     5x                 5x 2x 2x         2x 2x 2x     3x     75x                       75x 22x     75x             75x 5x 5x 5x 5x     75x           75x 3x 3x 3x 3x 3x     75x 6x 5x 5x 5x     5x   5x 5x 3x 3x             3x 3x 3x 3x 3x   2x 2x   2x   5x         6x    
import type { AuthSessionResponse, StoredAuthSession } from '@/models/auth';
import { API_BASE_URL } from '@/config/apiBaseUrl';
import { getCurrentLocale } from '@/contexts/LocaleContext';
import {
  clearStoredSession,
  readStoredSession,
  writeStoredSession,
} from '@/services/sessionStorage';
import { localizeApiErrorMessage } from '@/utils/apiErrorLocalization';
 
type SessionListener = (session: StoredAuthSession | null) => void;
 
let currentSession: StoredAuthSession | null = null;
let hydratePromise: Promise<StoredAuthSession | null> | null = null;
let refreshPromise: Promise<StoredAuthSession> | null = null;
let sessionVersion = 0;
const listeners = new Set<SessionListener>();
 
function notifyListeners() {
  for (const listener of listeners) {
    listener(currentSession);
  }
}
 
function mapAuthSession(response: AuthSessionResponse): StoredAuthSession {
  return {
    access_token: response.access_token,
    refresh_token: response.refresh_token,
    user: response.user,
  };
}
 
function isTerminalRefreshError(error: unknown): boolean {
  return (
    typeof error === 'object' &&
    error !== null &&
    'status' in error &&
    (error as { status?: unknown }).status === 401
  );
}
 
async function refreshSessionRequest(
  refreshToken: string,
): Promise<AuthSessionResponse> {
  const response = await fetch(`${API_BASE_URL}/auth/refresh`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Accept-Language': getCurrentLocale(),
    },
    body: JSON.stringify({ refresh_token: refreshToken }),
  });
 
  if (!response.ok) {
    const body = await response.json().catch(() => null);
    const error = new Error(
      body?.error?.message
        ? localizeApiErrorMessage(body.error.message)
        : 'Could not refresh the session.',
    ) as Error & { status?: number; code?: string };
    error.status = response.status;
    error.code = body?.error?.code;
    throw error;
  }
 
  return response.json();
}
 
export async function hydrateSession(): Promise<StoredAuthSession | null> {
  Iif (!hydratePromise) {
    hydratePromise = (async () => {
      currentSession = await readStoredSession();
      notifyListeners();
      return currentSession;
    })();
  }
 
  return hydratePromise;
}
 
export function getCurrentSession(): StoredAuthSession | null {
  return currentSession;
}
 
export function subscribeToSession(listener: SessionListener): () => void {
  listeners.add(listener);
  return () => {
    listeners.delete(listener);
  };
}
 
export async function setSession(session: StoredAuthSession): Promise<void> {
  await writeStoredSession(session);
  sessionVersion += 1;
  currentSession = session;
  notifyListeners();
}
 
export async function setSessionFromAuthResponse(
  response: AuthSessionResponse,
): Promise<void> {
  await setSession(mapAuthSession(response));
}
 
export async function clearSession(): Promise<void> {
  refreshPromise = null;
  await clearStoredSession();
  sessionVersion += 1;
  currentSession = null;
  notifyListeners();
}
 
export async function refreshSession(): Promise<StoredAuthSession> {
  if (!refreshPromise) {
    refreshPromise = (async () => {
      const session = currentSession ?? (await hydrateSession());
      Iif (!session?.refresh_token) {
        throw new Error('No refresh token available');
      }
      const refreshVersion = sessionVersion;
 
      try {
        const refreshed = await refreshSessionRequest(session.refresh_token);
        const nextSession = mapAuthSession(refreshed);
        Iif (
          currentSession === null ||
          sessionVersion !== refreshVersion ||
          currentSession.refresh_token !== session.refresh_token
        ) {
          throw new Error('Session changed during refresh');
        }
        await writeStoredSession(nextSession);
        sessionVersion += 1;
        currentSession = nextSession;
        notifyListeners();
        return nextSession;
      } catch (error) {
        if (isTerminalRefreshError(error)) {
          await clearSession();
        }
        throw error;
      } finally {
        refreshPromise = null;
      }
    })();
  }
 
  return refreshPromise;
}