learning_ai_common_plat/packages/react-auth/src/auth-context.tsx
saravanakumardb1 11e79aa4f9 feat(react-auth): add onLoginFallback to createAuthProvider config
Allows dashboards to provide fallback login logic (e.g. mock credentials)
when the API is unavailable. Used by admin-dashboard-web.
2026-02-12 22:21:25 -08:00

109 lines
3.3 KiB
TypeScript

'use client';
import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
import { createApiClient } from '@bytelyst/api-client';
import type { AuthConfig, AuthContextValue, BaseUser } from './types.js';
/**
* Create a typed auth provider + hook for a specific user type.
*
* @example
* ```tsx
* const { AuthProvider, useAuth } = createAuthProvider<AdminUser>({
* storagePrefix: "admin",
* loginEndpoint: "/auth/login",
* mapLoginResponse: (data) => ({
* user: data.user,
* accessToken: data.accessToken,
* refreshToken: data.refreshToken,
* }),
* });
* ```
*/
export function createAuthProvider<TUser extends BaseUser = BaseUser>(config: AuthConfig<TUser>) {
const { storagePrefix, loginEndpoint, mapLoginResponse, onLoginFallback, onLogout } = config;
const USER_KEY = `${storagePrefix}_auth_user`;
const TOKEN_KEY = `${storagePrefix}_access_token`;
const REFRESH_KEY = `${storagePrefix}_refresh_token`;
const AuthContext = createContext<AuthContextValue<TUser> | null>(null);
function getStoredUser(): TUser | null {
if (typeof window === 'undefined') return null;
try {
const stored = localStorage.getItem(USER_KEY);
return stored ? JSON.parse(stored) : null;
} catch {
return null;
}
}
function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<TUser | null>(getStoredUser);
const isLoading = false;
const api = createApiClient({
baseUrl: '/api',
getToken: () => (typeof window !== 'undefined' ? localStorage.getItem(TOKEN_KEY) : null),
});
const login = useCallback(
async (email: string, password: string) => {
const { data, error } = await api.safeFetch<unknown>(loginEndpoint, {
method: 'POST',
body: JSON.stringify({ email, password }),
});
if (data && !error) {
const mapped = mapLoginResponse(data);
setUser(mapped.user);
localStorage.setItem(USER_KEY, JSON.stringify(mapped.user));
localStorage.setItem(TOKEN_KEY, mapped.accessToken);
localStorage.setItem(REFRESH_KEY, mapped.refreshToken);
return true;
}
// Try fallback (e.g. mock credentials) when API is unavailable
if (error && onLoginFallback) {
const fallback = await onLoginFallback(email, password, error);
if (fallback) {
setUser(fallback.user);
localStorage.setItem(USER_KEY, JSON.stringify(fallback.user));
localStorage.setItem(TOKEN_KEY, fallback.accessToken);
localStorage.setItem(REFRESH_KEY, fallback.refreshToken);
return true;
}
}
return false;
},
[api]
);
const logout = useCallback(() => {
setUser(null);
localStorage.removeItem(USER_KEY);
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(REFRESH_KEY);
onLogout?.();
}, []);
return (
<AuthContext.Provider value={{ user, isAuthenticated: !!user, isLoading, login, logout }}>
{children}
</AuthContext.Provider>
);
}
function useAuth(): AuthContextValue<TUser> {
const ctx = useContext(AuthContext);
if (!ctx) {
throw new Error('useAuth must be used within an AuthProvider');
}
return ctx;
}
return { AuthProvider, useAuth };
}