fix(api-keys): no se podía asignar el scope vcard

La pantalla lo ofrecía y las rutas de /api/v2/vcard lo exigen, pero la lista de
scopes válidos del backend nunca lo incluyó: crear la llave devolvía "scope
inválido: vcard" y la integración de vCard no tenía forma de autenticarse.

La lista estaba escrita dos veces —una en Go y otra en el HTML— y se
desincronizaron. Ahora la vista la pide al backend, así que no puede volver a
ofrecer algo que el backend rechace, y un test fija que todo scope exigido por
las rutas se pueda asignar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-08-17 20:49:07 -05:00
co-authored by Claude Opus 5
parent 4135850e5b
commit dda5898c0d
4 changed files with 43 additions and 2 deletions
+11 -2
View File
@@ -105,7 +105,9 @@
<div>
<label class="block text-xs font-medium text-gray-600 mb-2">Scopes *</label>
<div class="border border-gray-200 rounded-lg p-3 space-y-2 bg-gray-50">
<template x-for="s in ['oss', 'query_runner', 'usuarios', 'pasarelas', 'vcard']" :key="s">
<!-- La lista viene del backend: cuando estaba escrita acá, la
pantalla ofrecía scopes que el backend rechazaba al guardar. -->
<template x-for="s in scopesDisponibles" :key="s">
<label class="flex items-center gap-2 cursor-pointer select-none">
<input type="checkbox" :checked="form.scopes.includes(s)"
@change="toggleScope(s)" class="rounded text-[#8eb02f] focus:ring-[#8eb02f]">
@@ -191,8 +193,15 @@ function apiKeysApp() {
showModal: false, editItem: null, deleteId: null, regenerarId: null, tokenResult: null,
errorMsg: '', successMsg: '', formError: '',
form: { nombre: '', ip_permitida: '', scopes: [], activa: true },
scopesDisponibles: [],
async init() { await this.load() },
async init() {
try {
const r = await fetch('/app/api-keys/scopes')
this.scopesDisponibles = await r.json()
} catch { this.scopesDisponibles = [] }
await this.load()
},
async load() {
this.loading = true; this.errorMsg = ''
@@ -2,6 +2,7 @@ package controllers
import (
"math"
"sort"
"strconv"
"strings"
@@ -17,6 +18,20 @@ var scopesValidos = map[string]bool{
"query_runner": true,
"usuarios": true,
"pasarelas": true,
"vcard": true,
}
// ScopesDisponibles devuelve la lista para que la vista la muestre en vez de
// tener su propia copia: la copia de la vista ya ofreció "vcard" cuando el
// backend todavía lo rechazaba.
// GET /app/api-keys/scopes
func ScopesDisponibles(c *fiber.Ctx) error {
lista := make([]string, 0, len(scopesValidos))
for s := range scopesValidos {
lista = append(lista, s)
}
sort.Strings(lista)
return c.JSON(lista)
}
// ApiKeysIndex renderiza el panel de administración de API keys.
+16
View File
@@ -0,0 +1,16 @@
package controllers
import "testing"
// La pantalla ofrecía "vcard" mientras el backend lo rechazaba al guardar, y el
// único síntoma era "scope inválido: vcard" al crear la llave. Los scopes que
// las rutas de /api/v2 exigen con RequireScope tienen que poder asignarse.
func TestScopesDeLasRutasSonAsignables(t *testing.T) {
// Sacados de los RequireScope() de rest/routes.
usadosEnRutas := []string{"oss", "query_runner", "usuarios", "pasarelas", "vcard"}
for _, s := range usadosEnRutas {
if !scopesValidos[s] {
t.Errorf("las rutas exigen el scope %q pero no se puede asignar a una API key", s)
}
}
}
+1
View File
@@ -346,6 +346,7 @@ func UserRoutes(app fiber.Router) {
// dar acceso administrativo — solo un administrador puede administrarlas.
protected.Get("/api-keys", middlewares.MenuMiddleware, controllers.ApiKeysIndex)
protected.Get("/api-keys/list", controllers.GetApiKeys)
protected.Get("/api-keys/scopes", controllers.ScopesDisponibles)
protected.Post("/api-keys", middlewares.SoloAdmin, controllers.CreateApiKeyHandler)
protected.Put("/api-keys/:id", middlewares.SoloAdmin, controllers.UpdateApiKeyHandler)
protected.Delete("/api-keys/:id", middlewares.SoloAdmin, controllers.DeleteApiKeyHandler)