From 24e1cf143901c4565ffb91d645dd741cc2d5283a Mon Sep 17 00:00:00 2001
From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com>
Date: Tue, 19 May 2026 20:08:50 -0500
Subject: [PATCH 1/4] feat: add VCard API integration with configuration and
proxy endpoints
- Implemented VCard API controller with methods to manage configuration (save, get).
- Added proxy functions to handle API requests for users, vcards, memberships, payments, transactions, logs, and miniwebs.
- Included error handling and response formatting for API interactions.
---
API_ADMIN_GUIA.md | 315 +++++++++++
build_output.txt | 0
migrations/migrate.go | 2 +
pkg/models/vcard_api_config.go | 53 ++
resources/views/vcard_api.html | 639 +++++++++++++++++++++++
rest/controllers/vcard_api_controller.go | 248 +++++++++
rest/routes/user.go | 34 +-
seed_integraciones.sql | 15 +-
8 files changed, 1303 insertions(+), 3 deletions(-)
create mode 100644 API_ADMIN_GUIA.md
create mode 100644 build_output.txt
create mode 100644 pkg/models/vcard_api_config.go
create mode 100644 resources/views/vcard_api.html
create mode 100644 rest/controllers/vcard_api_controller.go
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/migrations/migrate.go b/migrations/migrate.go
index 00d85c9..890f29b 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)
}
diff --git a/pkg/models/vcard_api_config.go b/pkg/models/vcard_api_config.go
new file mode 100644
index 0000000..ff90c49
--- /dev/null
+++ b/pkg/models/vcard_api_config.go
@@ -0,0 +1,53 @@
+package models
+
+import (
+"github.com/sujit-baniya/fiber-boilerplate/app"
+"gorm.io/gorm"
+)
+
+// VcardApiConfig almacena las credenciales para conectar con la API Admin del
+// sistema VCard externo (Laravel + Sanctum, prefijo /api/admin/*).
+type VcardApiConfig struct {
+gorm.Model
+Nombre string `json:"nombre" gorm:"column:nombre;not null"`
+BaseURL string `json:"base_url" gorm:"column:base_url;type:text;not null"`
+BearerToken string `json:"bearer_token" gorm:"column:bearer_token;type:text;not null"`
+Activo bool `json:"activo" gorm:"column:activo;default:true"`
+}
+
+func (VcardApiConfig) TableName() string { return "vcard_api_configs" }
+
+func GetVcardApiConfig() (*VcardApiConfig, error) {
+var cfg VcardApiConfig
+if err := app.Http.Database.DB.First(&cfg).Error; err != nil {
+return nil, err
+}
+return &cfg, nil
+}
+
+func UpsertVcardApiConfig(nombre, baseURL, bearerToken string, activo bool) (*VcardApiConfig, error) {
+var cfg VcardApiConfig
+err := app.Http.Database.DB.First(&cfg).Error
+if err != nil {
+cfg = VcardApiConfig{
+Nombre: nombre,
+BaseURL: baseURL,
+BearerToken: bearerToken,
+Activo: activo,
+}
+if createErr := app.Http.Database.DB.Create(&cfg).Error; createErr != nil {
+return nil, createErr
+}
+return &cfg, nil
+}
+cfg.Nombre = nombre
+cfg.BaseURL = baseURL
+if bearerToken != "" {
+cfg.BearerToken = bearerToken
+}
+cfg.Activo = activo
+if err := app.Http.Database.DB.Save(&cfg).Error; err != nil {
+return nil, err
+}
+return &cfg, 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/vcard_api_controller.go b/rest/controllers/vcard_api_controller.go
new file mode 100644
index 0000000..59d5dc7
--- /dev/null
+++ b/rest/controllers/vcard_api_controller.go
@@ -0,0 +1,248 @@
+package controllers
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+ "time"
+
+ "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")
+}
+
+// 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})
+}
+
+// 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,
+ },
+ })
+}
+
+// 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) {
+ return vcardDoWithBody(method, endpoint, nil, "", cfg)
+}
+
+// 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"})
+ }
+
+ 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)
+}
+
+// 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"})
+ }
+
+ 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")
+}
+
+func VcardApiUsuario(c *fiber.Ctx) error {
+ 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")
+}
+
+func VcardApiVcard(c *fiber.Ctx) error {
+ 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")
+}
+
+func VcardApiPlanes(c *fiber.Ctx) error {
+ 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")
+}
+
+func VcardApiPagos(c *fiber.Ctx) error {
+ 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")
+}
+
+func VcardApiTransacciones(c *fiber.Ctx) error {
+ 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")
+}
+
+func VcardApiLogs(c *fiber.Ctx) error {
+ 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")
+}
+
+func VcardApiMiniwebs(c *fiber.Ctx) error {
+ 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"))
+}
+
+func VcardApiMiniwebsByUsuario(c *fiber.Ctx) error {
+ 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 2243334..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)
@@ -215,6 +215,38 @@ func UserRoutes(app fiber.Router) {
protected.Get("/saas-api/logs", middlewares.MenuMiddleware, controllers.SaasDispatchLogIndex)
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)
+ // 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 ─────────────────────────────────────────────────────────────
protected.Get("/telegram", middlewares.MenuMiddleware, controllers.TelegramIndex)
protected.Get("/loadtelegram", controllers.GetTelegramConfigs)
diff --git a/seed_integraciones.sql b/seed_integraciones.sql
index e3ce624..52e41d4 100644
--- a/seed_integraciones.sql
+++ b/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;
From f577054e66827af841d740d4e322358232523789 Mon Sep 17 00:00:00 2001
From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com>
Date: Tue, 19 May 2026 20:12:21 -0500
Subject: [PATCH 2/4] feat: actualizar comentario de seed para incluir VCard
API
---
seed_integraciones.sql | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/seed_integraciones.sql b/seed_integraciones.sql
index 52e41d4..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
-- ============================================================
From af1b8f4bdba148c495e0887c8734a58d9ac2ee85 Mon Sep 17 00:00:00 2001
From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com>
Date: Tue, 19 May 2026 20:16:34 -0500
Subject: [PATCH 3/4] =?UTF-8?q?feat:=20agregar=20configuraci=C3=B3n=20de?=
=?UTF-8?q?=20integraci=C3=B3n=20para=20VCard=20API=20y=20actualizar=20see?=
=?UTF-8?q?d=20de=20m=C3=B3dulos?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
main.go | 2 ++
migrations/migrate.go | 1 +
2 files changed, 3 insertions(+)
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 890f29b..ea7e121 100755
--- a/migrations/migrate.go
+++ b/migrations/migrate.go
@@ -424,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
From 31259cbb719f52060e85cea785539e660990f57f Mon Sep 17 00:00:00 2001
From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com>
Date: Tue, 19 May 2026 20:26:25 -0500
Subject: [PATCH 4/4] =?UTF-8?q?feat:=20actualizar=20l=C3=B3gica=20de=20fil?=
=?UTF-8?q?trado=20de=20usuarios=20y=20establecer=20tipo=20de=20usuario=20?=
=?UTF-8?q?por=20defecto=20en=20creaci=C3=B3n?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
pkg/models/users.go | 4 ++--
rest/controllers/users_controller.go | 3 ++-
2 files changed, 4 insertions(+), 3 deletions(-)
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/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)