From c9a28cbb3969e7e304e159f3cc4210bb94164c5e Mon Sep 17 00:00:00 2001 From: Juan Pablo Restrepo Date: Tue, 23 Jun 2026 12:29:53 -0500 Subject: [PATCH] Para portainer --- .gitignore | 19 ++++- README-DOCKER.md | 136 +++++++++++++++++++++++++++++++++++ backend/Dockerfile | 6 +- backend/docker-entrypoint.sh | 29 ++++++++ backend/server.js | 63 ++++++++++++++-- data/sesiones.json | 15 +++- docker-compose.yml | 11 ++- 7 files changed, 268 insertions(+), 11 deletions(-) create mode 100644 README-DOCKER.md create mode 100644 backend/docker-entrypoint.sh diff --git a/.gitignore b/.gitignore index bcc61b5..a77f747 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,31 @@ -# Dependencias de Node (lo que te está dando el error) +# Dependencias de Node backend/node_modules/ +node_modules/ -# Archivos de entorno con credenciales secretas (si usas bases de datos o contraseñas) +# Archivos de entorno con credenciales secretas backend/.env backend/.env.local +.env +.env.local # Logs y reportes de errores +*.log backend/*.log backend/npm-debug.log* backend/yarn-debug.log* backend/yarn-error.log* +npm-debug.log* +yarn-debug.log* +yarn-error.log* # Archivos temporales del sistema operativo .DS_Store Thumbs.db + +# Docker (no subir imágenes/volúmenes locales) +.dockerignore +*.tar + +# IDEs +.vscode/ +.idea/ diff --git a/README-DOCKER.md b/README-DOCKER.md new file mode 100644 index 0000000..5e89b15 --- /dev/null +++ b/README-DOCKER.md @@ -0,0 +1,136 @@ +# Infinity — Despliegue con Docker + +Esta guía explica cómo llevar el proyecto a cualquier PC con Docker, usando Gitea como origen del código. + +## Requisitos en el PC destino + +- Docker instalado y corriendo. +- Docker Compose instalado (normalmente viene con Docker Desktop; en Linux puede ser el plugin `docker compose`). +- Git (para clonar desde Gitea). + +## Estructura del proyecto + +``` +. +├── backend/ # Código del servidor Node.js/Express +│ ├── Dockerfile +│ ├── docker-entrypoint.sh +│ ├── package.json +│ └── server.js +├── frontend/ # Archivos estáticos del catálogo y panel admin +├── data/ # JSONs persistentes (categorías, productos, usuarios...) +├── Imagenes/ # Fotos de productos +├── pedidos/ # PDFs de pedidos +├── videos/ # Videos del catálogo +├── agotados/ # Imágenes de productos agotados +├── docker-compose.yml +└── .dockerignore +``` + +## Pasos para ponerlo a correr en otro PC + +### 1. Clonar el repositorio desde Gitea + +```bash +git clone https://TU_GITEA/TU_USUARIO/infinity.git +cd infinity +``` + +### 2. Levantar el contenedor + +```bash +docker compose up --build -d +``` + +- Construye la imagen con el backend y el frontend empaquetados. +- Expone la aplicación en: http://localhost:6060 +- Persiste `data/`, `Imagenes/`, `pedidos/` y `videos/` en el host. + +### 3. Abrir en el navegador + +- Catálogo público: http://localhost:6060 +- Panel administrativo: http://localhost:6060/administrativo/ + +### Usuario por defecto (solo si no existe `data/contraseñas.json`) + +| Campo | Valor | +|------------|-------| +| Usuario | admin | +| Contraseña | admin | + +> Cambia esta contraseña desde el panel administrativo lo antes posible. + +## Flujo de trabajo: editar en este PC y actualizar el otro + +1. Haces los cambios en el código/frontend. +2. Subes a Gitea: + ```bash + git add . + git commit -m "descripción del cambio" + git push origin main + ``` +3. En el otro PC: + ```bash + cd infinity + git pull + docker compose down + docker compose up --build -d + ``` + +Con eso se actualiza tanto el backend como el frontend. + +## Datos importantes: qué se guarda y qué no + +Lo que **sí** se conserva al reconstruir el contenedor (está en volúmenes): + +- `data/` → JSONs (productos, categorías, usuarios, sesiones...) +- `Imagenes/` → fotos subidas +- `pedidos/` → PDFs generados +- `videos/` → videos subidos + +Lo que **no** se conserva (va dentro de la imagen): + +- El código del frontend y backend. Si lo actualizas, hay que hacer `docker compose up --build -d`. + +## Comandos útiles + +| Acción | Comando | +|--------|---------| +| Ver logs | `docker compose logs -f web` | +| Reiniciar | `docker compose restart web` | +| Apagar | `docker compose down` | +| Borrar TODO (incluidos datos locales) | `docker compose down -v` ⚠️ | +| Ver estado | `docker compose ps` | + +## Solución de problemas + +### "Cannot GET /" o pantalla en blanco + +1. Verifica que el contenedor esté corriendo: + ```bash + docker compose ps + ``` +2. Mira los logs: + ```bash + docker compose logs -f web + ``` +3. Asegúrate de haber usado `--build` después de `git pull`. + +### El admin no entra + +- Si la carpeta `data/` estaba vacía, se crea automáticamente el usuario `admin`/`admin`. +- Si ya existe `data/contraseñas.json`, ese archivo tiene prioridad. + +### Las imágenes o pedidos no aparecen + +- Confirma que las carpetas `Imagenes/`, `pedidos/` y `videos/` están en la misma ubicación que `docker-compose.yml`. +- En Windows con Docker Desktop, activa **Settings → Resources → File sharing** para la unidad donde esté el proyecto. + +## Notas de seguridad + +- Cambia `SESSION_SECRET` en `docker-compose.yml` por algo aleatorio y largo si vas a exponer el sitio a internet. +- El proyecto está pensado para red local / catálogo interno. No expongas el puerto 6060 a internet sin un reverse proxy (nginx/traefik) y HTTPS. + +## ¿Por qué se empaqueta el frontend dentro de la imagen? + +Así, cuando subes cambios a Gitea y haces `git pull` + `--build` en el otro PC, el frontend se actualiza automáticamente. Si se montara `frontend/` como volumen, el contenedor usaría siempre los archivos antiguos que ya estuvieran en el disco. diff --git a/backend/Dockerfile b/backend/Dockerfile index 9b3deaa..53d9209 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -9,9 +9,13 @@ RUN npm install --production # Copiar el código del backend COPY backend/ ./ -# Copiar el frontend (servido como estático) +# Copiar el frontend empaquetado (no se monta como volumen en producción) COPY frontend/ ./frontend/ +# Script de entrada: crea datos/carpetas por defecto si no existen en el volumen +RUN chmod +x ./docker-entrypoint.sh + EXPOSE 3000 +ENTRYPOINT ["./docker-entrypoint.sh"] CMD ["node", "server.js"] diff --git a/backend/docker-entrypoint.sh b/backend/docker-entrypoint.sh new file mode 100644 index 0000000..48cefee --- /dev/null +++ b/backend/docker-entrypoint.sh @@ -0,0 +1,29 @@ +#!/bin/sh +set -e + +# Crear carpetas de datos si no existen (volúmenes vacíos en PC nuevo) +mkdir -p /app/data +mkdir -p /app/Imagenes +mkdir -p /app/Imagenes/agotados +mkdir -p /app/Imagenes/sin-categoria +mkdir -p /app/pedidos +mkdir -p /app/videos + +# Crear contraseñas.json por defecto si no existe, con admin/admin +if [ ! -f /app/data/contraseñas.json ]; then + echo '{"usuarios":[{"usuario":"admin","contraseña":"admin","rol":"admin","permisos":[]}]}' > /app/data/contraseñas.json + echo "[entrypoint] ✅ Creado admin por defecto: usuario=admin / contraseña=admin" +fi + +# Crear el resto de JSON base si no existen +[ -f /app/data/productos.json ] || echo '[]' > /app/data/productos.json +[ -f /app/data/categorias.json ] || echo '[]' > /app/data/categorias.json +[ -f /app/data/sesiones.json ] || echo '[]' > /app/data/sesiones.json +[ -f /app/data/vendedores.json ] || echo '[]' > /app/data/vendedores.json +[ -f /app/data/redes.json ] || echo '[]' > /app/data/redes.json +[ -f /app/data/pedidos-meta.json ] || echo '[]' > /app/data/pedidos-meta.json + +# Asegurar que el backend pueda escribir en las carpetas montadas +chown -R node:node /app/data /app/Imagenes /app/pedidos /app/videos 2>/dev/null || true + +exec "$@" diff --git a/backend/server.js b/backend/server.js index 463704c..0a1338d 100644 --- a/backend/server.js +++ b/backend/server.js @@ -25,16 +25,61 @@ const IMG_DIR = path.join(__dirname, 'Imagenes'); const PEDIDOS_DIR = path.join(__dirname, 'pedidos'); const VIDEOS_DIR = path.join(__dirname, 'videos'); +// ════════════════════════════════ +// INICIALIZACIÓN DE DATOS / CARPETAS +// Se ejecuta antes de arrancar el servidor. Útil cuando se montan volúmenes +// vacíos en un PC nuevo por primera vez. +// ════════════════════════════════ +async function initDataFiles() { + try { + await fs.mkdir(DATA_DIR, { recursive: true }); + await fs.mkdir(IMG_DIR, { recursive: true }); + await fs.mkdir(path.join(IMG_DIR, 'agotados'), { recursive: true }); + await fs.mkdir(path.join(IMG_DIR, SIN_CATEGORIA_ID), { recursive: true }); + await fs.mkdir(PEDIDOS_DIR, { recursive: true }); + await fs.mkdir(VIDEOS_DIR, { recursive: true }); + + const defaults = { + 'contraseñas.json': { usuarios: [{ usuario: 'admin', contraseña: 'admin', rol: 'admin', permisos: [] }] }, + 'productos.json': [], + 'categorias.json': [], + 'sesiones.json': [], + 'vendedores.json': [], + 'redes.json': [], + 'pedidos-meta.json': [] + }; + + for (const [file, value] of Object.entries(defaults)) { + const filePath = path.join(DATA_DIR, file); + try { + await fs.access(filePath); + } catch { + await fs.writeFile(filePath, JSON.stringify(value, null, 2) + '\n', 'utf8'); + console.log(`[init] Creado ${file} por defecto`); + } + } + } catch (err) { + console.error('[init] Error inicializando datos:', err.message); + } +} + // ════════════════════════════════ // MIDDLEWARES // ════════════════════════════════ app.use(express.json()); app.use(cookieParser(SECRET)); + +// Servir frontend estático con index.html como fallback de carpeta app.use(express.static(FRONTEND_DIR)); app.use('/Imagenes', express.static(IMG_DIR, { maxAge: '7d' })); app.use('/pedidos', express.static(PEDIDOS_DIR)); app.use('/videos', express.static(VIDEOS_DIR)); +// Ruta raíz explícita: sirve el catálogo principal +app.get('/', (req, res) => { + res.sendFile(path.join(FRONTEND_DIR, 'index.html')); +}); + // Evita el 404 de favicon.ico app.get('/favicon.ico', (req, res) => res.status(204).end()); @@ -1506,7 +1551,9 @@ app.use((err, req, res, next) => { }); // ════════════════════════════════ -// FALLBACK para SPA-like del admin +// FALLBACK para SPA-like del admin y catálogo +// Sirve index.html para rutas desconocidas que parezcan de la SPA. +// ⚠️ Debe ir DESPUÉS de todas las rutas de API. // ════════════════════════════════ app.get('/administrativo', (req, res) => { res.sendFile(path.join(FRONTEND_DIR, 'administrativo', 'index.html')); @@ -1514,6 +1561,9 @@ app.get('/administrativo', (req, res) => { app.get('/administrativo/', (req, res) => { res.sendFile(path.join(FRONTEND_DIR, 'administrativo', 'index.html')); }); +app.get('/administrativo/*', (req, res) => { + res.sendFile(path.join(FRONTEND_DIR, 'administrativo', 'index.html')); +}); // ════════════════════════════════ // LIMPIEZA DE SESIONES A LAS 00:00 (America/Bogota) @@ -2389,7 +2439,10 @@ app.get('/api/redes/proxy', async (req, res) => { // ════════════════════════════════ // START // ════════════════════════════════ -app.listen(PORT, () => { - console.log(`∞ Infinity server corriendo en http://localhost:${PORT}`); - scheduleSessionCleanup(); -}); +(async () => { + await initDataFiles(); + app.listen(PORT, () => { + console.log(`∞ Infinity server corriendo en http://localhost:${PORT}`); + scheduleSessionCleanup(); + }); +})(); diff --git a/data/sesiones.json b/data/sesiones.json index fe51488..8c0c713 100644 --- a/data/sesiones.json +++ b/data/sesiones.json @@ -1 +1,14 @@ -[] +[ + { + "token": "a1883d5c1fb699692056fe9d50045970fcba1070dd38fb2447658070824a3d68", + "usuario": "admin", + "rol": "admin", + "createdAt": 1782235533863 + }, + { + "token": "98d1ef14b931cda699cd80b9b19bf2f1b67c6d66a15501a35a7dfde7d67cc27e", + "usuario": "admin", + "rol": "admin", + "createdAt": 1782235723745 + } +] diff --git a/docker-compose.yml b/docker-compose.yml index c8768df..9a50370 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,15 +7,22 @@ services: ports: - "6060:3000" volumes: - # Persistencia: los cambios en JSON se guardan en el host + # Persistencia: los cambios en JSON, imágenes, pedidos y videos se guardan en el host - ./data:/app/data - ./Imagenes:/app/Imagenes - ./pedidos:/app/pedidos - - ./frontend:/app/frontend - ./videos:/app/videos + # NOTA: el frontend va empaquetado dentro de la imagen, NO se monta como volumen. + # Así al hacer pull en otro PC se actualiza la interfaz automáticamente. environment: - NODE_ENV=production - PORT=3000 # ⚠️ Cambiar esta clave en producción por algo aleatorio y secreto - SESSION_SECRET=cambia-esta-clave-en-produccion-12345 restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/api/categorias"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s