Files
SKEEN-Proyecto/frontend-homenest/src/lib/auth.tsx
Consultoría Alcaraz Salazar a718592291 Initial commit: SKEEN Derma Experts - Sistema Integral de Gestión Clínica
- Frontend React (SKEEN Brand) con Vite, TypeScript, Tailwind
- Frontend Homenest (versión alternativa)
- Módulos Odoo 17 custom (citas, pacientes, monedero, pagos, ventas, inventario, whatsapp)
- WACRM fork (Next.js 16 + Supabase)
- Hermes + Bridge + Skills (Qwen3.6 via Nan Builders)
- Scripts de migración y operación
- Documentación extensiva en docs/
2026-07-20 07:44:23 +00:00

136 lines
4.2 KiB
TypeScript

import { createContext, useContext, useEffect, useState, useCallback } from 'react';
import type { FC, ReactNode } from 'react';
import { odooApi, type FrontendUser, type FrontendRole } from '../services/odoo';
const TOKEN_KEY = 'skeen_token';
const USER_KEY = 'skeen_user';
const ROLE_RANK: Record<FrontendRole, number> = {
lectura: 10,
medico: 50,
recepcion: 50,
admin: 100,
};
// Visibilidad del menú por ruta. Si una ruta no está listada, se muestra a todos los roles.
const MENU_ROLES: Record<string, FrontendRole[]> = {
'/': ['admin', 'recepcion', 'medico', 'lectura'],
'/agenda': ['admin', 'recepcion', 'medico'],
'/pacientes': ['admin', 'recepcion', 'medico', 'lectura'],
'/medicos': ['admin', 'recepcion'],
'/servicios': ['admin', 'recepcion', 'medico', 'lectura'],
'/productos': ['admin', 'recepcion', 'lectura'],
'/ventas': ['admin', 'recepcion'],
'/pagos': ['admin', 'recepcion'],
'/monedero': ['admin', 'recepcion', 'lectura'],
'/inventario': ['admin', 'recepcion', 'lectura'],
'/cortes': ['admin', 'recepcion'],
'/reportes': ['admin', 'recepcion', 'lectura'],
'/configuracion': ['admin'],
'/usuarios': ['admin'],
'/cumpleanos': ['admin', 'recepcion'],
'/wacrm/messages': ['admin', 'recepcion'],
'/wacrm/leads': ['admin', 'recepcion'],
};
interface AuthContextValue {
user: FrontendUser | null;
token: string | null;
loading: boolean;
login: (login: string, password: string) => Promise<FrontendUser>;
logout: () => Promise<void>;
hasRole: (min: FrontendRole) => boolean;
canSee: (href: string) => boolean;
}
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
export const AuthProvider: FC<{ children: ReactNode }> = ({ children }) => {
const [user, setUser] = useState<FrontendUser | null>(null);
const [token, setToken] = useState<string | null>(() => localStorage.getItem(TOKEN_KEY));
const [loading, setLoading] = useState(true);
const clear = useCallback(() => {
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(USER_KEY);
setToken(null);
setUser(null);
}, []);
// Validar token al arrancar
useEffect(() => {
let alive = true;
const t = localStorage.getItem(TOKEN_KEY);
if (!t) {
setLoading(false);
return;
}
// Hidratar usuario cacheado para evitar parpadeo
const cached = localStorage.getItem(USER_KEY);
if (cached) {
try { setUser(JSON.parse(cached)); } catch { /* ignore */ }
}
odooApi.me()
.then((res) => {
if (!alive) return;
if (res.status === 'success') {
setUser(res.user);
localStorage.setItem(USER_KEY, JSON.stringify(res.user));
} else {
clear();
}
})
.catch(() => { if (alive) clear(); })
.finally(() => { if (alive) setLoading(false); });
return () => { alive = false; };
}, [clear]);
const login = async (login: string, password: string): Promise<FrontendUser> => {
const res = await odooApi.login(login, password);
if (res.status !== 'success' || !res.token) {
throw new Error(res.message || 'Credenciales inválidas');
}
localStorage.setItem(TOKEN_KEY, res.token);
localStorage.setItem(USER_KEY, JSON.stringify(res.user));
setToken(res.token);
setUser(res.user);
return res.user;
};
const logout = async () => {
try { await odooApi.logout(); } catch { /* ignore */ }
clear();
};
const hasRole = (min: FrontendRole): boolean => {
if (!user) return false;
return (ROLE_RANK[user.role] ?? 0) >= (ROLE_RANK[min] ?? 0);
};
const canSee = (href: string): boolean => {
if (!user) return false;
const allowed = MENU_ROLES[href];
if (!allowed) return true;
return allowed.includes(user.role);
};
return (
<AuthContext.Provider value={{ user, token, loading, login, logout, hasRole, canSee }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = (): AuthContextValue => {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth debe usarse dentro de AuthProvider');
return ctx;
};
export const ROLE_LABELS: Record<FrontendRole, string> = {
admin: 'Administrador',
recepcion: 'Recepción',
medico: 'Médico',
lectura: 'Solo lectura',
};