package handlers import ( "net/http" "github.com/gin-gonic/gin" "switchboard-core/models" "switchboard-core/store" ) // ExtSecretsHandler serves extension secret management endpoints. // v0.29.0 CS3: Admin sets key-value secrets that Starlark extensions // can read via the secrets.get() module. // // Secrets are stored in GlobalConfig under key "ext_secrets:{packageID}" // as a JSON map: {"api_key": "sk-...", "webhook_token": "tok-..."}. type ExtSecretsHandler struct { stores store.Stores } func NewExtSecretsHandler(stores store.Stores) *ExtSecretsHandler { return &ExtSecretsHandler{stores: stores} } // GetSecrets returns the secret keys (not values) for a package. // GET /api/v1/admin/extensions/:id/secrets func (h *ExtSecretsHandler) GetSecrets(c *gin.Context) { pkgID := c.Param("id") configKey := "ext_secrets:" + pkgID configVal, err := h.stores.GlobalConfig.Get(c.Request.Context(), configKey) if err != nil { // Not found = empty secrets, not an error c.JSON(http.StatusOK, gin.H{"data": map[string]string{}}) return } // Return only keys, not values (security: don't expose secrets in GET) keys := make([]string, 0, len(configVal)) for k := range configVal { keys = append(keys, k) } c.JSON(http.StatusOK, gin.H{"data": gin.H{ "package_id": pkgID, "keys": keys, }}) } // SetSecrets upserts secrets for a package. // PUT /api/v1/admin/extensions/:id/secrets // Body: {"secrets": {"api_key": "sk-...", "webhook_token": "tok-..."}} func (h *ExtSecretsHandler) SetSecrets(c *gin.Context) { pkgID := c.Param("id") userID := c.GetString("user_id") // Verify package exists pkg, err := h.stores.Packages.Get(c.Request.Context(), pkgID) if err != nil || pkg == nil { c.JSON(http.StatusNotFound, gin.H{"error": "package not found"}) return } var body struct { Secrets map[string]string `json:"secrets" binding:"required"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request: " + err.Error()}) return } // Store as GlobalConfig JSONMap configKey := "ext_secrets:" + pkgID configVal := models.JSONMap{} for k, v := range body.Secrets { configVal[k] = v } if err := h.stores.GlobalConfig.Set(c.Request.Context(), configKey, configVal, userID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save secrets"}) return } c.JSON(http.StatusOK, gin.H{ "status": "saved", "package_id": pkgID, "key_count": len(body.Secrets), }) } // DeleteSecrets removes all secrets for a package. // DELETE /api/v1/admin/extensions/:id/secrets func (h *ExtSecretsHandler) DeleteSecrets(c *gin.Context) { pkgID := c.Param("id") userID := c.GetString("user_id") configKey := "ext_secrets:" + pkgID if err := h.stores.GlobalConfig.Set(c.Request.Context(), configKey, models.JSONMap{}, userID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete secrets"}) return } c.JSON(http.StatusOK, gin.H{"status": "deleted", "package_id": pkgID}) }