import { defineStore } from "pinia"; import { computed, ref } from "vue"; import type { LoginRequest, UserInfo } from "@geo/shared-types"; import { authApi, isAuthSessionExpiredError, refreshStoredAuthSession, shouldRefreshAccessToken, } from "@/lib/api"; import { clearStoredSession, readStoredSession, setStoredTokens, setStoredUser, subscribeStoredSession, type SessionSnapshot, } from "@/lib/session"; export const useAuthStore = defineStore("auth", () => { const accessToken = ref(null); const refreshToken = ref(null); const accessTokenExpiresAt = ref(null); const user = ref(null); const initialized = ref(false); let unsubscribe: (() => void) | null = null; let refreshTimer: number | null = null; let lifecycleRefreshRegistered = false; function clearRefreshTimer(): void { if (refreshTimer) { window.clearTimeout(refreshTimer); refreshTimer = null; } } function scheduleRefresh(): void { clearRefreshTimer(); if (!refreshToken.value || typeof window === "undefined") { return; } const nextRefreshAt = accessTokenExpiresAt.value ? accessTokenExpiresAt.value * 1000 - 2 * 60 * 1000 : Date.now() + 30 * 1000; const delay = Math.max(30 * 1000, nextRefreshAt - Date.now()); refreshTimer = window.setTimeout(() => { void refreshSession(); }, delay); } function sync(snapshot: SessionSnapshot = readStoredSession()): void { accessToken.value = snapshot.accessToken; refreshToken.value = snapshot.refreshToken; accessTokenExpiresAt.value = snapshot.accessTokenExpiresAt; user.value = snapshot.user; scheduleRefresh(); } async function refreshSession(): Promise { if (!refreshToken.value) { return; } try { await refreshStoredAuthSession(refreshToken.value); } catch (error) { if (isAuthSessionExpiredError(error)) { clearStoredSession({ notifyAuthExpired: true }); return; } scheduleRefreshRetry(); } } function scheduleRefreshRetry(): void { clearRefreshTimer(); if (!refreshToken.value || typeof window === "undefined") { return; } refreshTimer = window.setTimeout(() => { void refreshSession(); }, 60 * 1000); } async function refreshSessionIfNeeded(minRemainingMs = 2 * 60 * 1000): Promise { if (!refreshToken.value) { return; } if (!accessToken.value || shouldRefreshAccessToken(accessTokenExpiresAt.value, minRemainingMs)) { await refreshSession(); } } function registerLifecycleRefresh(): void { if (lifecycleRefreshRegistered || typeof window === "undefined") { return; } lifecycleRefreshRegistered = true; window.addEventListener("focus", () => { void refreshSessionIfNeeded(); }); document.addEventListener("visibilitychange", () => { if (document.visibilityState === "visible") { void refreshSessionIfNeeded(); } }); } async function bootstrap(): Promise { if (!unsubscribe) { unsubscribe = subscribeStoredSession(sync); } sync(); registerLifecycleRefresh(); try { if (refreshToken.value) { await refreshSessionIfNeeded(Number.POSITIVE_INFINITY); } if (accessToken.value) { const currentUser = await authApi.me(); setStoredUser(currentUser); } } catch (error) { if (isAuthSessionExpiredError(error)) { clearStoredSession({ notifyAuthExpired: true }); } } initialized.value = true; } async function login(payload: LoginRequest): Promise { const response = await authApi.login(payload); setStoredTokens({ accessToken: response.access_token, refreshToken: response.refresh_token, expiresAt: response.expires_at, }); setStoredUser(response.user); return response.user; } async function logout(): Promise { try { if (accessToken.value) { await authApi.logout(); } } finally { clearStoredSession(); } } const isAuthenticated = computed(() => Boolean(accessToken.value && user.value)); const membership = computed(() => user.value?.membership ?? null); const isMembershipBlocked = computed(() => Boolean(membership.value?.blocked)); const kolProfile = computed(() => user.value?.kol_profile ?? null); const isActiveKol = computed(() => kolProfile.value?.status === "active"); return { accessToken, refreshToken, accessTokenExpiresAt, user, initialized, isAuthenticated, membership, isMembershipBlocked, kolProfile, isActiveKol, bootstrap, login, logout, }; });