From 2d29bd88a1a1af40b0ee0193943a2ebad20868fe Mon Sep 17 00:00:00 2001 From: Jhonathan Guevara Date: Sun, 28 Dec 2025 14:00:58 -0500 Subject: [PATCH] cambios finales --- README.md | 98 ++++++++++++++++--- backend/src/schema.sql | 20 ++++ backend/src/server.js | 85 ++++++++++++++-- .../estadisticas-autorizaciones.ts | 32 ++++-- 4 files changed, 205 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 6f2f21c..59af1e8 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,9 @@ psql -U postgres -d postgres -c "ALTER ROLE postgres WITH PASSWORD 'JKHUG9876hBh HBA=$(psql -U postgres -d postgres -Atc "show hba_file"); cp "$HBA" "$HBA.bak"; sed -i 's/\btrust\b/scram-sha-256/g' "$HBA"; rc-service postgresql reload ``` -### 3) Subir codigo +### 3) Subir codigo (desde tu maquina) ```bash -mkdir -p /opt/saludut/backend +mkdir -p /opt/saludut/backend tar -czf backend.tar.gz \ --exclude="node_modules" \ --exclude=".env" \ @@ -82,17 +82,14 @@ ADMIN_EMAIL=admin@saludut.com ADMIN_NAME=Administrador Sistema ``` -### 6) Crear usuario admin (automatico) -Si defines `ADMIN_USER` y `ADMIN_PASS` en `.env`, el backend crea/actualiza el admin automaticamente al iniciar. - -### 7) Backend (instalar y correr) +### 6) Backend (instalar y correr) ```bash cd /opt/saludut/backend npm ci --omit=dev node src/server.js ``` -#### OpenRC (servicio backend) +#### 6.1) OpenRC (servicio backend) ```bash cat <<'EOF' > /etc/init.d/saludut-backend #!/sbin/openrc-run @@ -117,15 +114,19 @@ rc-update add saludut-backend default rc-service saludut-backend start ``` -### Frontend +### 7) Frontend + Nginx (deploy) +#### 7.1) Acceso SSH y paquetes base ```bash sed -i 's/#Port 22/Port 48952/g' /etc/ssh/sshd_config rc-service sshd restart ssh -p 48952 root@autorizacion.saludut.com apk add nginx certbot certbot-nginx nftables rsync +``` -#Ejecutar publishNftables.sh +#### 7.2) Publicar scripts (nftables + frontend) +```bash +# Ejecutar publishNftables.sh head -n1 ./scripts/publishNftables.sh | cat -A sed -i 's/\r$//' ./scripts/publishNftables.sh sed -i '1s|^.*$|#!/usr/bin/env bash|' ./scripts/publishNftables.sh @@ -133,15 +134,18 @@ chmod +x ./scripts/publishNftables.sh file ./scripts/publishNftables.sh bash ./scripts/publishNftables.sh -#Ejecutar publish.sh +# Ejecutar publish.sh head -n1 ./scripts/publish.sh | cat -A sed -i 's/\r$//' ./scripts/publish.sh sed -i '1s|^.*$|#!/usr/bin/env bash|' ./scripts/publish.sh chmod +x ./scripts/publish.sh file ./scripts/publish.sh bash ./scripts/publish.sh +``` -#Ejecutar el nginx de autorizacion.saludut.com +#### 7.3) Nginx HTTP + certbot +```bash +# Ejecutar el nginx de autorizacion.saludut.com scp -P 48952 scripts/vhost/autorizacion.saludut.com.conf root@autorizacion.saludut.com:/etc/nginx/http.d/ server { @@ -177,7 +181,10 @@ server { rc-service nginx start sudo certbot --nginx -d autorizacion.saludut.com +``` +#### 7.4) Nginx HTTPS (redirect + SSL) +```bash scp -P 48952 scripts/vhost/autorizacion.saludut.com.conf root@autorizacion.saludut.com:/etc/nginx/http.d/ server { @@ -197,7 +204,7 @@ server { client_max_body_size 60m; - # ✅ Angular está dentro de /browser + # Ojo: Angular esta dentro de /browser root /var/www/autorizacion.saludut.com/htdocs/browser; index index.html; @@ -225,5 +232,70 @@ server { } nginx -t && rc-service nginx restart - +``` + +#### 7.5) Python venv (scripts) +```bash +set -e + +# 1) Paquetes base para Python + compilacion (Alpine) +apk add --no-cache \ + python3 py3-pip py3-virtualenv \ + build-base python3-dev musl-dev \ + libffi-dev openssl-dev \ + openblas-dev lapack-dev gfortran \ + postgresql-dev + +# 2) Crear entorno virtual "general" para SaludUT +mkdir -p /opt/saludut +python3 -m venv /opt/saludut/venv +. /opt/saludut/venv/bin/activate + +# 3) Actualizar pip y herramientas +python -m pip install --upgrade pip setuptools wheel + +# 4) Librerias recomendadas (Excel + pandas + postgres + .env + utilidades comunes) +pip install pandas numpy openpyxl xlsxwriter xlrd python-dotenv psycopg2-binary requests pytz python-dateutil tqdm + +# (Opcional util si en algun punto generas PDF con python) +pip install reportlab || true + +# 5) Probar que todo importa bien +/opt/saludut/venv/bin/python -c "import pandas as pd; import openpyxl; import psycopg2; import dotenv; print('OK: pandas/openpyxl/psycopg2/dotenv')" + +# 6) Actualizar .env para que Node use el Python del venv +# (cambia PYTHON_PATH=python3 -> PYTHON_PATH=/opt/saludut/venv/bin/python) +sed -i 's|^PYTHON_PATH=.*|PYTHON_PATH=/opt/saludut/venv/bin/python|g' /opt/saludut/backend/.env + +# 7) Reiniciar backend y ver errores +rc-service saludut-backend restart || true +sleep 1 +tail -n 80 /var/log/saludut-backend.err || true +``` + +#### 7.6) LibreOffice + fuentes (opcional) +```bash +set -e + +apk update +apk add --no-cache \ + libreoffice \ + fontconfig \ + ttf-dejavu \ + ttf-liberation \ + font-noto \ + font-noto-cjk \ + poppler-utils \ + python3 py3-pip \ + py3-pandas py3-openpyxl py3-psycopg2 py3-dateutil py3-dotenv + +# cache de fuentes (clave para que no salgan cuadritos) +fc-cache -f -v + +# por si tu codigo usa librerias extra (opcional) +pip3 install --break-system-packages --upgrade pip +pip3 install --break-system-packages pdfplumber pypdf reportlab pillow + +# verificacion +python3 -c "import pandas, openpyxl, dotenv; print('OK', pandas.__version__, openpyxl.__version__, dotenv.__version__)" ``` diff --git a/backend/src/schema.sql b/backend/src/schema.sql index f084e69..d6300cb 100644 --- a/backend/src/schema.sql +++ b/backend/src/schema.sql @@ -96,6 +96,26 @@ CREATE TABLE IF NOT EXISTS usuario ( token_version INTEGER NOT NULL DEFAULT 1 ); +DROP FUNCTION IF EXISTS validar_contrasena(text); +CREATE OR REPLACE FUNCTION validar_contrasena(contrasena TEXT) +RETURNS BOOLEAN AS $$ +BEGIN + IF contrasena IS NULL THEN + RETURN false; + END IF; + IF length(contrasena) < 8 THEN + RETURN false; + END IF; + IF contrasena !~ '[A-Za-z]' THEN + RETURN false; + END IF; + IF contrasena !~ '[0-9]' THEN + RETURN false; + END IF; + RETURN true; +END; +$$ LANGUAGE plpgsql; + CREATE TABLE IF NOT EXISTS usuario_sede ( id_usuario_sede SERIAL PRIMARY KEY, id_usuario INTEGER NOT NULL REFERENCES usuario(id_usuario), diff --git a/backend/src/server.js b/backend/src/server.js index 00e0616..ef588fc 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -335,6 +335,60 @@ const ensureUsuarioTokenVersion = async () => { `); }; +const ensurePasswordValidationFunction = async () => { + await pool.query(` + DROP FUNCTION IF EXISTS validar_contrasena(text); + `); + + await pool.query(` + CREATE OR REPLACE FUNCTION validar_contrasena(contrasena TEXT) + RETURNS BOOLEAN AS $$ + BEGIN + IF contrasena IS NULL THEN + RETURN false; + END IF; + IF length(contrasena) < 8 THEN + RETURN false; + END IF; + IF contrasena !~ '[A-Za-z]' THEN + RETURN false; + END IF; + IF contrasena !~ '[0-9]' THEN + RETURN false; + END IF; + RETURN true; + END; + $$ LANGUAGE plpgsql; + `); +}; + +const validarContrasenaLocal = (password) => { + if (!password || password.length < 8) { + return false; + } + if (!/[A-Za-z]/.test(password)) { + return false; + } + if (!/[0-9]/.test(password)) { + return false; + } + return true; +}; + +const validarContrasena = async (password) => { + try { + const sqlValidar = 'SELECT validar_contrasena($1) as valida'; + const { rows } = await pool.query(sqlValidar, [password]); + return rows[0]?.valida === true; + } catch (error) { + console.warn( + 'validar_contrasena no disponible, usando validacion local:', + error?.message || error + ); + return validarContrasenaLocal(password); + } +}; + const ensureAdminFromEnv = async () => { const adminUser = process.env.ADMIN_USER; const adminPass = process.env.ADMIN_PASS; @@ -3270,16 +3324,19 @@ app.post('/api/auth/register', verificarToken, esAdministrador, async (req, res) console.log('Solicitud de registro:', req.body); - if (!username || !email || !password || !nombre_completo || !id_rol) { + if (!username || !email || !password || !nombre_completo || id_rol === undefined || id_rol === null) { return res.status(400).json({ error: 'Todos los campos son requeridos' }); } + const idRol = Number(id_rol); + if (!Number.isFinite(idRol)) { + return res.status(400).json({ error: 'id_rol invalido' }); + } + try { // Validar contraseña con la función de Postgres - const sqlValidar = 'SELECT validar_contrasena($1) as valida'; - const { rows } = await pool.query(sqlValidar, [password]); - - if (!rows[0].valida) { + const valida = await validarContrasena(password); + if (!valida) { return res.status(400).json({ error: 'La contraseña debe tener al menos 8 caracteres y contener letras y números' }); @@ -3293,6 +3350,11 @@ app.post('/api/auth/register', verificarToken, esAdministrador, async (req, res) return res.status(400).json({ error: 'El usuario o email ya están registrados' }); } + const rolCheck = await pool.query('SELECT id_rol FROM rol WHERE id_rol = $1', [idRol]); + if (rolCheck.rows.length === 0) { + return res.status(400).json({ error: 'El rol seleccionado no existe' }); + } + // Encriptar contraseña const passwordHash = await bcrypt.hash(password, 10); @@ -3304,13 +3366,13 @@ app.post('/api/auth/register', verificarToken, esAdministrador, async (req, res) `; const { rows: newUsuario } = await pool.query(insertSql, [ - username, email, passwordHash, nombre_completo, id_rol + username, email, passwordHash, nombre_completo, idRol ]); const usuarioCreado = newUsuario[0]; // Si es administrativo_sede, asignar sedes - if (id_rol == 2) { + if (idRol === 2) { let sedesAsignadas = Array.isArray(sedes) ? sedes : []; sedesAsignadas = sedesAsignadas .map((s) => String(s || '').trim()) @@ -3505,9 +3567,8 @@ app.patch('/api/usuarios/:id', verificarToken, esAdministrador, async (req, res) return res.status(400).json({ error: 'password no puede estar vacia' }); } - const sqlValidar = 'SELECT validar_contrasena($1) as valida'; - const { rows: validacion } = await pool.query(sqlValidar, [passwordValue]); - if (!validacion[0]?.valida) { + const valida = await validarContrasena(passwordValue); + if (!valida) { return res.status(400).json({ error: 'La contrasena debe tener al menos 8 caracteres y contener letras y numeros' }); @@ -3837,6 +3898,10 @@ ensureUsuarioTokenVersion().catch((error) => { console.error('Error inicializando token_version de usuario:', error.message); }); +ensurePasswordValidationFunction().catch((error) => { + console.error('Error inicializando validar_contrasena:', error.message); +}); + ensureAdminFromEnv().catch((error) => { console.warn('Error asegurando admin desde .env:', error.message); }); diff --git a/saludut-inpec/src/app/components/estadisticas-autorizaciones/estadisticas-autorizaciones.ts b/saludut-inpec/src/app/components/estadisticas-autorizaciones/estadisticas-autorizaciones.ts index f5873bf..19e6e75 100644 --- a/saludut-inpec/src/app/components/estadisticas-autorizaciones/estadisticas-autorizaciones.ts +++ b/saludut-inpec/src/app/components/estadisticas-autorizaciones/estadisticas-autorizaciones.ts @@ -205,7 +205,7 @@ export class EstadisticasAutorizacionesComponent implements OnInit { } formatDateLabel(fecha: string): string { - const date = new Date(fecha); + const date = this.parseDate(fecha); if (Number.isNaN(date.getTime())) return fecha; return date.toLocaleDateString('es-CO', { weekday: 'short', @@ -296,7 +296,7 @@ export class EstadisticasAutorizacionesComponent implements OnInit { if (this.periodo === 'dia') { return dias.map((dia) => { - const date = new Date(dia.fecha); + const date = this.parseDate(dia.fecha); const label = Number.isNaN(date.getTime()) ? '' : String(date.getDate()); @@ -313,13 +313,13 @@ export class EstadisticasAutorizacionesComponent implements OnInit { }); } - const rangoInicio = new Date(rango.inicio); - const rangoFin = new Date(rango.fin); + const rangoInicio = this.parseDate(rango.inicio); + const rangoFin = this.parseDate(rango.fin); if (this.periodo === 'semana') { const buckets = new Map(); dias.forEach((dia) => { - const date = new Date(dia.fecha); + const date = this.parseDate(dia.fecha); if (Number.isNaN(date.getTime())) return; const weekStart = this.getWeekStart(date); const weekEnd = this.addDays(weekStart, 6); @@ -352,7 +352,7 @@ export class EstadisticasAutorizacionesComponent implements OnInit { if (this.periodo === 'mes') { const buckets = new Map(); dias.forEach((dia) => { - const date = new Date(dia.fecha); + const date = this.parseDate(dia.fecha); if (Number.isNaN(date.getTime())) return; const key = `${date.getFullYear()}-${date.getMonth()}`; if (!buckets.has(key)) { @@ -384,7 +384,7 @@ export class EstadisticasAutorizacionesComponent implements OnInit { const buckets = new Map(); dias.forEach((dia) => { - const date = new Date(dia.fecha); + const date = this.parseDate(dia.fecha); if (Number.isNaN(date.getTime())) return; const year = date.getFullYear(); const key = String(year); @@ -438,6 +438,24 @@ export class EstadisticasAutorizacionesComponent implements OnInit { return a < b ? a : b; } + private parseDate(value: string): Date { + if (!value) { + return new Date('invalid'); + } + const clean = value.slice(0, 10); + const parts = clean.split('-'); + if (parts.length !== 3) { + return new Date(value); + } + const year = Number(parts[0]); + const month = Number(parts[1]); + const day = Number(parts[2]); + if (!year || !month || !day) { + return new Date(value); + } + return new Date(year, month - 1, day); + } + private formatWeekLabel(inicio: Date, fin: Date): string { const inicioDia = inicio.getDate(); const finDia = fin.getDate();