Skip to content

Conversation

@google-labs-jules
Copy link
Contributor

@google-labs-jules google-labs-jules bot commented Jan 20, 2026

User description

This PR fixes an issue where clearing the Search Provider API key in the settings was impossible because an empty value was interpreted as "preserve existing key".

Changes:

  • Backend: Updated SaveSearchProviderConfig and CheckSearchProviderConfig to accept a SearchProviderConfigRequest struct which includes an UpdateAPIKey boolean.
  • Backend: Logic updated to only preserve existing key if UpdateAPIKey is false and the provided key is empty.
  • Frontend: Updated settings page to track input changes on the API Key field and send update_api_key: true when modified.
  • Tests: Added internal/controller/settings_test.go to verify the fix.

PR created automatically by Jules for task 3120112355790831627 started by @Colin-XKL


PR Type

Bug fix


Description

  • Introduces update_api_key flag to distinguish intentional clearing from preserving existing API keys

  • Frontend tracks API key input changes and sends flag when modified

  • Backend logic updated to only preserve key if flag is false and key is empty

  • Comprehensive unit tests added for settings controller validation


Diagram Walkthrough

flowchart LR
  A["Frontend: Track apiKeyChanged"] -->|"Send update_api_key flag"| B["Backend: SearchProviderConfigRequest"]
  B -->|"Check UpdateAPIKey && APIKey"| C["Preserve or Clear Logic"]
  C -->|"UpdateAPIKey=true, APIKey=''"| D["Clear API Key"]
  C -->|"UpdateAPIKey=false, APIKey=''"| E["Preserve Existing Key"]
  D --> F["Save to Database"]
  E --> F
Loading

File Walkthrough

Relevant files
Enhancement
index.vue
Track API key changes and send update flag                             

web/admin/src/views/settings/search_provider/index.vue

  • Added apiKeyChanged ref to track when API key input is modified
  • Added @input event listener on password field to set apiKeyChanged
    flag
  • Updated handleSave to include update_api_key flag in POST request
  • Updated handleCheckConnection to include update_api_key flag in check
    request
  • Reset apiKeyChanged flag after successful config load
  • Call loadConfig() after successful save to refresh state
+6/-0     
Bug fix
settings.go
Add update flag to distinguish key clearing intent             

internal/controller/settings.go

  • Added new SearchProviderConfigRequest struct with UpdateAPIKey boolean
    field
  • Updated SaveSearchProviderConfig to use SearchProviderConfigRequest
    instead of direct config
  • Updated CheckSearchProviderConfig to use SearchProviderConfigRequest
    instead of direct config
  • Modified key preservation logic to check !req.UpdateAPIKey &&
    req.APIKey == "" condition
  • Only preserve existing key when update flag is false AND key is empty
+15/-10 
Tests
settings_test.go
Add unit tests for search provider config logic                   

internal/controller/settings_test.go

  • Created comprehensive unit test file for settings controller
  • Tests initial save with API key
  • Tests preserving key when UpdateAPIKey=false and key is empty
  • Tests clearing key when UpdateAPIKey=true and key is empty
  • Tests updating to new key with UpdateAPIKey=true
  • Tests implicit update when key is non-empty regardless of flag
  • Includes database setup and teardown with temporary directory
+143/-0 

Introduces `update_api_key` flag in save request to distinguish between intentional clearing and preserving existing key.
Adds `apiKeyChanged` tracking in frontend to set this flag.
Adds backend unit tests for settings controller.
@google-labs-jules
Copy link
Contributor Author

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@vercel
Copy link

vercel bot commented Jan 20, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
feed-craft-admin Ready Ready Preview, Comment Jan 20, 2026 5:08am
feed-craft-doc Ready Ready Preview, Comment Jan 20, 2026 5:08am

@qodo-code-review
Copy link
Contributor

ⓘ Your approaching your monthly quota for Qodo. Upgrade your plan

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Missing audit logging: The PR adds/updates API-key save behavior but does not add any audit log entry capturing
who changed/cleared the Search Provider API key and the outcome.

Referred Code
func SaveSearchProviderConfig(c *gin.Context) {
	var req SearchProviderConfigRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, util.APIResponse[any]{Msg: err.Error()})
		return
	}

	db := util.GetDatabase()

	// Fetch existing config to handle empty APIKey
	var existingCfg config.SearchProviderConfig
	_ = dao.GetJsonSetting(db, constant.KeySearchProviderConfig, &existingCfg)

	if !req.UpdateAPIKey && req.APIKey == "" {
		req.APIKey = existingCfg.APIKey
	}

	if err := dao.SetJsonSetting(db, constant.KeySearchProviderConfig, req.SearchProviderConfig); err != nil {
		c.JSON(http.StatusInternalServerError, util.APIResponse[any]{Msg: err.Error()})
		return
	}


 ... (clipped 3 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Ignored DB read error: The code explicitly ignores the error from dao.GetJsonSetting(...), which can silently
mask database failures and lead to unexpected key-preservation behavior.

Referred Code
db := util.GetDatabase()

// Fetch existing config to handle empty APIKey
var existingCfg config.SearchProviderConfig
_ = dao.GetJsonSetting(db, constant.KeySearchProviderConfig, &existingCfg)

if !req.UpdateAPIKey && req.APIKey == "" {
	req.APIKey = existingCfg.APIKey
}

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status:
Raw errors returned: Multiple responses return err.Error() (and concatenate provider creation errors) directly
to clients, which can expose internal implementation details in user-facing error
messages.

Referred Code
if err := c.ShouldBindJSON(&req); err != nil {
	c.JSON(http.StatusBadRequest, util.APIResponse[any]{Msg: err.Error()})
	return
}

db := util.GetDatabase()

// Fetch existing config to handle empty APIKey
var existingCfg config.SearchProviderConfig
_ = dao.GetJsonSetting(db, constant.KeySearchProviderConfig, &existingCfg)

if !req.UpdateAPIKey && req.APIKey == "" {
	req.APIKey = existingCfg.APIKey
}

if err := dao.SetJsonSetting(db, constant.KeySearchProviderConfig, req.SearchProviderConfig); err != nil {
	c.JSON(http.StatusInternalServerError, util.APIResponse[any]{Msg: err.Error()})
	return
}

c.JSON(http.StatusOK, util.APIResponse[any]{Msg: "success"})


 ... (clipped 21 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Missing request validation: The new request wrapper accepts externally provided fields (including api_url and
update_api_key) without visible server-side validation or authorization checks in the
diff, requiring confirmation that upstream middleware/enforcements exist.

Referred Code
func SaveSearchProviderConfig(c *gin.Context) {
	var req SearchProviderConfigRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, util.APIResponse[any]{Msg: err.Error()})
		return
	}

	db := util.GetDatabase()

	// Fetch existing config to handle empty APIKey
	var existingCfg config.SearchProviderConfig
	_ = dao.GetJsonSetting(db, constant.KeySearchProviderConfig, &existingCfg)

	if !req.UpdateAPIKey && req.APIKey == "" {
		req.APIKey = existingCfg.APIKey
	}

	if err := dao.SetJsonSetting(db, constant.KeySearchProviderConfig, req.SearchProviderConfig); err != nil {
		c.JSON(http.StatusInternalServerError, util.APIResponse[any]{Msg: err.Error()})
		return
	}


 ... (clipped 27 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@Colin-XKL
Copy link
Owner

@CodeRabbit review

@coderabbitai
Copy link

coderabbitai bot commented Jan 20, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

1 similar comment
@coderabbitai
Copy link

coderabbitai bot commented Jan 20, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@qodo-code-review
Copy link
Contributor

ⓘ Your approaching your monthly quota for Qodo. Upgrade your plan

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Handle error when fetching settings

Handle the error returned from dao.GetJsonSetting to prevent unintentionally
clearing the APIKey if fetching the existing configuration fails.

internal/controller/settings.go [52-58]

 	// Fetch existing config to handle empty APIKey
 	var existingCfg config.SearchProviderConfig
-	_ = dao.GetJsonSetting(db, constant.KeySearchProviderConfig, &existingCfg)
+	if err := dao.GetJsonSetting(db, constant.KeySearchProviderConfig, &existingCfg); err != nil {
+		c.JSON(http.StatusInternalServerError, util.APIResponse[any]{Msg: "Failed to retrieve existing settings: " + err.Error()})
+		return
+	}
 
 	if !req.UpdateAPIKey && req.APIKey == "" {
 		req.APIKey = existingCfg.APIKey
 	}

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies an unhandled error that could lead to unintentional data loss (clearing an API key), and the proposed fix is appropriate.

Medium
General
Include HasAPIKey in GET response

In GetSearchProviderConfig, update the response to use
SearchProviderConfigResponse and include the HasAPIKey field.

internal/controller/settings.go [28]

-c.JSON(http.StatusOK, util.APIResponse[config.SearchProviderConfig]{Data: cfg})
+c.JSON(http.StatusOK, util.APIResponse[SearchProviderConfigResponse]{Data: SearchProviderConfigResponse{
+    SearchProviderConfig: cfg,
+    HasAPIKey:            cfg.APIKey != "",
+}})

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly points out a missed update in GetSearchProviderConfig that is necessary for the frontend to function correctly with the new API key handling logic.

Medium
Learned
best practice
Add request input validation

Add backend-side validation for required fields like provider and api_url (and
validate api_url format) so invalid requests fail fast even if the UI misses
checks.

internal/controller/settings.go [43-50]

 func SaveSearchProviderConfig(c *gin.Context) {
 	var req SearchProviderConfigRequest
 	if err := c.ShouldBindJSON(&req); err != nil {
 		c.JSON(http.StatusBadRequest, util.APIResponse[any]{Msg: err.Error()})
 		return
 	}
+	if strings.TrimSpace(req.Provider) == "" {
+		c.JSON(http.StatusBadRequest, util.APIResponse[any]{Msg: "provider is required"})
+		return
+	}
+	if strings.TrimSpace(req.APIUrl) == "" {
+		c.JSON(http.StatusBadRequest, util.APIResponse[any]{Msg: "api_url is required"})
+		return
+	}
+	if _, err := url.ParseRequestURI(req.APIUrl); err != nil {
+		c.JSON(http.StatusBadRequest, util.APIResponse[any]{Msg: "invalid api_url: " + err.Error()})
+		return
+	}
 
 	db := util.GetDatabase()
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why:
Relevant best practice - Validate required inputs/configuration early and fail fast with clear errors before doing work.

Low
Return correct upstream error codes

Treat upstream/provider failures as 502 Bad Gateway (not 500) and include
contextual error details; also prefer formatted/wrapped errors to preserve
context.

internal/controller/settings.go [83-93]

 prv, err := provider.Get(req.Provider, &req.SearchProviderConfig)
 if err != nil {
-	c.JSON(http.StatusInternalServerError, util.APIResponse[any]{Msg: "Failed to create provider: " + err.Error()})
+	c.JSON(http.StatusBadGateway, util.APIResponse[any]{Msg: fmt.Sprintf("create provider failed: %v", err)})
 	return
 }
 
 _, err = prv.Fetch(c.Request.Context(), "FeedCraft")
 if err != nil {
-	c.JSON(http.StatusInternalServerError, util.APIResponse[any]{Msg: "Connection check failed: " + err.Error()})
+	c.JSON(http.StatusBadGateway, util.APIResponse[any]{Msg: fmt.Sprintf("connection check failed: %v", err)})
 	return
 }
  • Apply / Chat
Suggestion importance[1-10]: 5

__

Why:
Relevant best practice - Handle failures explicitly with correct HTTP status codes and error messages that include context.

Low
  • More

@coderabbitai
Copy link

coderabbitai bot commented Jan 20, 2026

Walkthrough

This change introduces conditional API key handling for search provider configuration. A new request wrapper carries an UpdateAPIKey flag, enabling the backend to selectively preserve or update API keys based on explicit user intent. The frontend tracks API key field modifications and transmits the flag accordingly. Tests validate behavior across multiple update scenarios.

Changes

Cohort / File(s) Summary
Backend Controller Changes
internal/controller/settings.go
Introduced SearchProviderConfigRequest wrapper with UpdateAPIKey flag. Modified SaveSearchProviderConfig and CheckSearchProviderConfig handlers to conditionally preserve existing API keys when the flag is not set and the key is empty. Now returns success JSON response.
Backend Tests
internal/controller/settings_test.go
Added comprehensive test suite for SaveSearchProviderConfig endpoint with 5 scenarios validating API key preservation and updates under different UpdateAPIKey flag and key value combinations. Uses temporary SQLite database.
Frontend UI
web/admin/src/views/settings/search_provider/index.vue
Introduced apiKeyChanged reactive flag to track modifications to the API key input field. Propagates update_api_key flag in request payloads and reloads configuration after successful saves.

Sequence Diagram

sequenceDiagram
    actor User
    participant Frontend as Frontend<br/>(Vue Component)
    participant Backend as Backend<br/>(Controller)
    participant Database as Database<br/>(DAO)
    
    User->>Frontend: Modifies API key field
    Frontend->>Frontend: Sets apiKeyChanged = true
    User->>Frontend: Clicks save
    Frontend->>Backend: POST /save {UpdateAPIKey: true, APIKey: "new-key"}
    Backend->>Database: Query existing config
    Database-->>Backend: Return current config
    Backend->>Backend: Update APIKey (UpdateAPIKey = true)
    Backend->>Database: Write updated config
    Database-->>Backend: Confirm write
    Backend-->>Frontend: Return 200 success
    Frontend->>Frontend: Reload config
    Frontend-->>User: Display updated settings
Loading

Poem

🐰 A key update with careful grace,
We track when changes take their place,
Update or keep, the choice is clear,
With flags that whisper: "Change me here!"
The config dances, fresh and right,
As API keys take their flight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main fix: allowing users to clear the search provider API key, which was the core issue addressed.
Description check ✅ Passed The description thoroughly explains the problem, the solution across backend/frontend/tests, and includes diagrams and file walkthroughs that clearly relate to the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@internal/controller/settings_test.go`:
- Around line 27-32: Replace the plain os.Setenv calls with test-scoped
environment cleanup: use t.Setenv("DB_SQLITE_PATH", tmpDir) and
t.Setenv("FC_DB_SQLITE_PATH", tmpDir) (or alternatively save original values and
defer restoring them) before calling util.GetDatabase(); keep the rest of the
test (including db.AutoMigrate(&dao.SystemSetting{})) unchanged so the
environment variables are cleaned up and won't pollute other tests.
🧹 Nitpick comments (3)
internal/controller/settings_test.go (3)

85-88: Error return values from dao.GetJsonSetting are ignored in verification steps.

While the test will likely fail on the subsequent assertion if there's an error, explicitly checking errors improves debuggability.

Proposed fix for one instance (apply similarly to others)
-	dao.GetJsonSetting(db, constant.KeySearchProviderConfig, &savedCfg)
+	if err := dao.GetJsonSetting(db, constant.KeySearchProviderConfig, &savedCfg); err != nil {
+		t.Fatalf("Failed to get setting: %v", err)
+	}

Also applies to: 103-106, 121-124, 139-142


19-143: Consider table-driven tests to reduce complexity and improve maintainability.

The static analysis flagged this function for having 101 lines and cyclomatic complexity of 14. While the current structure is readable, a table-driven approach would consolidate the test cases and make it easier to add new scenarios.

Example table-driven structure
func TestSaveSearchProviderConfig(t *testing.T) {
	// ... setup code ...

	tests := []struct {
		name           string
		req            SearchProviderConfigRequest
		expectedAPIKey string
	}{
		{
			name: "Initial Save with API Key",
			req: SearchProviderConfigRequest{
				SearchProviderConfig: config.SearchProviderConfig{
					Provider: "litellm", APIKey: "initial-key", APIUrl: "http://example.com",
				},
				UpdateAPIKey: true,
			},
			expectedAPIKey: "initial-key",
		},
		// ... other test cases ...
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			w := makeRequest(tt.req)
			if w.Code != http.StatusOK {
				t.Errorf("Expected 200, got %d", w.Code)
			}
			var savedCfg config.SearchProviderConfig
			if err := dao.GetJsonSetting(db, constant.KeySearchProviderConfig, &savedCfg); err != nil {
				t.Fatalf("Failed to get setting: %v", err)
			}
			if savedCfg.APIKey != tt.expectedAPIKey {
				t.Errorf("Expected %s, got %s", tt.expectedAPIKey, savedCfg.APIKey)
			}
		})
	}
}

29-29: Singleton database pattern could cause test isolation issues if additional tests are added.

util.GetDatabase() uses sync.Once for initialization, so any subsequent tests in the same test file that call it will reuse the same database instance. While this file currently contains only one test, adding more tests to settings_test.go or other test files could cause interference and flaky tests.

Consider using a test-scoped database instance or resetting the singleton between tests if the test suite expands.

Comment on lines +27 to +32
os.Setenv("DB_SQLITE_PATH", tmpDir)
os.Setenv("FC_DB_SQLITE_PATH", tmpDir)
db := util.GetDatabase()
if err := db.AutoMigrate(&dao.SystemSetting{}); err != nil {
t.Fatalf("Failed to migrate: %v", err)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Environment variables should be cleaned up to avoid test pollution.

Using os.Setenv without t.Setenv (Go 1.17+) or manual cleanup with defer can pollute other tests running in parallel.

Proposed fix using t.Setenv
-	os.Setenv("DB_SQLITE_PATH", tmpDir)
-	os.Setenv("FC_DB_SQLITE_PATH", tmpDir)
+	t.Setenv("DB_SQLITE_PATH", tmpDir)
+	t.Setenv("FC_DB_SQLITE_PATH", tmpDir)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
os.Setenv("DB_SQLITE_PATH", tmpDir)
os.Setenv("FC_DB_SQLITE_PATH", tmpDir)
db := util.GetDatabase()
if err := db.AutoMigrate(&dao.SystemSetting{}); err != nil {
t.Fatalf("Failed to migrate: %v", err)
}
t.Setenv("DB_SQLITE_PATH", tmpDir)
t.Setenv("FC_DB_SQLITE_PATH", tmpDir)
db := util.GetDatabase()
if err := db.AutoMigrate(&dao.SystemSetting{}); err != nil {
t.Fatalf("Failed to migrate: %v", err)
}
🤖 Prompt for AI Agents
In `@internal/controller/settings_test.go` around lines 27 - 32, Replace the plain
os.Setenv calls with test-scoped environment cleanup: use
t.Setenv("DB_SQLITE_PATH", tmpDir) and t.Setenv("FC_DB_SQLITE_PATH", tmpDir) (or
alternatively save original values and defer restoring them) before calling
util.GetDatabase(); keep the rest of the test (including
db.AutoMigrate(&dao.SystemSetting{})) unchanged so the environment variables are
cleaned up and won't pollute other tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants