Files
soft_usite/rest/controllers/api_key_admin_controller.go
Lizandro GuarnizoandClaude Opus 5 dda5898c0d 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>
2026-08-17 20:49:07 -05:00

173 lines
5.3 KiB
Go

package controllers
import (
"math"
"sort"
"strconv"
"strings"
"github.com/gofiber/fiber/v2"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
)
// scopesValidos son los grupos de /api/v2 que hoy sí respetan RequireScope.
// El resto de endpoints de /api/v2 sigue funcionando solo con la llave
// maestra (ADMIN_API_KEY) mientras se migran en una fase posterior.
var scopesValidos = map[string]bool{
"oss": true,
"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.
func ApiKeysIndex(c *fiber.Ctx) error {
return c.Render("api_keys", fiber.Map{
"user": c.Locals("user"),
"modules": c.Locals("userModules"),
}, "layouts/main")
}
func GetApiKeys(c *fiber.Ctx) error {
page, _ := strconv.Atoi(c.Query("page", "1"))
if page < 1 {
page = 1
}
limit := 20
offset := (page - 1) * limit
items, total, err := models.GetAllApiKeys(limit, offset)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{
"items": items,
"total": total,
"totalPages": int(math.Ceil(float64(total) / float64(limit))),
"page": page,
})
}
type apiKeyReq struct {
Nombre string `json:"nombre"`
IPPermitida string `json:"ip_permitida"`
Scopes []string `json:"scopes"`
Activa bool `json:"activa"`
}
func (r apiKeyReq) validar() (scopes []string, err error) {
if strings.TrimSpace(r.Nombre) == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "nombre es requerido")
}
if strings.TrimSpace(r.IPPermitida) == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "ip_permitida es requerida (IP exacta o CIDR)")
}
for _, s := range r.Scopes {
s = strings.ToLower(strings.TrimSpace(s))
if s == "" {
continue
}
if !scopesValidos[s] {
return nil, fiber.NewError(fiber.StatusBadRequest, "scope inválido: "+s)
}
scopes = append(scopes, s)
}
if len(scopes) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "selecciona al menos un scope")
}
return scopes, nil
}
// CreateApiKeyHandler crea una nueva API key y devuelve el token en texto
// plano — es la única respuesta donde vendrá completo.
func CreateApiKeyHandler(c *fiber.Ctx) error {
var req apiKeyReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
}
scopes, err := req.validar()
if err != nil {
return c.Status(err.(*fiber.Error).Code).JSON(fiber.Map{"error": err.Error()})
}
item := &models.ApiKey{
Nombre: strings.TrimSpace(req.Nombre),
IPPermitida: strings.TrimSpace(req.IPPermitida),
Scopes: models.JoinModulos(scopes),
Activa: true,
CreadoPorID: extraerUserID(c),
}
tokenPlano, err := models.CreateApiKey(item)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
"ok": true,
"id": item.ID,
"token": tokenPlano,
"aviso": "Guarda este token ahora: no se volverá a mostrar completo.",
})
}
func UpdateApiKeyHandler(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"})
}
var req apiKeyReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
}
scopes, verr := req.validar()
if verr != nil {
return c.Status(verr.(*fiber.Error).Code).JSON(fiber.Map{"error": verr.Error()})
}
updates := map[string]interface{}{
"nombre": strings.TrimSpace(req.Nombre),
"ip_permitida": strings.TrimSpace(req.IPPermitida),
"scopes": models.JoinModulos(scopes),
"activa": req.Activa,
}
if err := models.UpdateApiKey(uint(id), updates); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"ok": true})
}
func RegenerarApiKeyHandler(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"})
}
tokenPlano, err := models.RegenerarApiKeyToken(uint(id))
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"ok": true, "token": tokenPlano, "aviso": "El token anterior dejó de funcionar. Guarda este ahora, no se volverá a mostrar."})
}
func DeleteApiKeyHandler(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"})
}
if err := models.DeleteApiKey(uint(id)); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"ok": true})
}