diff --git a/CHANGELOG.md b/CHANGELOG.md index 9598619..6ddcaa5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog - project.linkedcorp.net +## [1.15.1] - 2026-09-15 +### Added +- `scripts/install.sh`: instalador para desplegar en un servidor nuevo. Verifica requisitos + (Node.js, npm, cliente mysql, systemd, espacio, puertos), pregunta dominio/puertos y base de + datos (MariaDB local auto-instalada o un servidor existente), genera `backend/.env` con + secretos aleatorios, corre migraciones + catálogos, compila el frontend y crea/arranca los + servicios systemd. No crea ningún usuario — el primer registro público se vuelve + superadministrador solo (bootstrap ya existente). +### Fixed +- Migración `006_seed_it_baseline.sql` fallaba (error de foreign key) al correr sobre una base de + datos nueva/vacía, porque insertaba proyectos de ejemplo específicos de LinkedCorp asumiendo + catálogos y un usuario ya existentes. Encontrado probando el instalador de punta a punta contra + una BD recién creada. Ahora detecta si esos prerequisitos no existen (instalación nueva, otra + empresa) y queda inerte para siempre ahí, sin afectar el comportamiento ya probado en dev/prod + (donde el marcador de "ya aplicado" sigue intacto). + ## [1.15.0] - 2026-09-15 ### Added - Marca de la empresa configurable: nombre y logo editables desde Configuración → Empresa (solo diff --git a/DEPLOY_WEB.md b/DEPLOY_WEB.md index 19b2541..b94b969 100644 --- a/DEPLOY_WEB.md +++ b/DEPLOY_WEB.md @@ -4,6 +4,30 @@ Arquetipo B (dos servicios separados). Primer deploy realizado el 2026-09-06. --- +## Instalación automatizada en un servidor NUEVO + +`scripts/install.sh` automatiza todo lo de esta sección para un servidor Debian/Ubuntu nuevo: +verifica requisitos (Node.js, npm, cliente mysql, systemd, espacio, puertos libres), pregunta +dominio/puertos y datos de base de datos (MariaDB local — se instala sola si hace falta — o un +servidor existente), crea la base + usuario, genera `backend/.env` con secretos aleatorios, +instala dependencias, corre migraciones + catálogos, compila el frontend y crea/arranca los +servicios systemd. + +```bash +# En el servidor nuevo, ya con el repo clonado en /opt/apps/project.linkedcorp.net/source: +cd /opt/apps/project.linkedcorp.net/source +sudo scripts/install.sh +``` + +No configura el reverse proxy/TLS (paso 11 más abajo, específico de cada proveedor) ni crea +ningún usuario: el primer registro público en `/registro` se vuelve superadministrador solo — no +hace falta `create-admin.js` en instalaciones nuevas. + +Las secciones de abajo son la referencia manual (lo que hace el script paso a paso) — útiles para +entender el porqué de cada cosa o para resolver un problema puntual sin volver a correr todo. + +--- + ## Primer despliegue en producción (ya ejecutado — referencia) ### 1. Entrar al servidor diff --git a/VERSION b/VERSION index 141f2e8..ace4423 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.15.0 +1.15.1 diff --git a/backend/database/migrations/006_seed_it_baseline.sql b/backend/database/migrations/006_seed_it_baseline.sql index f273bef..67ad2b2 100644 --- a/backend/database/migrations/006_seed_it_baseline.sql +++ b/backend/database/migrations/006_seed_it_baseline.sql @@ -6,159 +6,171 @@ SET NAMES utf8mb4; SET @already := (SELECT COUNT(*) FROM system_settings WHERE `key` = 'seed_it_baseline_2026_09'); -SET @sql := IF(@already = 0, 'DELETE FROM comments WHERE entity_type IN (\'project\',\'activity\')', 'SELECT 1'); +-- Instalacion nueva (otra empresa/servidor, catalogos y usuario 1 recien creados por +-- migrate-db.js + seed-db.js, todavia sin correr) — esta migracion es un fix de datos puntual +-- de la base de LinkedCorp, no algo que deba aplicarse en ningun otro lado. Si faltan estos +-- prerrequisitos, @should_seed queda en 0 y el marcador se escribe igual mas abajo para que +-- quede inerte para siempre en esa instalacion (no reintenta en cada migrate futuro). +SET @prereqs_ok := ( + (SELECT COUNT(*) FROM users) > 0 + AND (SELECT COUNT(*) FROM priorities WHERE id = 2) > 0 + AND (SELECT COUNT(*) FROM project_statuses WHERE id = 1) > 0 +); +SET @should_seed := (@already = 0 AND @prereqs_ok); + +SET @sql := IF(@should_seed, 'DELETE FROM comments WHERE entity_type IN (\'project\',\'activity\')', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @sql := IF(@already = 0, 'DELETE FROM attachments WHERE entity_type IN (\'project\',\'activity\')', 'SELECT 1'); +SET @sql := IF(@should_seed, 'DELETE FROM attachments WHERE entity_type IN (\'project\',\'activity\')', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @sql := IF(@already = 0, 'DELETE FROM notifications WHERE entity_type IN (\'project\',\'activity\')', 'SELECT 1'); +SET @sql := IF(@should_seed, 'DELETE FROM notifications WHERE entity_type IN (\'project\',\'activity\')', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @sql := IF(@already = 0, 'DELETE FROM projects', 'SELECT 1'); +SET @sql := IF(@should_seed, 'DELETE FROM projects', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @sql := IF(@already = 0, 'ALTER TABLE projects AUTO_INCREMENT = 1', 'SELECT 1'); +SET @sql := IF(@should_seed, 'ALTER TABLE projects AUTO_INCREMENT = 1', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @sql := IF(@already = 0, 'ALTER TABLE activities AUTO_INCREMENT = 1', 'SELECT 1'); +SET @sql := IF(@should_seed, 'ALTER TABLE activities AUTO_INCREMENT = 1', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @sql := IF(@already = 0, 'DELETE FROM users WHERE email = \'pflores@hotmail.es\'', 'SELECT 1'); +SET @sql := IF(@should_seed, 'DELETE FROM users WHERE email = \'pflores@hotmail.es\'', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -- Proyecto: Cumplimiento legal y proteccion de datos -SET @sql := IF(@already = 0, 'INSERT INTO projects (code, name, priority_id, status_id, owner_user_id, risk_level, created_by) VALUES (\'PENDING\', \'Cumplimiento legal y proteccion de datos\', 2, 1, 1, \'low\', 1)', 'SELECT 1'); +SET @sql := IF(@should_seed, 'INSERT INTO projects (code, name, priority_id, status_id, owner_user_id, risk_level, created_by) VALUES (\'PENDING\', \'Cumplimiento legal y proteccion de datos\', 2, 1, 1, \'low\', 1)', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @p := LAST_INSERT_ID(); -SET @sql := IF(@already = 0, 'UPDATE projects SET code = CONCAT(\'PRY-\', LPAD(@p,4,\'0\')) WHERE id = @p', 'SELECT 1'); +SET @sql := IF(@should_seed, 'UPDATE projects SET code = CONCAT(\'PRY-\', LPAD(@p,4,\'0\')) WHERE id = @p', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @sql := IF(@already = 0, 'INSERT IGNORE INTO project_members (project_id, user_id, role_in_project) VALUES (@p, 1, \'owner\')', 'SELECT 1'); +SET @sql := IF(@should_seed, 'INSERT IGNORE INTO project_members (project_id, user_id, role_in_project) VALUES (@p, 1, \'owner\')', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @sql := IF(@already = 0, 'INSERT INTO activities (project_id, code, name, priority_id, status_id, created_by) VALUES (@p, \'PENDING\', \'Implementar LOPD/LODPD\', 2, 1, 1)', 'SELECT 1'); +SET @sql := IF(@should_seed, 'INSERT INTO activities (project_id, code, name, priority_id, status_id, created_by) VALUES (@p, \'PENDING\', \'Implementar LOPD/LODPD\', 2, 1, 1)', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @a := LAST_INSERT_ID(); -SET @sql := IF(@already = 0, 'UPDATE activities SET code = CONCAT(\'ACT-\', LPAD(@a,4,\'0\')) WHERE id = @a', 'SELECT 1'); +SET @sql := IF(@should_seed, 'UPDATE activities SET code = CONCAT(\'ACT-\', LPAD(@a,4,\'0\')) WHERE id = @a', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @sql := IF(@already = 0, 'INSERT INTO activities (project_id, code, name, priority_id, status_id, created_by) VALUES (@p, \'PENDING\', \'Politica de cookies\', 2, 1, 1)', 'SELECT 1'); +SET @sql := IF(@should_seed, 'INSERT INTO activities (project_id, code, name, priority_id, status_id, created_by) VALUES (@p, \'PENDING\', \'Politica de cookies\', 2, 1, 1)', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @a := LAST_INSERT_ID(); -SET @sql := IF(@already = 0, 'UPDATE activities SET code = CONCAT(\'ACT-\', LPAD(@a,4,\'0\')) WHERE id = @a', 'SELECT 1'); +SET @sql := IF(@should_seed, 'UPDATE activities SET code = CONCAT(\'ACT-\', LPAD(@a,4,\'0\')) WHERE id = @a', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @sql := IF(@already = 0, 'INSERT INTO activities (project_id, code, name, priority_id, status_id, created_by) VALUES (@p, \'PENDING\', \'Cumplimiento WhatsApp Business\', 2, 1, 1)', 'SELECT 1'); +SET @sql := IF(@should_seed, 'INSERT INTO activities (project_id, code, name, priority_id, status_id, created_by) VALUES (@p, \'PENDING\', \'Cumplimiento WhatsApp Business\', 2, 1, 1)', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @a := LAST_INSERT_ID(); -SET @sql := IF(@already = 0, 'UPDATE activities SET code = CONCAT(\'ACT-\', LPAD(@a,4,\'0\')) WHERE id = @a', 'SELECT 1'); +SET @sql := IF(@should_seed, 'UPDATE activities SET code = CONCAT(\'ACT-\', LPAD(@a,4,\'0\')) WHERE id = @a', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @sql := IF(@already = 0, 'INSERT INTO activities (project_id, code, name, priority_id, status_id, created_by) VALUES (@p, \'PENDING\', \'Leyendas obligatorias en facturas\', 2, 1, 1)', 'SELECT 1'); +SET @sql := IF(@should_seed, 'INSERT INTO activities (project_id, code, name, priority_id, status_id, created_by) VALUES (@p, \'PENDING\', \'Leyendas obligatorias en facturas\', 2, 1, 1)', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @a := LAST_INSERT_ID(); -SET @sql := IF(@already = 0, 'UPDATE activities SET code = CONCAT(\'ACT-\', LPAD(@a,4,\'0\')) WHERE id = @a', 'SELECT 1'); +SET @sql := IF(@should_seed, 'UPDATE activities SET code = CONCAT(\'ACT-\', LPAD(@a,4,\'0\')) WHERE id = @a', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -- Proyecto: Estandarizar correos corporativos -SET @sql := IF(@already = 0, 'INSERT INTO projects (code, name, priority_id, status_id, owner_user_id, risk_level, created_by) VALUES (\'PENDING\', \'Estandarizar correos corporativos\', 2, 1, 1, \'low\', 1)', 'SELECT 1'); +SET @sql := IF(@should_seed, 'INSERT INTO projects (code, name, priority_id, status_id, owner_user_id, risk_level, created_by) VALUES (\'PENDING\', \'Estandarizar correos corporativos\', 2, 1, 1, \'low\', 1)', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @p := LAST_INSERT_ID(); -SET @sql := IF(@already = 0, 'UPDATE projects SET code = CONCAT(\'PRY-\', LPAD(@p,4,\'0\')) WHERE id = @p', 'SELECT 1'); +SET @sql := IF(@should_seed, 'UPDATE projects SET code = CONCAT(\'PRY-\', LPAD(@p,4,\'0\')) WHERE id = @p', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @sql := IF(@already = 0, 'INSERT IGNORE INTO project_members (project_id, user_id, role_in_project) VALUES (@p, 1, \'owner\')', 'SELECT 1'); +SET @sql := IF(@should_seed, 'INSERT IGNORE INTO project_members (project_id, user_id, role_in_project) VALUES (@p, 1, \'owner\')', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @sql := IF(@already = 0, 'INSERT INTO activities (project_id, code, name, priority_id, status_id, created_by) VALUES (@p, \'PENDING\', \'Cambiar los correos al nuevo formato (inicial + apellido, 2da inicial si duplica)\', 2, 1, 1)', 'SELECT 1'); +SET @sql := IF(@should_seed, 'INSERT INTO activities (project_id, code, name, priority_id, status_id, created_by) VALUES (@p, \'PENDING\', \'Cambiar los correos al nuevo formato (inicial + apellido, 2da inicial si duplica)\', 2, 1, 1)', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @a := LAST_INSERT_ID(); -SET @sql := IF(@already = 0, 'UPDATE activities SET code = CONCAT(\'ACT-\', LPAD(@a,4,\'0\')) WHERE id = @a', 'SELECT 1'); +SET @sql := IF(@should_seed, 'UPDATE activities SET code = CONCAT(\'ACT-\', LPAD(@a,4,\'0\')) WHERE id = @a', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -- Proyecto: Gestion de equipos y activos de TI -SET @sql := IF(@already = 0, 'INSERT INTO projects (code, name, priority_id, status_id, owner_user_id, risk_level, created_by) VALUES (\'PENDING\', \'Gestion de equipos y activos de TI\', 2, 1, 1, \'low\', 1)', 'SELECT 1'); +SET @sql := IF(@should_seed, 'INSERT INTO projects (code, name, priority_id, status_id, owner_user_id, risk_level, created_by) VALUES (\'PENDING\', \'Gestion de equipos y activos de TI\', 2, 1, 1, \'low\', 1)', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @p := LAST_INSERT_ID(); -SET @sql := IF(@already = 0, 'UPDATE projects SET code = CONCAT(\'PRY-\', LPAD(@p,4,\'0\')) WHERE id = @p', 'SELECT 1'); +SET @sql := IF(@should_seed, 'UPDATE projects SET code = CONCAT(\'PRY-\', LPAD(@p,4,\'0\')) WHERE id = @p', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @sql := IF(@already = 0, 'INSERT IGNORE INTO project_members (project_id, user_id, role_in_project) VALUES (@p, 1, \'owner\')', 'SELECT 1'); +SET @sql := IF(@should_seed, 'INSERT IGNORE INTO project_members (project_id, user_id, role_in_project) VALUES (@p, 1, \'owner\')', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @sql := IF(@already = 0, 'INSERT INTO activities (project_id, code, name, priority_id, status_id, created_by) VALUES (@p, \'PENDING\', \'Inventario de equipos (codigos / sistema de activos)\', 2, 1, 1)', 'SELECT 1'); +SET @sql := IF(@should_seed, 'INSERT INTO activities (project_id, code, name, priority_id, status_id, created_by) VALUES (@p, \'PENDING\', \'Inventario de equipos (codigos / sistema de activos)\', 2, 1, 1)', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @a := LAST_INSERT_ID(); -SET @sql := IF(@already = 0, 'UPDATE activities SET code = CONCAT(\'ACT-\', LPAD(@a,4,\'0\')) WHERE id = @a', 'SELECT 1'); +SET @sql := IF(@should_seed, 'UPDATE activities SET code = CONCAT(\'ACT-\', LPAD(@a,4,\'0\')) WHERE id = @a', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @sql := IF(@already = 0, 'INSERT INTO activities (project_id, code, name, priority_id, status_id, created_by) VALUES (@p, \'PENDING\', \'Revision periodica de equipos\', 2, 1, 1)', 'SELECT 1'); +SET @sql := IF(@should_seed, 'INSERT INTO activities (project_id, code, name, priority_id, status_id, created_by) VALUES (@p, \'PENDING\', \'Revision periodica de equipos\', 2, 1, 1)', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @a := LAST_INSERT_ID(); -SET @sql := IF(@already = 0, 'UPDATE activities SET code = CONCAT(\'ACT-\', LPAD(@a,4,\'0\')) WHERE id = @a', 'SELECT 1'); +SET @sql := IF(@should_seed, 'UPDATE activities SET code = CONCAT(\'ACT-\', LPAD(@a,4,\'0\')) WHERE id = @a', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @sql := IF(@already = 0, 'INSERT INTO activities (project_id, code, name, priority_id, status_id, created_by) VALUES (@p, \'PENDING\', \'Limpieza y mejoras de equipos\', 2, 1, 1)', 'SELECT 1'); +SET @sql := IF(@should_seed, 'INSERT INTO activities (project_id, code, name, priority_id, status_id, created_by) VALUES (@p, \'PENDING\', \'Limpieza y mejoras de equipos\', 2, 1, 1)', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @a := LAST_INSERT_ID(); -SET @sql := IF(@already = 0, 'UPDATE activities SET code = CONCAT(\'ACT-\', LPAD(@a,4,\'0\')) WHERE id = @a', 'SELECT 1'); +SET @sql := IF(@should_seed, 'UPDATE activities SET code = CONCAT(\'ACT-\', LPAD(@a,4,\'0\')) WHERE id = @a', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @sql := IF(@already = 0, 'INSERT INTO activities (project_id, code, name, priority_id, status_id, created_by) VALUES (@p, \'PENDING\', \'Respuestas automaticas de correo cuando alguien sale de la empresa\', 2, 1, 1)', 'SELECT 1'); +SET @sql := IF(@should_seed, 'INSERT INTO activities (project_id, code, name, priority_id, status_id, created_by) VALUES (@p, \'PENDING\', \'Respuestas automaticas de correo cuando alguien sale de la empresa\', 2, 1, 1)', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @a := LAST_INSERT_ID(); -SET @sql := IF(@already = 0, 'UPDATE activities SET code = CONCAT(\'ACT-\', LPAD(@a,4,\'0\')) WHERE id = @a', 'SELECT 1'); +SET @sql := IF(@should_seed, 'UPDATE activities SET code = CONCAT(\'ACT-\', LPAD(@a,4,\'0\')) WHERE id = @a', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -- Proyecto: Gestion de credenciales y licencias -SET @sql := IF(@already = 0, 'INSERT INTO projects (code, name, priority_id, status_id, owner_user_id, risk_level, created_by) VALUES (\'PENDING\', \'Gestion de credenciales y licencias\', 2, 1, 1, \'low\', 1)', 'SELECT 1'); +SET @sql := IF(@should_seed, 'INSERT INTO projects (code, name, priority_id, status_id, owner_user_id, risk_level, created_by) VALUES (\'PENDING\', \'Gestion de credenciales y licencias\', 2, 1, 1, \'low\', 1)', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @p := LAST_INSERT_ID(); -SET @sql := IF(@already = 0, 'UPDATE projects SET code = CONCAT(\'PRY-\', LPAD(@p,4,\'0\')) WHERE id = @p', 'SELECT 1'); +SET @sql := IF(@should_seed, 'UPDATE projects SET code = CONCAT(\'PRY-\', LPAD(@p,4,\'0\')) WHERE id = @p', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @sql := IF(@already = 0, 'INSERT IGNORE INTO project_members (project_id, user_id, role_in_project) VALUES (@p, 1, \'owner\')', 'SELECT 1'); +SET @sql := IF(@should_seed, 'INSERT IGNORE INTO project_members (project_id, user_id, role_in_project) VALUES (@p, 1, \'owner\')', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @sql := IF(@already = 0, 'INSERT INTO activities (project_id, code, name, priority_id, status_id, created_by) VALUES (@p, \'PENDING\', \'Archivo general de claves de sistemas y usuarios (BitWarden)\', 2, 1, 1)', 'SELECT 1'); +SET @sql := IF(@should_seed, 'INSERT INTO activities (project_id, code, name, priority_id, status_id, created_by) VALUES (@p, \'PENDING\', \'Archivo general de claves de sistemas y usuarios (BitWarden)\', 2, 1, 1)', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @a := LAST_INSERT_ID(); -SET @sql := IF(@already = 0, 'UPDATE activities SET code = CONCAT(\'ACT-\', LPAD(@a,4,\'0\')) WHERE id = @a', 'SELECT 1'); +SET @sql := IF(@should_seed, 'UPDATE activities SET code = CONCAT(\'ACT-\', LPAD(@a,4,\'0\')) WHERE id = @a', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @sql := IF(@already = 0, 'INSERT INTO activities (project_id, code, name, priority_id, status_id, created_by) VALUES (@p, \'PENDING\', \'Inventario de licencias de equipos\', 2, 1, 1)', 'SELECT 1'); +SET @sql := IF(@should_seed, 'INSERT INTO activities (project_id, code, name, priority_id, status_id, created_by) VALUES (@p, \'PENDING\', \'Inventario de licencias de equipos\', 2, 1, 1)', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @a := LAST_INSERT_ID(); -SET @sql := IF(@already = 0, 'UPDATE activities SET code = CONCAT(\'ACT-\', LPAD(@a,4,\'0\')) WHERE id = @a', 'SELECT 1'); +SET @sql := IF(@should_seed, 'UPDATE activities SET code = CONCAT(\'ACT-\', LPAD(@a,4,\'0\')) WHERE id = @a', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -- Proyecto: Infraestructura y redes -SET @sql := IF(@already = 0, 'INSERT INTO projects (code, name, priority_id, status_id, owner_user_id, risk_level, created_by) VALUES (\'PENDING\', \'Infraestructura y redes\', 2, 1, 1, \'low\', 1)', 'SELECT 1'); +SET @sql := IF(@should_seed, 'INSERT INTO projects (code, name, priority_id, status_id, owner_user_id, risk_level, created_by) VALUES (\'PENDING\', \'Infraestructura y redes\', 2, 1, 1, \'low\', 1)', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @p := LAST_INSERT_ID(); -SET @sql := IF(@already = 0, 'UPDATE projects SET code = CONCAT(\'PRY-\', LPAD(@p,4,\'0\')) WHERE id = @p', 'SELECT 1'); +SET @sql := IF(@should_seed, 'UPDATE projects SET code = CONCAT(\'PRY-\', LPAD(@p,4,\'0\')) WHERE id = @p', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @sql := IF(@already = 0, 'INSERT IGNORE INTO project_members (project_id, user_id, role_in_project) VALUES (@p, 1, \'owner\')', 'SELECT 1'); +SET @sql := IF(@should_seed, 'INSERT IGNORE INTO project_members (project_id, user_id, role_in_project) VALUES (@p, 1, \'owner\')', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @sql := IF(@already = 0, 'INSERT INTO activities (project_id, code, name, priority_id, status_id, created_by) VALUES (@p, \'PENDING\', \'Redes con el proveedor\', 2, 1, 1)', 'SELECT 1'); +SET @sql := IF(@should_seed, 'INSERT INTO activities (project_id, code, name, priority_id, status_id, created_by) VALUES (@p, \'PENDING\', \'Redes con el proveedor\', 2, 1, 1)', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @a := LAST_INSERT_ID(); -SET @sql := IF(@already = 0, 'UPDATE activities SET code = CONCAT(\'ACT-\', LPAD(@a,4,\'0\')) WHERE id = @a', 'SELECT 1'); +SET @sql := IF(@should_seed, 'UPDATE activities SET code = CONCAT(\'ACT-\', LPAD(@a,4,\'0\')) WHERE id = @a', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @sql := IF(@already = 0, 'INSERT INTO activities (project_id, code, name, priority_id, status_id, created_by) VALUES (@p, \'PENDING\', \'Server\', 2, 1, 1)', 'SELECT 1'); +SET @sql := IF(@should_seed, 'INSERT INTO activities (project_id, code, name, priority_id, status_id, created_by) VALUES (@p, \'PENDING\', \'Server\', 2, 1, 1)', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @a := LAST_INSERT_ID(); -SET @sql := IF(@already = 0, 'UPDATE activities SET code = CONCAT(\'ACT-\', LPAD(@a,4,\'0\')) WHERE id = @a', 'SELECT 1'); +SET @sql := IF(@should_seed, 'UPDATE activities SET code = CONCAT(\'ACT-\', LPAD(@a,4,\'0\')) WHERE id = @a', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @sql := IF(@already = 0, 'INSERT INTO activities (project_id, code, name, priority_id, status_id, created_by) VALUES (@p, \'PENDING\', \'Controlar el acceso a internet\', 2, 1, 1)', 'SELECT 1'); +SET @sql := IF(@should_seed, 'INSERT INTO activities (project_id, code, name, priority_id, status_id, created_by) VALUES (@p, \'PENDING\', \'Controlar el acceso a internet\', 2, 1, 1)', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @a := LAST_INSERT_ID(); -SET @sql := IF(@already = 0, 'UPDATE activities SET code = CONCAT(\'ACT-\', LPAD(@a,4,\'0\')) WHERE id = @a', 'SELECT 1'); +SET @sql := IF(@should_seed, 'UPDATE activities SET code = CONCAT(\'ACT-\', LPAD(@a,4,\'0\')) WHERE id = @a', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @sql := IF(@already = 0, 'INSERT INTO activities (project_id, code, name, priority_id, status_id, created_by) VALUES (@p, \'PENDING\', \'Mejoras continuas de la red\', 2, 1, 1)', 'SELECT 1'); +SET @sql := IF(@should_seed, 'INSERT INTO activities (project_id, code, name, priority_id, status_id, created_by) VALUES (@p, \'PENDING\', \'Mejoras continuas de la red\', 2, 1, 1)', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @a := LAST_INSERT_ID(); -SET @sql := IF(@already = 0, 'UPDATE activities SET code = CONCAT(\'ACT-\', LPAD(@a,4,\'0\')) WHERE id = @a', 'SELECT 1'); +SET @sql := IF(@should_seed, 'UPDATE activities SET code = CONCAT(\'ACT-\', LPAD(@a,4,\'0\')) WHERE id = @a', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -- Proyecto: Mejoras al sistema SAGA -SET @sql := IF(@already = 0, 'INSERT INTO projects (code, name, priority_id, status_id, owner_user_id, risk_level, created_by) VALUES (\'PENDING\', \'Mejoras al sistema SAGA\', 2, 1, 1, \'low\', 1)', 'SELECT 1'); +SET @sql := IF(@should_seed, 'INSERT INTO projects (code, name, priority_id, status_id, owner_user_id, risk_level, created_by) VALUES (\'PENDING\', \'Mejoras al sistema SAGA\', 2, 1, 1, \'low\', 1)', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @p := LAST_INSERT_ID(); -SET @sql := IF(@already = 0, 'UPDATE projects SET code = CONCAT(\'PRY-\', LPAD(@p,4,\'0\')) WHERE id = @p', 'SELECT 1'); +SET @sql := IF(@should_seed, 'UPDATE projects SET code = CONCAT(\'PRY-\', LPAD(@p,4,\'0\')) WHERE id = @p', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @sql := IF(@already = 0, 'INSERT IGNORE INTO project_members (project_id, user_id, role_in_project) VALUES (@p, 1, \'owner\')', 'SELECT 1'); +SET @sql := IF(@should_seed, 'INSERT IGNORE INTO project_members (project_id, user_id, role_in_project) VALUES (@p, 1, \'owner\')', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -SET @sql := IF(@already = 0, 'INSERT INTO activities (project_id, code, name, priority_id, status_id, created_by) VALUES (@p, \'PENDING\', \'Sistematizar mejoras en el sistema de SAGA\', 2, 1, 1)', 'SELECT 1'); +SET @sql := IF(@should_seed, 'INSERT INTO activities (project_id, code, name, priority_id, status_id, created_by) VALUES (@p, \'PENDING\', \'Sistematizar mejoras en el sistema de SAGA\', 2, 1, 1)', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @a := LAST_INSERT_ID(); -SET @sql := IF(@already = 0, 'UPDATE activities SET code = CONCAT(\'ACT-\', LPAD(@a,4,\'0\')) WHERE id = @a', 'SELECT 1'); +SET @sql := IF(@should_seed, 'UPDATE activities SET code = CONCAT(\'ACT-\', LPAD(@a,4,\'0\')) WHERE id = @a', 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @sql := IF(@already = 0, 'INSERT INTO system_settings (`key`, `value`) VALUES (\'seed_it_baseline_2026_09\', NOW())', 'SELECT 1'); diff --git a/backend/package.json b/backend/package.json index d0677fc..800238b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "project-linkedcorp-backend", - "version": "1.15.0", + "version": "1.15.1", "private": true, "type": "module", "main": "src/server.js", diff --git a/frontend/package.json b/frontend/package.json index c019800..e12a1b9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "project-linkedcorp-frontend", "private": true, - "version": "1.15.0", + "version": "1.15.1", "type": "module", "scripts": { "dev": "vite", diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000..65b5f71 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,389 @@ +#!/usr/bin/env bash +# Instalador de project.linkedcorp.net para un servidor NUEVO (Debian/Ubuntu, arquetipo B: +# backend + frontend como dos servicios systemd separados). +# +# Uso: cloná el repo en /opt/apps/project.linkedcorp.net/source (ver DEPLOY_WEB.md, paso +# "Clonar el repositorio") y desde ahi corré: +# +# sudo scripts/install.sh +# +# Que hace: +# 1. Verifica requisitos (Node.js, npm, cliente mysql, systemd, espacio, puertos libres). +# 2. Pregunta dominio, puertos, y datos de base de datos: MariaDB local (se instala sola si +# hace falta) o un servidor MySQL/MariaDB existente. +# 3. Crea la base de datos + usuario de la app (clave aleatoria), genera backend/.env con +# todos los secretos generados al azar (JWT_SECRET, ENCRYPTION_KEY). +# 4. Instala dependencias, compila el frontend, corre migraciones + catalogos base. +# 5. Crea y arranca los dos servicios systemd, verifica que respondan. +# +# Que NO hace (queda manual, es especifico de cada servidor/proveedor): +# - Reverse proxy + certificado TLS hacia el dominio publico (ver DEPLOY_WEB.md, paso +# "Configurar Nginx Proxy Manager"). +# - Crear un usuario de la app: el primer registro publico en /registro se vuelve +# superadministrador automaticamente (bootstrap ya implementado en el backend) — no hace +# falta ningun paso extra ni script aparte. +set -euo pipefail + +APP_NAME="project.linkedcorp.net" +SVC_BACKEND="project-linkedcorp-backend" +SVC_FRONTEND="project-linkedcorp-frontend" +MIN_NODE_MAJOR=20 +MIN_FREE_MB=1024 + +SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +C_OK="\033[32m"; C_FAIL="\033[31m"; C_WARN="\033[33m"; C_RESET="\033[0m" +FAILED=0 +ok() { echo -e " ${C_OK}OK${C_RESET} $1"; } +fail() { echo -e " ${C_FAIL}FALTA${C_RESET} $1"; FAILED=1; } +warn() { echo -e " ${C_WARN}AVISO${C_RESET} $1"; } + +require_root() { + if [[ $EUID -ne 0 ]]; then + echo "Este instalador necesita privilegios de root (instala paquetes, crea servicios systemd)." + echo "Ejecuta: sudo $0" + exit 1 + fi +} + +# ---------- 1. verificacion de requisitos ---------- +check_requirements() { + echo "== Verificando requisitos ==" + + if command -v node >/dev/null 2>&1; then + local major + major="$(node -v | sed 's/^v//' | cut -d. -f1)" + if [[ "$major" -ge "$MIN_NODE_MAJOR" ]]; then + ok "Node.js $(node -v)" + else + fail "Node.js $(node -v) — se requiere >= v${MIN_NODE_MAJOR}" + fi + else + fail "Node.js no instalado (requiere >= v${MIN_NODE_MAJOR})" + fi + + command -v npm >/dev/null 2>&1 && ok "npm $(npm -v)" || fail "npm no instalado" + command -v mysql >/dev/null 2>&1 && ok "cliente mysql" || fail "cliente mysql no instalado (paquete mariadb-client)" + command -v systemctl >/dev/null 2>&1 && ok "systemd" || fail "systemd no disponible (este instalador lo necesita)" + command -v openssl >/dev/null 2>&1 && ok "openssl" || fail "openssl no instalado (necesario para generar secretos)" + command -v curl >/dev/null 2>&1 && ok "curl" || fail "curl no instalado (necesario para verificar al final)" + + if command -v apt-get >/dev/null 2>&1; then + ok "apt-get disponible" + else + warn "apt-get no disponible — si eliges 'MariaDB local' vas a tener que instalarla vos antes de continuar" + fi + + local avail_kb + avail_kb="$(df -Pk /opt 2>/dev/null | tail -1 | awk '{print $4}')" + if [[ -n "$avail_kb" && "$avail_kb" -ge $((MIN_FREE_MB * 1024)) ]]; then + ok "espacio libre en /opt: $((avail_kb / 1024)) MB" + else + fail "menos de ${MIN_FREE_MB}MB libres en /opt" + fi + + if [[ -f "$SOURCE_DIR/backend/package.json" && -f "$SOURCE_DIR/frontend/package.json" ]]; then + ok "repo detectado en $SOURCE_DIR" + else + fail "no se encontro backend/frontend en $SOURCE_DIR — corre este script desde la raiz del repo ya clonado" + fi + + echo + if [[ "$FAILED" -ne 0 ]]; then + echo "Faltan requisitos — resuelve lo anterior antes de continuar." + exit 1 + fi + echo "Requisitos OK." + echo +} + +port_free() { + local port="$1" + ! ss -ltn 2>/dev/null | awk '{print $4}' | grep -qE "[.:]${port}\$" +} + +check_ports() { + echo "== Verificando puertos ==" + local bad=0 + for p in "$BACKEND_PORT" "$FRONTEND_PORT"; do + if port_free "$p"; then + ok "puerto $p libre" + else + echo -e " ${C_FAIL}OCUPADO${C_RESET} puerto $p" + bad=1 + fi + done + echo + if [[ "$bad" -ne 0 ]]; then + echo "Elige otro puerto o libera el que esta ocupado, y vuelve a correr el instalador." + exit 1 + fi +} + +# ---------- 2. configuracion interactiva ---------- +ask() { + local prompt="$1" default="$2" var + read -r -p "$prompt [$default]: " var + echo "${var:-$default}" +} + +ensure_run_user() { + if id "$RUN_USER" >/dev/null 2>&1; then + ok "usuario '$RUN_USER' ya existe" + else + echo "El usuario '$RUN_USER' no existe — se crea ahora (sistema, sin login)." + useradd --system --create-home --shell /usr/sbin/nologin "$RUN_USER" + ok "usuario '$RUN_USER' creado" + fi +} + +gather_config() { + echo "== Configuracion ==" + APP_ROOT="$(ask "Directorio de instalacion" "/opt/apps/${APP_NAME}")" + DOMAIN="$(ask "Dominio publico (URL completa, para CORS)" "https://${APP_NAME}")" + BACKEND_PORT="$(ask "Puerto del backend" "4019")" + FRONTEND_PORT="$(ask "Puerto del frontend" "5196")" + RUN_USER="$(ask "Usuario del sistema que corre los servicios" "${SUDO_USER:-deploy}")" + echo + ensure_run_user + echo + check_ports + + echo "Base de datos:" + echo " 1) MariaDB local (se instala en este mismo servidor si hace falta)" + echo " 2) Servidor MySQL/MariaDB existente (otra IP)" + local db_choice + db_choice="$(ask "Elige una opcion" "1")" + + DB_NAME="$(ask "Nombre de la base de datos" "project_linkedcorp")" + DB_APP_USER="$(ask "Usuario de la app para la BD" "project_linkedcorp")" + + if [[ "$db_choice" == "2" ]]; then + DB_LOCAL=0 + DB_HOST="$(ask "Host de la base de datos" "")" + DB_PORT="$(ask "Puerto" "3306")" + DB_ADMIN_USER="$(ask "Usuario admin de esa BD (solo para crear la BD/usuario — no se guarda)" "root")" + read -r -s -p "Clave del usuario admin: " DB_ADMIN_PASSWORD; echo + GRANT_HOST="$(ask "IP de este servidor (para el GRANT — desde donde se conecta la app)" "$(hostname -I 2>/dev/null | awk '{print $1}')")" + else + DB_LOCAL=1 + DB_HOST="127.0.0.1" + DB_PORT="3306" + GRANT_HOST="127.0.0.1" + fi + echo +} + +# ---------- 3. base de datos ---------- +setup_local_mariadb() { + if command -v mysql >/dev/null 2>&1 && systemctl is-active --quiet mariadb 2>/dev/null; then + ok "MariaDB ya esta instalada y corriendo" + return + fi + echo "Instalando MariaDB local..." + apt-get update -y + apt-get install -y mariadb-server + systemctl enable --now mariadb + ok "MariaDB instalada y arrancada" +} + +mysql_admin() { + # Ejecuta el SQL que llega por stdin como admin. Local: root via socket (sin clave, tipico en + # Debian/Ubuntu con auth_socket). Remoto: con host/usuario/clave provistos. + if [[ "$DB_LOCAL" -eq 1 ]]; then + mysql -u root + else + MYSQL_PWD="$DB_ADMIN_PASSWORD" mysql -h "$DB_HOST" -P "$DB_PORT" -u "$DB_ADMIN_USER" + fi +} + +create_database() { + echo "== Base de datos ==" + [[ "$DB_LOCAL" -eq 1 ]] && setup_local_mariadb + + DB_APP_PASSWORD="$(openssl rand -base64 18 | tr -d '/+=' | cut -c1-20)" + + if ! mysql_admin < "$APP_ROOT/backend/.env" < "$APP_ROOT/frontend/package.json" <<'EOF' +{ "name": "project-linkedcorp-frontend-runtime", "private": true, "version": "1.0.0", + "dependencies": { "serve": "^14.2.6" } } +EOF + (cd "$APP_ROOT/frontend" && npm install --omit=dev) + chown -R "$RUN_USER":"$RUN_USER" "$APP_ROOT/frontend" + ok "compilado y publicado en $APP_ROOT/frontend" + echo +} + +# ---------- 6. systemd ---------- +write_services() { + echo "== Servicios systemd ==" + cat > "/etc/systemd/system/${SVC_BACKEND}.service" < "/etc/systemd/system/${SVC_FRONTEND}.service" </dev/null; then + ok "backend responde en :${BACKEND_PORT}" + else + warn "backend no responde todavia — revisa: journalctl -u ${SVC_BACKEND} -n 50" + fi + if curl -sfo /dev/null "http://127.0.0.1:${FRONTEND_PORT}/"; then + ok "frontend responde en :${FRONTEND_PORT}" + else + warn "frontend no responde todavia — revisa: journalctl -u ${SVC_FRONTEND} -n 50" + fi + echo +} + +summary() { + echo "======================================================" + echo " Instalacion lista" + echo "======================================================" + echo " Codigo: ${SOURCE_DIR}" + echo " Runtime: ${APP_ROOT}" + echo " Backend: http://127.0.0.1:${BACKEND_PORT} (systemctl status ${SVC_BACKEND})" + echo " Frontend: http://127.0.0.1:${FRONTEND_PORT} (systemctl status ${SVC_FRONTEND})" + echo + echo " Falta (manual, fuera del alcance de este instalador):" + echo " - Reverse proxy + TLS hacia ${DOMAIN} (ver DEPLOY_WEB.md, 'Nginx Proxy Manager'):" + echo " / -> puerto ${FRONTEND_PORT}" + echo " /api -> puerto ${BACKEND_PORT}" + echo " - Entra a ${DOMAIN}/registro y crea el primer usuario: se vuelve" + echo " superadministrador automaticamente, sin pasos extra." + echo " - Configura el correo SMTP desde Configuracion una vez logueado (opcional)." + echo " - Opcional: find ${SOURCE_DIR} -type f -iname '*.md' -delete (limpia la documentacion" + echo " del checkout en el servidor de produccion)." + echo "======================================================" +} + +main() { + require_root + check_requirements + gather_config + create_database + setup_tree + write_env + install_backend + build_frontend + write_services + verify + summary +} + +main "$@"