Plataforma multi-usuario de catálogos digitales. Cada negocio tiene su propia página pública con productos, imágenes, precios, alta personalización y contacto directo por WhatsApp.
- Características
- Stack tecnológico
- Arquitectura del proyecto
- Instalación local
- Variables de entorno
- Primer arranque
- Flujo de uso
- Despliegue en Hostinger
- Consideraciones de seguridad
- Futuros pasos y mejoras
- Preguntas frecuentes
| Módulo | Detalle |
|---|---|
| 🔐 Autenticación | Sesiones con express-session, contraseñas con bcrypt |
| 🔑 Acceso controlado | Solo usuarios con clave de acceso pueden registrarse |
| 🏪 Perfil de negocio | Nombre, logo, descripción, contacto, redes sociales, horario |
| 📦 Catálogo de productos | CRUD completo con imágenes, categorías y estados |
| 🌐 Página pública | URL /shop/:slug accesible sin login |
| 💬 Botón WhatsApp | Enlace directo en cada producto y en la cabecera del negocio |
| 🔍 Filtros y búsqueda | Por nombre, descripción y categoría |
| 📄 Paginación | 12 productos por página |
| 👮 Panel admin | Usuarios, claves, negocios |
| 🛡️ Seguridad | Helmet, rate-limit en login, validación de inputs |
| 📱 Diseño responsive | TailwindCSS, mobile-first |
Backend: Node.js + Express.js
Templates: EJS (server-side rendering)
Base de datos: SQLite (better-sqlite3) ← reemplazable por PostgreSQL/MySQL
Sesiones: express-session + connect-sqlite3
Uploads: Multer
CSS: TailwindCSS (Play CDN)
Seguridad: Helmet, express-rate-limit, express-validator, bcrypt
Logging: Morgan
devisistore/
├── app.js ← Punto de entrada, configuración de Express
├── package.json
├── .env.example
│
├── config/
│ └── database.js ← Inicialización de SQLite y creación de tablas
│
├── models/ ← Funciones de acceso a la BD (sin ORM)
│ ├── User.js
│ ├── AccessKey.js
│ ├── Business.js
│ └── Product.js
│
├── controllers/ ← Lógica de negocio y respuesta HTTP
│ ├── authController.js
│ ├── adminController.js
│ ├── dashboardController.js
│ └── shopController.js
│
├── routes/ ← Definición de rutas Express
│ ├── authRoutes.js
│ ├── adminRoutes.js
│ ├── dashboardRoutes.js
│ └── shopRoutes.js
│
├── middlewares/
│ └── authMiddleware.js ← isAuth, isAdmin
│
├── views/ ← Plantillas EJS
│ ├── partials/ ← head, navbar, sidebar, footer, flash
│ ├── home/ ← Landing page
│ ├── auth/ ← Login, Register
│ ├── dashboard/ ← Panel del usuario
│ ├── admin/ ← Panel de administración
│ ├── shop/ ← Catálogo público
│ └── errors/ ← 404, 403, 500
│
├── public/
│ └── uploads/ ← Imágenes subidas por usuarios
│
└── utils/
└── helpers.js ← Funciones de utilidad
- Node.js 18+ (descargar)
- npm 9+ (viene con Node.js)
- Sistema operativo: Linux, macOS o Windows
# 1. Clonar o descomprimir el proyecto
cd devisistore
# 2. Instalar dependencias
npm install
# 3. Crear el archivo de entorno
cp .env.example .env
# 4. Editar .env con tus valores (ver sección Variables de entorno)
nano .env # o usa tu editor favorito
# 5. Iniciar en desarrollo
npm run dev
# 6. Iniciar en producción
npm startAbrir en el navegador: http://localhost:3000
Archivo: .env
# Puerto del servidor
PORT=3000
# Entorno (development | production)
NODE_ENV=development
# Secreto para firmar las sesiones (CAMBIAR en producción)
SESSION_SECRET=cambia_esto_por_algo_largo_y_aleatorio
# Ruta del archivo SQLite
DB_PATH=./devisistore.db
# Credenciales del administrador inicial
# Solo se usa la primera vez que se ejecuta la app
ADMIN_EMAIL=admin@tucatalogo.com
ADMIN_PASSWORD=Admin1234!
# Tamaño máximo de imágenes en MB
UPLOAD_MAX_SIZE_MB=5
# Rate limiting en login
LOGIN_MAX_ATTEMPTS=10
LOGIN_WINDOW_MINUTES=15
⚠️ Nunca subas el archivo.enva Git. Está en.gitignorepor defecto.
Al iniciar por primera vez, la aplicación:
- Crea la base de datos (
devisistore.db) con todas las tablas. - Crea el usuario administrador con las credenciales de
.env. - Imprime en consola:
🔑 Admin inicial creado: admin@...
- Ve a
/auth/logine inicia sesión con las credenciales de admin. - Ve a
/admin/keysy haz clic en "Generar clave". - Comparte esa clave con el primer usuario que quieras invitar.
- El usuario va a
/auth/register, pone su email, contraseña y la clave. - Listo — ya puede crear su negocio en
/dashboard/business.
Admin genera clave → Usuario se registra con la clave
→ Usuario crea su negocio → Sube productos
→ Comparte /shop/su-slug con sus clientes
→ Clientes ven el catálogo y contactan por WhatsApp
| Rol | Acceso |
|---|---|
admin |
Panel /admin, gestión de usuarios, claves y negocios |
user |
Panel /dashboard, su negocio y sus productos |
| (público) | Sólo páginas públicas /, /shop/:slug |
Hostinger Business Hosting soporta Node.js directamente desde el panel hPanel.
# En tu VPS o panel hPanel terminal:
# 1. Subir archivos (via FTP, Git o File Manager)
git clone https://github.com/tu-usuario/devisistore.git
cd devisistore
# 2. Instalar dependencias
npm install --production
# 3. Crear .env con variables de producción
cp .env.example .env
nano .env
# NODE_ENV=production
# PORT=3000 (o el que te asigne Hostinger)
# SESSION_SECRET=clave_muy_larga_y_aleatoria
# 4. Iniciar con PM2 (proceso persistente)
npm install -g pm2
pm2 start app.js --name "devisistore"
pm2 save
pm2 startup # para que se inicie solo al reiniciar el servidor- En hPanel → Sitios web → Node.js
- Seleccionar versión Node.js 18 o superior
- Punto de entrada:
app.js - Variables de entorno: agregar las del
.envdesde la interfaz - Hacer clic en Reiniciar aplicación
- En hPanel → Dominios → apuntar al directorio del proyecto
- Activar SSL/HTTPS gratuito (Let's Encrypt) desde hPanel
- Una vez con HTTPS, cambiar en
.env:NODE_ENV=production- Esto activa las cookies seguras (
Secure: true)
- Esto activa las cookies seguras (
Las imágenes se guardan en public/uploads/. En un VPS estándar no hay problema. Si usas un servicio con file system efímero (como algunos PaaS), considera migrar a almacenamiento externo (ver sección Futuros pasos).
- ✅ Helmet: headers HTTP seguros (CSP, HSTS, XSS protection)
- ✅ bcrypt con factor de coste 12 para contraseñas
- ✅ Rate limiting en
/auth/login(10 intentos / 15 min por IP) - ✅ express-validator: validación y sanitización de inputs
- ✅ Roles: middleware
isAutheisAdminen todas las rutas protegidas - ✅ Sesiones httpOnly para evitar acceso JS a la cookie
- ✅ Cookies
Secureen producción (NODE_ENV=production) - ✅ Ownership check: los productos solo pueden editarse por su propietario
- ✅ Multer: filtro de tipos de archivo y límite de tamaño
- CSRF protection: instalar
csurfo implementar double-submit cookienpm install csurf
- Sanitización HTML: usar
DOMPurifyosanitize-htmlen campos de textonpm install sanitize-html
- Rotación de SESSION_SECRET: usar valor largo y aleatorio en producción
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))" - Logs de auditoría: registrar eventos de seguridad (login fallido, etc.)
- Backup automático del archivo
devisistore.db - Revisar permisos del directorio
public/uploads/(solo escritura del proceso Node)
Actualmente las imágenes se guardan localmente. Para producción real se recomienda usar un servicio externo:
npm install @aws-sdk/client-s3 # Amazon S3
# o
npm install cloudinary # Cloudinary (más sencillo)En dashboardRoutes.js, reemplazar el diskStorage de Multer por memoryStorage y subir el buffer al servicio externo en el controlador.
Para múltiples usuarios concurrentes, SQLite puede tener problemas de escritura simultánea.
npm install pg # Driver de PostgreSQL
npm install knex # Query builder (opcional pero recomendado)Cambiar config/database.js para conectar con Knex/pg en lugar de better-sqlite3. Los modelos apenas necesitarían cambios si usas el mismo patrón de queries.
npm install csurfAgregar el middleware en app.js y pasar csrfToken() a cada formulario de las vistas.
Permite que cada negocio acceda con micafeteria.tucatalogo.com en vez de tucatalogo.com/shop/micafeteria.
Requiere:
- DNS wildcard
*.tucatalogo.comapuntando al servidor - Middleware en Express que detecte el subdominio y cargue el negocio correspondiente
// middleware de subdominio
app.use((req, res, next) => {
const host = req.hostname; // ej: micafeteria.tucatalogo.com
const parts = host.split('.');
if (parts.length > 2) {
req.businessSlug = parts[0];
}
next();
});Agregar tabla product_images con product_id y url.
Usar Multer con upload.array('images', 5) para múltiples archivos.
Crear tabla categories con nombre, color y emoji/icono.
Agregar CRUD en el dashboard.
npm install papaparse xlsxAgregar ruta POST /dashboard/products/import que procese el archivo y cree los productos en lote.
Tailwind soporta modo oscuro con la clase dark:. Agregar darkMode: 'class' en la config de Tailwind y un toggle en la navbar.
En el dashboard, mostrar un iframe o una ventana emergente con cómo se ve el catálogo público al mismo tiempo que se edita.
npm install sharpEn el controlador, procesar la imagen con Sharp antes de guardarla:
await sharp(req.file.buffer)
.resize(800, 800, { fit: 'inside', withoutEnlargement: true })
.webp({ quality: 80 })
.toFile(outputPath);Agregar campo plan en la tabla users (free | pro | enterprise).
Limitar el plan free a N productos o N negocios.
Integrar pasarela de pago:
npm install stripeFlujo:
- Usuario selecciona plan → redirigir a Stripe Checkout
- Webhook de Stripe actualiza el campo
plandel usuario - Middleware verifica el plan antes de permitir crear más productos
Agregar tabla orders:
CREATE TABLE orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
business_id INTEGER REFERENCES businesses(id),
customer_name TEXT,
customer_phone TEXT,
items TEXT, -- JSON con productos y cantidades
total REAL,
status TEXT DEFAULT 'pending',
created_at TEXT DEFAULT (datetime('now'))
);Los clientes pueden hacer pedidos desde el catálogo y el negocio los ve en su dashboard.
npm install nodemailerEnviar email cuando:
- Un usuario se registra (bienvenida)
- Un admin genera una clave (enviarla por email)
- Se recibe un pedido (si se implementa el sistema de pedidos)
Tabla page_views para contar visitas por catálogo:
CREATE TABLE page_views (
id INTEGER PRIMARY KEY AUTOINCREMENT,
business_id INTEGER REFERENCES businesses(id),
ip_hash TEXT,
visited_at TEXT DEFAULT (datetime('now'))
);Mostrar en el dashboard: "Tu catálogo tuvo X visitas este mes."
Exponer los productos de un negocio en JSON para integraciones externas:
GET /api/v1/shop/:slug/products
GET /api/v1/shop/:slug/products/:id
Autenticar con API key por negocio.
¿Puedo usar MySQL o PostgreSQL en lugar de SQLite?
Sí. better-sqlite3 es síncrono y muy simple. Para migrar, reemplaza config/database.js con una conexión a tu base de datos y adapta los modelos para usar queries asíncronas (async/await con pg o mysql2). Los modelos están aislados del resto de la app, por lo que el impacto es mínimo.
¿Qué pasa si un usuario sube una imagen maliciosa?
Multer ya filtra por extensión. Para mayor seguridad, añade validación del tipo MIME real con el paquete file-type:
npm install file-type¿Las imágenes se pierden al reiniciar el servidor?
En un VPS normal, no. Las imágenes están en public/uploads/ que es persistente. En servicios PaaS con file system efímero (Heroku, Railway), sí se pierden. Usa almacenamiento en la nube para esos casos.
¿Cómo cambio el nombre de la plataforma?
Busca DevisiStore en views/ y app.js. También puedes agregar una variable APP_NAME en .env y leerla desde app.js (res.locals.appName).
¿Puedo permitir que los negocios tengan múltiples usuarios?
Actualmente un usuario = un negocio. Para soportar equipos, agrega una tabla business_members con (business_id, user_id, role) y actualiza los middlewares de ownership.
MIT — libre para uso personal y comercial.
Desarrollado con ❤️ usando Node.js, Express y TailwindCSS.