Changeset 0.9.4 (#54)
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/crypto"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
)
|
||||
@@ -13,10 +14,11 @@ import (
|
||||
// ProviderConfigHandler handles user-facing provider config endpoints.
|
||||
type ProviderConfigHandler struct {
|
||||
stores store.Stores
|
||||
vault *crypto.KeyResolver
|
||||
}
|
||||
|
||||
func NewProviderConfigHandler(s store.Stores) *ProviderConfigHandler {
|
||||
return &ProviderConfigHandler{stores: s}
|
||||
func NewProviderConfigHandler(s store.Stores, vault *crypto.KeyResolver) *ProviderConfigHandler {
|
||||
return &ProviderConfigHandler{stores: s, vault: vault}
|
||||
}
|
||||
|
||||
// ListConfigs returns configs accessible to the user (global + personal + team).
|
||||
@@ -52,7 +54,7 @@ func (h *ProviderConfigHandler) ListConfigs(c *gin.Context) {
|
||||
Name: cfg.Name,
|
||||
Provider: cfg.Provider,
|
||||
Endpoint: cfg.Endpoint,
|
||||
HasKey: cfg.APIKeyEnc != "",
|
||||
HasKey: cfg.HasKey(),
|
||||
ModelDefault: cfg.ModelDefault,
|
||||
Scope: cfg.Scope,
|
||||
IsActive: cfg.IsActive,
|
||||
@@ -111,13 +113,32 @@ func (h *ProviderConfigHandler) CreateConfig(c *gin.Context) {
|
||||
Name: req.Name,
|
||||
Provider: req.Provider,
|
||||
Endpoint: req.Endpoint,
|
||||
APIKeyEnc: req.APIKey, // TODO: encrypt
|
||||
ModelDefault: req.ModelDefault,
|
||||
Scope: models.ScopePersonal,
|
||||
KeyScope: models.ScopePersonal,
|
||||
OwnerID: &userID,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
// Encrypt the API key with user's UEK (personal scope)
|
||||
if req.APIKey != "" {
|
||||
if h.vault != nil {
|
||||
enc, nonce, err := h.vault.EncryptForScope(req.APIKey, "personal", userID)
|
||||
if err != nil {
|
||||
if err == crypto.ErrVaultLocked {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "vault is locked — please log in again"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to encrypt API key"})
|
||||
return
|
||||
}
|
||||
cfg.APIKeyEnc = enc
|
||||
cfg.KeyNonce = nonce
|
||||
} else {
|
||||
cfg.APIKeyEnc = []byte(req.APIKey)
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.stores.Providers.Create(c.Request.Context(), cfg); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create config"})
|
||||
return
|
||||
@@ -133,7 +154,7 @@ func (h *ProviderConfigHandler) CreateConfig(c *gin.Context) {
|
||||
"scope": cfg.Scope,
|
||||
}
|
||||
|
||||
result, err := syncAndEnableProviderModels(c.Request.Context(), h.stores, cfg)
|
||||
result, err := syncAndEnableProviderModels(c.Request.Context(), h.stores, cfg, req.APIKey)
|
||||
if err != nil {
|
||||
// Provider created successfully but model fetch failed.
|
||||
// Return 201 (provider exists) with a warning so the frontend can show it.
|
||||
@@ -169,9 +190,23 @@ func (h *ProviderConfigHandler) UpdateConfig(c *gin.Context) {
|
||||
}
|
||||
|
||||
patch := req.ProviderConfigPatch
|
||||
// Transfer api_key → APIKeyEnc (json:"-" prevents auto-binding)
|
||||
// Transfer api_key → encrypted fields
|
||||
if req.APIKey != nil && *req.APIKey != "" {
|
||||
patch.APIKeyEnc = req.APIKey // TODO: encrypt
|
||||
if h.vault != nil {
|
||||
enc, nonce, err := h.vault.EncryptForScope(*req.APIKey, "personal", userID)
|
||||
if err != nil {
|
||||
if err == crypto.ErrVaultLocked {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "vault is locked — please log in again"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to encrypt API key"})
|
||||
return
|
||||
}
|
||||
patch.APIKeyEnc = enc
|
||||
patch.KeyNonce = nonce
|
||||
} else {
|
||||
patch.APIKeyEnc = []byte(*req.APIKey)
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.stores.Providers.Update(c.Request.Context(), id, patch); err != nil {
|
||||
@@ -228,7 +263,21 @@ func (h *ProviderConfigHandler) FetchModels(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
result, err := syncAndEnableProviderModels(c.Request.Context(), h.stores, cfg)
|
||||
// Decrypt the API key for the provider API call
|
||||
apiKey := ""
|
||||
if len(cfg.APIKeyEnc) > 0 {
|
||||
if h.vault != nil {
|
||||
apiKey, err = h.vault.Decrypt(cfg.APIKeyEnc, cfg.KeyNonce, cfg.KeyScope, userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "vault is locked — please log in again"})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
apiKey = string(cfg.APIKeyEnc)
|
||||
}
|
||||
}
|
||||
|
||||
result, err := syncAndEnableProviderModels(c.Request.Context(), h.stores, cfg, apiKey)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": "failed to fetch models: " + err.Error()})
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user