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/
This commit is contained in:
135
frontend-homenest/src/lib/auth.tsx
Normal file
135
frontend-homenest/src/lib/auth.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
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',
|
||||
};
|
||||
Reference in New Issue
Block a user