Fix response.ok JSDoc (2xx not 200), remove stale token refresh claim, remove non-actionable comment, fix CSRF comment placement, add 403 mapping rationale. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
117 lines
3.2 KiB
TypeScript
117 lines
3.2 KiB
TypeScript
/**
|
|
* 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<unknown> {
|
|
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<string | null> {
|
|
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<boolean> {
|
|
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;
|
|
}
|
|
}
|