/** * BetterAuth client for frontend authentication. * * This client handles: * - Sign in/out operations * - Session management * - Cookie-based session lifecycle */ import { createAuthClient } from "better-auth/react"; import { genericOAuthClient } from "better-auth/client/plugins"; import { API_BASE_URL } from "./config"; import { parseAuthError } from "./auth/auth-errors"; /** * Auth client instance configured for Mosaic Stack. */ export const authClient = createAuthClient({ baseURL: API_BASE_URL, basePath: "/auth", plugins: [genericOAuthClient()], }); /** * Export commonly used auth functions. */ export const { signIn, signOut, useSession, getSession } = authClient; /** * Sign in with email and password. * Returns the session on success, throws on failure. * * Uses direct fetch to POST credentials to BetterAuth's sign-in endpoint. * The email parameter accepts an email address used as the credential identifier. */ export async function signInWithCredentials(email: string, password: string): Promise { const response = await fetch(`${API_BASE_URL}/auth/sign-in/credentials`, { method: "POST", headers: { "Content-Type": "application/json", }, credentials: "include", // Include cookies body: JSON.stringify({ email, password }), }); if (!response.ok) { let errorBody: { message?: string } = {}; try { errorBody = (await response.json()) as { message?: string }; } catch (jsonError: unknown) { console.error( `[Auth] Failed to parse error response body (HTTP ${String(response.status)}):`, jsonError ); } const parsed = parseAuthError(errorBody.message ? new Error(errorBody.message) : response); throw new Error(parsed.message); } const data = (await response.json()) as unknown; return data; } /** * Get the current access token for API calls. * Returns null if not authenticated. */ export async function getAccessToken(): Promise { try { const session = await getSession(); if (!session.data?.user) { return null; } // Type assertion for custom user fields const user = session.data.user as { accessToken?: string; tokenExpiresAt?: number; }; if (!user.accessToken) { console.warn("[Auth] Session exists but no accessToken found"); return null; } // Check if token is expired (with 1 minute buffer) if (user.tokenExpiresAt && user.tokenExpiresAt - Date.now() < 60000) { // Token is expired or about to expire // The session will be refreshed automatically by BetterAuth // but we should return null to trigger a re-auth if needed return null; } return user.accessToken; } catch (error: unknown) { console.error("[Auth] Failed to get access token:", error); return null; } } /** * Check if the current user is an admin. */ export async function isAdmin(): Promise { try { const session = await getSession(); if (!session.data?.user) { return false; } const user = session.data.user as { isAdmin?: boolean }; return user.isAdmin === true; } catch (error: unknown) { console.error("[Auth] Failed to check admin status:", error); return false; } }