Allows dashboards to provide fallback login logic (e.g. mock credentials) when the API is unavailable. Used by admin-dashboard-web.
109 lines
3.3 KiB
TypeScript
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 };
|
|
}
|