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]" />
|
||||
<p class="text-xs text-gray-400 mt-1">Sin barra final. Ej: https://midominio.com</p>
|
||||
</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>
|
||||
<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..."
|
||||
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 class="flex items-center gap-2">
|
||||
<input x-model="form.activo" type="checkbox" id="vcardActivo" class="rounded" />
|
||||
@@ -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; }
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user