// Auth state for the host-facing app. // // The access token lives only in memory (useState — Nuxt's SSR-safe wrapper). // The refresh token lives in an HttpOnly cookie set by the API at // `/auth/refresh` scope, so JavaScript here can never read it. On a hard // reload we lose the access token but the cookie survives, so `bootstrap()` // calls `/auth/refresh` to mint a fresh access token + reload the user. interface AuthUser { id: string email: string name: string email_verified: boolean } interface AuthSuccess { access_token: string expires_at: string user: AuthUser } interface AuthState { user: AuthUser | null accessToken: string | null expiresAt: number | null // unix ms bootstrapped: boolean } function emptyState(): AuthState { return { user: null, accessToken: null, expiresAt: null, bootstrapped: false } } function apiBase(): string { return useRuntimeConfig().public.apiBase as string } async function postJSON(path: string, body?: unknown): Promise { return await $fetch(path, { baseURL: apiBase(), method: 'POST', body, credentials: 'include', headers: { 'Content-Type': 'application/json' }, }) } export function useAuth() { const state = useState('gg-auth', emptyState) function setSession(s: AuthSuccess) { state.value = { user: s.user, accessToken: s.access_token, expiresAt: Date.parse(s.expires_at) || (Date.now() + 14 * 60 * 1000), bootstrapped: true, } } function clearSession() { state.value = { ...emptyState(), bootstrapped: true } } async function signup(email: string, name: string, password: string, acceptTerms = false) { return await postJSON<{ status: string }>('/auth/signup', { email, name, password, accept_terms: acceptTerms, }) } async function login(email: string, password: string) { const s = await postJSON('/auth/login', { email, password }) setSession(s) return s } async function refresh(): Promise { try { const s = await postJSON('/auth/refresh') setSession(s) return true } catch { clearSession() return false } } async function logout() { try { await postJSON('/auth/logout') } catch { // Best-effort — clear local state regardless. } clearSession() } async function verifyEmail(token: string) { return await postJSON<{ status: string }>('/auth/verify-email', { token }) } async function forgotPassword(email: string) { return await postJSON<{ status: string }>('/auth/forgot-password', { email }) } async function resetPassword(token: string, newPassword: string) { return await postJSON<{ status: string }>('/auth/reset-password', { token, new_password: newPassword, }) } // Call on app entry / route guards. Returns true if the caller has a valid // session by the time it resolves. async function bootstrap(): Promise { if (!import.meta.client) return false if (state.value.bootstrapped && state.value.user) return true if (state.value.bootstrapped && !state.value.user) return false return await refresh() } // Hint to useApi: returns the current token if not yet expired. function liveAccessToken(): string | null { if (!state.value.accessToken || !state.value.expiresAt) return null // 5s skew to avoid sending a just-expired token. if (Date.now() + 5000 >= state.value.expiresAt) return null return state.value.accessToken } const isAuthenticated = computed(() => !!state.value.user) const user = computed(() => state.value.user) const bootstrapped = computed(() => state.value.bootstrapped) return { user, isAuthenticated, bootstrapped, signup, login, refresh, logout, verifyEmail, forgotPassword, resetPassword, bootstrap, liveAccessToken, clearSession, } }