@flavoai/fastfold
Version:
Flavo frontend package
185 lines • 6.98 kB
JavaScript
import React, { createContext, useContext, useState, useEffect, useCallback, useMemo } from 'react';
const FlavoAuthContext = createContext(null);
/**
* Resolves the signed-in end-user from the httpOnly session cookie. The
* cookie is unreadable to JS, so the server is the only source of truth —
* there is no client-side fallback.
*/
async function fetchCurrentUser() {
try {
const response = await fetch('/api/auth/me', {
credentials: 'include',
});
if (response.ok) {
const { user } = await response.json();
return user;
}
}
catch (_err) {
// /api/auth/me unreachable — treat the session as signed out.
}
return null;
}
/**
* Hook to access Flavo auth state and actions.
* Must be used within a FlavoAuthProvider.
*
* @example
* function MyComponent() {
* const { user, isAuthenticated, login, logout } = useFlavoAuth();
* if (!isAuthenticated) return <button onClick={login}>Sign in</button>;
* return <div>Welcome, {user?.displayName}! <button onClick={logout}>Sign out</button></div>;
* }
*/
export function useFlavoAuth() {
const context = useContext(FlavoAuthContext);
if (!context) {
throw new Error('useFlavoAuth must be used within a FlavoAuthProvider or FastfoldProvider with flavoAuth enabled.');
}
return context;
}
/**
* Context provider that manages the Flavo auth lifecycle:
* - Resolves the session from the httpOnly cookie on mount (via `/api/auth/me`)
* - Provides login/logout actions
*
* @example
* <FlavoAuthProvider>
* <App />
* </FlavoAuthProvider>
*/
export function FlavoAuthProvider({ children, config, }) {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const init = async () => {
setIsLoading(true);
const currentUser = await fetchCurrentUser();
if (currentUser) {
setUser(currentUser);
config?.onLogin?.(currentUser);
}
setIsLoading(false);
};
init();
}, []); // eslint-disable-line react-hooks/exhaustive-deps
// Expired-refresh dispatches `flavo:auth-expired` globally so the UI
// flips to signed-out even when the failure originates from a
// background fetch outside this component tree.
useEffect(() => {
if (typeof window === 'undefined')
return;
const onAuthExpired = () => {
setUser(null);
config?.onLogout?.();
};
window.addEventListener('flavo:auth-expired', onAuthExpired);
return () => window.removeEventListener('flavo:auth-expired', onAuthExpired);
}, [config?.onLogout]);
const login = useCallback(() => {
if (config?.appId) {
const base = (config.loginBaseUrl || 'https://flavo.ai').replace(/\/$/, '');
window.location.href = `${base}/api/app/auth/login/${encodeURIComponent(config.appId)}`;
}
else {
// Dedicated container mode routes through the local Fastfold server
// so FLAVO_APP_TOKEN stays server-side and never touches the browser.
window.location.href = '/api/auth/login';
}
}, [config?.appId, config?.loginBaseUrl]);
const logout = useCallback(async () => {
setUser(null);
// Fire-and-forget: UI must flip to signed-out immediately even if the
// server revoke call is unreachable, so local state is cleared first.
try {
await fetch('/api/auth/logout', {
method: 'POST',
credentials: 'include',
headers: { 'X-Requested-With': 'fetch' },
});
}
catch (_err) {
// Revoke endpoint unreachable; local state is already cleared above.
}
config?.onLogout?.();
}, [config?.onLogout]);
const value = useMemo(() => ({
user,
isLoading,
// The session JWT lives in an httpOnly cookie the client never sees, so
// authentication is gated on a resolved `user`, not on any client token.
isAuthenticated: !!user,
login,
logout,
}), [user, isLoading, login, logout]);
return React.createElement(FlavoAuthContext.Provider, { value }, children);
}
/**
* Drop-in component for the OAuth callback route (/auth). Waits for the
* provider to resolve the session cookie, then redirects to `redirectTo` on
* success or `/login?error=auth_failed` on failure.
*
* Renders `loadingComponent` (or nothing, by default) while the session is
* being validated. Style it to match the app's own theme so the transient
* paint looks native to the app.
*
* @example
* // In your router:
* <Route path="/auth" element={<FlavoAuthCallback redirectTo="/app" loadingComponent={<AppSkeleton />} />} />
*/
export function FlavoAuthCallback({ redirectTo = '/', loadingComponent, }) {
const { isLoading, isAuthenticated } = useFlavoAuth();
useEffect(() => {
if (!isLoading) {
if (isAuthenticated) {
window.location.href = redirectTo;
}
else {
window.location.href = '/login?error=auth_failed';
}
}
}, [isLoading, isAuthenticated, redirectTo]);
return (loadingComponent ?? null);
}
/**
* Wraps children and only renders them when the user is authenticated.
*
* While the session is being validated, renders `loadingComponent` — or
* nothing at all if the caller didn't pass one. Style it to match the app's
* own theme; a skeleton that mirrors the authenticated shell usually works
* best because the refresh paint becomes a still frame of the real page.
*
* When unauthenticated with no `fallback`, triggers `login()` and renders
* `loadingComponent` (or nothing) while the browser navigates to the Flavo
* login page.
*
* @example
* <ProtectedRoute fallback={<LoginPage />} loadingComponent={<AppSkeleton />}>
* <Dashboard />
* </ProtectedRoute>
*
* @example
* // Auto-redirect to login when unauthenticated
* <ProtectedRoute loadingComponent={<AppSkeleton />}>
* <Dashboard />
* </ProtectedRoute>
*/
export function ProtectedRoute({ children, fallback, loadingComponent, }) {
const { isLoading, isAuthenticated, login } = useFlavoAuth();
if (isLoading) {
return (loadingComponent ?? null);
}
if (!isAuthenticated) {
if (fallback) {
return fallback;
}
// Fire the redirect synchronously during render: there is no fallback
// to show, so deferring to useEffect would leave an empty frame before
// the navigation starts. The return paints loadingComponent for that
// frame so the user sees the app's own loading UI, not a blank screen.
login();
return (loadingComponent ?? null);
}
return children;
}
//# sourceMappingURL=auth.js.map