chore: import DH V2 D5.6.4 production baseline

This commit is contained in:
DH V2
2026-09-05 10:12:35 -03:00
commit 82213e72f5
757 changed files with 84218 additions and 0 deletions
+66
View File
@@ -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;
}
+14
View File
@@ -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;
}
+21
View File
@@ -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 />;
}