feat: agregar sistema de autenticación con usuarios y roles

- Agregar Login con autenticación por DNI y contraseña
- Crear tabla de usuarios con roles (admin, médico, enfermero)
- Implementar gestión de usuarios (CRUD) para administradores
- Agregar control de permisos basado en roles en el menú
- Persistir estado de sesión en backend
- Inicializar usuario admin por defecto (DNI: 12345678, pass: admin123)
- Corregir inicialización de arrays en el store para evitar errores al cargar estado
This commit is contained in:
2026-04-23 01:09:29 -03:00
parent 1d7a5e893f
commit 76e01bfd79
11 changed files with 869 additions and 31 deletions
+113 -5
View File
@@ -12,7 +12,9 @@ import type {
Interconsulta,
ATB,
Indicacion,
Vista
Vista,
Usuario,
RolUsuario
} from '@/types';
// Fallback UUID generator for non-secure contexts (HTTP without localhost)
@@ -44,10 +46,13 @@ interface HospitalState {
movimientosIndicaciones: any[];
vistaActual: Vista;
currentInternacionId?: string | null;
usuarios: Usuario[];
currentUser: Usuario | null;
isAuthenticated: boolean;
}
const API_BASE = import.meta.env.VITE_API_URL ?? `${window.location.origin}/api`;
const API_BASE = 'http://localhost:4001/api';
const defaultState = (): HospitalState => ({
pacientes: [],
@@ -63,7 +68,10 @@ const defaultState = (): HospitalState => ({
vistaActual: 'dashboard',
indicaciones: [],
movimientosIndicaciones: [],
camas: []
camas: [],
usuarios: [],
currentUser: null,
isAuthenticated: false,
});
export function useHospitalStore() {
@@ -79,8 +87,15 @@ export function useHospitalStore() {
if (!res.ok) throw new Error('no state');
const body = await res.json();
if (mounted && body) {
const normalized = body as HospitalState;
normalized.movimientosIndicaciones = normalized.movimientosIndicaciones || [];
const defaults = defaultState();
const normalized = {
...defaults,
...body,
estudiosComplementarios: body.estudiosComplementarios || [],
interconsultas: body.interconsultas || [],
atb: body.atb || [],
movimientosIndicaciones: body.movimientosIndicaciones || [],
};
setState(normalized);
}
} catch (err) {
@@ -542,6 +557,93 @@ const getEstadisticas = useCallback(() => {
setState(prev => ({ ...prev, currentInternacionId: id ?? null }));
}, []);
// Auth functions
const login = useCallback(async (dni: string, password: string) => {
try {
const res = await fetch(`${API_BASE}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ dni, password }),
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.error || 'Error en autenticación');
}
const user = await res.json();
setState(prev => ({ ...prev, currentUser: user, isAuthenticated: true }));
return user;
} catch (err) {
throw err;
}
}, []);
const logout = useCallback(() => {
setState(prev => {
const newState = { ...prev, currentUser: null, isAuthenticated: false };
fetch(`${API_BASE}/state`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...newState }),
}).catch(() => {});
return newState;
});
}, []);
const changePassword = useCallback(async (dni: string, oldPassword: string, newPassword: string) => {
const res = await fetch(`${API_BASE}/auth/change-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ dni, oldPassword, newPassword }),
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.error || 'Error al cambiar contraseña');
}
return true;
}, []);
const updateEmail = useCallback(async (dni: string, newEmail: string) => {
const res = await fetch(`${API_BASE}/auth/update-email`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ dni, newEmail }),
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.error || 'Error al actualizar email');
}
return true;
}, []);
const hasPermission = useCallback((permission: 'read' | 'write', section: string): boolean => {
const user = state.currentUser;
if (!user) return false;
if (user.rol === 'admin') return true;
if (user.rol === 'enfermero') {
if (section === 'glucemias' || section === 'signosvitales') return permission === 'read' || permission === 'write';
return permission === 'read';
}
if (user.rol === 'medico') {
if (section === 'miArea') return permission === 'read' || permission === 'write';
return permission === 'read';
}
return false;
}, [state.currentUser]);
const canAccessInternacion = useCallback((internacionId: string): boolean => {
const user = state.currentUser;
if (!user) return false;
const rol = user.rol as string;
if (rol === 'admin') return true;
if (rol === 'medico' && user.areaId) {
const internacion = state.internaciones.find(i => i.id === internacionId);
if (!internacion) return false;
const cama = state.camas.find(c => c.id === internacion.camaId);
return cama?.areaId === user.areaId;
}
return rol === 'admin';
}, [state.currentUser, state.internaciones, state.camas]);
return {
...state,
isLoaded,
@@ -594,5 +696,11 @@ const getEstadisticas = useCallback(() => {
actualizarLaboratorio,
setCurrentInternacion,
getEstadisticas,
login,
logout,
changePassword,
updateEmail,
hasPermission,
canAccessInternacion,
};
}