package sqlite import ( "context" "database/sql" "encoding/json" "fmt" "time" "git.gobha.me/xcaliber/chat-switchboard/models" "git.gobha.me/xcaliber/chat-switchboard/store" ) type ProviderStore struct{} func NewProviderStore() *ProviderStore { return &ProviderStore{} } const providerCols = `id, scope, owner_id, name, provider, endpoint, api_key_enc, key_nonce, key_scope, model_default, config, headers, settings, is_active, is_private, created_at, updated_at` func (s *ProviderStore) Create(ctx context.Context, cfg *models.ProviderConfig) error { cfg.ID = store.NewID() now := time.Now().UTC() cfg.CreatedAt = now cfg.UpdatedAt = now _, err := DB.ExecContext(ctx, ` INSERT INTO provider_configs (id, scope, owner_id, name, provider, endpoint, api_key_enc, key_nonce, key_scope, model_default, config, headers, settings, is_active, is_private, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, cfg.ID, cfg.Scope, models.NullString(cfg.OwnerID), cfg.Name, cfg.Provider, cfg.Endpoint, cfg.APIKeyEnc, cfg.KeyNonce, cfg.KeyScope, cfg.ModelDefault, ToJSON(cfg.Config), ToJSON(cfg.Headers), ToJSON(cfg.Settings), cfg.IsActive, cfg.IsPrivate, now.Format(timeFmt), now.Format(timeFmt), ) return err } func (s *ProviderStore) GetByID(ctx context.Context, id string) (*models.ProviderConfig, error) { row := DB.QueryRowContext(ctx, fmt.Sprintf("SELECT %s FROM provider_configs WHERE id = ?", providerCols), id) return scanProvider(row) } func (s *ProviderStore) Update(ctx context.Context, id string, patch models.ProviderConfigPatch) error { b := NewUpdate("provider_configs") if patch.Name != nil { b.Set("name", *patch.Name) } if patch.Endpoint != nil { b.Set("endpoint", *patch.Endpoint) } if patch.APIKeyEnc != nil { b.Set("api_key_enc", patch.APIKeyEnc) b.Set("key_nonce", patch.KeyNonce) } if patch.ModelDefault != nil { b.Set("model_default", *patch.ModelDefault) } if patch.Config != nil { b.SetJSON("config", patch.Config) } if patch.Headers != nil { b.SetJSON("headers", patch.Headers) } if patch.Settings != nil { b.SetJSON("settings", patch.Settings) } if patch.IsActive != nil { b.Set("is_active", *patch.IsActive) } if patch.IsPrivate != nil { b.Set("is_private", *patch.IsPrivate) } if !b.HasSets() { return nil } b.Where("id", id) _, err := b.Exec(DB) return err } func (s *ProviderStore) Delete(ctx context.Context, id string) error { _, err := DB.ExecContext(ctx, "DELETE FROM provider_configs WHERE id = ?", id) return err } func (s *ProviderStore) ListGlobal(ctx context.Context) ([]models.ProviderConfig, error) { return s.listByScope(ctx, models.ScopeGlobal, "") } func (s *ProviderStore) ListForTeam(ctx context.Context, teamID string) ([]models.ProviderConfig, error) { return s.listByScope(ctx, models.ScopeTeam, teamID) } func (s *ProviderStore) ListForUser(ctx context.Context, userID string) ([]models.ProviderConfig, error) { return s.listByScope(ctx, models.ScopePersonal, userID) } // ListAccessible returns all provider configs a user can access: // global + team (for teams they belong to) + personal. func (s *ProviderStore) ListAccessible(ctx context.Context, userID string) ([]models.ProviderConfig, error) { rows, err := DB.QueryContext(ctx, fmt.Sprintf(` SELECT %s FROM provider_configs WHERE is_active = 1 AND ( scope = 'global' OR (scope = 'personal' AND owner_id = ?) OR (scope = 'team' AND owner_id IN ( SELECT team_id FROM team_members WHERE user_id = ? )) ) ORDER BY scope, name`, providerCols), userID, userID) if err != nil { return nil, err } defer rows.Close() return scanProviders(rows) } // UserCanAccess checks if a user can use a specific provider config. func (s *ProviderStore) UserCanAccess(ctx context.Context, userID, configID string) (bool, error) { var exists bool err := DB.QueryRowContext(ctx, ` SELECT EXISTS( SELECT 1 FROM provider_configs WHERE id = ? AND is_active = 1 AND ( scope = 'global' OR (scope = 'personal' AND owner_id = ?) OR (scope = 'team' AND owner_id IN ( SELECT team_id FROM team_members WHERE user_id = ? )) ) )`, configID, userID, userID).Scan(&exists) return exists, err } // ── Internal helpers ──────────────────────── func (s *ProviderStore) listByScope(ctx context.Context, scope, ownerID string) ([]models.ProviderConfig, error) { var rows *sql.Rows var err error if scope == models.ScopeGlobal { rows, err = DB.QueryContext(ctx, fmt.Sprintf("SELECT %s FROM provider_configs WHERE scope = ? AND is_active = 1 ORDER BY name", providerCols), scope) } else { rows, err = DB.QueryContext(ctx, fmt.Sprintf("SELECT %s FROM provider_configs WHERE scope = ? AND owner_id = ? AND is_active = 1 ORDER BY name", providerCols), scope, ownerID) } if err != nil { return nil, err } defer rows.Close() return scanProviders(rows) } func scanProvider(row *sql.Row) (*models.ProviderConfig, error) { var p models.ProviderConfig var ownerID, modelDefault, keyScope sql.NullString var configJSON, headersJSON, settingsJSON []byte err := row.Scan( &p.ID, &p.Scope, &ownerID, &p.Name, &p.Provider, &p.Endpoint, &p.APIKeyEnc, &p.KeyNonce, &keyScope, &modelDefault, &configJSON, &headersJSON, &settingsJSON, &p.IsActive, &p.IsPrivate, st(&p.CreatedAt), st(&p.UpdatedAt), ) if err != nil { return nil, err } p.OwnerID = NullableStringPtr(ownerID) p.ModelDefault = modelDefault.String p.KeyScope = keyScope.String json.Unmarshal(configJSON, &p.Config) json.Unmarshal(headersJSON, &p.Headers) json.Unmarshal(settingsJSON, &p.Settings) return &p, nil } func scanProviders(rows *sql.Rows) ([]models.ProviderConfig, error) { var result []models.ProviderConfig for rows.Next() { var p models.ProviderConfig var ownerID, modelDefault, keyScope sql.NullString var configJSON, headersJSON, settingsJSON []byte err := rows.Scan( &p.ID, &p.Scope, &ownerID, &p.Name, &p.Provider, &p.Endpoint, &p.APIKeyEnc, &p.KeyNonce, &keyScope, &modelDefault, &configJSON, &headersJSON, &settingsJSON, &p.IsActive, &p.IsPrivate, st(&p.CreatedAt), st(&p.UpdatedAt), ) if err != nil { return nil, err } p.OwnerID = NullableStringPtr(ownerID) p.ModelDefault = modelDefault.String p.KeyScope = keyScope.String json.Unmarshal(configJSON, &p.Config) json.Unmarshal(headersJSON, &p.Headers) json.Unmarshal(settingsJSON, &p.Settings) result = append(result, p) } return result, rows.Err() }