chore: import DH V2 D5.6.4 production baseline
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import {
|
||||
changePassword as requestPasswordChange,
|
||||
getMe,
|
||||
login as requestLogin,
|
||||
logout as requestLogout,
|
||||
} from '../lib/api';
|
||||
import type { AuthUser } from '../lib/api';
|
||||
|
||||
interface AuthContextValue {
|
||||
loading: boolean;
|
||||
user: AuthUser | null;
|
||||
login: (identifier: string, password: string) => Promise<AuthUser>;
|
||||
logout: () => Promise<void>;
|
||||
changePassword: (currentPassword: string, newPassword: string) => Promise<void>;
|
||||
hasPermission: (permission: string) => boolean;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextValue | null>(null);
|
||||
|
||||
export function AuthProvider({ children }: PropsWithChildren) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [user, setUser] = useState<AuthUser | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
getMe()
|
||||
.then((current) => { if (active) setUser(current); })
|
||||
.catch(() => { if (active) setUser(null); })
|
||||
.finally(() => { if (active) setLoading(false); });
|
||||
|
||||
const unauthorized = () => setUser(null);
|
||||
window.addEventListener('dhv2:unauthorized', unauthorized);
|
||||
return () => {
|
||||
active = false;
|
||||
window.removeEventListener('dhv2:unauthorized', unauthorized);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const value = useMemo<AuthContextValue>(() => ({
|
||||
loading,
|
||||
user,
|
||||
login: async (identifier, password) => {
|
||||
const response = await requestLogin(identifier, password);
|
||||
setUser(response.user);
|
||||
return response.user;
|
||||
},
|
||||
logout: async () => {
|
||||
try { await requestLogout(); } finally { setUser(null); }
|
||||
},
|
||||
changePassword: async (currentPassword, newPassword) => {
|
||||
await requestPasswordChange(currentPassword, newPassword);
|
||||
setUser((current) => current ? { ...current, mustChangePassword: false } : current);
|
||||
},
|
||||
hasPermission: (permission) => Boolean(user?.permissions.includes(permission)),
|
||||
}), [loading, user]);
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) throw new Error('useAuth debe utilizarse dentro de AuthProvider');
|
||||
return context;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { useAuth } from './AuthContext';
|
||||
|
||||
export function PermissionGate({
|
||||
permission,
|
||||
children,
|
||||
fallback = null,
|
||||
}: {
|
||||
permission: string;
|
||||
children: ReactNode;
|
||||
fallback?: ReactNode;
|
||||
}) {
|
||||
return useAuth().hasPermission(permission) ? children : fallback;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Navigate, Outlet, useLocation } from 'react-router';
|
||||
import { useAuth } from './AuthContext';
|
||||
|
||||
export function ProtectedRoute() {
|
||||
const { loading, user } = useAuth();
|
||||
const location = useLocation();
|
||||
|
||||
if (loading) {
|
||||
return <div className="full-screen-state"><span className="spinner" />Verificando sesión…</div>;
|
||||
}
|
||||
if (!user) return <Navigate to="/login" replace state={{ from: location.pathname }} />;
|
||||
if (user.mustChangePassword && location.pathname !== '/change-password') {
|
||||
return <Navigate to="/change-password" replace />;
|
||||
}
|
||||
return <Outlet />;
|
||||
}
|
||||
|
||||
export function PermissionRoute({ permission }: { permission: string }) {
|
||||
const { hasPermission } = useAuth();
|
||||
return hasPermission(permission) ? <Outlet /> : <Navigate to="/sin-acceso" replace />;
|
||||
}
|
||||
Reference in New Issue
Block a user