diff --git a/Dockerfile b/Dockerfile index 321b5b9..49eb9f0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,15 +11,34 @@ RUN npm ci COPY . . RUN VITE_API_URL=$VITE_API_URL npm run build -# Stage 2: App only (no MariaDB) -FROM node:20-alpine +# Stage 2: Production environment with Node.js, Nginx, and MongoDB Community Edition +FROM node:20-bookworm-slim WORKDIR /app -# Install nginx and curl -RUN apk add --no-cache nginx curl bash +# Prevent interactive prompts during apt install +ENV DEBIAN_FRONTEND=noninteractive -# Copy built frontend to nginx +# Install dependencies needed for MongoDB repo and general setup +RUN apt-get update && apt-get install -y \ + gnupg \ + curl \ + nginx \ + procps \ + && rm -rf /var/lib/apt/lists/* + +# Add MongoDB official GPG key and repository (MongoDB 8.0) +RUN curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc | \ + gpg --dearmor -o /usr/share/keyrings/mongodb-server-8.0.gpg \ + && echo "deb [ signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] http://repo.mongodb.org/apt/debian bookworm/mongodb-org/8.0 main" | \ + tee /etc/apt/sources.list.d/mongodb-org-8.0.list + +# Install MongoDB Community Edition +RUN apt-get update && apt-get install -y \ + mongodb-org \ + && rm -rf /var/lib/apt/lists/* + +# Copy built frontend to nginx html folder COPY --from=builder /build/dist /usr/share/nginx/html # Copy server files @@ -28,24 +47,23 @@ RUN cd ./server && npm ci COPY server/ ./server/ -# Copy nginx config -COPY nginx.conf /etc/nginx/http.d/default.conf +# Configure Nginx for Debian +RUN rm -f /etc/nginx/sites-enabled/default +COPY nginx.conf /etc/nginx/conf.d/default.conf -# Create directories -RUN mkdir -p /run/nginx +# Create MongoDB data and run directories with correct permissions +RUN mkdir -p /data/db /var/log/mongodb /var/run/mongodb && \ + chown -R mongodb:mongodb /data/db /var/log/mongodb /var/run/mongodb # Copy entrypoint COPY docker-entrypoint.sh /docker-entrypoint.sh RUN chmod +x /docker-entrypoint.sh -EXPOSE 80 4000 +# ONLY expose port 80 (HTTP) +EXPOSE 80 -ENV DB_HOST=db -ENV DB_PORT=3306 -ENV DB_USER=hospital -ENV DB_PASSWORD=hospital123 ENV DB_NAME=hospital -ENV ADMIN_DNI=00000000 -ENV ADMIN_PASSWORD=Admin123! +ENV ADMIN_DNI=35242633 +ENV ADMIN_PASSWORD=admin123 -ENTRYPOINT ["/docker-entrypoint.sh"] \ No newline at end of file +ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/docker-compose.yml b/docker-compose.yml index f43cc3a..629d0d0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,46 +1,13 @@ - services: - db: - image: mariadb:10.11 - environment: - MYSQL_ROOT_PASSWORD: 1123581321Rt% - MYSQL_DATABASE: hospitalsantojanni - MYSQL_USER: hospitaluser - MYSQL_PASSWORD: 1123581321Rt% - ports: - - "3307:3306" - volumes: - - mariadb-data:/var/lib/mysql - restart: unless-stopped - healthcheck: - test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] - interval: 10s - timeout: 5s - retries: 5 - app: build: context: . args: - VITE_API_URL=/api ports: - - "8077:80" - - "4000:4000" + - "8084:80" environment: - DB_HOST: localhost - DB_PORT: "3307" - DB_USER: hospitaluser - DB_PASSWORD: 1123581321Rt% - DB_NAME: hospitalsantojanni + DB_NAME: hospital ADMIN_DNI: "35242633" ADMIN_PASSWORD: admin123 - volumes: - - nginx-data:/usr/share/nginx/html - depends_on: - db: - condition: service_healthy restart: unless-stopped - -volumes: - mariadb-data: - nginx-data: diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 72ddb97..d5c565b 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -1,34 +1,19 @@ #!/usr/bin/env bash set -e -DB_NAME="${DB_NAME:-hospital}" -DB_USER="${DB_USER:-hospital}" -DB_PASSWORD="${DB_PASSWORD:-hospital123}" +echo "Starting local MongoDB server (only accessible from inside container)..." +# Start mongod as the default mongodb user, bound to localhost (127.0.0.1) for internal access only +su -s /bin/bash -c "mongod --dbpath /data/db --bind_ip 127.0.0.1 --fork --logpath /var/log/mongodb/mongod.log" mongodb -echo "Waiting for MariaDB to be ready..." -for i in $(seq 1 60); do - mysql -h "${DB_HOST:-db}" -u "$DB_USER" -p"$DB_PASSWORD" "$DB_NAME" -e "SELECT 1" 2>/dev/null && break || sleep 2 +echo "Waiting for MongoDB to be ready..." +for i in $(seq 1 30); do + mongosh --eval "db.adminCommand('ping')" --quiet &>/dev/null && break + sleep 1 done -ADMIN_DNI="${ADMIN_DNI:-00000000}" -ADMIN_PASSWORD="${ADMIN_PASSWORD:-Admin123!}" +echo "Starting Nginx reverse proxy..." +nginx -echo "Starting nginx..." -nginx & - -echo "Waiting for API server..." -sleep 3 - -echo "Creating admin user via API..." -curl -s -X POST "http://localhost:4000/api/usuarios" \ - -H "Content-Type: application/json" \ - -d "{\"apellido\":\"Administrador\",\"nombre\":\"Sistema\",\"dni\":\"$ADMIN_DNI\",\"fechaNacimiento\":\"2024-01-01\",\"email\":\"admin@hospital.local\",\"rol\":\"admin\",\"password\":\"$ADMIN_PASSWORD\"}" \ - || echo "Admin user may already exist" - -echo "==========================================" -echo "ADMIN CREDENTIALS:" -echo "DNI: $ADMIN_DNI" -echo "Password: $ADMIN_PASSWORD" -echo "==========================================" - -exec node server/api-mariadb.js \ No newline at end of file +echo "Starting Node.js backend connected to local MongoDB..." +# Start the Node.js API server using MongoDB +exec node server/api-mongodb.js diff --git a/server/api-mongodb.js b/server/api-mongodb.js new file mode 100644 index 0000000..60076f8 --- /dev/null +++ b/server/api-mongodb.js @@ -0,0 +1,665 @@ +import express from 'express'; +import cors from 'cors'; +import { initDb, + getUsuarioByDni, getUsuarioById, getAllUsuarios, createUsuario, updateUsuario, deleteUsuario, verifyPassword, hashPassword, + getAllPacientes, getPacienteById, createPaciente, updatePaciente, deletePaciente, + getAllAreas, createArea, updateArea, deleteArea, + getAllCamas, getCamaById, createCama, updateCama, deleteCama, + getAllInternaciones, getInternacionById, createInternacion, updateInternacion, deleteInternacion, + getAllEvoluciones, createEvolucion, updateEvolucion, deleteEvolucion, + getAllLaboratorios, createLaboratorio, updateLaboratorio, deleteLaboratorio, + getAllAcidosBase, createAcidoBase, deleteAcidoBase, + getAllCultivos, createCultivo, updateCultivo, deleteCultivo, + getAllEstudiosComplementarios, createEstudioComplementario, deleteEstudioComplementario, + getAllInterconsultas, createInterconsulta, updateInterconsulta, deleteInterconsulta, + getAllAtb, createAtb, deleteAtb, + getAllIndicaciones, createIndicacion, updateIndicacion, deleteIndicacion, + getAllMovimientosIndicaciones, createMovimientoIndicacion, + getValue, setValue +} from './db-mongodb.js'; + +const app = express(); +app.use(cors({ + origin: true, + credentials: true +})); +app.use(express.json({ limit: '50mb' })); + +// UUID generator +function generateUUID() { + if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { + return crypto.randomUUID(); + } + 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); + }); +} + +// ========== STATE ENDPOINT (initial load) ========== +app.get('/api/state', async (req, res) => { + try { + const state = { + pacientes: await getAllPacientes(), + areas: await getAllAreas(), + camas: await getAllCamas(), + internaciones: await getAllInternaciones(), + evoluciones: (await getAllEvoluciones()).map(e => ({ + ...e, + signosVitales: typeof e.signosVitales === 'string' ? JSON.parse(e.signosVitales) : e.signosVitales, + examenFisico: typeof e.examenFisico === 'string' ? JSON.parse(e.examenFisico) : e.examenFisico + })), + laboratorios: (await getAllLaboratorios()).map(l => ({ ...l, resultados: typeof l.resultados === 'string' ? JSON.parse(l.resultados) : l.resultados })), + acidosBase: await getAllAcidosBase(), + cultivos: await getAllCultivos(), + estudiosComplementarios: await getAllEstudiosComplementarios(), + interconsultas: (await getAllInterconsultas()).map(ic => ({ ...ic, realizada: !!ic.realizada })), + atb: await getAllAtb(), + indicaciones: await getAllIndicaciones(), + movimientosIndicaciones: await getAllMovimientosIndicaciones(), + vistaActual: 'dashboard', + currentInternacionId: null + }; + res.json(state); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'failed to read state' }); + } +}); + +// ========== USUARIOS ========== +app.get('/api/usuarios', async (req, res) => { + try { + const usuarios = await getAllUsuarios(); + const usuariosSinPassword = usuarios.map(({ passwordHash, ...u }) => u); + res.json(usuariosSinPassword); + } catch (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 = 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) { + 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 = hashPassword(password); + + await updateUsuario(id, datos); + res.json({ ok: true }); + } catch (err) { + res.status(500).json({ error: 'Error al actualizar usuario' }); + } +}); + +app.delete('/api/usuarios/:id', async (req, res) => { + try { + await deleteUsuario(req.params.id); + res.json({ ok: true }); + } catch (err) { + res.status(500).json({ error: 'Error al eliminar usuario' }); + } +}); + +// ========== PACIENTES ========== +app.get('/api/pacientes', async (req, res) => { + try { + res.json(await getAllPacientes()); + } catch (err) { + res.status(500).json({ error: 'Error al obtener pacientes' }); + } +}); + +app.post('/api/pacientes', async (req, res) => { + try { + const paciente = { ...req.body, id: generateUUID() }; + await createPaciente(paciente); + res.json(paciente); + } catch (err) { + res.status(500).json({ error: 'Error al crear paciente' }); + } +}); + +app.put('/api/pacientes/:id', async (req, res) => { + try { + await updatePaciente(req.params.id, req.body); + res.json({ ok: true }); + } catch (err) { + res.status(500).json({ error: 'Error al actualizar paciente' }); + } +}); + +app.delete('/api/pacientes/:id', async (req, res) => { + try { + await deletePaciente(req.params.id); + res.json({ ok: true }); + } catch (err) { + res.status(500).json({ error: 'Error al eliminar paciente' }); + } +}); + +// ========== AREAS ========== +app.get('/api/areas', async (req, res) => { + try { + res.json(await getAllAreas()); + } catch (err) { + res.status(500).json({ error: 'Error al obtener areas' }); + } +}); + +app.post('/api/areas', async (req, res) => { + try { + const area = { ...req.body, id: generateUUID() }; + await createArea(area); + res.json(area); + } catch (err) { + res.status(500).json({ error: 'Error al crear area' }); + } +}); + +app.put('/api/areas/:id', async (req, res) => { + try { + await updateArea(req.params.id, req.body); + res.json({ ok: true }); + } catch (err) { + res.status(500).json({ error: 'Error al actualizar area' }); + } +}); + +app.delete('/api/areas/:id', async (req, res) => { + try { + await deleteArea(req.params.id); + res.json({ ok: true }); + } catch (err) { + res.status(500).json({ error: 'Error al eliminar area' }); + } +}); + +// ========== CAMAS ========== +app.get('/api/camas', async (req, res) => { + try { + res.json(await getAllCamas()); + } catch (err) { + res.status(500).json({ error: 'Error al obtener camas' }); + } +}); + +app.put('/api/camas/:id', async (req, res) => { + try { + await updateCama(req.params.id, req.body); + res.json({ ok: true }); + } catch (err) { + res.status(500).json({ error: 'Error al actualizar cama' }); + } +}); + +app.post('/api/camas', async (req, res) => { + try { + const cama = req.body; + if (!cama.id) { + cama.id = generateUUID(); + } + await createCama(cama); + res.json(cama); + } catch (err) { + res.status(500).json({ error: 'Error al crear cama' }); + } +}); + +app.delete('/api/camas/:id', async (req, res) => { + try { + await deleteCama(req.params.id); + res.json({ ok: true }); + } catch (err) { + res.status(500).json({ error: 'Error al eliminar cama' }); + } +}); + +// ========== INTERNACIONES ========== +app.get('/api/internaciones', async (req, res) => { + try { + res.json(await getAllInternaciones()); + } catch (err) { + res.status(500).json({ error: 'Error al obtener internaciones' }); + } +}); + +app.post('/api/internaciones', async (req, res) => { + try { + const internacion = { ...req.body, id: generateUUID(), activa: true }; + await createInternacion(internacion); + res.json(internacion); + } catch (err) { + res.status(500).json({ error: 'Error al crear internacion' }); + } +}); + +app.put('/api/internaciones/:id', async (req, res) => { + try { + await updateInternacion(req.params.id, req.body); + res.json({ ok: true }); + } catch (err) { + res.status(500).json({ error: 'Error al actualizar internacion' }); + } +}); + +app.delete('/api/internaciones/:id', async (req, res) => { + try { + await deleteInternacion(req.params.id); + res.json({ ok: true }); + } catch (err) { + res.status(500).json({ error: 'Error al eliminar internacion' }); + } +}); + +// ========== EVOLUCIONES ========== +app.get('/api/evoluciones', async (req, res) => { + try { + res.json(await getAllEvoluciones()); + } catch (err) { + res.status(500).json({ error: 'Error al obtener evoluciones' }); + } +}); + +app.post('/api/evoluciones', async (req, res) => { + try { + const evolucion = { ...req.body, id: generateUUID() }; + await createEvolucion(evolucion); + res.json(evolucion); + } catch (err) { + res.status(500).json({ error: 'Error al crear evolucion' }); + } +}); + +app.put('/api/evoluciones/:id', async (req, res) => { + try { + await updateEvolucion(req.params.id, req.body); + res.json({ ok: true }); + } catch (err) { + res.status(500).json({ error: 'Error al actualizar evolucion' }); + } +}); + +app.delete('/api/evoluciones/:id', async (req, res) => { + try { + await deleteEvolucion(req.params.id); + res.json({ ok: true }); + } catch (err) { + res.status(500).json({ error: 'Error al eliminar evolucion' }); + } +}); + +// ========== LABORATORIOS ========== +app.get('/api/laboratorios', async (req, res) => { + try { + res.json(await getAllLaboratorios()); + } catch (err) { + res.status(500).json({ error: 'Error al obtener laboratorios' }); + } +}); + +app.post('/api/laboratorios', async (req, res) => { + try { + const laboratorio = { ...req.body, id: generateUUID() }; + await createLaboratorio(laboratorio); + res.json(laboratorio); + } catch (err) { + res.status(500).json({ error: 'Error al crear laboratorio' }); + } +}); + +app.put('/api/laboratorios/:id', async (req, res) => { + try { + await updateLaboratorio(req.params.id, req.body); + res.json({ ok: true }); + } catch (err) { + res.status(500).json({ error: 'Error al actualizar laboratorio' }); + } +}); + +app.delete('/api/laboratorios/:id', async (req, res) => { + try { + await deleteLaboratorio(req.params.id); + res.json({ ok: true }); + } catch (err) { + res.status(500).json({ error: 'Error al eliminar laboratorio' }); + } +}); + +// ========== ACIDOS BASE ========== +app.get('/api/acid-os-base', async (req, res) => { + try { + res.json(await getAllAcidosBase()); + } catch (err) { + res.status(500).json({ error: 'Error al obtener acidos base' }); + } +}); + +app.post('/api/acid-os-base', async (req, res) => { + try { + const acido = { ...req.body, id: generateUUID() }; + await createAcidoBase(acido); + res.json(acido); + } catch (err) { + res.status(500).json({ error: 'Error al crear acido base' }); + } +}); + +app.delete('/api/acid-os-base/:id', async (req, res) => { + try { + await deleteAcidoBase(req.params.id); + res.json({ ok: true }); + } catch (err) { + res.status(500).json({ error: 'Error al eliminar acido base' }); + } +}); + +// ========== CULTIVOS ========== +app.get('/api/cultivos', async (req, res) => { + try { + res.json(await getAllCultivos()); + } catch (err) { + res.status(500).json({ error: 'Error al obtener cultivos' }); + } +}); + +app.post('/api/cultivos', async (req, res) => { + try { + const cultivo = { ...req.body, id: generateUUID() }; + await createCultivo(cultivo); + res.json(cultivo); + } catch (err) { + res.status(500).json({ error: 'Error al crear cultivo' }); + } +}); + +app.put('/api/cultivos/:id', async (req, res) => { + try { + await updateCultivo(req.params.id, req.body); + res.json({ ok: true }); + } catch (err) { + res.status(500).json({ error: 'Error al actualizar cultivo' }); + } +}); + +app.delete('/api/cultivos/:id', async (req, res) => { + try { + await deleteCultivo(req.params.id); + res.json({ ok: true }); + } catch (err) { + res.status(500).json({ error: 'Error al eliminar cultivo' }); + } +}); + +// ========== ESTUDIOS COMPLEMENTARIOS ========== +app.get('/api/estudios-complementarios', async (req, res) => { + try { + res.json(await getAllEstudiosComplementarios()); + } catch (err) { + res.status(500).json({ error: 'Error al obtener estudios' }); + } +}); + +app.post('/api/estudios-complementarios', async (req, res) => { + try { + const estudio = { ...req.body, id: generateUUID() }; + await createEstudioComplementario(estudio); + res.json(estudio); + } catch (err) { + res.status(500).json({ error: 'Error al crear estudio' }); + } +}); + +app.delete('/api/estudios-complementarios/:id', async (req, res) => { + try { + await deleteEstudioComplementario(req.params.id); + res.json({ ok: true }); + } catch (err) { + res.status(500).json({ error: 'Error al eliminar estudio' }); + } +}); + +// ========== INTERCONSULTAS ========== +app.get('/api/interconsultas', async (req, res) => { + try { + res.json(await getAllInterconsultas()); + } catch (err) { + res.status(500).json({ error: 'Error al obtener interconsultas' }); + } +}); + +app.post('/api/interconsultas', async (req, res) => { + try { + const interconsulta = { ...req.body, id: generateUUID() }; + await createInterconsulta(interconsulta); + res.json(interconsulta); + } catch (err) { + res.status(500).json({ error: 'Error al crear interconsulta' }); + } +}); + +app.put('/api/interconsultas/:id', async (req, res) => { + try { + await updateInterconsulta(req.params.id, req.body); + res.json({ ok: true }); + } catch (err) { + res.status(500).json({ error: 'Error al actualizar interconsulta' }); + } +}); + +app.delete('/api/interconsultas/:id', async (req, res) => { + try { + await deleteInterconsulta(req.params.id); + res.json({ ok: true }); + } catch (err) { + res.status(500).json({ error: 'Error al eliminar interconsulta' }); + } +}); + +// ========== ATB ========== +app.get('/api/atb', async (req, res) => { + try { + res.json(await getAllAtb()); + } catch (err) { + res.status(500).json({ error: 'Error al obtener ATB' }); + } +}); + +app.post('/api/atb', async (req, res) => { + try { + const atb = { ...req.body, id: generateUUID() }; + await createAtb(atb); + res.json(atb); + } catch (err) { + res.status(500).json({ error: 'Error al crear ATB' }); + } +}); + +app.delete('/api/atb/:id', async (req, res) => { + try { + await deleteAtb(req.params.id); + res.json({ ok: true }); + } catch (err) { + res.status(500).json({ error: 'Error al eliminar ATB' }); + } +}); + +// ========== INDICACIONES ========== +app.get('/api/indicaciones', async (req, res) => { + try { + res.json(await getAllIndicaciones()); + } catch (err) { + res.status(500).json({ error: 'Error al obtener indicaciones' }); + } +}); + +app.post('/api/indicaciones', async (req, res) => { + try { + const indicacion = { ...req.body, id: generateUUID() }; + await createIndicacion(indicacion); + res.json(indicacion); + } catch (err) { + res.status(500).json({ error: 'Error al crear indicacion' }); + } +}); + +app.put('/api/indicaciones/:id', async (req, res) => { + try { + await updateIndicacion(req.params.id, req.body); + res.json({ ok: true }); + } catch (err) { + res.status(500).json({ error: 'Error al actualizar indicacion' }); + } +}); + +app.delete('/api/indicaciones/:id', async (req, res) => { + try { + await deleteIndicacion(req.params.id); + res.json({ ok: true }); + } catch (err) { + res.status(500).json({ error: 'Error al eliminar indicacion' }); + } +}); + +// ========== MOVIMIENTOS INDICACIONES ========== +app.get('/api/movimientos-indicaciones', async (req, res) => { + try { + res.json(await getAllMovimientosIndicaciones()); + } catch (err) { + res.status(500).json({ error: 'Error al obtener movimientos' }); + } +}); + +app.post('/api/movimientos-indicaciones', async (req, res) => { + try { + const movimiento = { ...req.body, id: generateUUID() }; + console.log('Creating movimiento:', movimiento); + await createMovimientoIndicacion(movimiento); + res.json(movimiento); + } catch (err) { + console.error('Error creating movimiento:', err); + res.status(500).json({ error: err.message || 'Error al crear movimiento' }); + } +}); + +// ========== AUTH ========== +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 = verifyPassword(dni, password); + if (!validPassword) { + return res.status(401).json({ error: 'Contraseña incorrecta' }); + } + + const { passwordHash, ...userWithoutPassword } = usuario; + res.json(userWithoutPassword); + } catch (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 = verifyPassword(dni, oldPassword); + if (!validPassword) { + return res.status(401).json({ error: 'Contraseña actual incorrecta' }); + } + + const newHash = hashPassword(newPassword); + const usuario = await getUsuarioByDni(dni); + if (usuario) { + await updateUsuario(usuario.id, { passwordHash: newHash }); + } + res.json({ ok: true }); + } catch (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' }); + } + + const usuario = await getUsuarioByDni(dni); + if (usuario) { + await updateUsuario(usuario.id, { email: newEmail }); + } + res.json({ ok: true }); + } catch (err) { + res.status(500).json({ error: 'Error al actualizar email' }); + } +}); + +// Initialize DB and start server +const PORT = process.env.PORT || 4000; +const HOST = process.env.HOST || '0.0.0.0'; + +initDb().then(() => { + app.listen(Number(PORT), HOST, () => { + console.log(`MongoDB Server running on http://${HOST}:${PORT}`); + }); +}).catch(err => { + console.error('Failed to initialize database:', err); + process.exit(1); +}); diff --git a/server/db-mongodb.js b/server/db-mongodb.js new file mode 100644 index 0000000..d9b86c4 --- /dev/null +++ b/server/db-mongodb.js @@ -0,0 +1,582 @@ +import { MongoClient } from 'mongodb'; +import bcrypt from 'bcryptjs'; +const { compareSync, hashSync } = bcrypt; + +export function generateUUID() { + if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { + return crypto.randomUUID(); + } + 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); + }); +} + +const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017'; +const DB_NAME = process.env.DB_NAME || 'hospital'; + +let client; +let db; + +export async function initDb() { + try { + client = new MongoClient(MONGODB_URI, { + connectTimeoutMS: 10000, + }); + await client.connect(); + db = client.db(DB_NAME); + console.log(`Connected to MongoDB database: ${DB_NAME}`); + + // Create unique index for users and patients DNI + await db.collection('usuarios').createIndex({ dni: 1 }, { unique: true }); + await db.collection('pacientes').createIndex({ dni: 1 }, { unique: true }); + await db.collection('areas').createIndex({ nombre: 1 }, { unique: true }); + + // Check if default admin exists + const adminCount = await db.collection('usuarios').countDocuments(); + if (adminCount === 0) { + const adminDni = process.env.ADMIN_DNI || '12345678'; + const adminPassword = process.env.ADMIN_PASSWORD || 'admin123'; + const adminHash = hashSync(adminPassword, 10); + + const adminUser = { + id: 'admin-default', + apellido: 'Administrador', + nombre: 'Sistema', + dni: adminDni, + fechaNacimiento: '1990-01-01', + email: 'admin@hospital.local', + rol: 'admin', + passwordHash: adminHash, + fechaCreacion: new Date().toISOString().split('T')[0] + }; + await db.collection('usuarios').insertOne(adminUser); + console.log(`Default admin created: DNI ${adminDni} / Password ${adminPassword}`); + } + } catch (error) { + console.error('Failed to initialize MongoDB:', error); + throw error; + } +} + +// Helper to remove MongoDB's internal _id from returned objects to keep schema clean +function cleanDoc(doc) { + if (!doc) return null; + const { _id, ...rest } = doc; + return rest; +} + +function cleanDocs(docs) { + return docs.map(cleanDoc); +} + +// ========== USUARIOS ========== +export async function getUsuarioByDni(dni) { + const user = await db.collection('usuarios').findOne({ dni }); + return cleanDoc(user); +} + +export async function getUsuarioById(id) { + const user = await db.collection('usuarios').findOne({ id }); + return cleanDoc(user); +} + +export async function getAllUsuarios() { + const users = await db.collection('usuarios').find().sort({ apellido: 1, nombre: 1 }).toArray(); + return cleanDocs(users); +} + +export async function createUsuario(usuario) { + await db.collection('usuarios').insertOne(usuario); +} + +export async function updateUsuario(id, datos) { + const updateDoc = {}; + for (const [key, val] of Object.entries(datos)) { + if (val !== undefined) { + updateDoc[key] = val; + } + } + if (Object.keys(updateDoc).length > 0) { + await db.collection('usuarios').updateOne({ id }, { $set: updateDoc }); + } +} + +export async function deleteUsuario(id) { + await db.collection('usuarios').deleteOne({ id }); +} + +export async function verifyPassword(dni, password) { + const user = await db.collection('usuarios').findOne({ dni }); + if (!user) return false; + return compareSync(password, user.passwordHash); +} + +export function hashPassword(password) { + return hashSync(password, 10); +} + +// ========== PACIENTES ========== +export async function getAllPacientes() { + const pacientes = await db.collection('pacientes').find().toArray(); + return cleanDocs(pacientes); +} + +export async function getPacienteById(id) { + const paciente = await db.collection('pacientes').findOne({ id }); + return cleanDoc(paciente); +} + +export async function createPaciente(paciente) { + await db.collection('pacientes').insertOne(paciente); +} + +export async function updatePaciente(id, datos) { + const updateDoc = {}; + for (const [key, val] of Object.entries(datos)) { + if (key !== 'id' && val !== undefined) { + updateDoc[key] = val; + } + } + if (Object.keys(updateDoc).length > 0) { + await db.collection('pacientes').updateOne({ id }, { $set: updateDoc }); + } +} + +export async function deletePaciente(id) { + await db.collection('pacientes').deleteOne({ id }); +} + +// ========== AREAS ========== +export async function getAllAreas() { + const areas = await db.collection('areas').find().toArray(); + return cleanDocs(areas); +} + +export async function createArea(area) { + await db.collection('areas').insertOne(area); +} + +export async function updateArea(id, datos) { + if (datos.nombre !== undefined) { + await db.collection('areas').updateOne({ id }, { $set: { nombre: datos.nombre } }); + } +} + +export async function deleteArea(id) { + await db.collection('areas').deleteOne({ id }); +} + +// ========== CAMAS ========== +export async function getAllCamas() { + const camas = await db.collection('camas').find().toArray(); + return cleanDocs(camas); +} + +export async function getCamaById(id) { + const cama = await db.collection('camas').findOne({ id }); + return cleanDoc(cama); +} + +export async function createCama(cama) { + const id = cama.id || generateUUID(); + const doc = { + id, + numero: cama.numero, + areaId: cama.areaId, + tipo: cama.tipo || 'Estándar', + estado: cama.estado || 'Disponible', + pacienteId: cama.pacienteId || null, + internacionId: cama.internacionId || null + }; + await db.collection('camas').insertOne(doc); +} + +export async function updateCama(id, updates) { + const updateDoc = {}; + for (const [key, val] of Object.entries(updates)) { + if (val !== undefined) { + updateDoc[key] = val; + } + } + if (Object.keys(updateDoc).length > 0) { + await db.collection('camas').updateOne({ id }, { $set: updateDoc }); + } +} + +export async function deleteCama(id) { + await db.collection('camas').deleteOne({ id }); +} + +// ========== INTERNACIONES ========== +export async function getAllInternaciones() { + const internaciones = await db.collection('internaciones').find().toArray(); + return cleanDocs(internaciones).map(i => ({ ...i, activa: !!i.activa })); +} + +export async function getInternacionById(id) { + const internacion = await db.collection('internaciones').findOne({ id }); + if (internacion) internacion.activa = !!internacion.activa; + return cleanDoc(internacion); +} + +export async function createInternacion(internacion) { + const doc = { + id: internacion.id, + pacienteId: internacion.pacienteId, + camaId: internacion.camaId || null, + areaId: internacion.areaId || null, + fechaIngresoHospital: internacion.fechaIngresoHospital || null, + fechaIngresoClinica: internacion.fechaIngresoClinica || null, + fechaEgreso: internacion.fechaEgreso || null, + diagnosticoIngreso: internacion.diagnosticoIngreso || null, + motivoConsulta: internacion.motivoConsulta || null, + enfermedadActual: internacion.enfermedadActual || null, + antecedentesEnfermedadActual: internacion.antecedentesEnfermedadActual || null, + diagnosticoEgreso: internacion.diagnosticoEgreso || null, + medicoIngresante: internacion.medicoIngresante || null, + motivoEgreso: internacion.motivoEgreso || null, + activa: internacion.activa ? 1 : 0, + apache: internacion.apache || null, + derivacion: internacion.derivacion || null + }; + await db.collection('internaciones').insertOne(doc); +} + +export async function updateInternacion(id, datos) { + const updateDoc = {}; + for (const [key, val] of Object.entries(datos)) { + if (val !== undefined) { + if (key === 'activa') { + updateDoc.activa = val ? 1 : 0; + } else { + updateDoc[key] = val; + } + } + } + if (Object.keys(updateDoc).length > 0) { + await db.collection('internaciones').updateOne({ id }, { $set: updateDoc }); + } +} + +export async function deleteInternacion(id) { + await db.collection('internaciones').deleteOne({ id }); +} + +// ========== EVOLUCIONES ========== +export async function getAllEvoluciones() { + const evoluciones = await db.collection('evoluciones').find().toArray(); + return cleanDocs(evoluciones); +} + +export async function createEvolucion(evolucion) { + const doc = { + id: evolucion.id, + internacionId: evolucion.internacionId, + fecha: evolucion.fecha, + hora: evolucion.hora || '', + medico: evolucion.medico || '', + signosVitales: JSON.stringify(evolucion.signosVitales || {}), + examenFisico: JSON.stringify(evolucion.examenFisico || {}), + novedades: evolucion.novedades || '', + comentarios: evolucion.comentarios || evolucion.comentario || '', + pendientes: evolucion.pendientes || '' + }; + await db.collection('evoluciones').insertOne(doc); +} + +export async function updateEvolucion(id, datos) { + const updateDoc = {}; + for (const [key, val] of Object.entries(datos)) { + if (val !== undefined) { + if (key === 'comentario') { + updateDoc.comentarios = val; + } else if (key === 'signosVitales' || key === 'examenFisico') { + updateDoc[key] = JSON.stringify(val); + } else { + updateDoc[key] = val; + } + } + } + if (Object.keys(updateDoc).length > 0) { + await db.collection('evoluciones').updateOne({ id }, { $set: updateDoc }); + } +} + +export async function deleteEvolucion(id) { + await db.collection('evoluciones').deleteOne({ id }); +} + +// ========== LABORATORIOS ========== +export async function getAllLaboratorios() { + const laboratorios = await db.collection('laboratorios').find().toArray(); + return cleanDocs(laboratorios); +} + +export async function createLaboratorio(laboratorio) { + const doc = { + id: laboratorio.id, + pacienteId: laboratorio.pacienteId, + internacionId: laboratorio.internacionId || null, + fecha: laboratorio.fecha, + hora: laboratorio.hora || null, + tipo: laboratorio.tipo, + resultados: JSON.stringify(laboratorio.resultados || {}), + observaciones: laboratorio.observaciones || null, + medicoSolicitante: laboratorio.medicoSolicitante || null + }; + await db.collection('laboratorios').insertOne(doc); +} + +export async function updateLaboratorio(id, datos) { + const updateDoc = {}; + for (const [key, val] of Object.entries(datos)) { + if (val !== undefined) { + if (key === 'resultados') { + updateDoc.resultados = JSON.stringify(val); + } else { + updateDoc[key] = val; + } + } + } + if (Object.keys(updateDoc).length > 0) { + await db.collection('laboratorios').updateOne({ id }, { $set: updateDoc }); + } +} + +export async function deleteLaboratorio(id) { + await db.collection('laboratorios').deleteOne({ id }); +} + +// ========== ACIDOS BASE ========== +export async function getAllAcidosBase() { + const acidos = await db.collection('acidosbase').find().toArray(); + return cleanDocs(acidos); +} + +export async function createAcidoBase(acido) { + const doc = { + id: acido.id, + pacienteId: acido.pacienteId, + internacionId: acido.internacionId || null, + fecha: acido.fecha, + hora: acido.hora || null, + ph: acido.ph !== undefined ? Number(acido.ph) : null, + pco2: acido.pco2 !== undefined ? Number(acido.pco2) : null, + po2: acido.po2 !== undefined ? Number(acido.po2) : null, + hco3: acido.hco3 !== undefined ? Number(acido.hco3) : null, + be: acido.be !== undefined ? Number(acido.be) : null, + sato2: acido.sato2 !== undefined ? Number(acido.sato2) : null, + lactato: acido.lactato !== undefined ? Number(acido.lactato) : null, + interpretacion: acido.interpretacion || null, + fio2: acido.fio2 !== undefined ? Number(acido.fio2) : null + }; + await db.collection('acidosbase').insertOne(doc); +} + +export async function deleteAcidoBase(id) { + await db.collection('acidosbase').deleteOne({ id }); +} + +// ========== CULTIVOS ========== +export async function getAllCultivos() { + const cultivos = await db.collection('cultivos').find().toArray(); + return cleanDocs(cultivos); +} + +export async function createCultivo(cultivo) { + const doc = { + id: cultivo.id, + pacienteId: cultivo.pacienteId, + internacionId: cultivo.internacionId, + fechaToma: cultivo.fechaToma || null, + protocolo: cultivo.protocolo || null, + fechaResultado: cultivo.fechaResultado || null, + tipoMuestra: cultivo.tipoMuestra || null, + germen: cultivo.germen || null, + sensible: cultivo.sensible || null, + resistente: cultivo.resistente || null, + estado: cultivo.estado || 'NAF/Pendiente', + observaciones: cultivo.observaciones || null + }; + await db.collection('cultivos').insertOne(doc); +} + +export async function updateCultivo(id, datos) { + const updateDoc = {}; + for (const [key, val] of Object.entries(datos)) { + if (val !== undefined) { + updateDoc[key] = val; + } + } + if (Object.keys(updateDoc).length > 0) { + await db.collection('cultivos').updateOne({ id }, { $set: updateDoc }); + } +} + +export async function deleteCultivo(id) { + await db.collection('cultivos').deleteOne({ id }); +} + +// ========== ESTUDIOS COMPLEMENTARIOS ========== +export async function getAllEstudiosComplementarios() { + const estudios = await db.collection('estudiosComplementarios').find().toArray(); + return cleanDocs(estudios); +} + +export async function createEstudioComplementario(estudio) { + const doc = { + id: estudio.id, + pacienteId: estudio.pacienteId, + internacionId: estudio.internacionId || null, + fecha: estudio.fecha || null, + tipo: estudio.tipo || null, + resultado: estudio.resultado || null + }; + await db.collection('estudiosComplementarios').insertOne(doc); +} + +export async function deleteEstudioComplementario(id) { + await db.collection('estudiosComplementarios').deleteOne({ id }); +} + +// ========== INTERCONSULTAS ========== +export async function getAllInterconsultas() { + const interconsultas = await db.collection('interconsultas').find().toArray(); + return cleanDocs(interconsultas).map(ic => ({ ...ic, realizada: !!ic.realizada })); +} + +export async function createInterconsulta(ic) { + const doc = { + id: ic.id, + pacienteId: ic.pacienteId, + internacionId: ic.internacionId, + fecha: ic.fecha || null, + servicioInterconsultado: ic.servicioInterconsultado, + motivo: ic.motivo || null, + respuestaInterconsulta: ic.respuestaInterconsulta || null, + respuestaFecha: ic.respuestaFecha || null + }; + await db.collection('interconsultas').insertOne(doc); +} + +export async function updateInterconsulta(id, datos) { + const updateDoc = {}; + for (const [key, val] of Object.entries(datos)) { + if (val !== undefined) { + updateDoc[key] = val; + } + } + if (Object.keys(updateDoc).length > 0) { + await db.collection('interconsultas').updateOne({ id }, { $set: updateDoc }); + } +} + +export async function deleteInterconsulta(id) { + await db.collection('interconsultas').deleteOne({ id }); +} + +// ========== ATB ========== +export async function getAllAtb() { + const atb = await db.collection('atb').find().toArray(); + return cleanDocs(atb); +} + +export async function createAtb(atb) { + const doc = { + id: atb.id, + pacienteId: atb.pacienteId, + internacionId: atb.internacionId, + antibiotico: atb.antibiotico, + fechaInicio: atb.fechaInicio, + fechaFinalizacion: atb.fechaFinalizacion || null + }; + await db.collection('atb').insertOne(doc); +} + +export async function deleteAtb(id) { + await db.collection('atb').deleteOne({ id }); +} + +// ========== INDICACIONES ========== +export async function getAllIndicaciones() { + const indicaciones = await db.collection('indicaciones').find().toArray(); + return cleanDocs(indicaciones); +} + +export async function createIndicacion(ind) { + const doc = { + id: ind.id, + internacionId: ind.internacionId, + tipo: ind.tipo, + droga: ind.droga || null, + dosis: ind.dosis || null, + frecuenciaHoras: ind.frecuenciaHoras || null, + via: ind.via || null, + tipoPlan: ind.tipoPlan || null, + tipoPlan2: ind.tipoPlan2 || null, + cantidadMl: ind.cantidadMl || null, + cantidadMl2: ind.cantidadMl2 || null, + tiempoHoras: ind.tiempoHoras || null, + estado: ind.estado || 'Activa', + medicoCrea: ind.medicoCrea || null, + fechaCrea: ind.fechaCrea || null, + tipoInsulina: ind.tipoInsulina || null, + unidadesDesayuno: ind.unidadesDesayuno || null, + unidadesAlmuerzo: ind.unidadesAlmuerzo || null, + unidadesNoche: ind.unidadesNoche || null, + indicacionNoFco: ind.indicacionNoFco || null + }; + await db.collection('indicaciones').insertOne(doc); +} + +export async function updateIndicacion(id, datos) { + const updateDoc = {}; + for (const [key, val] of Object.entries(datos)) { + if (val !== undefined) { + updateDoc[key] = val; + } + } + if (Object.keys(updateDoc).length > 0) { + await db.collection('indicaciones').updateOne({ id }, { $set: updateDoc }); + } +} + +export async function deleteIndicacion(id) { + await db.collection('indicaciones').deleteOne({ id }); +} + +// ========== MOVIMIENTOS INDICACIONES ========== +export async function getAllMovimientosIndicaciones() { + const movimientos = await db.collection('movimientos_indicaciones').find().toArray(); + return cleanDocs(movimientos); +} + +export async function createMovimientoIndicacion(mov) { + const doc = { + id: mov.id, + indicacionId: mov.indicacionId, + internacionId: mov.internacionId, + tipo: mov.tipo, + fecha: mov.fecha, + profesional: mov.profesional, + indicacionPrevia: mov.indicacionPrevia || null, + indicacionNueva: mov.indicacionNueva || null + }; + await db.collection('movimientos_indicaciones').insertOne(doc); +} + +// ========== KV STORE ========== +export async function getValue(key) { + const doc = await db.collection('kv').findOne({ key }); + return doc?.value ?? null; +} + +export async function setValue(key, value) { + await db.collection('kv').updateOne( + { key }, + { $set: { key, value } }, + { upsert: true } + ); +} diff --git a/server/package.json b/server/package.json index d9a6489..d30a9bb 100644 --- a/server/package.json +++ b/server/package.json @@ -10,6 +10,7 @@ "cors": "^2.8.5", "express": "^4.21.2", "http-proxy-middleware": "^3.0.5", - "mariadb": "^3.4.0" + "mariadb": "^3.4.0", + "mongodb": "^6.8.0" } }