From 45032dae3fd9eaa71ffd8989723cda371b72c008 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Tue, 19 May 2026 20:44:40 -0500 Subject: [PATCH] =?UTF-8?q?feat(vcard):=20agregar=20login=20con=20email+pa?= =?UTF-8?q?ssword=20para=20obtener=20token=20autom=C3=A1ticamente?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Nuevo endpoint POST /app/vcard-api/login que llama a /api/login del sistema VCard externo - Modal de configuración con campos email+password y botón 'Obtener Token' - El token se auto-rellena en el campo Bearer Token al conectar exitosamente - Se mantiene la opción de pegar el token manualmente como fallback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- resources/views/vcard_api.html | 53 +++++++++++++++++++++++- rest/controllers/vcard_api_controller.go | 39 +++++++++++++++++ rest/routes/user.go | 1 + 3 files changed, 92 insertions(+), 1 deletion(-) diff --git a/resources/views/vcard_api.html b/resources/views/vcard_api.html index 1172dab..62cc243 100644 --- a/resources/views/vcard_api.html +++ b/resources/views/vcard_api.html @@ -333,11 +333,34 @@ class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />

Sin barra final. Ej: https://midominio.com

+ +
+

Obtener token automáticamente

+
+
+ + +
+
+ + +
+
+ +
+
-

Token Sanctum de un usuario con rol administrador.

+

Se rellena automáticamente al obtener el token, o pégalo manualmente.

@@ -441,6 +464,10 @@ function vcardApiApp() { configModal: false, configError: '', form: { nombre: '', base_url: '', bearer_token: '', activo: true }, + loginForm: { email: '', password: '' }, + loginLoading: false, + loginMsg: '', + loginOk: false, membresiaModal: false, membresiaUser: null, membresiaInfo: null, @@ -472,6 +499,30 @@ function vcardApiApp() { } catch(e) {} }, + async obtenerToken() { + this.loginMsg = ''; + if (!this.form.base_url.trim()) { this.loginMsg = 'Ingresa primero la URL Base.'; this.loginOk = false; return; } + if (!this.loginForm.email || !this.loginForm.password) { this.loginMsg = 'Email y contraseña son requeridos.'; this.loginOk = false; return; } + this.loginLoading = true; + try { + const r = await fetch('/app/vcard-api/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ base_url: this.form.base_url, email: this.loginForm.email, password: this.loginForm.password }) + }); + const d = await r.json(); + if (d.ok && d.token) { + this.form.bearer_token = d.token; + this.loginMsg = '✓ Token obtenido. Haz clic en Guardar para aplicar.'; + this.loginOk = true; + } else { + this.loginMsg = d.error || 'Error al obtener el token.'; + this.loginOk = false; + } + } catch(e) { this.loginMsg = e.message; this.loginOk = false; } + this.loginLoading = false; + }, + async saveConfig() { this.configError = ''; if (!this.form.base_url.trim()) { this.configError = 'La URL base es requerida.'; return; } diff --git a/rest/controllers/vcard_api_controller.go b/rest/controllers/vcard_api_controller.go index 59d5dc7..10ea151 100644 --- a/rest/controllers/vcard_api_controller.go +++ b/rest/controllers/vcard_api_controller.go @@ -23,6 +23,45 @@ func VcardApiIndex(c *fiber.Ctx) error { return c.Render("vcard_api", data, "layouts/main") } +// VcardApiLogin llama a /api/login del sistema VCard externo y devuelve el access_token. +func VcardApiLogin(c *fiber.Ctx) error { + type Req struct { + BaseURL string `json:"base_url"` + Email string `json:"email"` + Password string `json:"password"` + } + var req Req + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"}) + } + req.BaseURL = strings.TrimRight(strings.TrimSpace(req.BaseURL), "/") + if req.BaseURL == "" || req.Email == "" || req.Password == "" { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "base_url, email y password son requeridos"}) + } + + payload, _ := json.Marshal(map[string]string{"email": req.Email, "password": req.Password}) + client := &http.Client{Timeout: 15 * time.Second} + resp, err := client.Post(req.BaseURL+"/api/login", "application/json", strings.NewReader(string(payload))) + if err != nil { + return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"error": "No se pudo conectar: " + err.Error()}) + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + + var result map[string]interface{} + if err := json.Unmarshal(body, &result); err != nil { + return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"error": "Respuesta inválida del servidor"}) + } + if token, ok := result["access_token"].(string); ok && token != "" { + return c.JSON(fiber.Map{"ok": true, "token": token}) + } + msg := "Credenciales incorrectas" + if m, ok := result["message"].(string); ok && m != "" { + msg = m + } + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": msg}) +} + // VcardApiSaveConfig guarda / actualiza la configuración (base URL + token). func VcardApiSaveConfig(c *fiber.Ctx) error { type Req struct { diff --git a/rest/routes/user.go b/rest/routes/user.go index 5aab020..1a133b8 100755 --- a/rest/routes/user.go +++ b/rest/routes/user.go @@ -217,6 +217,7 @@ func UserRoutes(app fiber.Router) { // ─── VCard API (integración Admin Laravel) ──────────────────────────────── protected.Get("/vcard-api", middlewares.MenuMiddleware, controllers.VcardApiIndex) + protected.Post("/vcard-api/login", controllers.VcardApiLogin) protected.Get("/vcard-api/config", controllers.VcardApiGetConfig) protected.Post("/vcard-api/config", controllers.VcardApiSaveConfig) // Usuarios (GET)