import { appDatabase, AppDatabaseKey, appStorage, AppStorageKey } from '../storage/local'; import { API_URL } from '../constants/api-url'; import { WEB_URL } from '../constants/web-url'; import { clientReplace, notifySessionEnd, notifySessionStart } from './client-navigation'; import { mapUserPrivileges } from './map-user-privileges'; import { safeInternalPath } from './safe-internal-path'; import type { AuthUser, TokenPair } from './auth.types'; interface TerminateOptions { /** When true, appends ?redirect= so the user returns to their page after re-login. * Defaults to false (intended for 401 / expired token). * Set to false for manual logout so the user lands on a clean login page. */ preserveRedirect?: boolean; } export async function persistTokenPair(tokens: TokenPair): Promise { await appDatabase.setItem(AppDatabaseKey.ACCESS_TOKEN, tokens.accessToken); await appDatabase.setItem(AppDatabaseKey.REFRESH_TOKEN, tokens.refreshToken); } export async function persistUserSession(me: AuthUser): Promise { const label = me.isSuperadmin ? 'Superadmin' : (me.privilege?.name ?? me.username); await appStorage.setItem(AppStorageKey.USER_ID, me.id); await appDatabase.setItem(AppDatabaseKey.USER_PROFILE, { id: me.id, username: me.username, isSuperadmin: me.isSuperadmin, privilege: me.privilege, name: me.username, label, }); await appDatabase.setItem(AppDatabaseKey.USER_PRIVILEGE, mapUserPrivileges(me.permissions, me.isSuperadmin)); } export async function terminateAuthSession(options: TerminateOptions = {}): Promise { const { preserveRedirect = false } = options; await appDatabase.removeItem(AppDatabaseKey.USER_PRIVILEGE); await appDatabase.removeItem(AppDatabaseKey.USER_PROFILE); await appDatabase.removeItem(AppDatabaseKey.ACCESS_TOKEN); await appDatabase.removeItem(AppDatabaseKey.REFRESH_TOKEN); await appStorage.removeItem(AppStorageKey.USER_ID); let loginUrl = WEB_URL.LOGIN; const isLoginPage = window.location.pathname === WEB_URL.LOGIN || window.location.pathname.startsWith(`${WEB_URL.LOGIN}/`); if (!isLoginPage) { if (preserveRedirect) { loginUrl += `?redirect=${encodeURIComponent(window.location.pathname)}`; } notifySessionEnd(); clientReplace(loginUrl); } } export async function initiateAuthSession(tokens: TokenPair, me: AuthUser): Promise { if (!tokens.accessToken) { throw new Error('Login response missing access token'); } await persistTokenPair(tokens); await persistUserSession(me); notifySessionStart(); const params = new URLSearchParams(window.location.search); clientReplace(safeInternalPath(params.get('redirect'), WEB_URL.APP)); } let refreshInFlight: Promise | null = null; export async function refreshAuthSession(): Promise { if (!refreshInFlight) { refreshInFlight = rotateRefreshToken().finally(() => { refreshInFlight = null; }); } return refreshInFlight; } async function rotateRefreshToken(): Promise { const refreshToken = await appDatabase.getItem(AppDatabaseKey.REFRESH_TOKEN); if (!refreshToken) { throw new Error('Missing refresh token'); } const { apiClient } = await import('./api-client'); const response = await apiClient.request({ url: API_URL.AUTH_REFRESH, method: 'POST', data: { refreshToken }, skipAuthRefresh: true, }); const tokens: TokenPair = { accessToken: response.data.accessToken, refreshToken: response.data.refreshToken, }; await persistTokenPair(tokens); return tokens; } export async function logoutAuthSession(): Promise { const refreshToken = await appDatabase.getItem(AppDatabaseKey.REFRESH_TOKEN); try { if (refreshToken) { const { apiClient } = await import('./api-client'); await apiClient.request({ url: API_URL.AUTH_REVOKE, method: 'POST', data: { refreshToken }, skipAuthRefresh: true, }); } } catch { // Always clear the local session, even if the server revoke call fails. } await terminateAuthSession({ preserveRedirect: false }); }