diff --git a/README.md b/README.md index 949ff65..d875a0f 100644 --- a/README.md +++ b/README.md @@ -37,20 +37,23 @@ tar -xzf /opt/saludut/saludut-frontend.tar.gz -C /opt/saludut/ ``` ### 4) Inicializar esquema y datos -Este repo no incluye el esquema base (tablas `usuario`, `rol`, `autorizacion`, etc). Restaura tu dump base primero. - -Luego ejecuta los SQL que si estan en el repo, en este orden: +Primero genera el hash del admin y reemplaza `` en `backend/src/comandos.sql`: +```bash +node /opt/saludut/backend/src/generate-hash.js +``` + +Luego ejecuta el setup unico (esquema + CUPS + roles/admin): +```bash +psql -U saludut_user -d saludut_db -f /opt/saludut/backend/src/setup.sql +``` + +Opcional: cargar datos masivos desde archivos: ```bash -psql -U saludut_user -d saludut_db -f /opt/saludut/backend/src/cups_schema.sql -psql -U saludut_user -d saludut_db -f /opt/saludut/backend/src/cups_referencia.sql -psql -U saludut_user -d saludut_db -f /opt/saludut/backend/src/cups_cubiertos.sql psql -U saludut_user -d saludut_db -f /opt/saludut/backend/src/establecimiento.sql psql -U saludut_user -d saludut_db -f /opt/saludut/backend/src/paciente.sql psql -U saludut_user -d saludut_db -f /opt/saludut/backend/src/ingreso.sql ``` -Si tu esquema tiene restricciones, ajusta el orden de los inserts para respetar las FK. - ### 5) Backend (.env) Crear `backend/.env` en el servidor: ``` @@ -61,28 +64,17 @@ DB_PASSWORD=TU_PASSWORD_FUERTE DB_NAME=saludut_db JWT_SECRET=CAMBIA_ESTE_SECRETO JWT_EXPIRES_IN=24h -PORT=3000 +PORT=45231 SOFFICE_PATH=/usr/bin/soffice PYTHON_PATH=python3 +ADMIN_USER=admin +ADMIN_PASS=admin123 +ADMIN_EMAIL=admin@saludut.gov.co +ADMIN_NAME=Administrador Sistema ``` ### 6) Crear usuario admin (manual) -Generar hash bcrypt: -```bash -node /opt/saludut/backend/src/generate-hash.js -``` - -Luego en PostgreSQL: -```sql -SELECT id_rol FROM rol WHERE nombre_rol = 'administrador'; - -INSERT INTO usuario - (username, email, password_hash, nombre_completo, id_rol, activo, fecha_creacion) -VALUES - ('admin', 'admin@saludut.local', '', 'Administrador Sistema', , true, NOW()); -``` - -Si tu esquema tiene columnas obligatorias adicionales, completa esos campos. +Si defines `ADMIN_USER` y `ADMIN_PASS` en `.env`, el backend crea/actualiza el admin automaticamente al iniciar. ### 7) Backend (instalar y correr) ```bash @@ -135,13 +127,20 @@ El frontend lee la base desde `window.__SALUDUT_CONFIG__`: ``` -Si el backend esta en otro dominio, cambia `apiBaseUrl` antes de publicar. +Si no usas proxy y expones el puerto directo: +```html + +``` ### 10) Nginx + SSL (ejemplo) ```nginx server { listen 80; - server_name saludut.tu-dominio.com; + server_name autorizaciones.saludut.com; root /var/www/saludut; index index.html; @@ -151,7 +150,7 @@ server { } location /api/ { - proxy_pass http://127.0.0.1:3000/; + proxy_pass http://127.0.0.1:45231/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -162,7 +161,7 @@ server { Luego: ```bash rc-service nginx start -certbot --nginx -d saludut.tu-dominio.com +certbot --nginx -d autorizaciones.saludut.com ``` ## Desarrollo diff --git a/backend/src/comandos.sql b/backend/src/comandos.sql new file mode 100644 index 0000000..c27c643 --- /dev/null +++ b/backend/src/comandos.sql @@ -0,0 +1,27 @@ +BEGIN; + +-- Roles base +INSERT INTO rol (nombre_rol, descripcion) +VALUES + ('administrador', 'Puede cargar pacientes, descargar PDFs y ver todas las autorizaciones'), + ('administrativo_sede', 'Solo puede generar autorizaciones para su sede') +ON CONFLICT (nombre_rol) DO NOTHING; + +-- Usuario admin (reemplaza con el generado) +INSERT INTO usuario (username, email, password_hash, nombre_completo, id_rol, activo) +VALUES ( + 'admin', + 'admin@saludut.gov.co', + '', + 'Administrador Sistema', + (SELECT id_rol FROM rol WHERE nombre_rol = 'administrador'), + true +) +ON CONFLICT (username) DO UPDATE SET + email = EXCLUDED.email, + password_hash = EXCLUDED.password_hash, + nombre_completo = EXCLUDED.nombre_completo, + id_rol = EXCLUDED.id_rol, + activo = true; + +COMMIT; diff --git a/backend/src/schema.sql b/backend/src/schema.sql new file mode 100644 index 0000000..b0ca0e8 --- /dev/null +++ b/backend/src/schema.sql @@ -0,0 +1,207 @@ +BEGIN; + +-- ===== Paciente ===== +CREATE TABLE IF NOT EXISTS paciente ( + interno text PRIMARY KEY NOT NULL, + tipo_documento text, + numero_documento text, + primer_apellido text, + segundo_apellido text, + primer_nombre text, + segundo_nombre text, + fecha_nacimiento date, + edad numeric, + sexo text, + activo boolean DEFAULT true +); + +CREATE OR REPLACE FUNCTION calcular_edad() +RETURNS trigger AS $$ +BEGIN + NEW.edad := EXTRACT(YEAR FROM age(current_date, NEW.fecha_nacimiento))::int; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS trg_calcular_edad ON paciente; +CREATE TRIGGER trg_calcular_edad +BEFORE INSERT OR UPDATE OF fecha_nacimiento +ON paciente +FOR EACH ROW +EXECUTE FUNCTION calcular_edad(); + +-- ===== Establecimiento ===== +CREATE TABLE IF NOT EXISTS establecimiento ( + codigo_establecimiento text PRIMARY KEY, + nombre_establecimiento text, + epc_ciudad text, + epc_departamento text, + regional text, + regional_normalizada text +); + +-- ===== IPS ===== +CREATE TABLE IF NOT EXISTS ips ( + id_ips serial PRIMARY KEY, + nit text, + nombre_ips text, + codigo_ips text, + direccion text, + telefono text, + departamento text, + municipio text +); + +-- ===== Ingreso ===== +CREATE TABLE IF NOT EXISTS ingreso ( + interno text PRIMARY KEY REFERENCES paciente(interno), + codigo_establecimiento text REFERENCES establecimiento(codigo_establecimiento), + estado text, + fecha_ingreso date, + nacionalidad text, + tiempo_reclusion text +); + +-- ===== Autorizante ===== +CREATE TABLE IF NOT EXISTS autorizante ( + numero_documento bigint PRIMARY KEY, + tipo_documento text NOT NULL, + nombre text NOT NULL, + telefono text, + cargo text, + activo boolean DEFAULT true +); + +-- ===== Roles y usuarios ===== +CREATE TABLE IF NOT EXISTS rol ( + id_rol SERIAL PRIMARY KEY, + nombre_rol VARCHAR(50) UNIQUE NOT NULL, + descripcion TEXT, + fecha_creacion TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS usuario ( + id_usuario SERIAL PRIMARY KEY, + username VARCHAR(50) UNIQUE NOT NULL, + email VARCHAR(100) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + nombre_completo VARCHAR(200) NOT NULL, + id_rol INTEGER NOT NULL REFERENCES rol(id_rol), + activo BOOLEAN DEFAULT true, + fecha_creacion TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + ultimo_login TIMESTAMP, + intentos_fallidos INTEGER DEFAULT 0, + bloqueado_hasta TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS usuario_sede ( + id_usuario_sede SERIAL PRIMARY KEY, + id_usuario INTEGER NOT NULL REFERENCES usuario(id_usuario), + codigo_establecimiento VARCHAR(50) NOT NULL REFERENCES establecimiento(codigo_establecimiento), + fecha_asignacion TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(id_usuario, codigo_establecimiento) +); + +CREATE INDEX IF NOT EXISTS idx_usuario_username ON usuario(username); +CREATE INDEX IF NOT EXISTS idx_usuario_email ON usuario(email); +CREATE INDEX IF NOT EXISTS idx_usuario_rol ON usuario(id_rol); +CREATE INDEX IF NOT EXISTS idx_usuario_sede_usuario ON usuario_sede(id_usuario); +CREATE INDEX IF NOT EXISTS idx_usuario_sede_sede ON usuario_sede(codigo_establecimiento); + +-- ===== Consecutivo autorizacion ===== +CREATE TABLE IF NOT EXISTS consecutivo_autorizacion ( + id smallint PRIMARY KEY, + codigo text NOT NULL +); + +CREATE OR REPLACE FUNCTION next_numero_autorizacion() +RETURNS text AS $$ +DECLARE + actual text; + prefijo constant text := 'UTUSCPG'; + letra text; + num int; + nuevo text; +BEGIN + SELECT codigo INTO actual + FROM consecutivo_autorizacion + WHERE id = 1 + FOR UPDATE; + + IF actual IS NULL THEN + actual := prefijo || 'B00'; + END IF; + + letra := substring(actual from 8 for 1); + num := substring(actual from 9 for 2)::int; + + IF num < 99 THEN + num := num + 1; + ELSE + letra := chr(ascii(letra) + 1); + num := 1; + END IF; + + nuevo := prefijo || letra || LPAD(num::text, 2, '0'); + + UPDATE consecutivo_autorizacion + SET codigo = nuevo + WHERE id = 1; + + RETURN nuevo; +END; +$$ LANGUAGE plpgsql; + +-- ===== Autorizacion ===== +CREATE TABLE IF NOT EXISTS autorizacion ( + numero_autorizacion text PRIMARY KEY DEFAULT next_numero_autorizacion(), + interno text NOT NULL REFERENCES paciente(interno), + id_ips integer NOT NULL REFERENCES ips(id_ips), + numero_documento_autorizante bigint NOT NULL REFERENCES autorizante(numero_documento), + fecha_autorizacion date NOT NULL DEFAULT current_date, + observacion text, + cup_codigo varchar(20), + tipo_autorizacion varchar(50) NOT NULL DEFAULT 'consultas_externas', + tipo_servicio varchar(50), + version integer NOT NULL DEFAULT 1 +); + +INSERT INTO consecutivo_autorizacion (id, codigo) +VALUES (1, COALESCE((SELECT MAX(numero_autorizacion) FROM autorizacion), 'UTUSCPGB00')) +ON CONFLICT (id) DO NOTHING; + +CREATE OR REPLACE FUNCTION asegurar_numero_unico() +RETURNS TRIGGER AS $$ +BEGIN + IF NEW.numero_autorizacion IS NULL OR NEW.numero_autorizacion = '' THEN + NEW.numero_autorizacion := next_numero_autorizacion(); + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS trg_asegurar_numero_unico ON autorizacion; +CREATE TRIGGER trg_asegurar_numero_unico +BEFORE INSERT ON autorizacion +FOR EACH ROW +EXECUTE FUNCTION asegurar_numero_unico(); + +-- ===== Versiones de autorizacion ===== +CREATE TABLE IF NOT EXISTS autorizacion_version ( + id SERIAL PRIMARY KEY, + numero_autorizacion VARCHAR(50) NOT NULL, + version INTEGER NOT NULL, + id_ips INTEGER NOT NULL, + numero_documento_autorizante BIGINT NOT NULL, + fecha_autorizacion DATE, + observacion TEXT, + cup_codigo VARCHAR(20), + tipo_autorizacion VARCHAR(50), + tipo_servicio VARCHAR(50), + fecha_version TIMESTAMP NOT NULL DEFAULT NOW() +); + +CREATE UNIQUE INDEX IF NOT EXISTS autorizacion_version_unique +ON autorizacion_version (numero_autorizacion, version); + +COMMIT; diff --git a/backend/src/server.js b/backend/src/server.js index af638dd..c8a991b 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -265,6 +265,64 @@ const ensureAutorizacionVersionTables = async () => { `); }; +const ensureAdminFromEnv = async () => { + const adminUser = process.env.ADMIN_USER; + const adminPass = process.env.ADMIN_PASS; + if (!adminUser || !adminPass) { + return; + } + + const adminEmail = process.env.ADMIN_EMAIL || `${adminUser}@saludut.local`; + const adminName = process.env.ADMIN_NAME || 'Administrador Sistema'; + + try { + const tablesRes = await pool.query(` + SELECT + to_regclass('public.rol') AS rol, + to_regclass('public.usuario') AS usuario + `); + const tables = tablesRes.rows[0] || {}; + if (!tables.rol || !tables.usuario) { + console.warn('Tablas de usuarios no existen, omitiendo admin automatico.'); + return; + } + + await pool.query(` + INSERT INTO rol (nombre_rol, descripcion) VALUES + ('administrador', 'Puede cargar pacientes, descargar PDFs y ver todas las autorizaciones'), + ('administrativo_sede', 'Solo puede generar autorizaciones para su sede') + ON CONFLICT (nombre_rol) DO NOTHING; + `); + + const roleRes = await pool.query( + "SELECT id_rol FROM rol WHERE nombre_rol = 'administrador' LIMIT 1" + ); + if (!roleRes.rows[0]) { + console.warn('No se encontro el rol administrador.'); + return; + } + + const passwordHash = await bcrypt.hash(adminPass, 10); + await pool.query( + ` + INSERT INTO usuario (username, email, password_hash, nombre_completo, id_rol, activo) + VALUES ($1, $2, $3, $4, $5, true) + ON CONFLICT (username) DO UPDATE SET + email = EXCLUDED.email, + password_hash = EXCLUDED.password_hash, + nombre_completo = EXCLUDED.nombre_completo, + id_rol = EXCLUDED.id_rol, + activo = true; + `, + [adminUser, adminEmail, passwordHash, adminName, roleRes.rows[0].id_rol] + ); + + console.log('Usuario admin asegurado desde .env.'); + } catch (error) { + console.warn('No se pudo crear admin desde .env:', error.message); + } +}; + // --- 1. DEFINICIÓN DE MIDDLEWARES (ANTES DE USARLOS) --- // Es crucial definir los middlewares aquí para que puedan ser usados por las rutas que se definan más abajo. // Esto resuelve el error: "Cannot access 'verificarToken' before initialization". @@ -2211,6 +2269,10 @@ ensureAutorizacionVersionTables().catch((error) => { console.error('Error inicializando tablas de versiones:', error.message); }); +ensureAdminFromEnv().catch((error) => { + console.warn('Error asegurando admin desde .env:', error.message); +}); + // --- 3. INICIO DEL SERVIDOR --- app.listen(PORT, () => { diff --git a/backend/src/setup.sql b/backend/src/setup.sql new file mode 100644 index 0000000..be87407 --- /dev/null +++ b/backend/src/setup.sql @@ -0,0 +1,7 @@ +\\set ON_ERROR_STOP on + +\\i schema.sql +\\i cups_schema.sql +\\i cups_referencia.sql +\\i cups_cubiertos.sql +\\i comandos.sql