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:
co-authored by
Claude Opus 5
parent
4135850e5b
commit
dda5898c0d
@@ -105,7 +105,9 @@
|
|||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-medium text-gray-600 mb-2">Scopes *</label>
|
<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">
|
<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">
|
<label class="flex items-center gap-2 cursor-pointer select-none">
|
||||||
<input type="checkbox" :checked="form.scopes.includes(s)"
|
<input type="checkbox" :checked="form.scopes.includes(s)"
|
||||||
@change="toggleScope(s)" class="rounded text-[#8eb02f] focus:ring-[#8eb02f]">
|
@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,
|
showModal: false, editItem: null, deleteId: null, regenerarId: null, tokenResult: null,
|
||||||
errorMsg: '', successMsg: '', formError: '',
|
errorMsg: '', successMsg: '', formError: '',
|
||||||
form: { nombre: '', ip_permitida: '', scopes: [], activa: true },
|
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() {
|
async load() {
|
||||||
this.loading = true; this.errorMsg = ''
|
this.loading = true; this.errorMsg = ''
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package controllers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -17,6 +18,20 @@ var scopesValidos = map[string]bool{
|
|||||||
"query_runner": true,
|
"query_runner": true,
|
||||||
"usuarios": true,
|
"usuarios": true,
|
||||||
"pasarelas": 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.
|
// ApiKeysIndex renderiza el panel de administración de API keys.
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -346,6 +346,7 @@ func UserRoutes(app fiber.Router) {
|
|||||||
// dar acceso administrativo — solo un administrador puede administrarlas.
|
// dar acceso administrativo — solo un administrador puede administrarlas.
|
||||||
protected.Get("/api-keys", middlewares.MenuMiddleware, controllers.ApiKeysIndex)
|
protected.Get("/api-keys", middlewares.MenuMiddleware, controllers.ApiKeysIndex)
|
||||||
protected.Get("/api-keys/list", controllers.GetApiKeys)
|
protected.Get("/api-keys/list", controllers.GetApiKeys)
|
||||||
|
protected.Get("/api-keys/scopes", controllers.ScopesDisponibles)
|
||||||
protected.Post("/api-keys", middlewares.SoloAdmin, controllers.CreateApiKeyHandler)
|
protected.Post("/api-keys", middlewares.SoloAdmin, controllers.CreateApiKeyHandler)
|
||||||
protected.Put("/api-keys/:id", middlewares.SoloAdmin, controllers.UpdateApiKeyHandler)
|
protected.Put("/api-keys/:id", middlewares.SoloAdmin, controllers.UpdateApiKeyHandler)
|
||||||
protected.Delete("/api-keys/:id", middlewares.SoloAdmin, controllers.DeleteApiKeyHandler)
|
protected.Delete("/api-keys/:id", middlewares.SoloAdmin, controllers.DeleteApiKeyHandler)
|
||||||
|
|||||||
Reference in New Issue
Block a user