diff --git a/API_ADMIN_GUIA.md b/API_ADMIN_GUIA.md
new file mode 100644
index 0000000..458b5b6
--- /dev/null
+++ b/API_ADMIN_GUIA.md
@@ -0,0 +1,315 @@
+# API Admin — Guia de Implementacion
+
+> Stack: Laravel + MongoDB + Laravel Sanctum
+> Prefijo base: /api/admin/*
+> Seguridad: Bearer Token (Sanctum) + rol administrador
+
+---
+
+## 1. Archivos creados / modificados
+
+| Archivo | Descripcion |
+|---------|-------------|
+| app/Http/Middleware/AdminApiMiddleware.php | Verifica rol=administrador. Retorna JSON 403 (sin redirect). |
+| app/Http/Kernel.php | Registra alias admin.api |
+| app/Http/Controllers/Api/Admin/UsuarioController.php | CRUD usuarios + activar/desactivar |
+| app/Http/Controllers/Api/Admin/VcardController.php | Listar, ver, editar vcards; por usuario |
+| app/Http/Controllers/Api/Admin/MembresiaController.php | Ver/activar/desactivar membresia + cambiar plan |
+| app/Http/Controllers/Api/Admin/PagoController.php | Historial HealthPagos + transacciones wallet |
+| app/Http/Controllers/Api/Admin/LogController.php | Log de movimientos global y por usuario |
+| app/Http/Controllers/Api/Admin/MiniWebController.php | Miniwebs con usuario y vcard vinculada |
+| routes/api.php | 34 rutas nuevas bajo /api/admin/* |
+
+---
+
+## 2. Implementar en otro proyecto
+
+### 2.1 Requisitos previos
+- Laravel >= 10
+- mongodb/laravel-mongodb instalado
+- laravel/sanctum configurado
+- Modelo User con HasApiTokens y relacion rol() -> Rol
+- Modelo Rol con campo nombre (valor "administrador")
+
+### 2.2 Copiar estos archivos
+```
+app/Http/Middleware/AdminApiMiddleware.php
+app/Http/Controllers/Api/Admin/UsuarioController.php
+app/Http/Controllers/Api/Admin/VcardController.php
+app/Http/Controllers/Api/Admin/MembresiaController.php
+app/Http/Controllers/Api/Admin/PagoController.php
+app/Http/Controllers/Api/Admin/LogController.php
+app/Http/Controllers/Api/Admin/MiniWebController.php
+```
+
+### 2.3 Registrar middleware en app/Http/Kernel.php
+```php
+protected $routeMiddleware = [
+ // ... existentes ...
+ 'admin.api' => \App\Http\Middleware\AdminApiMiddleware::class,
+];
+```
+
+### 2.4 Agregar rutas en routes/api.php
+```php
+use App\Http\Controllers\Api\Admin\UsuarioController as AdminUsuario;
+use App\Http\Controllers\Api\Admin\VcardController as AdminVcard;
+use App\Http\Controllers\Api\Admin\MembresiaController as AdminMembresia;
+use App\Http\Controllers\Api\Admin\PagoController as AdminPago;
+use App\Http\Controllers\Api\Admin\LogController as AdminLog;
+use App\Http\Controllers\Api\Admin\MiniWebController as AdminMiniWeb;
+
+Route::middleware(['auth:sanctum', 'admin.api'])->prefix('admin')->name('admin.')->group(function () {
+
+ // Usuarios
+ Route::get('usuarios', [AdminUsuario::class, 'index']);
+ Route::get('usuarios/{id}', [AdminUsuario::class, 'show']);
+ Route::put('usuarios/{id}', [AdminUsuario::class, 'update']);
+ Route::post('usuarios/{id}/activar', [AdminUsuario::class, 'activar']);
+ Route::post('usuarios/{id}/desactivar', [AdminUsuario::class, 'desactivar']);
+
+ // VCards
+ Route::get('vcards', [AdminVcard::class, 'index']);
+ Route::get('vcards/{id}', [AdminVcard::class, 'show']);
+ Route::put('vcards/{id}', [AdminVcard::class, 'update']);
+ Route::get('usuarios/{userId}/vcards', [AdminVcard::class, 'byUsuario']);
+
+ // Membresias
+ Route::get('planes', [AdminMembresia::class, 'planes']);
+ Route::get('usuarios/{id}/membresia', [AdminMembresia::class, 'show']);
+ Route::post('usuarios/{id}/activar-membresia', [AdminMembresia::class, 'activar']);
+ Route::post('usuarios/{id}/desactivar-membresia', [AdminMembresia::class, 'desactivar']);
+ Route::post('usuarios/{id}/cambiar-plan', [AdminMembresia::class, 'cambiarPlan']);
+
+ // Pagos
+ Route::get('pagos', [AdminPago::class, 'index']);
+ Route::get('transacciones', [AdminPago::class, 'transacciones']);
+ Route::get('usuarios/{userId}/pagos', [AdminPago::class, 'byUsuario']);
+ Route::get('usuarios/{userId}/transacciones', [AdminPago::class, 'transaccionesByUsuario']);
+
+ // Logs
+ Route::get('logs', [AdminLog::class, 'index']);
+ Route::get('usuarios/{userId}/logs', [AdminLog::class, 'byUsuario']);
+
+ // MiniWebs
+ Route::get('miniwebs', [AdminMiniWeb::class, 'index']);
+ Route::get('miniwebs/{id}', [AdminMiniWeb::class, 'show']);
+ Route::get('usuarios/{userId}/miniwebs', [AdminMiniWeb::class, 'byUsuario']);
+});
+```
+
+---
+
+## 3. Flujo de autenticacion
+
+### Login
+```http
+POST /api/login
+Content-Type: application/json
+
+{
+ "email": "admin@example.com",
+ "password": "secret"
+}
+```
+Respuesta:
+```json
+{
+ "access_token": "1|abc123...",
+ "token_type": "Bearer",
+ "user": { ... }
+}
+```
+
+### Usar token en cada peticion
+```
+Authorization: Bearer 1|abc123...
+```
+
+### Logout
+```http
+POST /api/logout
+Authorization: Bearer 1|abc123...
+```
+
+---
+
+## 4. Referencia de endpoints
+
+### Usuarios /api/admin/usuarios
+
+| Metodo | Ruta | Descripcion | Query params |
+|--------|------|-------------|--------------|
+| GET | /api/admin/usuarios | Lista paginada | search, estado, plan_id, per_page |
+| GET | /api/admin/usuarios/{id} | Detalle: plan, vcards, wallet, perfil | — |
+| PUT | /api/admin/usuarios/{id} | Edita name, email, rol_id, plan_id | — |
+| POST | /api/admin/usuarios/{id}/activar | Activa cuenta (estado=true) | — |
+| POST | /api/admin/usuarios/{id}/desactivar | Desactiva cuenta (estado=false) | — |
+
+Ejemplo PUT:
+```json
+{ "name": "Juan Garcia", "email": "juan@empresa.com", "plan_id": "664abc123" }
+```
+
+---
+
+### VCards /api/admin/vcards
+
+| Metodo | Ruta | Descripcion | Query params |
+|--------|------|-------------|--------------|
+| GET | /api/admin/vcards | Lista paginada | search, estado, user_id, per_page |
+| GET | /api/admin/vcards/{id} | Detalle con usuario | — |
+| PUT | /api/admin/vcards/{id} | Edita datos basicos | — |
+| GET | /api/admin/usuarios/{userId}/vcards | Vcards de un usuario | — |
+
+Campos editables: nombre, cargo, descripcion, telefono, whatsapp, email, empresa,
+direccion, website, pais, ciudad, estado, privacidad, es_borrador
+
+---
+
+### Membresias
+
+| Metodo | Ruta | Descripcion | Body |
+|--------|------|-------------|------|
+| GET | /api/admin/planes | Lista planes activos | — |
+| GET | /api/admin/usuarios/{id}/membresia | Estado de membresia | — |
+| POST | /api/admin/usuarios/{id}/activar-membresia | Activa/renueva | { "anual": true, "dias": 365 } |
+| POST | /api/admin/usuarios/{id}/cambiar-plan | Cambia plan | { "plan_id": "abc", "anual": false } |
+| POST | /api/admin/usuarios/{id}/desactivar-membresia | Desactiva | — |
+
+Logica activar-membresia:
+- Si ya tiene membresia vigente, los dias se SUMAN (no se pisa la fecha).
+- anual:true = +365 dias. anual:false = +30 dias (o los dias que se indiquen).
+
+---
+
+### Pagos
+
+| Metodo | Ruta | Descripcion | Query params |
+|--------|------|-------------|--------------|
+| GET | /api/admin/pagos | Historial global HealthPagos | user_id, estado, moneda, desde, hasta, per_page |
+| GET | /api/admin/transacciones | Transacciones wallet global | type, status, per_page |
+| GET | /api/admin/usuarios/{id}/pagos | Pagos de un usuario + total pagado | — |
+| GET | /api/admin/usuarios/{id}/transacciones | Wallet de un usuario | — |
+
+Valores estado (HealthPago): pendiente, completado, fallido
+Valores type (Transaction): deposit, withdraw, transfer_in, transfer_out, purchase, adjustment
+Valores status (Transaction): pending, completed, failed
+
+---
+
+### Logs
+
+| Metodo | Ruta | Descripcion | Query params |
+|--------|------|-------------|--------------|
+| GET | /api/admin/logs | Log global de movimientos | user_id, type, search, desde, hasta, per_page |
+| GET | /api/admin/usuarios/{id}/logs | Logs de un usuario | — |
+
+Valores type: create, edit, delete, delete cuenta
+
+---
+
+### MiniWebs
+
+| Metodo | Ruta | Descripcion | Query params |
+|--------|------|-------------|--------------|
+| GET | /api/admin/miniwebs | Lista paginada | search, user_id, per_page |
+| GET | /api/admin/miniwebs/{id} | Detalle con usuario + vcard vinculada | — |
+| GET | /api/admin/usuarios/{id}/miniwebs | Miniwebs de un usuario | — |
+
+---
+
+## 5. Respuestas de error
+
+| HTTP | Cuando |
+|------|--------|
+| 401 | Token ausente o invalido |
+| 403 | Autenticado pero sin rol administrador |
+| 404 | Recurso no encontrado |
+| 422 | Validacion fallida |
+
+Ejemplo 403:
+```json
+{
+ "error": "Acceso denegado. Se requieren privilegios de administrador.",
+ "code": 403
+}
+```
+
+---
+
+## 6. Modelos requeridos
+
+| Modelo | Coleccion MongoDB | Relaciones usadas |
+|--------|------------------|-------------------|
+| User | users | rol, plan, perfil, wallet, vcards, setup |
+| Vcard | vcard | user |
+| Planes | planes | tarifas |
+| HealthPago | health_pagos | — |
+| Transaction | transactions | wallet |
+| Wallet | wallets | user |
+| Log | log | user |
+| MiniWeb | miniwebs | user, vcard |
+| Rol | rol | — |
+
+---
+
+## 7. Paginacion
+
+Todos los listados retornan estructura estandar de Laravel:
+```json
+{
+ "current_page": 1,
+ "data": [ ... ],
+ "per_page": 20,
+ "total": 150,
+ "last_page": 8,
+ "next_page_url": "https://...",
+ "prev_page_url": null
+}
+```
+Maximo per_page: 100
+
+---
+
+## 8. Prueba rapida con curl
+
+```bash
+# 1. Login y guardar token
+TOKEN=$(curl -s -X POST https://tudominio.com/api/login \
+ -H "Content-Type: application/json" \
+ -d '{"email":"admin@example.com","password":"secret"}' \
+ | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
+
+# 2. Listar usuarios
+curl https://tudominio.com/api/admin/usuarios \
+ -H "Authorization: Bearer $TOKEN"
+
+# 3. Vcards de un usuario
+curl https://tudominio.com/api/admin/usuarios/USER_ID/vcards \
+ -H "Authorization: Bearer $TOKEN"
+
+# 4. Activar membresia anual
+curl -X POST https://tudominio.com/api/admin/usuarios/USER_ID/activar-membresia \
+ -H "Authorization: Bearer $TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{"anual": true}'
+
+# 5. Cambiar plan
+curl -X POST https://tudominio.com/api/admin/usuarios/USER_ID/cambiar-plan \
+ -H "Authorization: Bearer $TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{"plan_id": "PLAN_ID"}'
+
+# 6. Historial de pagos con filtros
+curl "https://tudominio.com/api/admin/pagos?estado=completado&desde=2026-01-01&per_page=50" \
+ -H "Authorization: Bearer $TOKEN"
+
+# 7. Logs de un usuario
+curl https://tudominio.com/api/admin/usuarios/USER_ID/logs \
+ -H "Authorization: Bearer $TOKEN"
+
+# 8. MiniWebs con busqueda
+curl "https://tudominio.com/api/admin/miniwebs?search=empresa" \
+ -H "Authorization: Bearer $TOKEN"
+```
diff --git a/build_output.txt b/build_output.txt
new file mode 100644
index 0000000..e69de29
diff --git a/main.go b/main.go
index b2fda94..e2034de 100755
--- a/main.go
+++ b/main.go
@@ -73,6 +73,8 @@ func main() {
&models.PartnerComunicado{},
// Portal: recuperación de contraseña
&models.PortalPasswordResetToken{},
+ // Integración VCard API Admin
+ &models.VcardApiConfig{},
)
// Seed automático (idempotente) de módulos del sistema
migrations.SeedRenovaciones()
diff --git a/migrations/migrate.go b/migrations/migrate.go
index 00d85c9..ea7e121 100755
--- a/migrations/migrate.go
+++ b/migrations/migrate.go
@@ -90,6 +90,8 @@ func Migrate() {
// Submódulo Partner
&models.PartnerRecurso{},
&models.PartnerComunicado{},
+ // Integración VCard API Admin (Laravel)
+ &models.VcardApiConfig{},
); err != nil {
log.Fatalf("Error during main migration: %v", err)
}
@@ -422,6 +424,7 @@ func SeedIntegraciones() {
entries := []struct{ title, desc, url string }{
{"Hostinger", "Panel de VPS, dominios, hosting y DNS de Hostinger", "/app/hostinger"},
{"Cloudflare", "Gestión de zonas, DNS, SSL y firewall en Cloudflare", "/app/cloudflare"},
+ {"VCard API", "Integración con el sistema VCard externo (Laravel + Sanctum): usuarios, membresías, vcards, pagos y más", "/app/vcard-api"},
}
var insertados []models.Submodules
diff --git a/pkg/models/users.go b/pkg/models/users.go
index 22cb7ca..5e10eb0 100755
--- a/pkg/models/users.go
+++ b/pkg/models/users.go
@@ -76,8 +76,8 @@ func AllUsersSistema(limit, offset int, search string) ([]Users, int64, error) {
db = db.Where("name LIKE ? OR nombre_usuario LIKE ?", "%"+search+"%", "%"+search+"%")
}
- // Filtrar por tipo_usuario = 'sistema'
- db = db.Where("tipo_usuario = ?", "sistema")
+ // Filtrar por tipo_usuario = 'sistema' o vacío (excluye usuarios 'gas' y otros tipos)
+ db = db.Where("tipo_usuario = ? OR tipo_usuario = '' OR tipo_usuario IS NULL", "sistema")
// Obtener el total de usuarios
if err := db.Count(&total).Error; err != nil {
diff --git a/resources/views/vcard_api.html b/resources/views/vcard_api.html
new file mode 100644
index 0000000..1172dab
--- /dev/null
+++ b/resources/views/vcard_api.html
@@ -0,0 +1,639 @@
+
+
+
+
+
+

+
+
+
+
+
+
+
+
VCard API
+
Integración con el sistema VCard externo (Laravel + Sanctum). Gestiona usuarios, vcards, membresías y más.
+
+
+
+
+
+
+ Sin configuración activa. Haz clic en "Configurar API" para ingresar la URL base y el Bearer Token del sistema VCard.
+
+
+
+
+
+
+
+ Inactiva
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Sin datos. Haz clic en "Cargar".
+
+
+
+
+ | Nombre |
+ Email |
+ Plan |
+ Estado |
+ Acciones |
+
+
+
+
+
+ |
+ |
+ |
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
Sin datos. Haz clic en "Cargar".
+
+
+
+
+ | Nombre |
+ Empresa |
+ Email |
+ Estado |
+ Privacidad |
+
+
+
+
+
+ |
+ |
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
Sin datos. Haz clic en "Cargar".
+
+
+
+
+
+
Sin datos. Haz clic en "Cargar".
+
+
+
+
+ | Referencia |
+ Usuario |
+ Monto |
+ Estado |
+ Fecha |
+
+
+
+
+
+ |
+ |
+ |
+
+
+ |
+ |
+
+
+
+
+
+
+
+
+
+
Sin datos. Haz clic en "Cargar".
+
+
+
+
+ | Tipo |
+ Monto |
+ Estado |
+ Descripción |
+ Fecha |
+
+
+
+
+
+ |
+
+ |
+ |
+
+
+ |
+ |
+ |
+
+
+
+
+
+
+
+
+
+
Sin datos. Haz clic en "Cargar".
+
+
+
+
+ | Tipo |
+ Usuario |
+ Descripción |
+ Fecha |
+
+
+
+
+
+ |
+
+ |
+ |
+ |
+ |
+
+
+
+
+
+
+
+
+
+
Sin datos. Haz clic en "Cargar".
+
+
+
+
+ | Título |
+ Usuario |
+ VCard vinculada |
+ Estado |
+
+
+
+
+
+ |
+ |
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Configurar VCard API
+
+
+
+
+
+
+
+
+
+
+
Sin barra final. Ej: https://midominio.com
+
+
+
+
+
Token Sanctum de un usuario con rol administrador.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Gestionar Membresía
+
+
+
+
+
+
+
+
Plan:
+
Vence:
+
Estado:
+
+
+
+
+
+
Activar / Renovar
+
+
+
+
+
+
+
+
+
+
+
+
Cambiar Plan
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/rest/controllers/users_controller.go b/rest/controllers/users_controller.go
index 5957740..deec73c 100755
--- a/rest/controllers/users_controller.go
+++ b/rest/controllers/users_controller.go
@@ -374,6 +374,8 @@ func CreateUser(c *fiber.Ctx) error {
})
}
+ m.TipoUsuario = "sistema"
+
// Crea el nuevo módulo en la base de datos
if err := models.CreateUser(m); err != nil { // Asegúrate de tener una función para crear un módulo
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
@@ -441,7 +443,6 @@ func CreateUserGas(c *fiber.Ctx) error {
fmt.Println("Llamando a GetValidarMedidor con NombreUsuario:", m.NombreUsuario, "y NumeroMedidor:", m.NumeroMedidor)
// Aquí llamamos a la función GetValidarMedidor pasándole el código y medidor directamente desde el cuerpo de la solicitud
-
// Obtén el RoleID basado en el RoleName
roleName := "Usuarios"
role, err := models.FindRoleByName(roleName)
diff --git a/rest/controllers/vcard_api_controller.go b/rest/controllers/vcard_api_controller.go
index f6c7ba0..59d5dc7 100644
--- a/rest/controllers/vcard_api_controller.go
+++ b/rest/controllers/vcard_api_controller.go
@@ -1,182 +1,248 @@
package controllers
import (
-"encoding/json"
-"fmt"
-"io"
-"net/http"
-"strings"
-"time"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+ "time"
-"github.com/gofiber/fiber/v2"
-"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
+ "github.com/gofiber/fiber/v2"
+ "github.com/sujit-baniya/fiber-boilerplate/pkg/models"
)
// VcardApiIndex renderiza el panel de integración VCard.
func VcardApiIndex(c *fiber.Ctx) error {
-cfg, _ := models.GetVcardApiConfig()
-data := fiber.Map{
-"user": c.Locals("user").(map[string]interface{}),
-"modules": c.Locals("userModules"),
-"config": cfg,
-}
-return c.Render("vcard_api", data, "layouts/main")
+ cfg, _ := models.GetVcardApiConfig()
+ data := fiber.Map{
+ "user": c.Locals("user").(map[string]interface{}),
+ "modules": c.Locals("userModules"),
+ "config": cfg,
+ }
+ return c.Render("vcard_api", data, "layouts/main")
}
// VcardApiSaveConfig guarda / actualiza la configuración (base URL + token).
func VcardApiSaveConfig(c *fiber.Ctx) error {
-type Req struct {
-Nombre string `json:"nombre"`
-BaseURL string `json:"base_url"`
-BearerToken string `json:"bearer_token"`
-Activo bool `json:"activo"`
-}
-var req Req
-if err := c.BodyParser(&req); err != nil {
-return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
-}
-if strings.TrimSpace(req.BaseURL) == "" {
-return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "base_url es requerido"})
-}
-cfg, err := models.UpsertVcardApiConfig(req.Nombre, strings.TrimRight(strings.TrimSpace(req.BaseURL), "/"), req.BearerToken, req.Activo)
-if err != nil {
-return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
-}
-return c.JSON(fiber.Map{"ok": true, "config": cfg})
+ type Req struct {
+ Nombre string `json:"nombre"`
+ BaseURL string `json:"base_url"`
+ BearerToken string `json:"bearer_token"`
+ Activo bool `json:"activo"`
+ }
+ var req Req
+ if err := c.BodyParser(&req); err != nil {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
+ }
+ if strings.TrimSpace(req.BaseURL) == "" {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "base_url es requerido"})
+ }
+ cfg, err := models.UpsertVcardApiConfig(req.Nombre, strings.TrimRight(strings.TrimSpace(req.BaseURL), "/"), req.BearerToken, req.Activo)
+ if err != nil {
+ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
+ }
+ return c.JSON(fiber.Map{"ok": true, "config": cfg})
}
// VcardApiGetConfig devuelve la configuración actual (sin exponer el token completo).
func VcardApiGetConfig(c *fiber.Ctx) error {
-cfg, err := models.GetVcardApiConfig()
-if err != nil {
-return c.JSON(fiber.Map{"config": nil})
-}
-masked := "••••••••"
-if cfg.BearerToken == "" {
-masked = ""
-}
-return c.JSON(fiber.Map{
-"config": fiber.Map{
-"ID": cfg.ID,
-"nombre": cfg.Nombre,
-"base_url": cfg.BaseURL,
-"bearer_token": masked,
-"activo": cfg.Activo,
-},
-})
+ cfg, err := models.GetVcardApiConfig()
+ if err != nil {
+ return c.JSON(fiber.Map{"config": nil})
+ }
+ masked := "••••••••"
+ if cfg.BearerToken == "" {
+ masked = ""
+ }
+ return c.JSON(fiber.Map{
+ "config": fiber.Map{
+ "ID": cfg.ID,
+ "nombre": cfg.Nombre,
+ "base_url": cfg.BaseURL,
+ "bearer_token": masked,
+ "activo": cfg.Activo,
+ },
+ })
}
// vcardDo ejecuta una llamada a la API Admin VCard y devuelve el body como json.RawMessage.
func vcardDo(method, endpoint string, cfg *models.VcardApiConfig) ([]byte, int, error) {
-url := fmt.Sprintf("%s%s", cfg.BaseURL, endpoint)
-client := &http.Client{Timeout: 15 * time.Second}
-req, err := http.NewRequest(method, url, nil)
-if err != nil {
-return nil, 0, err
+ return vcardDoWithBody(method, endpoint, nil, "", cfg)
}
-req.Header.Set("Authorization", "Bearer "+cfg.BearerToken)
-req.Header.Set("Accept", "application/json")
-resp, err := client.Do(req)
-if err != nil {
-return nil, 0, err
-}
-defer resp.Body.Close()
-body, _ := io.ReadAll(io.LimitReader(resp.Body, 512*1024))
-return body, resp.StatusCode, nil
+// vcardDoWithBody ejecuta una llamada a la API Admin VCard con body opcional.
+func vcardDoWithBody(method, endpoint string, reqBody io.Reader, contentType string, cfg *models.VcardApiConfig) ([]byte, int, error) {
+ url := fmt.Sprintf("%s%s", cfg.BaseURL, endpoint)
+ client := &http.Client{Timeout: 15 * time.Second}
+ req, err := http.NewRequest(method, url, reqBody)
+ if err != nil {
+ return nil, 0, err
+ }
+ req.Header.Set("Authorization", "Bearer "+cfg.BearerToken)
+ req.Header.Set("Accept", "application/json")
+ if contentType != "" {
+ req.Header.Set("Content-Type", contentType)
+ }
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, 0, err
+ }
+ defer resp.Body.Close()
+ body, _ := io.ReadAll(io.LimitReader(resp.Body, 512*1024))
+ return body, resp.StatusCode, nil
}
// proxyVcard extrae la config, llama al endpoint y devuelve el resultado al frontend.
func proxyVcard(c *fiber.Ctx, method, endpoint string) error {
-cfg, err := models.GetVcardApiConfig()
-if err != nil {
-return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Configura primero la API VCard"})
-}
-if !cfg.Activo {
-return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "La integración está inactiva"})
+ cfg, err := models.GetVcardApiConfig()
+ if err != nil {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Configura primero la API VCard"})
+ }
+ if !cfg.Activo {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "La integración está inactiva"})
+ }
+
+ qs := string(c.Request().URI().QueryString())
+ ep := endpoint
+ if qs != "" {
+ ep = endpoint + "?" + qs
+ }
+
+ body, status, err := vcardDo(method, ep, cfg)
+ if err != nil {
+ return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"error": err.Error()})
+ }
+ var result json.RawMessage
+ if err := json.Unmarshal(body, &result); err != nil {
+ result = json.RawMessage(fmt.Sprintf(`{"raw": %q}`, string(body)))
+ }
+ c.Status(status)
+ return c.JSON(result)
}
-// Pasar query params del request original
-qs := string(c.Request().URI().QueryString())
-ep := endpoint
-if qs != "" {
-ep = endpoint + "?" + qs
-}
+// proxyVcardMutate reenvía el body del request original a la API VCard.
+func proxyVcardMutate(c *fiber.Ctx, method, endpoint string) error {
+ cfg, err := models.GetVcardApiConfig()
+ if err != nil {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Configura primero la API VCard"})
+ }
+ if !cfg.Activo {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "La integración está inactiva"})
+ }
-body, status, err := vcardDo(method, ep, cfg)
-if err != nil {
-return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"error": err.Error()})
-}
-var result json.RawMessage
-if err := json.Unmarshal(body, &result); err != nil {
-result = json.RawMessage(fmt.Sprintf(`{"raw": %q}`, string(body)))
-}
-c.Status(status)
-return c.JSON(result)
+ var bodyReader io.Reader
+ ct := ""
+ if raw := c.Body(); len(raw) > 0 {
+ bodyReader = strings.NewReader(string(raw))
+ ct = c.Get("Content-Type", "application/json")
+ }
+
+ body, status, err := vcardDoWithBody(method, endpoint, bodyReader, ct, cfg)
+ if err != nil {
+ return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"error": err.Error()})
+ }
+ var result json.RawMessage
+ if err := json.Unmarshal(body, &result); err != nil {
+ result = json.RawMessage(fmt.Sprintf(`{"raw": %q}`, string(body)))
+ }
+ c.Status(status)
+ return c.JSON(result)
}
// ─── Proxy endpoints ──────────────────────────────────────────────────────────
func VcardApiUsuarios(c *fiber.Ctx) error {
-return proxyVcard(c, http.MethodGet, "/api/admin/usuarios")
+ return proxyVcard(c, http.MethodGet, "/api/admin/usuarios")
}
func VcardApiUsuario(c *fiber.Ctx) error {
-return proxyVcard(c, http.MethodGet, "/api/admin/usuarios/"+c.Params("id"))
+ return proxyVcard(c, http.MethodGet, "/api/admin/usuarios/"+c.Params("id"))
}
func VcardApiVcards(c *fiber.Ctx) error {
-return proxyVcard(c, http.MethodGet, "/api/admin/vcards")
+ return proxyVcard(c, http.MethodGet, "/api/admin/vcards")
}
func VcardApiVcard(c *fiber.Ctx) error {
-return proxyVcard(c, http.MethodGet, "/api/admin/vcards/"+c.Params("id"))
+ return proxyVcard(c, http.MethodGet, "/api/admin/vcards/"+c.Params("id"))
}
func VcardApiVcardsByUsuario(c *fiber.Ctx) error {
-return proxyVcard(c, http.MethodGet, "/api/admin/usuarios/"+c.Params("userId")+"/vcards")
+ return proxyVcard(c, http.MethodGet, "/api/admin/usuarios/"+c.Params("userId")+"/vcards")
}
func VcardApiPlanes(c *fiber.Ctx) error {
-return proxyVcard(c, http.MethodGet, "/api/admin/planes")
+ return proxyVcard(c, http.MethodGet, "/api/admin/planes")
}
func VcardApiMembresia(c *fiber.Ctx) error {
-return proxyVcard(c, http.MethodGet, "/api/admin/usuarios/"+c.Params("id")+"/membresia")
+ return proxyVcard(c, http.MethodGet, "/api/admin/usuarios/"+c.Params("id")+"/membresia")
}
func VcardApiPagos(c *fiber.Ctx) error {
-return proxyVcard(c, http.MethodGet, "/api/admin/pagos")
+ return proxyVcard(c, http.MethodGet, "/api/admin/pagos")
}
func VcardApiPagosByUsuario(c *fiber.Ctx) error {
-return proxyVcard(c, http.MethodGet, "/api/admin/usuarios/"+c.Params("userId")+"/pagos")
+ return proxyVcard(c, http.MethodGet, "/api/admin/usuarios/"+c.Params("userId")+"/pagos")
}
func VcardApiTransacciones(c *fiber.Ctx) error {
-return proxyVcard(c, http.MethodGet, "/api/admin/transacciones")
+ return proxyVcard(c, http.MethodGet, "/api/admin/transacciones")
}
func VcardApiTransaccionesByUsuario(c *fiber.Ctx) error {
-return proxyVcard(c, http.MethodGet, "/api/admin/usuarios/"+c.Params("userId")+"/transacciones")
+ return proxyVcard(c, http.MethodGet, "/api/admin/usuarios/"+c.Params("userId")+"/transacciones")
}
func VcardApiLogs(c *fiber.Ctx) error {
-return proxyVcard(c, http.MethodGet, "/api/admin/logs")
+ return proxyVcard(c, http.MethodGet, "/api/admin/logs")
}
func VcardApiLogsByUsuario(c *fiber.Ctx) error {
-return proxyVcard(c, http.MethodGet, "/api/admin/usuarios/"+c.Params("userId")+"/logs")
+ return proxyVcard(c, http.MethodGet, "/api/admin/usuarios/"+c.Params("userId")+"/logs")
}
func VcardApiMiniwebs(c *fiber.Ctx) error {
-return proxyVcard(c, http.MethodGet, "/api/admin/miniwebs")
+ return proxyVcard(c, http.MethodGet, "/api/admin/miniwebs")
}
func VcardApiMiniweb(c *fiber.Ctx) error {
-return proxyVcard(c, http.MethodGet, "/api/admin/miniwebs/"+c.Params("id"))
+ return proxyVcard(c, http.MethodGet, "/api/admin/miniwebs/"+c.Params("id"))
}
func VcardApiMiniwebsByUsuario(c *fiber.Ctx) error {
-return proxyVcard(c, http.MethodGet, "/api/admin/usuarios/"+c.Params("userId")+"/miniwebs")
+ return proxyVcard(c, http.MethodGet, "/api/admin/usuarios/"+c.Params("userId")+"/miniwebs")
+}
+
+// ─── Mutación endpoints ───────────────────────────────────────────────────────
+
+func VcardApiUsuarioUpdate(c *fiber.Ctx) error {
+ return proxyVcardMutate(c, http.MethodPut, "/api/admin/usuarios/"+c.Params("id"))
+}
+
+func VcardApiUsuarioActivar(c *fiber.Ctx) error {
+ return proxyVcardMutate(c, http.MethodPost, "/api/admin/usuarios/"+c.Params("id")+"/activar")
+}
+
+func VcardApiUsuarioDesactivar(c *fiber.Ctx) error {
+ return proxyVcardMutate(c, http.MethodPost, "/api/admin/usuarios/"+c.Params("id")+"/desactivar")
+}
+
+func VcardApiVcardUpdate(c *fiber.Ctx) error {
+ return proxyVcardMutate(c, http.MethodPut, "/api/admin/vcards/"+c.Params("id"))
+}
+
+func VcardApiActivarMembresia(c *fiber.Ctx) error {
+ return proxyVcardMutate(c, http.MethodPost, "/api/admin/usuarios/"+c.Params("id")+"/activar-membresia")
+}
+
+func VcardApiDesactivarMembresia(c *fiber.Ctx) error {
+ return proxyVcardMutate(c, http.MethodPost, "/api/admin/usuarios/"+c.Params("id")+"/desactivar-membresia")
+}
+
+func VcardApiCambiarPlan(c *fiber.Ctx) error {
+ return proxyVcardMutate(c, http.MethodPost, "/api/admin/usuarios/"+c.Params("id")+"/cambiar-plan")
}
diff --git a/rest/routes/user.go b/rest/routes/user.go
index 77f8732..5aab020 100755
--- a/rest/routes/user.go
+++ b/rest/routes/user.go
@@ -21,7 +21,7 @@ func UserRoutes(app fiber.Router) {
// Rutas de la aplicación
- // web me redireccione a /
+ // web me redireccione a /
protected.Get("/web", func(c *fiber.Ctx) error { return c.Redirect("/", http.StatusSeeOther) })
app.Get("/web", func(c *fiber.Ctx) error { return c.Redirect("/", http.StatusSeeOther) })
protected.Get("/", controllers.App)
@@ -216,27 +216,38 @@ func UserRoutes(app fiber.Router) {
protected.Get("/loadsaasdispatchlogs", controllers.GetSaasDispatchLogs)
// ─── VCard API (integración Admin Laravel) ────────────────────────────────
-protected.Get("/vcard-api", middlewares.MenuMiddleware, controllers.VcardApiIndex)
-protected.Get("/vcard-api/config", controllers.VcardApiGetConfig)
-protected.Post("/vcard-api/config", controllers.VcardApiSaveConfig)
-protected.Get("/vcard-api/usuarios", controllers.VcardApiUsuarios)
-protected.Get("/vcard-api/usuarios/:id/membresia", controllers.VcardApiMembresia)
-protected.Get("/vcard-api/usuarios/:userId/vcards", controllers.VcardApiVcardsByUsuario)
-protected.Get("/vcard-api/usuarios/:userId/pagos", controllers.VcardApiPagosByUsuario)
-protected.Get("/vcard-api/usuarios/:userId/transacciones", controllers.VcardApiTransaccionesByUsuario)
-protected.Get("/vcard-api/usuarios/:userId/logs", controllers.VcardApiLogsByUsuario)
-protected.Get("/vcard-api/usuarios/:userId/miniwebs", controllers.VcardApiMiniwebsByUsuario)
-protected.Get("/vcard-api/usuarios/:id", controllers.VcardApiUsuario)
-protected.Get("/vcard-api/vcards", controllers.VcardApiVcards)
-protected.Get("/vcard-api/vcards/:id", controllers.VcardApiVcard)
-protected.Get("/vcard-api/planes", controllers.VcardApiPlanes)
-protected.Get("/vcard-api/pagos", controllers.VcardApiPagos)
-protected.Get("/vcard-api/transacciones", controllers.VcardApiTransacciones)
-protected.Get("/vcard-api/logs", controllers.VcardApiLogs)
-protected.Get("/vcard-api/miniwebs", controllers.VcardApiMiniwebs)
-protected.Get("/vcard-api/miniwebs/:id", controllers.VcardApiMiniweb)
+ protected.Get("/vcard-api", middlewares.MenuMiddleware, controllers.VcardApiIndex)
+ protected.Get("/vcard-api/config", controllers.VcardApiGetConfig)
+ protected.Post("/vcard-api/config", controllers.VcardApiSaveConfig)
+ // Usuarios (GET)
+ protected.Get("/vcard-api/usuarios", controllers.VcardApiUsuarios)
+ protected.Get("/vcard-api/usuarios/:id/membresia", controllers.VcardApiMembresia)
+ protected.Get("/vcard-api/usuarios/:userId/vcards", controllers.VcardApiVcardsByUsuario)
+ protected.Get("/vcard-api/usuarios/:userId/pagos", controllers.VcardApiPagosByUsuario)
+ protected.Get("/vcard-api/usuarios/:userId/transacciones", controllers.VcardApiTransaccionesByUsuario)
+ protected.Get("/vcard-api/usuarios/:userId/logs", controllers.VcardApiLogsByUsuario)
+ protected.Get("/vcard-api/usuarios/:userId/miniwebs", controllers.VcardApiMiniwebsByUsuario)
+ protected.Get("/vcard-api/usuarios/:id", controllers.VcardApiUsuario)
+ // Usuarios (mutaciones)
+ protected.Put("/vcard-api/usuarios/:id", controllers.VcardApiUsuarioUpdate)
+ protected.Post("/vcard-api/usuarios/:id/activar", controllers.VcardApiUsuarioActivar)
+ protected.Post("/vcard-api/usuarios/:id/desactivar", controllers.VcardApiUsuarioDesactivar)
+ protected.Post("/vcard-api/usuarios/:id/activar-membresia", controllers.VcardApiActivarMembresia)
+ protected.Post("/vcard-api/usuarios/:id/desactivar-membresia", controllers.VcardApiDesactivarMembresia)
+ protected.Post("/vcard-api/usuarios/:id/cambiar-plan", controllers.VcardApiCambiarPlan)
+ // VCards
+ protected.Get("/vcard-api/vcards", controllers.VcardApiVcards)
+ protected.Get("/vcard-api/vcards/:id", controllers.VcardApiVcard)
+ protected.Put("/vcard-api/vcards/:id", controllers.VcardApiVcardUpdate)
+ // Resto (solo GET)
+ protected.Get("/vcard-api/planes", controllers.VcardApiPlanes)
+ protected.Get("/vcard-api/pagos", controllers.VcardApiPagos)
+ protected.Get("/vcard-api/transacciones", controllers.VcardApiTransacciones)
+ protected.Get("/vcard-api/logs", controllers.VcardApiLogs)
+ protected.Get("/vcard-api/miniwebs", controllers.VcardApiMiniwebs)
+ protected.Get("/vcard-api/miniwebs/:id", controllers.VcardApiMiniweb)
-// ─── Telegram ─────────────────────────────────────────────────────────────
+ // ─── Telegram ─────────────────────────────────────────────────────────────
protected.Get("/telegram", middlewares.MenuMiddleware, controllers.TelegramIndex)
protected.Get("/loadtelegram", controllers.GetTelegramConfigs)
protected.Post("/telegram", controllers.CreateTelegramConfig)
diff --git a/seed_integraciones.sql b/seed_integraciones.sql
index e3ce624..0df9789 100644
--- a/seed_integraciones.sql
+++ b/seed_integraciones.sql
@@ -1,5 +1,5 @@
-- ============================================================
--- Seed: módulo Integraciones (Hostinger + Cloudflare)
+-- Seed: módulo Integraciones (Hostinger + Cloudflare + VCard API)
-- Ejecutar en la base de datos PostgreSQL de producción:
-- psql -h HOST -U usite -d usite -f seed_integraciones.sql
-- ============================================================
@@ -35,12 +35,23 @@ WHERE NOT EXISTS (
SELECT 1 FROM submodules WHERE url = '/app/cloudflare' AND deleted_at IS NULL
);
--- 4. Asignar ambos submódulos a TODOS los roles (ignorar duplicados)
+-- 4. Insertar submódulo VCard API si no existe
+INSERT INTO submodules (title, description, url, module_id, modified_at, created_at, updated_at)
+SELECT 'VCard API',
+ 'Integración con el sistema VCard externo (Laravel + Sanctum): usuarios, membresías, vcards, pagos y más',
+ '/app/vcard-api',
+ (SELECT id FROM modules WHERE title = 'Integraciones' AND deleted_at IS NULL LIMIT 1),
+ NOW(), NOW(), NOW()
+WHERE NOT EXISTS (
+ SELECT 1 FROM submodules WHERE url = '/app/vcard-api' AND deleted_at IS NULL
+);
+
+-- 5. Asignar todos los submódulos de Integraciones a TODOS los roles (ignorar duplicados)
INSERT INTO roles_submodules (role_id, submodule_id)
SELECT r.id, s.id
FROM roles r
CROSS JOIN submodules s
-WHERE s.url IN ('/app/hostinger', '/app/cloudflare')
+WHERE s.url IN ('/app/hostinger', '/app/cloudflare', '/app/vcard-api')
AND r.deleted_at IS NULL
AND s.deleted_at IS NULL
ON CONFLICT DO NOTHING;