feat(vcard): agregar login con email+password para obtener token automáticamente
- 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>
This commit is contained in:
co-authored by
Copilot
parent
31259cbb71
commit
45032dae3f
@@ -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]" />
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
|
||||||
<p class="text-xs text-gray-400 mt-1">Sin barra final. Ej: https://midominio.com</p>
|
<p class="text-xs text-gray-400 mt-1">Sin barra final. Ej: https://midominio.com</p>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Login para obtener token automáticamente -->
|
||||||
|
<div class="border border-gray-200 rounded-lg p-4 space-y-3 bg-gray-50">
|
||||||
|
<p class="text-xs font-medium text-gray-600 uppercase tracking-wide">Obtener token automáticamente</p>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-gray-600 mb-1">Email admin</label>
|
||||||
|
<input x-model="loginForm.email" type="email" placeholder="admin@dominio.com"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f] bg-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-gray-600 mb-1">Contraseña</label>
|
||||||
|
<input x-model="loginForm.password" type="password" placeholder="••••••••"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f] bg-white" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button @click="obtenerToken()" :disabled="loginLoading"
|
||||||
|
class="w-full px-4 py-2 text-sm border border-[#8eb02f] text-[#8eb02f] rounded-lg hover:bg-[#8eb02f] hover:text-white transition-colors disabled:opacity-50">
|
||||||
|
<span x-text="loginLoading ? 'Conectando...' : '⚡ Obtener Token'"></span>
|
||||||
|
</button>
|
||||||
|
<div x-show="loginMsg" class="text-xs rounded px-3 py-2"
|
||||||
|
:class="loginOk ? 'bg-green-50 text-green-700 border border-green-200' : 'bg-red-50 text-red-600 border border-red-200'"
|
||||||
|
x-text="loginMsg"></div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Bearer Token <span class="text-red-500">*</span></label>
|
<label class="block text-sm font-medium text-gray-700 mb-1">Bearer Token <span class="text-red-500">*</span></label>
|
||||||
<input x-model="form.bearer_token" type="password" placeholder="1|abc123..."
|
<input x-model="form.bearer_token" type="password" placeholder="1|abc123..."
|
||||||
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
|
||||||
<p class="text-xs text-gray-400 mt-1">Token Sanctum de un usuario con rol administrador.</p>
|
<p class="text-xs text-gray-400 mt-1">Se rellena automáticamente al obtener el token, o pégalo manualmente.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<input x-model="form.activo" type="checkbox" id="vcardActivo" class="rounded" />
|
<input x-model="form.activo" type="checkbox" id="vcardActivo" class="rounded" />
|
||||||
@@ -441,6 +464,10 @@ function vcardApiApp() {
|
|||||||
configModal: false,
|
configModal: false,
|
||||||
configError: '',
|
configError: '',
|
||||||
form: { nombre: '', base_url: '', bearer_token: '', activo: true },
|
form: { nombre: '', base_url: '', bearer_token: '', activo: true },
|
||||||
|
loginForm: { email: '', password: '' },
|
||||||
|
loginLoading: false,
|
||||||
|
loginMsg: '',
|
||||||
|
loginOk: false,
|
||||||
membresiaModal: false,
|
membresiaModal: false,
|
||||||
membresiaUser: null,
|
membresiaUser: null,
|
||||||
membresiaInfo: null,
|
membresiaInfo: null,
|
||||||
@@ -472,6 +499,30 @@ function vcardApiApp() {
|
|||||||
} catch(e) {}
|
} 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() {
|
async saveConfig() {
|
||||||
this.configError = '';
|
this.configError = '';
|
||||||
if (!this.form.base_url.trim()) { this.configError = 'La URL base es requerida.'; return; }
|
if (!this.form.base_url.trim()) { this.configError = 'La URL base es requerida.'; return; }
|
||||||
|
|||||||
@@ -23,6 +23,45 @@ func VcardApiIndex(c *fiber.Ctx) error {
|
|||||||
return c.Render("vcard_api", data, "layouts/main")
|
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).
|
// VcardApiSaveConfig guarda / actualiza la configuración (base URL + token).
|
||||||
func VcardApiSaveConfig(c *fiber.Ctx) error {
|
func VcardApiSaveConfig(c *fiber.Ctx) error {
|
||||||
type Req struct {
|
type Req struct {
|
||||||
|
|||||||
@@ -217,6 +217,7 @@ func UserRoutes(app fiber.Router) {
|
|||||||
|
|
||||||
// ─── VCard API (integración Admin Laravel) ────────────────────────────────
|
// ─── VCard API (integración Admin Laravel) ────────────────────────────────
|
||||||
protected.Get("/vcard-api", middlewares.MenuMiddleware, controllers.VcardApiIndex)
|
protected.Get("/vcard-api", middlewares.MenuMiddleware, controllers.VcardApiIndex)
|
||||||
|
protected.Post("/vcard-api/login", controllers.VcardApiLogin)
|
||||||
protected.Get("/vcard-api/config", controllers.VcardApiGetConfig)
|
protected.Get("/vcard-api/config", controllers.VcardApiGetConfig)
|
||||||
protected.Post("/vcard-api/config", controllers.VcardApiSaveConfig)
|
protected.Post("/vcard-api/config", controllers.VcardApiSaveConfig)
|
||||||
// Usuarios (GET)
|
// Usuarios (GET)
|
||||||
|
|||||||
Reference in New Issue
Block a user