This commit is contained in:
Jhonathan Guevara 2025-12-23 13:57:30 -05:00
parent 4444dbe92b
commit 0ba872401f
Signed by: jhonathan_guevara
GPG Key ID: 619239F12DCBE55B
5 changed files with 331 additions and 29 deletions

View File

@ -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 `<HASH_BCRYPT>` 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', '<HASH_BCRYPT>', 'Administrador Sistema', <ID_ROL>, 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__`:
</script>
```
Si el backend esta en otro dominio, cambia `apiBaseUrl` antes de publicar.
Si no usas proxy y expones el puerto directo:
```html
<script>
window.__SALUDUT_CONFIG__ = {
apiBaseUrl: 'https://autorizaciones.saludut.com:45231'
};
</script>
```
### 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

27
backend/src/comandos.sql Normal file
View File

@ -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 <HASH_BCRYPT> con el generado)
INSERT INTO usuario (username, email, password_hash, nombre_completo, id_rol, activo)
VALUES (
'admin',
'admin@saludut.gov.co',
'<HASH_BCRYPT>',
'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;

207
backend/src/schema.sql Normal file
View File

@ -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;

View File

@ -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, () => {

7
backend/src/setup.sql Normal file
View File

@ -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