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 @@ + +
+ + +
+ Cargando... +
+ +
+ + +
+
+

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".
+
+ + + + + + + + + + + + + +
NombreEmailPlanEstadoAcciones
+
+
+ + +
+
Sin datos. Haz clic en "Cargar".
+
+ + + + + + + + + + + + + +
NombreEmpresaEmailEstadoPrivacidad
+
+
+ + +
+
Sin datos. Haz clic en "Cargar".
+
+ +
+
+ + +
+
Sin datos. Haz clic en "Cargar".
+
+ + + + + + + + + + + + + +
ReferenciaUsuarioMontoEstadoFecha
+
+
+ + +
+
Sin datos. Haz clic en "Cargar".
+
+ + + + + + + + + + + + + +
TipoMontoEstadoDescripciónFecha
+
+
+ + +
+
Sin datos. Haz clic en "Cargar".
+
+ + + + + + + + + + + + +
TipoUsuarioDescripciónFecha
+
+
+ + +
+
Sin datos. Haz clic en "Cargar".
+
+ + + + + + + + + + + + +
TítuloUsuarioVCard vinculadaEstado
+
+
+ + +
+ Total: +
+ + + +
+
+ +
+ + + + + + + +
+ + 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)