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 @@ + +
+ + +
+ 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/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;