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
+98
View File
@@ -2,6 +2,7 @@ import sqlite3 from 'sqlite3';
import { open } from 'sqlite';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import bcrypt from 'bcryptjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const DB_PATH = __dirname + '/data/hospital.db';
@@ -19,6 +20,22 @@ export async function openDb() {
);
`);
await db.exec(`
CREATE TABLE IF NOT EXISTS usuarios (
id TEXT PRIMARY KEY,
apellido TEXT NOT NULL,
nombre TEXT NOT NULL,
dni TEXT UNIQUE NOT NULL,
fechaNacimiento TEXT NOT NULL,
email TEXT NOT NULL,
rol TEXT NOT NULL CHECK(rol IN ('admin', 'medico', 'enfermero')),
matriculaProfesional TEXT,
passwordHash TEXT NOT NULL,
areaId TEXT,
fechaCreacion TEXT NOT NULL
);
`);
return db;
}
@@ -34,3 +51,84 @@ export async function setValue(key, value) {
await db.run('INSERT INTO kv (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value', key, value);
await db.close();
}
export async function getUsuarioByDni(dni) {
const db = await openDb();
const row = await db.get('SELECT * FROM usuarios WHERE dni = ?', dni);
await db.close();
return row || null;
}
export async function getUsuarioById(id) {
const db = await openDb();
const row = await db.get('SELECT * FROM usuarios WHERE id = ?', id);
await db.close();
return row || null;
}
export async function getAllUsuarios() {
const db = await openDb();
const rows = await db.all('SELECT * FROM usuarios ORDER BY apellido, nombre');
await db.close();
return rows;
}
export async function createUsuario(usuario) {
const db = await openDb();
await db.run(
`INSERT INTO usuarios (id, apellido, nombre, dni, fechaNacimiento, email, rol, matriculaProfesional, passwordHash, areaId, fechaCreacion)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
usuario.id,
usuario.apellido,
usuario.nombre,
usuario.dni,
usuario.fechaNacimiento,
usuario.email,
usuario.rol,
usuario.matriculaProfesional || null,
usuario.passwordHash,
usuario.areaId || null,
usuario.fechaCreacion
);
await db.close();
}
export async function updateUsuario(id, datos) {
const db = await openDb();
const fields = [];
const values = [];
if (datos.apellido !== undefined) { fields.push('apellido = ?'); values.push(datos.apellido); }
if (datos.nombre !== undefined) { fields.push('nombre = ?'); values.push(datos.nombre); }
if (datos.fechaNacimiento !== undefined) { fields.push('fechaNacimiento = ?'); values.push(datos.fechaNacimiento); }
if (datos.email !== undefined) { fields.push('email = ?'); values.push(datos.email); }
if (datos.rol !== undefined) { fields.push('rol = ?'); values.push(datos.rol); }
if (datos.matriculaProfesional !== undefined) { fields.push('matriculaProfesional = ?'); values.push(datos.matriculaProfesional); }
if (datos.passwordHash !== undefined) { fields.push('passwordHash = ?'); values.push(datos.passwordHash); }
if (datos.areaId !== undefined) { fields.push('areaId = ?'); values.push(datos.areaId); }
if (fields.length > 0) {
values.push(id);
await db.run(`UPDATE usuarios SET ${fields.join(', ')} WHERE id = ?`, values);
}
await db.close();
}
export async function deleteUsuario(id) {
const db = await openDb();
await db.run('DELETE FROM usuarios WHERE id = ?', id);
await db.close();
}
export async function verifyPassword(dni, password) {
const db = await openDb();
const row = await db.get('SELECT passwordHash FROM usuarios WHERE dni = ?', dni);
await db.close();
if (!row) return false;
return bcrypt.compareSync(password, row.passwordHash);
}
export async function hashPassword(password) {
return bcrypt.hashSync(password, 10);
}
+200 -1
View File
@@ -1,6 +1,6 @@
import express from 'express';
import cors from 'cors';
import { getValue, setValue } from './db.js';
import { getValue, setValue, getUsuarioByDni, getUsuarioById, getAllUsuarios, createUsuario, updateUsuario, deleteUsuario, verifyPassword, hashPassword } from './db.js';
const app = express();
app.use(cors());
@@ -34,3 +34,202 @@ app.put('/api/state', async (req, res) => {
app.listen(PORT, () => {
console.log(`API server listening on http://localhost:${PORT}`);
});
// Crear usuario admin inicial si no existe
async function initAdminUser() {
try {
const adminExists = await getUsuarioByDni('12345678');
if (!adminExists) {
const passwordHash = await hashPassword('admin123');
const adminUser = {
id: generateUUID(),
apellido: 'Admin',
nombre: 'Sistema',
dni: '12345678',
fechaNacimiento: '1970-01-01',
email: 'admin@hospital.gob.ar',
rol: 'admin',
matriculaProfesional: null,
passwordHash,
areaId: null,
fechaCreacion: new Date().toISOString().split('T')[0]
};
await createUsuario(adminUser);
console.log('Usuario admin creado: DNI 12345678, Password admin123');
}
} catch (err) {
console.error('Error creating admin user:', err);
}
}
initAdminUser();
// === AUTENTICACIÓN ===
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
app.post('/api/auth/login', async (req, res) => {
try {
const { dni, password } = req.body;
if (!dni || !password) {
return res.status(400).json({ error: 'DNI y contraseña requeridos' });
}
const usuario = await getUsuarioByDni(dni);
if (!usuario) {
return res.status(401).json({ error: 'Usuario no encontrado' });
}
const validPassword = await verifyPassword(dni, password);
if (!validPassword) {
return res.status(401).json({ error: 'Contraseña incorrecta' });
}
const { passwordHash, ...userWithoutPassword } = usuario;
res.json(userWithoutPassword);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Error en autenticación' });
}
});
app.post('/api/auth/change-password', async (req, res) => {
try {
const { dni, oldPassword, newPassword } = req.body;
if (!dni || !oldPassword || !newPassword) {
return res.status(400).json({ error: 'Todos los campos son requeridos' });
}
const validPassword = await verifyPassword(dni, oldPassword);
if (!validPassword) {
return res.status(401).json({ error: 'Contraseña actual incorrecta' });
}
const newHash = await hashPassword(newPassword);
await updateUsuarioByDni(dni, { passwordHash: newHash });
res.json({ ok: true });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Error al cambiar contraseña' });
}
});
app.put('/api/auth/update-email', async (req, res) => {
try {
const { dni, newEmail } = req.body;
if (!dni || !newEmail) {
return res.status(400).json({ error: 'DNI y email son requeridos' });
}
await updateUsuarioByDni(dni, { email: newEmail });
res.json({ ok: true });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Error al actualizar email' });
}
});
// === GESTIÓN DE USUARIOS (ADMIN) ===
app.get('/api/usuarios', async (req, res) => {
try {
const usuarios = await getAllUsuarios();
const usuariosSinPassword = usuarios.map(({ passwordHash, ...u }) => u);
res.json(usuariosSinPassword);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Error al obtener usuarios' });
}
});
app.post('/api/usuarios', async (req, res) => {
try {
const { apellido, nombre, dni, fechaNacimiento, email, rol, matriculaProfesional, password, areaId } = req.body;
if (!apellido || !nombre || !dni || !fechaNacimiento || !email || !rol || !password) {
return res.status(400).json({ error: 'Todos los campos obligatorios deben estar presentes' });
}
const existente = await getUsuarioByDni(dni);
if (existente) {
return res.status(400).json({ error: 'Ya existe un usuario con ese DNI' });
}
const passwordHash = await hashPassword(password);
const usuario = {
id: generateUUID(),
apellido,
nombre,
dni,
fechaNacimiento,
email,
rol,
matriculaProfesional: matriculaProfesional || null,
passwordHash,
areaId: areaId || null,
fechaCreacion: new Date().toISOString().split('T')[0]
};
await createUsuario(usuario);
res.json({ ...usuario, passwordHash: undefined });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Error al crear usuario' });
}
});
app.put('/api/usuarios/:id', async (req, res) => {
try {
const { id } = req.params;
const { apellido, nombre, fechaNacimiento, email, rol, matriculaProfesional, areaId, password } = req.body;
const usuario = await getUsuarioById(id);
if (!usuario) {
return res.status(404).json({ error: 'Usuario no encontrado' });
}
const datos = {};
if (apellido !== undefined) datos.apellido = apellido;
if (nombre !== undefined) datos.nombre = nombre;
if (fechaNacimiento !== undefined) datos.fechaNacimiento = fechaNacimiento;
if (email !== undefined) datos.email = email;
if (rol !== undefined) datos.rol = rol;
if (matriculaProfesional !== undefined) datos.matriculaProfesional = matriculaProfesional;
if (areaId !== undefined) datos.areaId = areaId;
if (password) {
datos.passwordHash = await hashPassword(password);
}
await updateUsuario(id, datos);
res.json({ ok: true });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Error al actualizar usuario' });
}
});
app.delete('/api/usuarios/:id', async (req, res) => {
try {
const { id } = req.params;
await deleteUsuario(id);
res.json({ ok: true });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Error al eliminar usuario' });
}
});
// Helper para actualizar por DNI
async function updateUsuarioByDni(dni, datos) {
const { getUsuarioByDni, updateUsuario } = await import('./db.js');
const usuario = await getUsuarioByDni(dni);
if (usuario) {
await updateUsuario(usuario.id, datos);
}
}
+4 -2
View File
@@ -6,9 +6,11 @@
"start": "node index.js"
},
"dependencies": {
"better-sqlite3": "^11.7.0",
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"express": "^4.21.2",
"http-proxy-middleware": "^3.0.5"
"http-proxy-middleware": "^3.0.5",
"sqlite": "^5.1.7",
"sqlite3": "^5.1.7"
}
}