From 8e15844e5bab185c627f0598101c72320aadf71a Mon Sep 17 00:00:00 2001 From: Perry Daniels Date: Sat, 8 Aug 2026 11:24:39 -0600 Subject: [PATCH 1/3] Opciones de depuracion de conexiones y restricciones de uso --- .wwebjs_cache/2.3000.1044792184.html | 80 +++++ README.en.md | 427 +++++++++++++++++++++++++ README.md | 356 +++++++++++++-------- logs/access-2026-08-08.log | 9 + package-lock.json | 18 ++ package.json | 1 + src/controllers/authController.js | 75 ++++- src/controllers/uploadController.js | 95 +++++- src/index.js | 23 +- src/middleware/masterAuthMiddleware.js | 90 +++++- src/routes/api.js | 81 ++++- src/services/sessionManager.js | 317 ++++++++++++++++-- src/swagger.js | 16 +- src/utils/logger.js | 66 ++++ src/utils/spamProtector.js | 68 ++++ 15 files changed, 1526 insertions(+), 196 deletions(-) create mode 100644 .wwebjs_cache/2.3000.1044792184.html create mode 100644 README.en.md create mode 100644 logs/access-2026-08-08.log create mode 100644 src/utils/logger.js create mode 100644 src/utils/spamProtector.js diff --git a/.wwebjs_cache/2.3000.1044792184.html b/.wwebjs_cache/2.3000.1044792184.html new file mode 100644 index 0000000..9b9d2f3 --- /dev/null +++ b/.wwebjs_cache/2.3000.1044792184.html @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + +WhatsApp Web + + + + +
WhatsApp
 Cifrado de extremo a extremo
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.en.md b/README.en.md new file mode 100644 index 0000000..4681132 --- /dev/null +++ b/README.en.md @@ -0,0 +1,427 @@ +# Simple WhatsApp API + +A multi-session, easy-to-deploy API that allows you to send text messages and attachments from multiple WhatsApp accounts with minimal setup. + +## Features + +- **Multi-Session Support**: Connect and manage multiple WhatsApp accounts simultaneously. Each session is tied to its own unique API key. +- **Easy Login**: Scan a QR code just once per session to connect your WhatsApp account. +- **Persistent Sessions**: Sessions are saved locally, so the server can be restarted without needing to log in again. +- **Clean Logout & Unlinking**: Endpoint to logout and revoke session credentials on WhatsApp servers and remove them from the phone app. +- **Abandoned Session Cleanup**: Endpoint to clean up inactive sessions based on an inactive days threshold. +- **Built-in Anti-Spam Protection**: Configurable delay between messages, minute rate limits, and daily quotas per session. +- **Multi-Master Key Support by App**: Assign unique master keys to different systems (e.g., Sales, CRM) to audit usage per app. +- **Daily Access Logs by Timezone**: Log requests in daily rotating files (`logs/access-YYYY-MM-DD.log`) formatted in your local timezone (e.g. Guatemala `America/Guatemala`). +- **Send Text Messages**: A simple endpoint to send plain text messages from a specific session. +- **Send Attachments**: Send images, videos, or documents directly via file upload, URL, or a Base64 encoded string. +- **File Uploads**: A dedicated endpoint to upload files and receive a temporary URL, perfect for sending as attachments later. +- **Secure**: + - Protect your entire server with one or multiple **Master API Keys**. + - Manage individual WhatsApp sessions with a per-session **API Key**. +- **Dynamic QR Codes**: Fetch the login QR code for any session as a string or a PNG image via API endpoints. + +--- + +## 🚀 Getting Started + +### **Prerequisites** + +- [Node.js](https://nodejs.org/) (v16 or higher recommended) +- One or more WhatsApp accounts + +### **1. Clone and Install Dependencies** + +```bash +git clone https://github.com/Codegres-com/Simple-Whatsapp-API.git +cd Simple-Whatsapp-API +npm install +``` + +### **2. Configure Your Environment** + +Create a `.env` file in the root of the project and add your configuration: + +```env +# .env + +# Master keys per application (Format key:AppName separated by comma) +MASTER_API_KEYS=key_ventas:SistemaVentas,key_crm:ModuloCRM,Chimpanzee24.gt:AppPrincipal + +# Legacy single master key compatibility +MASTER_API_KEY=Chimpanzee24.gt + +PORT=3000 + +# Custom Swagger UI Configuration +SWAGGER_TITLE=My Custom API +SWAGGER_DESCRIPTION=WhatsApp integration service. +SERVER_URL=http://localhost:3000 + +# Daily access logs (1 = enabled, 0 = disabled) +ENABLE_LOGS=1 +TIMEZONE=America/Guatemala + +# Per-session Anti-Spam protection +RATE_LIMIT_PER_MINUTE=15 # Max messages per minute per session (0 disables) +MESSAGE_DELAY_MS=2000 # Delay between consecutive messages in ms (2000 = 2s) +DAILY_MESSAGE_LIMIT=500 # Daily limit per session (0 disables) +``` + +### **3. Start the Server** + +```bash +npm start +``` + +--- + +## 🐳 Running with Docker + +You can also run this application using Docker and Docker Compose for a more isolated and reproducible setup. + +### **1. Using Docker Compose (Recommended)** + +This is the easiest way to get started. + +1. **Create an Environment File**: + Create a `.env` file in the root of the project. This file will be used by Docker Compose to set the environment variables inside the container. + + ``` + # .env + + # The master key to protect the server + MASTER_API_KEY=yoursecretkey + + # The port to run the server on (optional, defaults to 3000) + PORT=3000 + ``` + +2. **Build and Run the Container**: + Run the following command to build the Docker image and start the container in the background: + + ```bash + docker compose up --build -d + ``` + + The server will now be running on the port you specified (or the default, 3000). + +3. **To Stop the Server**: + ```bash + docker compose down + ``` + +### **2. Using the Pre-built Docker Hub Image** + +If you don't want to build the image from the source, you can use the pre-built image from Docker Hub. + +1. **Pull the Image**: + ```bash + docker pull codegres/simple-whatsapp-api:latest + ``` + +2. **Run the Image**: + You still need to provide the environment variables and mount the volumes. + + ```bash + docker run -d \ + -p 3000:3000 \ + -e MASTER_API_KEY="yoursecretkey" \ + -e PORT="3000" \ + --name whatsapp-api-container \ + -v whatsapp_sessions:/usr/src/app/sessions \ + -v whatsapp_uploads:/usr/src/app/uploads \ + codegres/simple-whatsapp-api:latest + ``` + +### **3. Using Docker (Manual Build)** + + +If you prefer not to use Docker Compose, you can build and run the container manually. + +1. **Build the Docker Image**: + ```bash + docker build -t whatsapp-api . + ``` + +2. **Run the Docker Container**: + You must pass the `MASTER_API_KEY` and map the port. You also need to create and mount volumes to persist the `sessions` and `uploads` data. + + ```bash + docker run -d \ + -p 3000:3000 \ + -e MASTER_API_KEY="yoursecretkey" \ + -e PORT="3000" \ + --name whatsapp-api-container \ + -v whatsapp_sessions:/usr/src/app/sessions \ + -v whatsapp_uploads:/usr/src/app/uploads \ + whatsapp-api + ``` + - `-d`: Run in detached mode. + - `-p`: Map port 3000 on your host to port 3000 in the container. + - `-e`: Set environment variables. + - `--name`: Assign a name to the container. + - `-v`: Mount named volumes to persist data. + +--- + +## 📖 API Documentation + +This project includes interactive API documentation powered by Swagger UI and a pre-configured Postman collection to make testing and integration as easy as possible. + +### **Swagger UI** + +Once the server is running, you can access the interactive Swagger UI in your browser. This interface allows you to view all available endpoints, see their parameters, and test them live. + +- **URL**: [http://localhost:3000/api-docs](http://localhost:3000/api-docs) + +When you open the Swagger UI, the `X-MASTER-KEY` will be pre-authorized with the default value (`SUPER_SECRET_KEY` or the value from your `.env` file), so you can start making requests to the protected endpoints immediately. + +### **Postman Collection** + +A Postman collection is included in the root of this project to help you get started quickly. + +1. **Import the Collection**: + - Find the `whatsapp_api_collection.json` file in the project's root directory. + - In Postman, click **Import** and upload the file. + +2. **Configure Environment (Optional)**: + - The collection comes with a pre-request script that automatically adds the `X-MASTER-KEY` header to every request. + - By default, it uses `SUPER_SECRET_KEY`. To use your own key, create a new Postman Environment, add a variable named `MASTER_API_KEY`, and set its value to your key from the `.env` file. + +All endpoints are prefixed with `/api`. + +### **Authentication** + +This API uses a two-key system for security and session management. Both keys can be provided in either the request header or the request body for `POST` requests, giving you more flexibility. For `GET` requests, they must be in the header. + +1. **Master Key (`X-MASTER-KEY`)**: + - This is a global key that grants access to the entire API server. + - It can be included in the `X-MASTER-KEY` header or as a field in the JSON request body. The header will always take precedence if both are provided. + - This is the key you set in your `.env` file. + +2. **Session Key (`X-API-KEY`)**: + - This key identifies a specific WhatsApp session (i.e., a specific phone number). + - For `POST` requests, it can be in the `X-API-KEY` header or a field in the JSON/form-data body. + - For `GET` requests, it must be in the `X-API-KEY` header. + - You can invent any unique string for each session (e.g., `user1_phone`, `work_account`, a random hash, etc.). + - The first time a new `X-API-KEY` is used with the `/connect` endpoint, a new session will be created for it. + +--- + +## 📲 Connecting a Session + +To use a WhatsApp account, you must first connect it to a session key. + +1. Choose a unique `X-API-KEY` for the account you want to connect (e.g., `my-personal-whatsapp`). +2. Make a request to one of the connection endpoints with both the master key and your chosen session key. The server will generate a QR code for that specific session. + + - **GET `/api/connect`**: Returns the QR code as a string. + - **GET `/api/connect/image`**: Returns the QR code as a PNG image. + +3. Open WhatsApp on your phone, go to **Settings > Linked Devices**, and scan the QR code. + +Once connected, the server will save the session data in the `./sessions` folder. You won't need to scan the code again for this session unless you log out. Repeat this process for each WhatsApp account you want to use, assigning a different `X-API-KEY` to each. + +--- + +## **Endpoints** + +#### 1. **Get Connection Status / QR Code** + +- **Endpoint**: `GET /connect` +- **Description**: Get the current connection status for a session. If a QR code is available, it will be returned as a string. If not, the session status is returned. +- **Headers**: + - `X-MASTER-KEY: your_global_master_key_here` + - `X-API-KEY: your_unique_session_key` +- **Response (When QR is ready)**: `200 OK` with the QR string in the body. +- **Response (When connected)**: `200 OK` + ```json + { + "sessionId": "your_unique_session_key", + "status": "Connected" + } + ``` + +#### 2. **Get QR Code Image** + +- **Endpoint**: `GET /connect/image` +- **Description**: Fetches the login QR code as a PNG image. +- **Headers**: + - `X-MASTER-KEY: your_global_master_key_here` + - `X-API-KEY: your_unique_session_key` +- **Response**: `200 OK` with `Content-Type: image/png`. + +#### 3. **Logout Single Session** + +- **Endpoint**: `POST /logout` +- **Description**: Closes the active WhatsApp session, unlinks the device from the phone, and deletes local session storage. +- **Headers**: + - `X-MASTER-KEY: your_global_master_key_here` + - `X-API-KEY: your_unique_session_key` +- **Response**: + ```json + { + "message": "Session logged out and deleted successfully." + } + ``` + +#### 4. **Logout All Sessions** + +- **Endpoint**: `POST /logout-all` +- **Description**: Closes and unlinks all WhatsApp sessions stored on the server. +- **Headers**: `X-MASTER-KEY: your_global_master_key_here` +- **Response**: + ```json + { + "message": "All sessions logged out and deleted successfully." + } + ``` + +#### 5. **Clean Up Abandoned Sessions** + +- **Endpoint**: `POST /cleanup-inactive` +- **Description**: Unlinks and purges sessions that have been inactive for more than a specified number of days. +- **Headers**: `X-MASTER-KEY: your_global_master_key_here` +- **Query Parameters** (optional): `days` (default `7`). +- **Response**: + ```json + { + "success": true, + "message": "Cleanup completed. Removed 2 inactive session(s) older than 7 day(s).", + "cleanedCount": 2, + "cleanedSessions": ["session1", "session2"], + "thresholdDays": 7 + } + ``` + +#### 6. **Upload an Attachment File (for later use)** + +- **Endpoint**: `POST /upload` +- **Description**: Upload a file to get a temporary URL. The URL is valid for 5 minutes and can be used in the `/send` or `/send-attachment` (URL method) endpoints. +- **Headers**: `X-MASTER-KEY: your_global_master_key_here` +- **Body**: `multipart/form-data` with a single field named `file`. +- **Response**: + ```json + { + "message": "File uploaded successfully.", + "url": "http://localhost:3000/uploads/1678886400000-123456789.jpg" + } + ``` +- **Example `curl` Request**: + ```bash + curl -X POST http://localhost:3000/api/upload \ + -H "X-MASTER-KEY: your_global_master_key_here" \ + -F "file=@/path/to/your/image.jpg" + ``` + +#### 4. **Clean up temporary uploaded files** + +- **Endpoint**: `POST /upload/cleanup` +- **Description**: Purges and permanently deletes temporary uploaded files in the `uploads/` folder older than a specified number of minutes. +- **Headers**: `X-MASTER-KEY: your_global_master_key_here` +- **Query Parameters** (optional): + - `minutes`: Threshold in minutes (default `5` or value set in `UPLOAD_FILE_TTL_MINUTES`). +- **Response**: + ```json + { + "success": true, + "message": "Cleanup completed. Removed 2 file(s) older than 5 minute(s).", + "cleanedCount": 2, + "cleanedFiles": ["1678886400000-sample1.jpg", "1678886400000-sample2.pdf"], + "thresholdMinutes": 5 + } + ``` + +#### 5. **Send Message (simple GET)** + +- **Endpoint**: `GET /send` +- **Description**: A simple GET request to send a text message or an attachment via URL. +- **Headers**: + - `X-MASTER-KEY: your_global_master_key_here` + - `X-API-KEY: your_unique_session_key` +- **Query Parameters**: + - `number`: The recipient's phone number (e.g., `+1234567890`). + - `message`: The text message to send. + - `attachmentUrl` (optional): A URL to a file to send as an attachment. The `message` will be used as the caption. +- **Example `curl` Request**: + ```bash + curl "http://localhost:3000/api/send?number=+1234567890&message=Hello&attachmentUrl=http://localhost:3000/uploads/file.jpg" \ + -H "X-MASTER-KEY: your_global_master_key_here" \ + -H "X-API-KEY: your_unique_session_key" + ``` + +#### 5. **Send Text Message (POST)** + +- **Endpoint**: `POST /send-message` +- **Headers**: `X-MASTER-KEY: your_global_master_key_here` +- **Description**: Sends a plain text message. The `X-API-KEY` can be in the header or, as shown below, in the request body. +- **Payload**: `application/json` + ```json + { + "X-API-KEY": "your_unique_session_key", + "to": "+1234567890", + "message": "Hello from the API!" + } + ``` + +#### 6. **Send Attachment (POST)** + +- **Endpoint**: `POST /send-attachment` +- **Description**: Sends an attachment to a specified number. This endpoint supports three methods: direct file upload, from a URL, or from a Base64 string. +- **Headers**: `X-MASTER-KEY: your_global_master_key_here` + +--- + +##### **Method 1: Direct File Upload** + +- **Content-Type**: `multipart/form-data` +- **Description**: The `X-API-KEY` can be in the header or, as shown below, as a form field. +- **Body Fields**: + - `X-API-KEY`: Your unique session key. + - `to`: The recipient's phone number. + - `file`: The file to be sent. + - `caption` (optional): A caption for the file. +- **Example `curl` Request**: + ```bash + curl -X POST http://localhost:3000/api/send-attachment \ + -H "X-MASTER-KEY: your_global_master_key_here" \ + -F "X-API-KEY=your_unique_session_key" \ + -F "to=+1234567890" \ + -F "file=@/path/to/your/document.pdf" \ + -F "caption=Here is the document you requested." + ``` + +--- + +##### **Method 2: From URL or Base64** + +- **Content-Type**: `application/json` +- **Description**: The `X-API-KEY` can be in the header or, as shown below, in the request body. +- **Payload**: + ```json + { + "X-API-KEY": "your_unique_session_key", + "to": "+1234567890", + "file": "url_or_base64_string", + "type": "image/png", // Required only for Base64 + "caption": "Optional caption" + } + ``` +- **Notes**: + - If `file` is a URL, the server will download it. + - If `file` is a Base64 string, you **must** provide the correct `type` (MIME type). +- **Example `curl` Request (URL)**: + ```bash + curl -X POST http://localhost:3000/api/send-attachment \ + -H "Content-Type: application/json" \ + -H "X-MASTER-KEY: your_global_master_key_here" \ + -d '{"X-API-KEY": "your_unique_session_key", "to": "+1234567890", "file": "https://i.imgur.com/some-image.jpeg", "caption": "From a URL"}' + ``` + +--- + +## ⚠️ Limitations + +- You must keep your phone connected to the internet for the API to work. +- This API uses an unofficial library (`whatsapp-web.js`), which may have a risk of your number being banned by WhatsApp if used for spamming. Use responsibly. +- This API only supports sending messages and does not handle incoming messages or webhooks. \ No newline at end of file diff --git a/README.md b/README.md index 73b52f3..f2e2f3f 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,35 @@ # Simple WhatsApp API -A multi-session, easy-to-deploy API that allows you to send text messages and attachments from multiple WhatsApp accounts with minimal setup. - -## Features - -- **Multi-Session Support**: Connect and manage multiple WhatsApp accounts simultaneously. Each session is tied to its own unique API key. -- **Easy Login**: Scan a QR code just once per session to connect your WhatsApp account. -- **Persistent Sessions**: Sessions are saved locally, so the server can be restarted without needing to log in again. -- **Send Text Messages**: A simple endpoint to send plain text messages from a specific session. -- **Send Attachments**: Send images, videos, or documents directly via file upload, URL, or a Base64 encoded string. -- **File Uploads**: A dedicated endpoint to upload files and receive a temporary URL, perfect for sending as attachments later. -- **Secure**: - - Protect your entire server with a global **Master API Key**. - - Manage individual WhatsApp sessions with a per-session **API Key**. -- **Dynamic QR Codes**: Fetch the login QR code for any session as a string or a PNG image via API endpoints. +Una API multi-sesión y fácil de desplegar que te permite enviar mensajes de texto y archivos adjuntos desde múltiples cuentas de WhatsApp con una configuración mínima. + +## Características + +- **Soporte multi-sesión**: Conecta y gestiona varias cuentas de WhatsApp simultáneamente. Cada sesión está vinculada a su propia clave API única. +- **Inicio de sesión sencillo**: Escanea un código QR una sola vez por sesión para conectar tu cuenta de WhatsApp. +- **Sesiones persistentes**: Las sesiones se guardan localmente, por lo que el servidor puede reiniciarse sin necesidad de volver a iniciar sesión. +- **Desvinculación limpia (Logout)**: Endpoint para cerrar sesión que revoca las credenciales de WhatsApp en el servidor y las elimina del teléfono. +- **Depuración automática de sesiones abandonadas**: Endpoint para limpiar sesiones inactivas según un umbral de días. +- **Protección Anti-Spam integrada**: Pausas automáticas configurables entre envíos, límite de mensajes por minuto y cuota máxima diaria por sesión. +- **Soporte de Múltiples Claves Maestras por Aplicación**: Asigna claves maestras únicas por sistema (ej. Ventas, CRM) para rastrear el uso. +- **Logs de acceso diarios por zona horaria**: Registra accesos y peticiones en logs diarios rotativos (`logs/access-YYYY-MM-DD.log`) configurados en horario local (ej. Guatemala `America/Guatemala`). +- **Envío de mensajes de texto**: Un endpoint simple para enviar mensajes de texto plano desde una sesión específica. +- **Envío de archivos adjuntos**: Envía imágenes, videos o documentos directamente mediante carga de archivos, URL o una cadena codificada en Base64. +- **Carga de archivos**: Un endpoint dedicado para subir archivos y recibir una URL temporal, ideal para enviarlos como adjuntos más tarde. +- **Seguridad**: + - Protege todo el servidor con una o varias **Claves API maestras**. + - Gestiona sesiones individuales de WhatsApp con una **Clave API** por sesión. +- **Códigos QR dinámicos**: Obtén el código QR de inicio de sesión de cualquier sesión como cadena de texto o imagen PNG mediante endpoints de la API. --- -## 🚀 Getting Started +## 🚀 Primeros pasos -### **Prerequisites** +### **Requisitos previos** -- [Node.js](https://nodejs.org/) (v16 or higher recommended) -- One or more WhatsApp accounts +- [Node.js](https://nodejs.org/) (se recomienda v16 o superior) +- Una o más cuentas de WhatsApp -### **1. Clone and Install Dependencies** +### **1. Clonar e instalar dependencias** ```bash git clone https://github.com/Codegres-com/Simple-Whatsapp-API.git @@ -32,18 +37,37 @@ cd Simple-Whatsapp-API npm install ``` -### **2. Configure Your Environment** +### **2. Configurar el entorno** -Create a `.env` file in the root of the project and add your Master API Key. You can invent any secret strings for the keys. +Crea un archivo `.env` en la raíz del proyecto y añade la configuración deseada: -``` +```env # .env -# This key protects the entire server. All API requests must include it. -MASTER_API_KEY=your_global_master_key_here +# Claves maestras por aplicación (Formato clave:NombreAplicacion separadas por coma) +MASTER_API_KEYS=key_ventas:SistemaVentas,key_crm:ModuloCRM,Chimpanzee24.gt:AppPrincipal + +# Compatibilidad con clave única legacy +MASTER_API_KEY=Chimpanzee24.gt + +PORT=3000 + +# Configuración personalizada de Swagger UI +SWAGGER_TITLE=Mi API Personalizada +SWAGGER_DESCRIPTION=Servicio de integración para el envío de mensajes de WhatsApp. +SERVER_URL=http://localhost:3000 + +# Registro de logs diarios (1 = activado, 0 = desactivado) +ENABLE_LOGS=1 +TIMEZONE=America/Guatemala + +# Protección Anti-Spam por Sesión +RATE_LIMIT_PER_MINUTE=15 # Máximo mensajes por minuto por sesión (0 desactiva) +MESSAGE_DELAY_MS=2000 # Pausa entre mensajes en milisegundos (2000 = 2s) +DAILY_MESSAGE_LIMIT=500 # Límite diario por sesión (0 desactiva) ``` -### **3. Start the Server** +### **3. Iniciar el servidor** ```bash npm start @@ -51,52 +75,52 @@ npm start --- -## 🐳 Running with Docker +## 🐳 Ejecución con Docker -You can also run this application using Docker and Docker Compose for a more isolated and reproducible setup. +También puedes ejecutar esta aplicación con Docker y Docker Compose para un entorno más aislado y reproducible. -### **1. Using Docker Compose (Recommended)** +### **1. Usar Docker Compose (recomendado)** -This is the easiest way to get started. +Es la forma más sencilla de empezar. -1. **Create an Environment File**: - Create a `.env` file in the root of the project. This file will be used by Docker Compose to set the environment variables inside the container. +1. **Crear un archivo de entorno**: + Crea un archivo `.env` en la raíz del proyecto. Este archivo será usado por Docker Compose para establecer las variables de entorno dentro del contenedor. ``` # .env - # The master key to protect the server + # La clave maestra para proteger el servidor MASTER_API_KEY=yoursecretkey - # The port to run the server on (optional, defaults to 3000) + # El puerto en el que se ejecutará el servidor (opcional, por defecto 3000) PORT=3000 ``` -2. **Build and Run the Container**: - Run the following command to build the Docker image and start the container in the background: +2. **Construir y ejecutar el contenedor**: + Ejecuta el siguiente comando para construir la imagen de Docker e iniciar el contenedor en segundo plano: ```bash docker compose up --build -d ``` - The server will now be running on the port you specified (or the default, 3000). + El servidor estará ejecutándose en el puerto que hayas especificado (o el predeterminado, 3000). -3. **To Stop the Server**: +3. **Para detener el servidor**: ```bash docker compose down ``` -### **2. Using the Pre-built Docker Hub Image** +### **2. Usar la imagen preconstruida de Docker Hub** -If you don't want to build the image from the source, you can use the pre-built image from Docker Hub. +Si no quieres construir la imagen desde el código fuente, puedes usar la imagen preconstruida de Docker Hub. -1. **Pull the Image**: +1. **Descargar la imagen**: ```bash docker pull codegres/simple-whatsapp-api:latest ``` -2. **Run the Image**: - You still need to provide the environment variables and mount the volumes. +2. **Ejecutar la imagen**: + Aún necesitas proporcionar las variables de entorno y montar los volúmenes. ```bash docker run -d \ @@ -109,18 +133,17 @@ If you don't want to build the image from the source, you can use the pre-built codegres/simple-whatsapp-api:latest ``` -### **3. Using Docker (Manual Build)** - +### **3. Usar Docker (construcción manual)** -If you prefer not to use Docker Compose, you can build and run the container manually. +Si prefieres no usar Docker Compose, puedes construir y ejecutar el contenedor manualmente. -1. **Build the Docker Image**: +1. **Construir la imagen de Docker**: ```bash docker build -t whatsapp-api . ``` -2. **Run the Docker Container**: - You must pass the `MASTER_API_KEY` and map the port. You also need to create and mount volumes to persist the `sessions` and `uploads` data. +2. **Ejecutar el contenedor de Docker**: + Debes pasar la `MASTER_API_KEY` y mapear el puerto. También necesitas crear y montar volúmenes para persistir los datos de `sessions` y `uploads`. ```bash docker run -d \ @@ -132,85 +155,85 @@ If you prefer not to use Docker Compose, you can build and run the container man -v whatsapp_uploads:/usr/src/app/uploads \ whatsapp-api ``` - - `-d`: Run in detached mode. - - `-p`: Map port 3000 on your host to port 3000 in the container. - - `-e`: Set environment variables. - - `--name`: Assign a name to the container. - - `-v`: Mount named volumes to persist data. + - `-d`: Ejecutar en modo detached (segundo plano). + - `-p`: Mapear el puerto 3000 del host al puerto 3000 del contenedor. + - `-e`: Establecer variables de entorno. + - `--name`: Asignar un nombre al contenedor. + - `-v`: Montar volúmenes con nombre para persistir los datos. --- -## 📖 API Documentation +## 📖 Documentación de la API -This project includes interactive API documentation powered by Swagger UI and a pre-configured Postman collection to make testing and integration as easy as possible. +Este proyecto incluye documentación interactiva de la API con Swagger UI y una colección de Postman preconfigurada para facilitar las pruebas y la integración. ### **Swagger UI** -Once the server is running, you can access the interactive Swagger UI in your browser. This interface allows you to view all available endpoints, see their parameters, and test them live. +Una vez que el servidor esté en ejecución, puedes acceder a la interfaz interactiva de Swagger UI en tu navegador. Esta interfaz te permite ver todos los endpoints disponibles, consultar sus parámetros y probarlos en vivo. - **URL**: [http://localhost:3000/api-docs](http://localhost:3000/api-docs) -When you open the Swagger UI, the `X-MASTER-KEY` will be pre-authorized with the default value (`SUPER_SECRET_KEY` or the value from your `.env` file), so you can start making requests to the protected endpoints immediately. +Al abrir Swagger UI, la `X-MASTER-KEY` estará preautorizada con el valor predeterminado (`SUPER_SECRET_KEY` o el valor de tu archivo `.env`), para que puedas empezar a hacer peticiones a los endpoints protegidos de inmediato. -### **Postman Collection** +### **Colección de Postman** -A Postman collection is included in the root of this project to help you get started quickly. +En la raíz de este proyecto se incluye una colección de Postman para ayudarte a empezar rápidamente. -1. **Import the Collection**: - - Find the `whatsapp_api_collection.json` file in the project's root directory. - - In Postman, click **Import** and upload the file. +1. **Importar la colección**: + - Encuentra el archivo `whatsapp_api_collection.json` en el directorio raíz del proyecto. + - En Postman, haz clic en **Import** y sube el archivo. -2. **Configure Environment (Optional)**: - - The collection comes with a pre-request script that automatically adds the `X-MASTER-KEY` header to every request. - - By default, it uses `SUPER_SECRET_KEY`. To use your own key, create a new Postman Environment, add a variable named `MASTER_API_KEY`, and set its value to your key from the `.env` file. +2. **Configurar el entorno (opcional)**: + - La colección incluye un script de pre-solicitud que añade automáticamente el encabezado `X-MASTER-KEY` a cada petición. + - Por defecto, usa `SUPER_SECRET_KEY`. Para usar tu propia clave, crea un nuevo entorno de Postman, añade una variable llamada `MASTER_API_KEY` y establece su valor con la clave de tu archivo `.env`. -All endpoints are prefixed with `/api`. +Todos los endpoints tienen el prefijo `/api`. -### **Authentication** +### **Autenticación** -This API uses a two-key system for security and session management. Both keys can be provided in either the request header or the request body for `POST` requests, giving you more flexibility. For `GET` requests, they must be in the header. +Esta API utiliza un sistema de dos claves para la seguridad y la gestión de sesiones. Ambas claves pueden proporcionarse en el encabezado de la petición o en el cuerpo de las peticiones `POST`, lo que te da más flexibilidad. En las peticiones `GET`, deben ir en el encabezado. -1. **Master Key (`X-MASTER-KEY`)**: - - This is a global key that grants access to the entire API server. - - It can be included in the `X-MASTER-KEY` header or as a field in the JSON request body. The header will always take precedence if both are provided. - - This is the key you set in your `.env` file. +1. **Clave maestra (`X-MASTER-KEY`)**: + - Es una clave global que concede acceso a todo el servidor de la API. + - Puede incluirse en el encabezado `X-MASTER-KEY` o como campo en el cuerpo JSON de la petición. El encabezado siempre tendrá prioridad si se proporcionan ambos. + - Es la clave que configuras en tu archivo `.env`. -2. **Session Key (`X-API-KEY`)**: - - This key identifies a specific WhatsApp session (i.e., a specific phone number). - - For `POST` requests, it can be in the `X-API-KEY` header or a field in the JSON/form-data body. - - For `GET` requests, it must be in the `X-API-KEY` header. - - You can invent any unique string for each session (e.g., `user1_phone`, `work_account`, a random hash, etc.). - - The first time a new `X-API-KEY` is used with the `/connect` endpoint, a new session will be created for it. +2. **Clave de sesión (`X-API-KEY`)**: + - Esta clave identifica una sesión específica de WhatsApp (es decir, un número de teléfono concreto). + - En las peticiones `POST`, puede ir en el encabezado `X-API-KEY` o como campo en el cuerpo JSON/form-data. + - En las peticiones `GET`, debe ir en el encabezado `X-API-KEY`. + - Puedes inventar cualquier cadena única para cada sesión (por ejemplo, `user1_phone`, `work_account`, un hash aleatorio, etc.). + - La primera vez que se use una nueva `X-API-KEY` con el endpoint `/connect`, se creará una nueva sesión para ella. --- -## 📲 Connecting a Session +## 📲 Conectar una sesión -To use a WhatsApp account, you must first connect it to a session key. +Para usar una cuenta de WhatsApp, primero debes conectarla a una clave de sesión. -1. Choose a unique `X-API-KEY` for the account you want to connect (e.g., `my-personal-whatsapp`). -2. Make a request to one of the connection endpoints with both the master key and your chosen session key. The server will generate a QR code for that specific session. +1. Elige una `X-API-KEY` única para la cuenta que quieras conectar (por ejemplo, `my-personal-whatsapp`). +2. Haz una petición a uno de los endpoints de conexión con la clave maestra y la clave de sesión elegida. El servidor generará un código QR para esa sesión específica. - - **GET `/api/connect`**: Returns the QR code as a string. - - **GET `/api/connect/image`**: Returns the QR code as a PNG image. + - **GET `/api/connect`**: Devuelve el código QR como cadena de texto. + - **GET `/api/connect/image`**: Devuelve el código QR como imagen PNG. -3. Open WhatsApp on your phone, go to **Settings > Linked Devices**, and scan the QR code. +3. Abre WhatsApp en tu teléfono, ve a **Ajustes > Dispositivos vinculados** y escanea el código QR. -Once connected, the server will save the session data in the `./sessions` folder. You won't need to scan the code again for this session unless you log out. Repeat this process for each WhatsApp account you want to use, assigning a different `X-API-KEY` to each. +Una vez conectado, el servidor guardará los datos de la sesión en la carpeta `./sessions`. No necesitarás volver a escanear el código para esta sesión a menos que cierres sesión. Repite este proceso para cada cuenta de WhatsApp que quieras usar, asignando una `X-API-KEY` diferente a cada una. --- ## **Endpoints** -#### 1. **Get Connection Status / QR Code** +#### 1. **Obtener estado de conexión / código QR** - **Endpoint**: `GET /connect` -- **Description**: Get the current connection status for a session. If a QR code is available, it will be returned as a string. If not, the session status is returned. -- **Headers**: +- **Descripción**: Obtiene el estado de conexión actual de una sesión. Si hay un código QR disponible, se devolverá como cadena de texto. Si no, se devuelve el estado de la sesión. +- **Encabezados**: - `X-MASTER-KEY: your_global_master_key_here` - `X-API-KEY: your_unique_session_key` -- **Response (When QR is ready)**: `200 OK` with the QR string in the body. -- **Response (When connected)**: `200 OK` +- **Respuesta (cuando el QR está listo)**: `200 OK` con la cadena del QR en el cuerpo. +- **Respuesta (cuando está conectado)**: `200 OK` ```json { "sessionId": "your_unique_session_key", @@ -218,59 +241,120 @@ Once connected, the server will save the session data in the `./sessions` folder } ``` -#### 2. **Get QR Code as Image** +#### 2. **Obtener código QR como imagen** - **Endpoint**: `GET /connect/image` -- **Description**: Get the session's QR code as a PNG image. -- **Headers**: +- **Descripción**: Obtiene el código QR de la sesión como imagen PNG. +- **Encabezados**: - `X-MASTER-KEY: your_global_master_key_here` - `X-API-KEY: your_unique_session_key` -- **Response**: `200 OK` with `Content-Type: image/png`. +- **Respuesta**: `200 OK` con `Content-Type: image/png`. -#### 3. **Upload an Attachment (for later use)** +#### 3. **Cerrar sesión de una cuenta (Logout)** + +- **Endpoint**: `POST /logout` +- **Descripción**: Cierra la sesión activa de WhatsApp desvinculando el dispositivo del teléfono y elimina los datos locales. +- **Encabezados**: + - `X-MASTER-KEY: your_global_master_key_here` + - `X-API-KEY: your_unique_session_key` +- **Respuesta**: + ```json + { + "message": "Session logged out and deleted successfully." + } + ``` + +#### 4. **Cerrar todas las sesiones (Logout All)** + +- **Endpoint**: `POST /logout-all` +- **Descripción**: Cierra y desvincula todas las sesiones de WhatsApp del servidor. +- **Encabezados**: `X-MASTER-KEY: your_global_master_key_here` +- **Respuesta**: + ```json + { + "message": "All sessions logged out and deleted successfully." + } + ``` + +#### 5. **Depurar sesiones abandonadas** + +- **Endpoint**: `POST /cleanup-inactive` +- **Descripción**: Desvincula y elimina sesiones inactivas según el umbral de días indicado. +- **Encabezados**: `X-MASTER-KEY: your_global_master_key_here` +- **Parámetros de consulta** (opcional): `days` (por defecto `7`). +- **Respuesta**: + ```json + { + "success": true, + "message": "Cleanup completed. Removed 2 inactive session(s) older than 7 day(s).", + "cleanedCount": 2, + "cleanedSessions": ["session1", "session2"], + "thresholdDays": 7 + } + ``` + +#### 6. **Subir un archivo adjunto (para uso posterior)** - **Endpoint**: `POST /upload` -- **Description**: Upload a file to get a temporary URL. The URL is valid for 5 minutes and can be used in the `/send` or `/send-attachment` (URL method) endpoints. -- **Headers**: `X-MASTER-KEY: your_global_master_key_here` -- **Body**: `multipart/form-data` with a single field named `file`. -- **Response**: +- **Descripción**: Sube un archivo para obtener una URL temporal. La URL es válida durante 5 minutos y puede usarse en los endpoints `/send` o `/send-attachment` (método por URL). +- **Encabezados**: `X-MASTER-KEY: your_global_master_key_here` +- **Cuerpo**: `multipart/form-data` con un único campo llamado `file`. +- **Respuesta**: ```json { "message": "File uploaded successfully.", "url": "http://localhost:3000/uploads/1678886400000-123456789.jpg" } ``` -- **Example `curl` Request**: +- **Ejemplo de petición con `curl`**: ```bash curl -X POST http://localhost:3000/api/upload \ -H "X-MASTER-KEY: your_global_master_key_here" \ -F "file=@/path/to/your/image.jpg" ``` -#### 4. **Send Message (Simple GET)** +#### 4. **Limpiar archivos adjuntos temporales** + +- **Endpoint**: `POST /upload/cleanup` +- **Descripción**: Depura y elimina permanentemente los archivos temporales subidos a la carpeta `uploads/` que superen un umbral de tiempo en minutos. +- **Encabezados**: `X-MASTER-KEY: your_global_master_key_here` +- **Parámetros de consulta** (opcional): + - `minutes`: Umbral en minutos (por defecto `5` o el valor configurado en `UPLOAD_FILE_TTL_MINUTES`). +- **Respuesta**: + ```json + { + "success": true, + "message": "Cleanup completed. Removed 2 file(s) older than 5 minute(s).", + "cleanedCount": 2, + "cleanedFiles": ["1678886400000-sample1.jpg", "1678886400000-sample2.pdf"], + "thresholdMinutes": 5 + } + ``` + +#### 5. **Enviar mensaje (GET simple)** - **Endpoint**: `GET /send` -- **Description**: A simple GET request to send a text message or an attachment via URL. -- **Headers**: +- **Descripción**: Una petición GET simple para enviar un mensaje de texto o un archivo adjunto mediante URL. +- **Encabezados**: - `X-MASTER-KEY: your_global_master_key_here` - `X-API-KEY: your_unique_session_key` -- **Query Parameters**: - - `number`: The recipient's phone number (e.g., `+1234567890`). - - `message`: The text message to send. - - `attachmentUrl` (optional): A URL to a file to send as an attachment. The `message` will be used as the caption. -- **Example `curl` Request**: +- **Parámetros de consulta**: + - `number`: El número de teléfono del destinatario (por ejemplo, `+1234567890`). + - `message`: El mensaje de texto a enviar. + - `attachmentUrl` (opcional): Una URL de un archivo para enviar como adjunto. El `message` se usará como pie de foto. +- **Ejemplo de petición con `curl`**: ```bash curl "http://localhost:3000/api/send?number=+1234567890&message=Hello&attachmentUrl=http://localhost:3000/uploads/file.jpg" \ -H "X-MASTER-KEY: your_global_master_key_here" \ -H "X-API-KEY: your_unique_session_key" ``` -#### 5. **Send Text Message (POST)** +#### 5. **Enviar mensaje de texto (POST)** - **Endpoint**: `POST /send-message` -- **Headers**: `X-MASTER-KEY: your_global_master_key_here` -- **Description**: Sends a plain text message. The `X-API-KEY` can be in the header or, as shown below, in the request body. -- **Payload**: `application/json` +- **Encabezados**: `X-MASTER-KEY: your_global_master_key_here` +- **Descripción**: Envía un mensaje de texto plano. La `X-API-KEY` puede ir en el encabezado o, como se muestra abajo, en el cuerpo de la petición. +- **Carga útil**: `application/json` ```json { "X-API-KEY": "your_unique_session_key", @@ -279,24 +363,24 @@ Once connected, the server will save the session data in the `./sessions` folder } ``` -#### 6. **Send Attachment (POST)** +#### 6. **Enviar archivo adjunto (POST)** - **Endpoint**: `POST /send-attachment` -- **Description**: Sends an attachment to a specified number. This endpoint supports three methods: direct file upload, from a URL, or from a Base64 string. -- **Headers**: `X-MASTER-KEY: your_global_master_key_here` +- **Descripción**: Envía un archivo adjunto a un número especificado. Este endpoint admite tres métodos: carga directa de archivo, desde una URL o desde una cadena Base64. +- **Encabezados**: `X-MASTER-KEY: your_global_master_key_here` --- -##### **Method 1: Direct File Upload** +##### **Método 1: Carga directa de archivo** - **Content-Type**: `multipart/form-data` -- **Description**: The `X-API-KEY` can be in the header or, as shown below, as a form field. -- **Body Fields**: - - `X-API-KEY`: Your unique session key. - - `to`: The recipient's phone number. - - `file`: The file to be sent. - - `caption` (optional): A caption for the file. -- **Example `curl` Request**: +- **Descripción**: La `X-API-KEY` puede ir en el encabezado o, como se muestra abajo, como campo del formulario. +- **Campos del cuerpo**: + - `X-API-KEY`: Tu clave de sesión única. + - `to`: El número de teléfono del destinatario. + - `file`: El archivo a enviar. + - `caption` (opcional): Un pie de foto para el archivo. +- **Ejemplo de petición con `curl`**: ```bash curl -X POST http://localhost:3000/api/send-attachment \ -H "X-MASTER-KEY: your_global_master_key_here" \ @@ -308,24 +392,24 @@ Once connected, the server will save the session data in the `./sessions` folder --- -##### **Method 2: From URL or Base64** +##### **Método 2: Desde URL o Base64** - **Content-Type**: `application/json` -- **Description**: The `X-API-KEY` can be in the header or, as shown below, in the request body. -- **Payload**: +- **Descripción**: La `X-API-KEY` puede ir en el encabezado o, como se muestra abajo, en el cuerpo de la petición. +- **Carga útil**: ```json { "X-API-KEY": "your_unique_session_key", "to": "+1234567890", "file": "url_or_base64_string", - "type": "image/png", // Required only for Base64 + "type": "image/png", // Obligatorio solo para Base64 "caption": "Optional caption" } ``` -- **Notes**: - - If `file` is a URL, the server will download it. - - If `file` is a Base64 string, you **must** provide the correct `type` (MIME type). -- **Example `curl` Request (URL)**: +- **Notas**: + - Si `file` es una URL, el servidor la descargará. + - Si `file` es una cadena Base64, **debes** proporcionar el `type` correcto (tipo MIME). +- **Ejemplo de petición con `curl` (URL)**: ```bash curl -X POST http://localhost:3000/api/send-attachment \ -H "Content-Type: application/json" \ @@ -335,8 +419,8 @@ Once connected, the server will save the session data in the `./sessions` folder --- -## ⚠️ Limitations +## ⚠️ Limitaciones -- You must keep your phone connected to the internet for the API to work. -- This API uses an unofficial library (`whatsapp-web.js`), which may have a risk of your number being banned by WhatsApp if used for spamming. Use responsibly. -- This API only supports sending messages and does not handle incoming messages or webhooks. \ No newline at end of file +- Debes mantener tu teléfono conectado a internet para que la API funcione. +- Esta API utiliza una biblioteca no oficial (`whatsapp-web.js`), que puede conllevar el riesgo de que WhatsApp bloquee tu número si se usa para enviar spam. Úsala con responsabilidad. +- Esta API solo admite el envío de mensajes y no gestiona mensajes entrantes ni webhooks. diff --git a/logs/access-2026-08-08.log b/logs/access-2026-08-08.log new file mode 100644 index 0000000..c982c1f --- /dev/null +++ b/logs/access-2026-08-08.log @@ -0,0 +1,9 @@ +[2026-08-08T16:33:54.476Z] App: "SistemaVentas" | Key: "key***" | Method: GET | Endpoint: /api/connect | Status: 200 | IP: ::1 +[2026-08-08T16:34:19.040Z] App: "ModuloCRM" | Key: "key***" | Method: GET | Endpoint: /api/connect | Status: 200 | IP: ::1 +[2026-08-08T16:36:29.915Z] App: "UNAUTHORIZED" | Key: "dfg***" | Method: POST | Endpoint: /api/logout-all | Status: 401 | IP: ::1 | Invalid or missing master API key +[2026-08-08T16:36:58.373Z] App: "UNAUTHORIZED" | Key: "Mur***" | Method: POST | Endpoint: /api/logout-all | Status: 401 | IP: ::1 | Invalid or missing master API key +[2026-08-08T16:37:31.855Z] App: "AppPrincipal" | Key: "Chi***" | Method: POST | Endpoint: /api/logout-all | Status: 200 | IP: ::1 +[2026-08-08T16:56:50.096Z] App: "AppPrincipal" | Key: "Chi***" | Method: POST | Endpoint: /api/logout-all | Status: 200 | IP: ::1 +[2026-08-08T10:59:26.175-06:00] App: "AppPrincipal" | Key: "Chi***" | Method: GET | Endpoint: /api/connect?X-API-KEY=test-tz | Status: 200 | IP: ::1 +[2026-08-08T11:18:11.356-06:00] App: "AppPrincipal" | Key: "Chi***" | Method: POST | Endpoint: /api/upload/cleanup?minutes=5 | Status: 200 | IP: ::1 +[2026-08-08T11:19:50.605-06:00] App: "AppPrincipal" | Key: "Chi***" | Method: POST | Endpoint: /api/upload/cleanup?minutes=5 | Status: 200 | IP: ::1 diff --git a/package-lock.json b/package-lock.json index 1cbb725..5bcd765 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "cors": "^2.8.6", "dotenv": "^17.2.3", "express": "^5.1.0", "multer": "^2.0.2", @@ -627,6 +628,23 @@ "license": "MIT", "optional": true }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", diff --git a/package.json b/package.json index 5a909f9..bca8f18 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ }, "homepage": "https://github.com/Codegres-com/Simple-Whatsapp-API#readme", "dependencies": { + "cors": "^2.8.6", "dotenv": "^17.2.3", "express": "^5.1.0", "multer": "^2.0.2", diff --git a/src/controllers/authController.js b/src/controllers/authController.js index 14485d7..c41e74f 100644 --- a/src/controllers/authController.js +++ b/src/controllers/authController.js @@ -1,20 +1,19 @@ const qrcode = require('qrcode'); -const { getStatus, initializeClient } = require('../services/sessionManager'); +const { getStatus, initializeClient, waitForSession } = require('../services/sessionManager'); const { getSessionId } = require('../utils/apiKeyExtractor'); /** * Handles the /connect endpoint. * Returns the QR code string for the session if available, otherwise the current status. */ -const getQrCodeString = (req, res) => { +const getQrCodeString = async (req, res) => { const sessionId = getSessionId(req); if (!sessionId) { return res.status(400).json({ error: 'X-API-KEY header is required.' }); } - // Initialize or get the session - initializeClient(sessionId); - const { status, qrCode } = getStatus(sessionId); + // Wait for session to generate QR or reach status + const { status, qrCode } = await waitForSession(sessionId); if (status === 'QR Code Generated' && qrCode) { res.status(200).send(qrCode); @@ -27,15 +26,14 @@ const getQrCodeString = (req, res) => { * Handles the /connect/image endpoint. * Returns the QR code for the session as a PNG image. */ -const getQrCodeImage = (req, res) => { +const getQrCodeImage = async (req, res) => { const sessionId = getSessionId(req); if (!sessionId) { return res.status(400).json({ error: 'X-API-KEY header is required.' }); } - // Initialize or get the session - initializeClient(sessionId); - const { status, qrCode } = getStatus(sessionId); + // Wait for session to generate QR or reach status + const { status, qrCode } = await waitForSession(sessionId); if (status === 'QR Code Generated' && qrCode) { qrcode.toBuffer(qrCode, (err, buffer) => { @@ -53,7 +51,64 @@ const getQrCodeImage = (req, res) => { } }; +/** + * Handles closing a specific session. + */ +const logout = async (req, res) => { + const sessionId = getSessionId(req); + if (!sessionId) { + return res.status(400).json({ error: 'X-API-KEY header is required.' }); + } + + try { + const { destroySession } = require('../services/sessionManager'); + await destroySession(sessionId); + res.status(200).json({ success: true, message: `Session ${sessionId} has been closed and removed.` }); + } catch (error) { + console.error(`Failed to logout session ${sessionId}:`, error); + res.status(500).json({ success: false, error: error.message }); + } +}; + +/** + * Handles closing all active sessions and cleaning up session data. + */ +const logoutAll = async (req, res) => { + try { + const { destroyAllSessions } = require('../services/sessionManager'); + await destroyAllSessions(); + res.status(200).json({ success: true, message: 'All active sessions have been closed and data removed.' }); + } catch (error) { + console.error('Failed to logout all sessions:', error); + res.status(500).json({ success: false, error: error.message }); + } +}; + +/** + * Handles cleaning up inactive/abandoned sessions. + */ +const cleanupInactive = async (req, res) => { + try { + const { cleanInactiveSessions } = require('../services/sessionManager'); + const days = parseFloat(req.query.days || (req.body ? req.body.days : undefined) || 7); + const result = await cleanInactiveSessions(days); + res.status(200).json({ + success: true, + message: `Cleanup completed. Removed ${result.cleanedCount} inactive session(s) older than ${result.thresholdDays} day(s).`, + cleanedCount: result.cleanedCount, + cleanedSessions: result.cleanedSessions, + thresholdDays: result.thresholdDays + }); + } catch (error) { + console.error('Failed to clean inactive sessions:', error); + res.status(500).json({ success: false, error: error.message }); + } +}; + module.exports = { getQrCodeString, - getQrCodeImage + getQrCodeImage, + logout, + logoutAll, + cleanupInactive }; \ No newline at end of file diff --git a/src/controllers/uploadController.js b/src/controllers/uploadController.js index e13844b..005d496 100644 --- a/src/controllers/uploadController.js +++ b/src/controllers/uploadController.js @@ -1,4 +1,43 @@ const fs = require('fs'); +const path = require('path'); + +const UPLOADS_DIR = './uploads'; + +/** + * Cleans up temporary uploaded files older than maxAgeMinutes. + * @param {number} maxAgeMinutes + * @returns {{cleanedCount: number, cleanedFiles: string[], thresholdMinutes: number}} + */ +const cleanUploadedFiles = (maxAgeMinutes = 5) => { + const minutes = parseFloat(maxAgeMinutes) || 5; + const cutoffTime = Date.now() - (minutes * 60 * 1000); + const cleanedFiles = []; + + if (!fs.existsSync(UPLOADS_DIR)) { + return { cleanedCount: 0, cleanedFiles, thresholdMinutes: minutes }; + } + + const files = fs.readdirSync(UPLOADS_DIR); + for (const file of files) { + const filePath = path.join(UPLOADS_DIR, file); + try { + const stats = fs.statSync(filePath); + if (stats.isFile() && stats.mtimeMs < cutoffTime) { + fs.unlinkSync(filePath); + cleanedFiles.push(file); + console.log(`Cleaned temporary uploaded file: ${file}`); + } + } catch (err) { + console.error(`Failed to inspect/delete file ${file}:`, err.message); + } + } + + return { + cleanedCount: cleanedFiles.length, + cleanedFiles, + thresholdMinutes: minutes + }; +}; /** * Handles the /upload endpoint. @@ -10,24 +49,58 @@ const uploadFile = (req, res) => { } const fileUrl = `${req.protocol}://${req.get('host')}/uploads/${req.file.filename}`; + const ttlMinutes = parseFloat(process.env.UPLOAD_FILE_TTL_MINUTES || '5'); - // Schedule file deletion after 5 minutes + // Schedule file deletion setTimeout(() => { - fs.unlink(req.file.path, (unlinkErr) => { - if (unlinkErr) { - console.error(`Failed to delete temporary file: ${req.file.path}`, unlinkErr); - } else { - console.log(`Deleted temporary file: ${req.file.path}`); - } - }); - }, 5 * 60 * 1000); // 5 minutes + if (fs.existsSync(req.file.path)) { + fs.unlink(req.file.path, (unlinkErr) => { + if (unlinkErr) { + console.error(`Failed to delete temporary file: ${req.file.path}`, unlinkErr); + } else { + console.log(`Deleted temporary file: ${req.file.path}`); + } + }); + } + }, ttlMinutes * 60 * 1000); res.status(200).json({ message: 'File uploaded successfully.', - url: fileUrl + url: fileUrl, + expiresInMinutes: ttlMinutes }); }; +/** + * Endpoint handler for manual upload cleanup. + */ +const cleanupUploads = (req, res) => { + try { + const minutes = parseFloat(req.query.minutes || (req.body ? req.body.minutes : undefined) || process.env.UPLOAD_FILE_TTL_MINUTES || 5); + const result = cleanUploadedFiles(minutes); + res.status(200).json({ + success: true, + message: `Cleanup completed. Removed ${result.cleanedCount} file(s) older than ${result.thresholdMinutes} minute(s).`, + cleanedCount: result.cleanedCount, + cleanedFiles: result.cleanedFiles, + thresholdMinutes: result.thresholdMinutes + }); + } catch (error) { + console.error('Failed to cleanup uploads:', error); + res.status(500).json({ success: false, error: error.message }); + } +}; + +// Run automatic background cleanup every 10 minutes on server +setInterval(() => { + try { + const ttlMinutes = parseFloat(process.env.UPLOAD_FILE_TTL_MINUTES || '5'); + cleanUploadedFiles(ttlMinutes); + } catch (err) {} +}, 10 * 60 * 1000); + module.exports = { - uploadFile + uploadFile, + cleanupUploads, + cleanUploadedFiles }; \ No newline at end of file diff --git a/src/index.js b/src/index.js index 133f232..30e94bd 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,6 @@ require('dotenv').config(); const express = require('express'); +const cors = require('cors'); const swaggerUi = require('swagger-ui-express'); const swaggerSpec = require('./swagger'); const apiRoutes = require('./routes/api'); @@ -7,6 +8,9 @@ const apiRoutes = require('./routes/api'); const app = express(); const port = process.env.PORT || 3000; +// Enable CORS for local requests +app.use(cors()); + // Middleware to parse JSON bodies app.use(express.json({ limit: '50mb' })); // Increase limit for Base64 files app.use(express.urlencoded({ extended: true, limit: '50mb' })); @@ -16,22 +20,24 @@ app.use('/uploads', express.static('uploads')); // Swagger UI setup const swaggerUiOptions = { - customJs: ` - window.onload = function() { + customSiteTitle: process.env.SWAGGER_TITLE || process.env.APP_TITLE || 'Simple WhatsApp API', + customJsStr: ` + window.addEventListener('load', function() { setTimeout(function() { const key = '${process.env.MASTER_API_KEY || "SUPER_SECRET_KEY"}'; const ui = window.ui; if (ui) { ui.preauthorizeApiKey("ApiKeyAuth", key); } - }, 200); - }; + }, 300); + }); `, swaggerOptions: { // The validatorUrl is set to null to disable the validation of the OpenAPI specification. validatorUrl: null, // The defaultModelsExpandDepth option is set to -1 to hide the "Models" section in the Swagger UI. defaultModelsExpandDepth: -1, + persistAuthorization: true, }, }; app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, swaggerUiOptions)); @@ -47,6 +53,15 @@ app.get('/', (req, res) => { res.send('WhatsApp API Server is running. Use the /api endpoints to interact.'); }); +// Process error handlers to prevent crashes on browser disconnects +process.on('unhandledRejection', (reason) => { + console.error('Unhandled Rejection (caught):', reason && reason.message ? reason.message : reason); +}); + +process.on('uncaughtException', (err) => { + console.error('Uncaught Exception (caught):', err && err.message ? err.message : err); +}); + // Start the server app.listen(port, () => { console.log(`Server is running on http://localhost:${port}`); diff --git a/src/middleware/masterAuthMiddleware.js b/src/middleware/masterAuthMiddleware.js index 8a3e6e9..1ff0ab2 100644 --- a/src/middleware/masterAuthMiddleware.js +++ b/src/middleware/masterAuthMiddleware.js @@ -1,25 +1,97 @@ require('dotenv').config(); +const { logAccess } = require('../utils/logger'); -const MASTER_API_KEY = process.env.MASTER_API_KEY; +/** + * Parses configured master API keys from environment variables. + * Returns a Map of key -> appName. + */ +const getRegisteredKeysMap = () => { + const keyMap = new Map(); + + const rawKeys = process.env.MASTER_API_KEYS; + if (rawKeys) { + let parsed = false; + // Try JSON parsing + if (rawKeys.trim().startsWith('{')) { + try { + const jsonObj = JSON.parse(rawKeys); + for (const [k, v] of Object.entries(jsonObj)) { + keyMap.set(k.trim(), String(v).trim()); + } + parsed = true; + } catch (e) {} + } + // If not JSON, parse key:appName pairs or comma-separated list + if (!parsed) { + const pairs = rawKeys.split(','); + for (const item of pairs) { + const trimmed = item.trim(); + if (!trimmed) continue; + if (trimmed.includes(':')) { + const parts = trimmed.split(':'); + const k = parts[0].trim(); + const app = parts.slice(1).join(':').trim(); + if (k) keyMap.set(k, app || 'RegisteredApp'); + } else { + keyMap.set(trimmed, 'RegisteredApp'); + } + } + } + } + + // Fallback or legacy MASTER_API_KEY support + if (process.env.MASTER_API_KEY) { + const legacyKey = process.env.MASTER_API_KEY.trim(); + if (!keyMap.has(legacyKey)) { + keyMap.set(legacyKey, 'DefaultMasterApp'); + } + } + + return keyMap; +}; /** - * Middleware to protect all API routes with a Master API key. - * The key can be provided in the 'X-MASTER-KEY' header or in the request body. + * Middleware to protect all API routes with Master API keys. + * Supports multiple keys assigned to different apps, and logs daily access. */ const masterApiKeyAuth = (req, res, next) => { - // Check for the key in the header first, then in the body. const masterKey = req.get('X-MASTER-KEY') || (req.body ? req.body['X-MASTER-KEY'] : undefined); + const ip = req.ip || req.connection.remoteAddress || 'unknown'; + + const registeredKeys = getRegisteredKeysMap(); - if (!MASTER_API_KEY) { - // If the master key is not set in the environment, deny all requests. - console.error("MASTER_API_KEY is not set in the environment."); - return res.status(500).json({ error: 'Server configuration error.' }); + if (registeredKeys.size === 0) { + console.error("No MASTER_API_KEYS or MASTER_API_KEY configured in environment."); + return res.status(500).json({ error: 'Server configuration error: No master API keys configured.' }); } - if (!masterKey || masterKey !== MASTER_API_KEY) { + if (!masterKey || !registeredKeys.has(masterKey)) { + const keyMasked = masterKey ? `${masterKey.substring(0, Math.min(3, masterKey.length))}***` : 'NONE'; + logAccess({ + appName: 'UNAUTHORIZED', + keyMasked, + endpoint: req.originalUrl || req.url, + method: req.method, + ip, + status: 401, + details: 'Invalid or missing master API key' + }); return res.status(401).json({ error: 'Unauthorized: Missing or invalid Master API key.' }); } + const appName = registeredKeys.get(masterKey); + req.appName = appName; + const keyMasked = `${masterKey.substring(0, Math.min(3, masterKey.length))}***`; + + logAccess({ + appName, + keyMasked, + endpoint: req.originalUrl || req.url, + method: req.method, + ip, + status: 200 + }); + next(); }; diff --git a/src/routes/api.js b/src/routes/api.js index 5db5c49..672ce16 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -2,9 +2,9 @@ const express = require('express'); const router = express.Router(); // Import controllers -const { getQrCodeString, getQrCodeImage } = require('../controllers/authController'); +const { getQrCodeString, getQrCodeImage, logout, logoutAll, cleanupInactive } = require('../controllers/authController'); const { sendTextMessage, sendAttachmentMessage, sendFromApi } = require('../controllers/messageController'); -const { uploadFile } = require('../controllers/uploadController'); +const { uploadFile, cleanupUploads } = require('../controllers/uploadController'); const upload = require('../middleware/uploadMiddleware'); /** @@ -73,6 +73,62 @@ router.get('/connect', getQrCodeString); */ router.get('/connect/image', getQrCodeImage); +/** + * @swagger + * /api/logout: + * post: + * summary: Close specific session + * tags: [Authentication] + * description: Closes and removes a specific WhatsApp session identified by `X-API-KEY`. + * parameters: + * - in: header + * name: X-API-KEY + * schema: + * type: string + * required: true + * description: Your unique session key. + * responses: + * 200: + * description: Session closed successfully. + * 400: + * description: Missing X-API-KEY header. + */ +router.post('/logout', logout); + +/** + * @swagger + * /api/logout-all: + * post: + * summary: Close all sessions + * tags: [Authentication] + * description: Closes all active WhatsApp sessions and deletes stored session data. + * responses: + * 200: + * description: All sessions closed successfully. + */ +router.post('/logout-all', logoutAll); + +/** + * @swagger + * /api/cleanup-inactive: + * post: + * summary: Clean up abandoned inactive sessions + * tags: [Authentication] + * description: Identifies and removes sessions that have not been active for a specified number of days. + * parameters: + * - in: query + * name: days + * schema: + * type: number + * default: 7 + * required: false + * description: Number of inactive days threshold. Sessions unused for longer than this will be unlinked and deleted. + * responses: + * 200: + * description: Cleanup completed successfully. + */ +router.post('/cleanup-inactive', cleanupInactive); + /** * @swagger * /api/send-message: @@ -201,6 +257,27 @@ router.post('/send-attachment', upload.single('file'), sendAttachmentMessage); */ router.post('/upload', upload.single('file'), uploadFile); +/** + * @swagger + * /api/upload/cleanup: + * post: + * summary: Clean up temporary uploaded files + * tags: [File Upload] + * description: Deletes temporary uploaded files in the uploads folder older than a specified number of minutes. + * parameters: + * - in: query + * name: minutes + * schema: + * type: number + * default: 5 + * required: false + * description: Threshold in minutes. Uploaded files older than this will be permanently purged. + * responses: + * 200: + * description: Upload cleanup completed successfully. + */ +router.post('/upload/cleanup', cleanupUploads); + /** * @swagger * /api/send: diff --git a/src/services/sessionManager.js b/src/services/sessionManager.js index dbe8812..6e83158 100644 --- a/src/services/sessionManager.js +++ b/src/services/sessionManager.js @@ -28,9 +28,18 @@ const initializeClient = (sessionId) => { const client = new Client({ authStrategy: new LocalAuth({ dataPath: sessionDataPath }), + webVersionCache: { + type: 'remote', + remotePath: 'https://raw.githubusercontent.com/wppconnect-team/wa-version/main/html/2.2412.54.html' + }, puppeteer: { headless: true, - args: ['--no-sandbox', '--disable-setuid-sandbox'] + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--disable-gpu' + ] } }); @@ -62,16 +71,32 @@ const initializeClient = (sessionId) => { client.on('auth_failure', (msg) => { console.error(`Authentication failure for session ${sessionId}:`, msg); session.status = 'Authentication Failure'; - // Clean up and remove the failed session - fs.rmdirSync(sessionDataPath, { recursive: true }); sessions.delete(sessionId); + try { + if (fs.existsSync(sessionDataPath)) { + fs.rmSync(sessionDataPath, { recursive: true, force: true }); + } + } catch (err) { + console.error(`Error deleting session dir for ${sessionId}:`, err); + } }); - client.on('disconnected', (reason) => { + client.on('disconnected', async (reason) => { console.log(`Client for session ${sessionId} was logged out:`, reason); session.status = 'Disconnected'; - client.destroy().catch(err => console.error(`Error destroying client for session ${sessionId}:`, err)); sessions.delete(sessionId); + try { + await client.destroy(); + } catch (err) { + // Silently ignore target closed errors during destroy + } + try { + if (fs.existsSync(sessionDataPath)) { + fs.rmSync(sessionDataPath, { recursive: true, force: true }); + } + } catch (err) { + console.error(`Error deleting session dir for ${sessionId}:`, err); + } }); client.initialize().catch(err => { @@ -90,12 +115,10 @@ const initializeClient = (sessionId) => { const getStatus = (sessionId) => { const session = sessions.get(sessionId); if (!session) { - // If no session exists, initialize one. - const newSession = initializeClient(sessionId); return { - id: newSession.id, - status: newSession.status, - qrCode: newSession.qrCode + id: sessionId, + status: 'Disconnected', + qrCode: null }; } return { @@ -105,6 +128,25 @@ const getStatus = (sessionId) => { }; }; +/** + * Helper to get the correct WhatsApp serialized chat ID. + */ +const getFormattedChatId = async (client, to) => { + let cleanNumber = to.replace(/[^0-9]/g, ''); + let formatted = `${cleanNumber}@c.us`; + try { + const numberDetails = await client.getNumberId(cleanNumber); + if (numberDetails && numberDetails._serialized) { + formatted = numberDetails._serialized; + } + } catch (err) { + console.warn(`getNumberId check failed for ${to}, using ${formatted}`); + } + return formatted; +}; + +const { validateAntiSpam } = require('../utils/spamProtector'); + /** * Sends a text message from a specific session. * @param {string} sessionId - The session ID. @@ -112,13 +154,25 @@ const getStatus = (sessionId) => { * @param {string} message - The message to send. */ const sendMessage = async (sessionId, to, message) => { - const session = sessions.get(sessionId); + let session = sessions.get(sessionId); if (!session || session.status !== 'Connected') { - throw new Error(`Session ${sessionId} is not connected.`); + const sessionDataPath = `${SESSIONS_DIR}/session-${sessionId}`; + if (!fs.existsSync(sessionDataPath)) { + throw new Error(`Session ${sessionId} is not connected. Please scan the QR code first using GET /api/connect/image.`); + } + const sessionState = await waitForSession(sessionId, 15000); + session = sessions.get(sessionId); + if (!session || sessionState.status !== 'Connected') { + throw new Error(`Session ${sessionId} is not connected. Please scan the QR code first using GET /api/connect/image.`); + } } - const chatId = `${to.replace('+', '')}@c.us`; - await session.client.sendMessage(chatId, message); - console.log(`Message sent to ${to} from session ${sessionId}`); + // Validate anti-spam restrictions (rate limits, delays, daily quotas) + await validateAntiSpam(sessionId); + + touchSessionActivity(sessionId); + const chatId = await getFormattedChatId(session.client, to); + await session.client.sendMessage(chatId, message, { sendSeen: false }); + console.log(`Message sent to ${to} (${chatId}) from session ${sessionId}`); }; /** @@ -130,10 +184,22 @@ const sendMessage = async (sessionId, to, message) => { * @param {string} [type] - The MIME type, required for Base64 encoded files. */ const sendAttachment = async (sessionId, to, file, caption, type) => { - const session = sessions.get(sessionId); + let session = sessions.get(sessionId); if (!session || session.status !== 'Connected') { - throw new Error(`Session ${sessionId} is not connected.`); + const sessionDataPath = `${SESSIONS_DIR}/session-${sessionId}`; + if (!fs.existsSync(sessionDataPath)) { + throw new Error(`Session ${sessionId} is not connected. Please scan the QR code first using GET /api/connect/image.`); + } + const sessionState = await waitForSession(sessionId, 15000); + session = sessions.get(sessionId); + if (!session || sessionState.status !== 'Connected') { + throw new Error(`Session ${sessionId} is not connected. Please scan the QR code first using GET /api/connect/image.`); + } } + // Validate anti-spam restrictions (rate limits, delays, daily quotas) + await validateAntiSpam(sessionId); + + touchSessionActivity(sessionId); let media; if (fs.existsSync(file)) { @@ -151,14 +217,225 @@ const sendAttachment = async (sessionId, to, file, caption, type) => { media = new MessageMedia(type, base64Data, 'file'); } - const chatId = `${to.replace('+', '')}@c.us`; - await session.client.sendMessage(chatId, media, { caption }); - console.log(`Attachment sent to ${to} from session ${sessionId}`); + const chatId = await getFormattedChatId(session.client, to); + await session.client.sendMessage(chatId, media, { caption, sendSeen: false }); + console.log(`Attachment sent to ${to} (${chatId}) from session ${sessionId}`); +}; + +/** + * Waits for a session to finish initializing (e.g. until QR code is generated or session connects). + * @param {string} sessionId - The session ID. + * @param {number} [timeoutMs=30000] - Max wait time in ms. + * @returns {Promise<{status: string, qrCode?: string, id: string}>} + */ +const waitForSession = (sessionId, timeoutMs = 30000) => { + return new Promise((resolve) => { + initializeClient(sessionId); + const current = getStatus(sessionId); + if (current.status !== 'Initializing') { + return resolve(current); + } + + const startTime = Date.now(); + const interval = setInterval(() => { + const status = getStatus(sessionId); + if (status.status !== 'Initializing' || (Date.now() - startTime) >= timeoutMs) { + clearInterval(interval); + resolve(status); + } + }, 500); + }); +}; + +/** + * Destroys a single session and removes its stored data. + * @param {string} sessionId + */ +const destroySession = async (sessionId) => { + let session = sessions.get(sessionId); + const sessionDataPath = `${SESSIONS_DIR}/session-${sessionId}`; + + // If session exists on disk but not in memory, initialize it briefly to perform logout + if (!session && fs.existsSync(sessionDataPath)) { + session = initializeClient(sessionId); + await waitForSession(sessionId, 10000); + } + + if (session) { + sessions.delete(sessionId); + if (session.client) { + try { + console.log(`Unlinking WhatsApp device session: ${sessionId}...`); + await session.client.logout(); + console.log(`WhatsApp device session ${sessionId} unlinked successfully.`); + } catch (err) { + console.error(`Error unlinking WhatsApp session ${sessionId}:`, err.message); + } + try { + await session.client.destroy(); + } catch (err) { + // Ignore destroy errors + } + } + } + + // Wait for Puppeteer process to fully release file locks + await new Promise((resolve) => setTimeout(resolve, 1500)); + + try { + if (fs.existsSync(sessionDataPath)) { + fs.rmSync(sessionDataPath, { recursive: true, force: true }); + } + } catch (err) { + console.error(`Error removing session folder ${sessionId}:`, err); + } +}; + +/** + * Destroys all active WhatsApp sessions and clears the sessions directory. + */ +const destroyAllSessions = async () => { + // Scan disk for offline sessions so we can unlink them as well + if (fs.existsSync(SESSIONS_DIR)) { + const entries = fs.readdirSync(SESSIONS_DIR); + for (const entry of entries) { + if (entry.startsWith('session-')) { + const sessionId = entry.replace('session-', ''); + if (!sessions.has(sessionId)) { + initializeClient(sessionId); + } + } + } + } + + const activeSessions = Array.from(sessions.values()); + sessions.clear(); + + for (const session of activeSessions) { + if (session.client) { + try { + console.log(`Unlinking WhatsApp device session: ${session.id}...`); + await session.client.logout(); + console.log(`WhatsApp device session ${session.id} unlinked successfully.`); + } catch (err) { + console.error(`Error unlinking WhatsApp session ${session.id}:`, err.message); + } + try { + await session.client.destroy(); + } catch (err) { + // Ignore destroy errors + } + } + } + + // Wait for Chromium processes to shut down and release file handles + await new Promise((resolve) => setTimeout(resolve, 1500)); + + try { + if (fs.existsSync(SESSIONS_DIR)) { + fs.rmSync(SESSIONS_DIR, { recursive: true, force: true }); + fs.mkdirSync(SESSIONS_DIR, { recursive: true }); + } + } catch (err) { + console.error('Error cleaning up sessions directory:', err); + } +}; + +/** + * Updates last activity timestamp for a session. + * @param {string} sessionId + */ +const touchSessionActivity = (sessionId) => { + const now = Date.now(); + const session = sessions.get(sessionId); + if (session) { + session.lastActivity = now; + } + const sessionDataPath = `${SESSIONS_DIR}/session-${sessionId}`; + if (fs.existsSync(sessionDataPath)) { + try { + fs.writeFileSync(`${sessionDataPath}/meta.json`, JSON.stringify({ lastActivity: now }), 'utf8'); + } catch (err) {} + } +}; + +/** + * Gets last activity timestamp for a session. + * @param {string} sessionId + * @returns {number} + */ +const getSessionLastActivity = (sessionId) => { + const session = sessions.get(sessionId); + if (session && session.lastActivity) { + return session.lastActivity; + } + const metaPath = `${SESSIONS_DIR}/session-${sessionId}/meta.json`; + if (fs.existsSync(metaPath)) { + try { + const data = JSON.parse(fs.readFileSync(metaPath, 'utf8')); + if (data && data.lastActivity) return data.lastActivity; + } catch (err) {} + } + const sessionDataPath = `${SESSIONS_DIR}/session-${sessionId}`; + if (fs.existsSync(sessionDataPath)) { + try { + const stats = fs.statSync(sessionDataPath); + return stats.mtimeMs; + } catch (err) {} + } + return Date.now(); +}; + +/** + * Cleans up sessions that have been inactive for more than maxInactiveDays. + * @param {number} maxInactiveDays - Number of inactive days threshold. + * @returns {Promise<{cleanedCount: number, cleanedSessions: string[], thresholdDays: number}>} + */ +const cleanInactiveSessions = async (maxInactiveDays) => { + const days = parseFloat(maxInactiveDays) || 7; + const cutoffTime = Date.now() - (days * 24 * 60 * 60 * 1000); + const allSessionIds = new Set(); + + // Collect active sessions in memory + for (const id of sessions.keys()) { + allSessionIds.add(id); + } + + // Collect session folders from disk + if (fs.existsSync(SESSIONS_DIR)) { + const entries = fs.readdirSync(SESSIONS_DIR); + for (const entry of entries) { + if (entry.startsWith('session-')) { + allSessionIds.add(entry.replace('session-', '')); + } + } + } + + const cleanedSessions = []; + for (const sessionId of allSessionIds) { + const lastActivity = getSessionLastActivity(sessionId); + if (lastActivity < cutoffTime) { + console.log(`Cleaning inactive session ${sessionId} (last active: ${new Date(lastActivity).toISOString()})`); + await destroySession(sessionId); + cleanedSessions.push(sessionId); + } + } + + return { + cleanedCount: cleanedSessions.length, + cleanedSessions, + thresholdDays: days + }; }; module.exports = { initializeClient, getStatus, + waitForSession, + destroySession, + destroyAllSessions, + cleanInactiveSessions, + touchSessionActivity, sendMessage, sendAttachment }; \ No newline at end of file diff --git a/src/swagger.js b/src/swagger.js index b26df15..04adc44 100644 --- a/src/swagger.js +++ b/src/swagger.js @@ -1,16 +1,24 @@ +require('dotenv').config(); const swaggerJSDoc = require('swagger-jsdoc'); +const title = process.env.SWAGGER_TITLE || process.env.APP_TITLE || 'Simple WhatsApp API'; +const baseDescription = process.env.SWAGGER_DESCRIPTION || process.env.APP_DESCRIPTION || 'A simple API to send WhatsApp messages, built with Node.js and whatsapp-web.js.'; +const description = `${baseDescription}\n\n*Basado en: Simple WhatsApp API*`; + +const serverUrl = process.env.SERVER_URL || process.env.API_URL || `http://localhost:${process.env.PORT || 3000}`; +const serverDescription = process.env.SERVER_DESCRIPTION || 'API Server'; + const swaggerDefinition = { openapi: '3.0.0', info: { - title: 'Simple WhatsApp API', + title: title, version: '1.0.0', - description: 'A simple API to send WhatsApp messages, built with Node.js and whatsapp-web.js.', + description: description, }, servers: [ { - url: 'https://simple-whatsapp-api.kp7b0h3vueu5g.ap-south-1.cs.amazonlightsail.com', - description: 'Development server', + url: serverUrl, + description: serverDescription, }, ], components: { diff --git a/src/utils/logger.js b/src/utils/logger.js new file mode 100644 index 0000000..df78ed6 --- /dev/null +++ b/src/utils/logger.js @@ -0,0 +1,66 @@ +const fs = require('fs'); +const path = require('path'); + +const LOGS_DIR = './logs'; + +if (!fs.existsSync(LOGS_DIR)) { + fs.mkdirSync(LOGS_DIR, { recursive: true }); +} + +/** + * Helper to get date/time parts in specified timezone (default America/Guatemala). + */ +const getTimezoneParts = (date = new Date()) => { + const tz = process.env.TIMEZONE || 'America/Guatemala'; + const formatter = new Intl.DateTimeFormat('en-US', { + timeZone: tz, + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + fractionalSecondDigits: 3, + hour12: false + }); + const parts = {}; + for (const p of formatter.formatToParts(date)) { + parts[p.type] = p.value; + } + return parts; +}; + +/** + * Gets formatted date string YYYY-MM-DD in Guatemala timezone for daily log file naming. + */ +const getDailyLogFileName = () => { + const p = getTimezoneParts(new Date()); + return path.join(LOGS_DIR, `access-${p.year}-${p.month}-${p.day}.log`); +}; + +/** + * Appends an entry to the daily access log in Guatemala timezone. + */ +const logAccess = ({ appName, keyMasked, endpoint, method, ip, status, details }) => { + // Check if logging is enabled (1 = enabled, 0 = disabled) + const enabled = process.env.ENABLE_LOGS ?? process.env.GENERATE_LOGS ?? '1'; + if (enabled !== '1' && enabled !== 'true') { + return; + } + + const p = getTimezoneParts(new Date()); + const ms = p.fractionalSecond ? `.${p.fractionalSecond}` : ''; + const timestamp = `${p.year}-${p.month}-${p.day}T${p.hour}:${p.minute}:${p.second}${ms}-06:00`; + + const logLine = `[${timestamp}] App: "${appName}" | Key: "${keyMasked}" | Method: ${method} | Endpoint: ${endpoint} | Status: ${status} | IP: ${ip}${details ? ' | ' + details : ''}\n`; + try { + const logFile = getDailyLogFileName(); + fs.appendFileSync(logFile, logLine, 'utf8'); + } catch (err) { + console.error('Failed to write to access log:', err); + } +}; + +module.exports = { + logAccess +}; diff --git a/src/utils/spamProtector.js b/src/utils/spamProtector.js new file mode 100644 index 0000000..61f6304 --- /dev/null +++ b/src/utils/spamProtector.js @@ -0,0 +1,68 @@ +/** + * Anti-Spam Manager for WhatsApp API + */ +const sessionSendHistory = new Map(); + +/** + * Validates anti-spam rules before sending a message for a given session. + * @param {string} sessionId + */ +const validateAntiSpam = async (sessionId) => { + const rateLimitPerMinute = parseInt(process.env.RATE_LIMIT_PER_MINUTE || '15', 10); + const dailyLimit = parseInt(process.env.DAILY_MESSAGE_LIMIT || '500', 10); + const minDelayMs = parseInt(process.env.MESSAGE_DELAY_MS || '2000', 10); + + const now = Date.now(); + let history = sessionSendHistory.get(sessionId); + + if (!history) { + history = { + lastSendTime: 0, + minuteTimestamps: [], + dailyCount: 0, + lastDailyReset: new Date().toDateString() + }; + sessionSendHistory.set(sessionId, history); + } + + // Reset daily count if a new day has started + const today = new Date().toDateString(); + if (history.lastDailyReset !== today) { + history.dailyCount = 0; + history.lastDailyReset = today; + } + + // Check daily limit + if (dailyLimit > 0 && history.dailyCount >= dailyLimit) { + throw new Error(`Anti-Spam Restriction: Session "${sessionId}" reached its daily limit of ${dailyLimit} messages.`); + } + + // Check rate limit per minute + const oneMinuteAgo = now - 60000; + history.minuteTimestamps = history.minuteTimestamps.filter(t => t > oneMinuteAgo); + + if (rateLimitPerMinute > 0 && history.minuteTimestamps.length >= rateLimitPerMinute) { + const oldestInWindow = history.minuteTimestamps[0]; + const waitSeconds = Math.ceil((oldestInWindow + 60000 - now) / 1000); + throw new Error(`Anti-Spam Restriction: Rate limit exceeded (${rateLimitPerMinute} msgs/min). Please wait ${waitSeconds}s before sending again.`); + } + + // Apply delay between consecutive messages + if (minDelayMs > 0 && history.lastSendTime > 0) { + const elapsed = now - history.lastSendTime; + if (elapsed < minDelayMs) { + const waitMs = minDelayMs - elapsed; + await new Promise(resolve => setTimeout(resolve, waitMs)); + } + } + + // Record send timestamp + const sendTime = Date.now(); + history.lastSendTime = sendTime; + history.minuteTimestamps.push(sendTime); + history.dailyCount += 1; +}; + +module.exports = { + validateAntiSpam +}; From 651e68634818a0aa4a7fb9229aa1d966fa2b28ac Mon Sep 17 00:00:00 2001 From: Perry Daniels Date: Sat, 8 Aug 2026 12:42:20 -0600 Subject: [PATCH 2/3] obtener lista de sesiones activas --- .gitignore | 11 ++++++- src/controllers/authController.js | 22 +++++++++++++- src/routes/api.js | 17 ++++++++++- src/services/sessionManager.js | 50 +++++++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index d0584a4..9ecc32e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,13 @@ /sessions/ # Environment -.env \ No newline at end of file +.env + +# Logs +/logs/ + +# Uploads +/uploads/ + +# Deploy +deploy.sh diff --git a/src/controllers/authController.js b/src/controllers/authController.js index c41e74f..c4339bd 100644 --- a/src/controllers/authController.js +++ b/src/controllers/authController.js @@ -105,10 +105,30 @@ const cleanupInactive = async (req, res) => { } }; +/** + * Handles listing active sessions and total counts. + */ +const getActiveSessions = async (req, res) => { + try { + const { listSessions } = require('../services/sessionManager'); + const data = listSessions(); + res.status(200).json({ + success: true, + totalCount: data.totalCount, + activeMemoryCount: data.activeMemoryCount, + sessions: data.sessions + }); + } catch (error) { + console.error('Failed to list active sessions:', error); + res.status(500).json({ success: false, error: error.message }); + } +}; + module.exports = { getQrCodeString, getQrCodeImage, logout, logoutAll, - cleanupInactive + cleanupInactive, + getActiveSessions }; \ No newline at end of file diff --git a/src/routes/api.js b/src/routes/api.js index 672ce16..fa9d102 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -2,7 +2,7 @@ const express = require('express'); const router = express.Router(); // Import controllers -const { getQrCodeString, getQrCodeImage, logout, logoutAll, cleanupInactive } = require('../controllers/authController'); +const { getQrCodeString, getQrCodeImage, logout, logoutAll, cleanupInactive, getActiveSessions } = require('../controllers/authController'); const { sendTextMessage, sendAttachmentMessage, sendFromApi } = require('../controllers/messageController'); const { uploadFile, cleanupUploads } = require('../controllers/uploadController'); const upload = require('../middleware/uploadMiddleware'); @@ -108,6 +108,21 @@ router.post('/logout', logout); */ router.post('/logout-all', logoutAll); +/** + * @swagger + * /api/sessions: + * get: + * summary: List active sessions and counts + * tags: [Authentication] + * description: Returns a list of all active/stored sessions along with total counts. + * responses: + * 200: + * description: List of sessions and counts. + * 500: + * description: Server error. + */ +router.get('/sessions', getActiveSessions); + /** * @swagger * /api/cleanup-inactive: diff --git a/src/services/sessionManager.js b/src/services/sessionManager.js index 6e83158..4e593db 100644 --- a/src/services/sessionManager.js +++ b/src/services/sessionManager.js @@ -428,6 +428,55 @@ const cleanInactiveSessions = async (maxInactiveDays) => { }; }; +/** + * Lists all active and stored sessions with details and counts. + * @returns {{ totalCount: number, activeMemoryCount: number, sessions: Array<{ sessionId: string, status: string, isReady: boolean, lastActivity: string }> }} + */ +const listSessions = () => { + const sessionMap = new Map(); + + // Collect active memory sessions + for (const [id, session] of sessions.entries()) { + const lastActivityMs = getSessionLastActivity(id); + sessionMap.set(id, { + sessionId: id, + status: session.status || 'Unknown', + isReady: session.status === 'Ready', + inMemory: true, + lastActivity: new Date(lastActivityMs).toISOString() + }); + } + + // Collect sessions stored on disk that might not be loaded in memory + if (fs.existsSync(SESSIONS_DIR)) { + const entries = fs.readdirSync(SESSIONS_DIR); + for (const entry of entries) { + if (entry.startsWith('session-')) { + const id = entry.replace('session-', ''); + if (!sessionMap.has(id)) { + const lastActivityMs = getSessionLastActivity(id); + sessionMap.set(id, { + sessionId: id, + status: 'Stored (Inactive in memory)', + isReady: false, + inMemory: false, + lastActivity: new Date(lastActivityMs).toISOString() + }); + } + } + } + } + + const sessionList = Array.from(sessionMap.values()); + const activeMemoryCount = sessionList.filter(s => s.inMemory && s.isReady).length; + + return { + totalCount: sessionList.length, + activeMemoryCount, + sessions: sessionList + }; +}; + module.exports = { initializeClient, getStatus, @@ -435,6 +484,7 @@ module.exports = { destroySession, destroyAllSessions, cleanInactiveSessions, + listSessions, touchSessionActivity, sendMessage, sendAttachment From 6612d5c7b5b33cb2c70a33901a72355ee211a188 Mon Sep 17 00:00:00 2001 From: Perry Daniels Date: Sun, 16 Aug 2026 10:36:13 -0600 Subject: [PATCH 3/3] web portada principal --- .gitignore | 4 + package.json | 2 +- public/favicon.svg | 17 + public/index.html | 1623 ++++++++++++++++++++++++++++++++++++++++++++ public/logo.svg | 78 +++ src/index.js | 89 ++- src/swagger.js | 6 +- 7 files changed, 1805 insertions(+), 14 deletions(-) create mode 100644 public/favicon.svg create mode 100644 public/index.html create mode 100644 public/logo.svg diff --git a/.gitignore b/.gitignore index 9ecc32e..da8bae8 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,7 @@ # Deploy deploy.sh + +# IA Context +/.agents +/.clinerules diff --git a/package.json b/package.json index bca8f18..3c1fa59 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "app", "version": "1.0.0", - "description": "Simple Whatsapp API - Just connect with QR and start sending messages instantly.", + "description": "Mk WhatsApp API - Gateway REST para mensajería y automatización con conexión QR.", "main": "index.js", "scripts": { "start": "node src/index.js", diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..8f5c0d4 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..a030073 --- /dev/null +++ b/public/index.html @@ -0,0 +1,1623 @@ + + + + + + Mk WhatsApp API | Plataforma de Mensajería & Gateway REST + + + + + + + + + + + + + +
+ +
+ + +
+
+
+ + Gateway REST multi-sesión independiente +
+ +

+ La API de WhatsApp más ágil para tus sistemas y aplicaciones +

+ +

+ Conecta tus números mediante código QR en segundos, envía mensajes de texto, imágenes, audios y documentos sin pagar tarifas exorbitantes por conversación. +

+ + + + +
+
+
+ + + +
+ +
+ + + + +
+
+ +
+ + +
+# Ejemplo de envío a través del Gateway seguro +curl -X POST "https://gateway.midominio.com/v1/messages/dispatch" \ + -H "Content-Type: application/json" \ + -H "x-api-key: TU_MASTER_API_KEY" \ + -d '{ + "X-API-KEY": "instancia_empresa_01", + "to": "50212345678", + "message": "¡Hola! Tu notificación ha sido procesada exitosamente 🚀" + }'
+ + + + + + +
+
+
+
+ + +
+
+
+ Capacidades Principales +

Diseñada para desarrolladores y empresas

+

Toda la potencia de WhatsApp Web empaquetada en un servidor API modular, seguro y fácil de escalar.

+
+ +
+
+
+ +
+

Multi-Instancia & Sesiones

+

Maneja múltiples números de WhatsApp en el mismo servidor simplemente enviando un identificador de sesión único en el header X-API-KEY.

+
+ +
+
+ +
+

Archivos & Multimedia

+

Envía documentos PDF, imágenes, videos y notas de voz mediante carga directa (Multipart), enlaces públicos (URL) o cadenas codificadas en Base64.

+
+ +
+
+ +
+

Conexión QR Dinámica

+

Genera y visualiza códigos QR directamente como imagen PNG en el navegador o como string ASCII para integrarlo en tu propio panel de administración.

+
+ +
+
+ +
+

Seguridad Master Key

+

Protege todas las rutas críticas del gateway con autenticación centralizada por llave maestra, evitando accesos no autorizados a la API.

+
+ +
+
+ +
+

Limpieza Automática

+

Mantenimiento programable para purgar archivos temporales y sesiones inactivas huérfanas, optimizando el consumo de disco y memoria RAM.

+
+ +
+
+ +
+

Arquitectura RESTful

+

Estructura JSON limpia, códigos de estado HTTP estándar y respuestas predecibles para integrar con cualquier lenguaje o software existente.

+
+
+
+
+ + +
+
+
+ Tarifas & Uso Responsable +

Planes diseñados para sistemas de gestión

+

Tarifas accesibles para el envío moderado de respuestas, notificaciones y confirmaciones automáticas.

+
+ + +
+
⚠️
+
+

Uso Transaccional & Protección de tu Número

+

+ Esta API está construida exclusivamente para interactuar con aplicaciones de gestión (ERP, CRM, e-commerce, agendas) enviando confirmaciones de pedidos, citas y respuestas de uso moderado. No está permitida para spam ni publicidad masiva. Utilizarla para envíos masivos indiscriminados conlleva un alto riesgo de que WhatsApp suspenda o bloquee definitivamente el número telefónico utilizado. +

+
+
+ + +
+ Facturación Mensual + + Facturación Anual + Ahorra 20% +
+ +
+ +
+
+

Plan Básico

+

Para microempresas que requieren notificaciones automáticas y alertas puntuales.

+
+ $ + 9 + /mes +
+
+ +
    +
  • + + 1 Sesión WhatsApp Activa +
  • +
  • + + Hasta 600 mensajes / mes +
  • +
  • + + Confirmaciones de pedidos y citas +
  • +
  • + + Conexión QR por interfaz web +
  • +
  • + + Envío de PDFs y multimedia pesada +
  • +
  • + + Múltiples sesiones simultáneas +
  • +
+ + +
+ + + + + +
+
+

Plan Multi-Sucursal

+

Para empresas con varias sucursales o líneas de atención que requieren uso controlado.

+
+ $ + 39 + /mes +
+
+ +
    +
  • + + Hasta 4 Sesiones (Multi-número) +
  • +
  • + + Hasta 6,000 mensajes / mes (Límite seguro) +
  • +
  • + + Protección anti-saturación de línea +
  • +
  • + + Archivos pesados (PDFs, Audio, Docs) +
  • +
  • + + SLA 99.9% de disponibilidad del gateway +
  • +
  • + + Asesoría de buenas prácticas de envío +
  • +
+ + +
+
+ + +
+
+ Calculadora de Ahorro Transaccional +

¿Cuánto ahorras frente a Meta Cloud API?

+

+ Meta cobra aproximadamente $0.035 a $0.050 USD por cada conversación iniciada. Con Mk WhatsApp API mantienes una tarifa plana accesible. +

+ + + +
+ +
+ Ahorro Mensual Estimado +
$51 USD
+

+ Costo Meta: ~$70/mes vs Costo Mk API: ~$19/mes (Ahorro del 73%) +

+
+
+
+
+ + +
+
+
+ Módulos de Integración +

Servicios y Capacidades Disponibles

+

Conectividad de alto rendimiento protegida por autenticación y llave de acceso.

+
+ +
+
+
+ AUTH / QR +
+
Vinculación de Dispositivo & Generador QR
+
Establece la conexión en tiempo real generando un código QR dinámico listo para escanear.
+
+
+ +
+ +
+
+ MESSAGING / TEXT +
+
Despacho de Mensajería & Notificaciones
+
Envío automatizado de mensajes de texto, alertas y confirmaciones con soporte para emojis.
+
+
+ +
+ +
+
+ MEDIA / UPLOAD +
+
Transmisión Multimedia & Documentos
+
Envío de facturas en PDF, comprobantes en imagen, notas de audio y videos de hasta 50MB.
+
+
+ +
+ +
+
+ MONITOR / SESSIONS +
+
Monitor de Conectividad & Instancias Activas
+
Consulta métricas de disponibilidad, salud del gateway y número de sesiones conectadas.
+
+
+ +
+ +
+
+ SECURITY / LOGOUT +
+
Desconexión Segura & Cierre de Instancias
+
Invalida y purga credenciales de sesión bajo demanda garantizando privacidad.
+
+
+ +
+
+
+
+ + +
+ + + + + + + + + + diff --git a/public/logo.svg b/public/logo.svg new file mode 100644 index 0000000..38a9d31 --- /dev/null +++ b/public/logo.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/index.js b/src/index.js index 30e94bd..4c09be6 100644 --- a/src/index.js +++ b/src/index.js @@ -15,12 +15,83 @@ app.use(cors()); app.use(express.json({ limit: '50mb' })); // Increase limit for Base64 files app.use(express.urlencoded({ extended: true, limit: '50mb' })); -// Serve static files from the 'uploads' directory -app.use('/uploads', express.static('uploads')); +const path = require('path'); -// Swagger UI setup +// Serve static files from the 'uploads' and 'public' directory +app.use('/uploads', express.static(path.join(__dirname, '../uploads'))); +app.use(express.static(path.join(__dirname, '../public'))); + +// Middleware de autenticación para la documentación técnica +const docsAuthMiddleware = (req, res, next) => { + const requiredPass = process.env.DOCS_PASSWORD || 'Chimpanzee24.gt'; + const authHeader = req.headers.authorization; + + if (!authHeader) { + res.set('WWW-Authenticate', 'Basic realm="Documentacion Restringida"'); + return res.status(401).send(` + + + + + Acceso Restringido - Documentación + + + +
+

🔒 Autenticación Requerida

+

Debes ingresar las credenciales autorizadas para consultar la documentación técnica de la API.

+ ← Volver al inicio +
+ + + `); + } + + const auth = Buffer.from(authHeader.split(' ')[1] || '', 'base64').toString().split(':'); + const user = auth[0]; + const pass = auth.slice(1).join(':'); + + if (pass === requiredPass || user === requiredPass) { + return next(); + } + + res.set('WWW-Authenticate', 'Basic realm="Documentacion Restringida"'); + return res.status(401).send(` + + + + + Error de Autenticación + + + +
+

❌ Acceso Denegado

+
Error: Contraseña incorrecta
+

La contraseña ingresada no es válida para visualizar la documentación técnica.

+ ← Volver al inicio +
+ + + `); +}; + +// UI Docs setup (Protegido por contraseña) const swaggerUiOptions = { - customSiteTitle: process.env.SWAGGER_TITLE || process.env.APP_TITLE || 'Simple WhatsApp API', + customSiteTitle: process.env.APP_TITLE || 'WhatsApp API Documentation', customJsStr: ` window.addEventListener('load', function() { setTimeout(function() { @@ -33,14 +104,12 @@ const swaggerUiOptions = { }); `, swaggerOptions: { - // The validatorUrl is set to null to disable the validation of the OpenAPI specification. validatorUrl: null, - // The defaultModelsExpandDepth option is set to -1 to hide the "Models" section in the Swagger UI. defaultModelsExpandDepth: -1, persistAuthorization: true, }, }; -app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, swaggerUiOptions)); +app.use('/api-docs', docsAuthMiddleware, swaggerUi.serve, swaggerUi.setup(swaggerSpec, swaggerUiOptions)); const masterApiKeyAuth = require('./middleware/masterAuthMiddleware'); @@ -48,9 +117,9 @@ const masterApiKeyAuth = require('./middleware/masterAuthMiddleware'); app.use('/api', masterApiKeyAuth); app.use('/api', apiRoutes); -// Welcome route +// Welcome route - Landing Page app.get('/', (req, res) => { - res.send('WhatsApp API Server is running. Use the /api endpoints to interact.'); + res.sendFile(path.join(__dirname, '../public/index.html')); }); // Process error handlers to prevent crashes on browser disconnects @@ -65,5 +134,5 @@ process.on('uncaughtException', (err) => { // Start the server app.listen(port, () => { console.log(`Server is running on http://localhost:${port}`); - console.log(`Swagger docs available at http://localhost:${port}/api-docs`); + console.log(`API docs available at http://localhost:${port}/api-docs (Protected)`); }); \ No newline at end of file diff --git a/src/swagger.js b/src/swagger.js index 04adc44..e1dcd59 100644 --- a/src/swagger.js +++ b/src/swagger.js @@ -1,9 +1,9 @@ require('dotenv').config(); const swaggerJSDoc = require('swagger-jsdoc'); -const title = process.env.SWAGGER_TITLE || process.env.APP_TITLE || 'Simple WhatsApp API'; -const baseDescription = process.env.SWAGGER_DESCRIPTION || process.env.APP_DESCRIPTION || 'A simple API to send WhatsApp messages, built with Node.js and whatsapp-web.js.'; -const description = `${baseDescription}\n\n*Basado en: Simple WhatsApp API*`; +const title = process.env.SWAGGER_TITLE || process.env.APP_TITLE || 'Mk WhatsApp API'; +const baseDescription = process.env.SWAGGER_DESCRIPTION || process.env.APP_DESCRIPTION || 'Gateway REST para mensajería y automatización de WhatsApp.'; +const description = `${baseDescription}`; const serverUrl = process.env.SERVER_URL || process.env.API_URL || `http://localhost:${process.env.PORT || 3000}`; const serverDescription = process.env.SERVER_DESCRIPTION || 'API Server';