package handlers import ( "encoding/json" "log" "net/http" "github.com/gin-gonic/gin" capspkg "git.gobha.me/xcaliber/chat-switchboard/capabilities" "git.gobha.me/xcaliber/chat-switchboard/database" "git.gobha.me/xcaliber/chat-switchboard/models" "git.gobha.me/xcaliber/chat-switchboard/providers" ) // ── Team Provider Handlers ────────────────── // ListTeamProviders returns API configs scoped to a team. func (h *TeamHandler) ListTeamProviders(c *gin.Context) { teamID := getTeamID(c) rows, err := database.DB.Query(database.Q(` SELECT id, name, provider, endpoint, api_key_enc, model_default, config, is_active, is_private, created_at, updated_at FROM provider_configs WHERE scope = 'team' AND owner_id = $1 ORDER BY name ASC `), teamID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list team providers"}) return } defer rows.Close() type teamProvider struct { ID string `json:"id"` Name string `json:"name"` Provider string `json:"provider"` Endpoint string `json:"endpoint"` HasKey bool `json:"has_key"` ModelDefault *string `json:"model_default"` Config map[string]interface{} `json:"config"` IsActive bool `json:"is_active"` IsPrivate bool `json:"is_private"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } configs := make([]teamProvider, 0) for rows.Next() { var p teamProvider var apiKeyEnc []byte var configRaw string if err := rows.Scan(&p.ID, &p.Name, &p.Provider, &p.Endpoint, &apiKeyEnc, &p.ModelDefault, &configRaw, &p.IsActive, &p.IsPrivate, &p.CreatedAt, &p.UpdatedAt); err != nil { continue } p.HasKey = len(apiKeyEnc) > 0 p.Config = parseJSONBConfig(configRaw) configs = append(configs, p) } c.JSON(http.StatusOK, gin.H{ "data": configs, "allow_team_providers": isTeamProvidersAllowed(teamID), }) } // CreateTeamProvider creates an API config scoped to a team. func (h *TeamHandler) CreateTeamProvider(c *gin.Context) { teamID := getTeamID(c) if !isTeamProvidersAllowed(teamID) { c.JSON(http.StatusForbidden, gin.H{"error": "team providers are not enabled for this team"}) return } var req struct { Name string `json:"name" binding:"required,max=100"` Provider string `json:"provider" binding:"required"` Endpoint string `json:"endpoint" binding:"required"` APIKey string `json:"api_key"` ModelDefault string `json:"model_default,omitempty"` Config map[string]interface{} `json:"config,omitempty"` Headers map[string]string `json:"headers,omitempty"` IsPrivate bool `json:"is_private,omitempty"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if _, err := providers.Get(req.Provider); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "unsupported provider: " + req.Provider, "supported_providers": providers.List(), }) return } configJSON := "{}" if req.Config != nil { b, _ := json.Marshal(req.Config) configJSON = string(b) } headersJSON := "{}" if req.Headers != nil { b, _ := json.Marshal(req.Headers) headersJSON = string(b) } // Encrypt the API key for team scope var apiKeyEnc, keyNonce []byte if req.APIKey != "" { if h.vault != nil { var err error apiKeyEnc, keyNonce, err = h.vault.EncryptForScope(req.APIKey, "team", "") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to encrypt API key"}) return } } else { apiKeyEnc = []byte(req.APIKey) } } id, err := database.InsertReturningID(` INSERT INTO provider_configs (scope, owner_id, name, provider, endpoint, api_key_enc, key_nonce, key_scope, model_default, config, is_private, headers) VALUES ($1, $2, $3, $4, $5, $6, $7, 'team', $8, $9::jsonb, $10, $11::jsonb) RETURNING id `, "team", teamID, req.Name, req.Provider, req.Endpoint, apiKeyEnc, keyNonce, req.ModelDefault, configJSON, req.IsPrivate, headersJSON, ) if err != nil { log.Printf("[WARN] Failed to create team provider: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create provider"}) return } c.JSON(http.StatusCreated, gin.H{"id": id}) } // UpdateTeamProvider updates a team-scoped API config. func (h *TeamHandler) UpdateTeamProvider(c *gin.Context) { teamID := getTeamID(c) providerID := c.Param("id") var req struct { Name *string `json:"name,omitempty"` Endpoint *string `json:"endpoint,omitempty"` APIKey *string `json:"api_key,omitempty"` ModelDefault *string `json:"model_default,omitempty"` Config map[string]interface{} `json:"config,omitempty"` Headers map[string]string `json:"headers,omitempty"` IsActive *bool `json:"is_active,omitempty"` IsPrivate *bool `json:"is_private,omitempty"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Verify provider belongs to this team var count int database.DB.QueryRow(database.Q(`SELECT COUNT(*) FROM provider_configs WHERE id = $1 AND scope = 'team' AND owner_id = $2`), providerID, teamID).Scan(&count) if count == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "provider not found in this team"}) return } // Build dynamic update using ? placeholders (works on both dialects) setClauses := []string{"updated_at = " + database.Q("NOW()")} args := []interface{}{} fieldCount := 0 addSet := func(col string, val interface{}) { setClauses = append(setClauses, col+" = ?") args = append(args, val) fieldCount++ } if req.Name != nil { addSet("name", *req.Name) } if req.Endpoint != nil { addSet("endpoint", *req.Endpoint) } if req.APIKey != nil && *req.APIKey != "" { if h.vault != nil { enc, nonce, err := h.vault.EncryptForScope(*req.APIKey, "team", "") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to encrypt API key"}) return } addSet("api_key_enc", enc) addSet("key_nonce", nonce) } else { addSet("api_key_enc", []byte(*req.APIKey)) } } if req.ModelDefault != nil { addSet("model_default", *req.ModelDefault) } if req.IsActive != nil { addSet("is_active", *req.IsActive) } if req.IsPrivate != nil { addSet("is_private", *req.IsPrivate) } if req.Config != nil { b, _ := json.Marshal(req.Config) addSet("config", string(b)) } if req.Headers != nil { b, _ := json.Marshal(req.Headers) addSet("headers", string(b)) } if fieldCount == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "no fields to update"}) return } args = append(args, providerID, teamID) // Build final query with ? placeholders, then convert to $N for Postgres query := "UPDATE provider_configs SET " for i, s := range setClauses { if i > 0 { query += ", " } query += s } query += " WHERE id = ? AND scope = 'team' AND owner_id = ?" if database.IsPostgres() { query = convertPlaceholders(query) } _, err := database.DB.Exec(query, args...) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update provider"}) return } c.JSON(http.StatusOK, gin.H{"id": providerID, "updated": true}) } // DeleteTeamProvider removes a team-scoped API config. func (h *TeamHandler) DeleteTeamProvider(c *gin.Context) { teamID := getTeamID(c) providerID := c.Param("id") result, err := database.DB.Exec(database.Q(` DELETE FROM provider_configs WHERE id = $1 AND scope = 'team' AND owner_id = $2 `), providerID, teamID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete provider"}) return } rows, _ := result.RowsAffected() if rows == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "provider not found in this team"}) return } c.JSON(http.StatusOK, gin.H{"deleted": true}) } // ListTeamProviderModels lists models available from a team provider (live query). func (h *TeamHandler) ListTeamProviderModels(c *gin.Context) { teamID := getTeamID(c) providerID := c.Param("id") var name, providerType, endpoint string var apiKeyEnc, keyNonce []byte var keyScope string var headersJSON []byte err := database.DB.QueryRow(database.Q(` SELECT name, provider, endpoint, api_key_enc, key_nonce, key_scope, headers FROM provider_configs WHERE id = $1 AND scope = 'team' AND owner_id = $2 AND is_active = true `), providerID, teamID).Scan(&name, &providerType, &endpoint, &apiKeyEnc, &keyNonce, &keyScope, &headersJSON) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "provider not found"}) return } provider, err := providers.Get(providerType) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "unsupported provider"}) return } key := "" if len(apiKeyEnc) > 0 { if h.vault != nil { var err error key, err = h.vault.Decrypt(apiKeyEnc, keyNonce, keyScope, "") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to decrypt API key"}) return } } else { key = string(apiKeyEnc) } } var customHeaders map[string]string _ = json.Unmarshal(headersJSON, &customHeaders) modelList, err := provider.ListModels(c.Request.Context(), providers.ProviderConfig{ Endpoint: endpoint, APIKey: key, CustomHeaders: customHeaders, }) if err != nil { c.JSON(http.StatusBadGateway, gin.H{"error": "failed to fetch models: " + err.Error()}) return } type modelInfo struct { ID string `json:"id"` Type string `json:"type"` Capabilities models.ModelCapabilities `json:"capabilities"` } out := make([]modelInfo, 0, len(modelList)) for _, m := range modelList { caps := capspkg.ResolveIntrinsic(m.ID, &m.Capabilities, nil) caps.MaxOutputTokens = capspkg.ResolveMaxOutput(m.ID, caps) out = append(out, modelInfo{ID: m.ID, Type: m.Type, Capabilities: caps}) } c.JSON(http.StatusOK, gin.H{"models": out, "provider": name}) } // parseJSONBConfig parses a JSONB text string into a map. func parseJSONBConfig(raw string) map[string]interface{} { if raw == "" || raw == "{}" || raw == "null" { return map[string]interface{}{} } var m map[string]interface{} if err := json.Unmarshal([]byte(raw), &m); err != nil { return map[string]interface{}{} } return m } // isTeamProvidersAllowed checks if team providers are enabled. func isTeamProvidersAllowed(teamID string) bool { if database.DB == nil { return false } var globalVal string err := database.DB.QueryRow(` SELECT value FROM global_settings WHERE key = 'allow_team_providers' `).Scan(&globalVal) if err == nil && globalVal == "false" { return false } var settingsJSON []byte err = database.DB.QueryRow(database.Q(`SELECT settings FROM teams WHERE id = $1`), teamID).Scan(&settingsJSON) if err != nil { return true } var settings map[string]interface{} if err := json.Unmarshal(settingsJSON, &settings); err != nil { return true } if v, ok := settings["allow_team_providers"]; ok { if b, ok := v.(bool); ok { return b } } return true }