refactor: replace APIKey with HasAPIKey in AIConfig and related components
This commit is contained in:
@@ -30,7 +30,7 @@ type AIConfigResponse struct {
|
||||
Name string `json:"name"`
|
||||
Provider enums.AIProvider `json:"provider"`
|
||||
BaseURL string `json:"baseUrl"`
|
||||
APIKey string `json:"apiKey"`
|
||||
HasAPIKey bool `json:"hasApiKey"`
|
||||
ModelType enums.AIModelType `json:"modelType"`
|
||||
ModelName string `json:"modelName"`
|
||||
Dimension int `json:"dimension"`
|
||||
@@ -51,7 +51,7 @@ func BuildAIConfigResponse(item *models.AIConfig) AIConfigResponse {
|
||||
Name: item.Name,
|
||||
Provider: item.Provider,
|
||||
BaseURL: item.BaseURL,
|
||||
APIKey: item.APIKey,
|
||||
HasAPIKey: item.APIKey != "",
|
||||
ModelType: item.ModelType,
|
||||
ModelName: item.ModelName,
|
||||
Dimension: item.Dimension,
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"cs-ai-agent/internal/models"
|
||||
)
|
||||
|
||||
func TestBuildAIConfigResponseOmitsAPIKey(t *testing.T) {
|
||||
payload, err := json.Marshal(BuildAIConfigResponse(&models.AIConfig{
|
||||
ID: 1,
|
||||
Name: "test",
|
||||
APIKey: "sk-secret",
|
||||
}))
|
||||
if err != nil {
|
||||
t.Fatalf("marshal response error = %v", err)
|
||||
}
|
||||
|
||||
var decoded map[string]any
|
||||
if err := json.Unmarshal(payload, &decoded); err != nil {
|
||||
t.Fatalf("unmarshal response error = %v", err)
|
||||
}
|
||||
if _, ok := decoded["apiKey"]; ok {
|
||||
t.Fatalf("apiKey should not be exposed: %s", payload)
|
||||
}
|
||||
if got, ok := decoded["hasApiKey"].(bool); !ok || !got {
|
||||
t.Fatalf("hasApiKey = %v, want true: %s", decoded["hasApiKey"], payload)
|
||||
}
|
||||
}
|
||||
@@ -110,11 +110,10 @@ func (s *aIConfigService) UpdateAIConfig(req request.UpdateAIConfigRequest, oper
|
||||
return err
|
||||
}
|
||||
|
||||
return repositories.AIConfigRepository.Updates(sqls.DB(), req.ID, map[string]any{
|
||||
columns := map[string]any{
|
||||
"name": item.Name,
|
||||
"provider": item.Provider,
|
||||
"base_url": item.BaseURL,
|
||||
"api_key": item.APIKey,
|
||||
"model_type": item.ModelType,
|
||||
"model_name": item.ModelName,
|
||||
"dimension": item.Dimension,
|
||||
@@ -128,7 +127,11 @@ func (s *aIConfigService) UpdateAIConfig(req request.UpdateAIConfigRequest, oper
|
||||
"update_user_id": operator.UserID,
|
||||
"update_user_name": operator.Username,
|
||||
"updated_at": time.Now(),
|
||||
})
|
||||
}
|
||||
if item.APIKey != "" {
|
||||
columns["api_key"] = item.APIKey
|
||||
}
|
||||
return repositories.AIConfigRepository.Updates(sqls.DB(), req.ID, columns)
|
||||
}
|
||||
|
||||
func (s *aIConfigService) DeleteAIConfig(id int64, operator *dto.AuthPrincipal) error {
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"cs-ai-agent/internal/models"
|
||||
"cs-ai-agent/internal/pkg/dto"
|
||||
"cs-ai-agent/internal/pkg/dto/request"
|
||||
"cs-ai-agent/internal/pkg/enums"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
|
||||
func TestUpdateAIConfigKeepsAPIKeyWhenRequestAPIKeyBlank(t *testing.T) {
|
||||
db := setupAIConfigServiceTestDB(t)
|
||||
item := &models.AIConfig{
|
||||
Name: "old",
|
||||
Provider: enums.AIProviderOpenAI,
|
||||
BaseURL: "https://old.example.com",
|
||||
APIKey: "sk-existing",
|
||||
ModelType: enums.AIModelTypeLLM,
|
||||
ModelName: "old-model",
|
||||
TimeoutMS: 30000,
|
||||
AuditFields: models.AuditFields{CreatedAt: time.Now(), UpdatedAt: time.Now()},
|
||||
}
|
||||
if err := db.Create(item).Error; err != nil {
|
||||
t.Fatalf("create ai config error = %v", err)
|
||||
}
|
||||
|
||||
err := AIConfigService.UpdateAIConfig(request.UpdateAIConfigRequest{
|
||||
ID: item.ID,
|
||||
CreateAIConfigRequest: request.CreateAIConfigRequest{
|
||||
Name: "new",
|
||||
Provider: enums.AIProviderOpenAI,
|
||||
BaseURL: "https://new.example.com",
|
||||
APIKey: " ",
|
||||
ModelType: enums.AIModelTypeLLM,
|
||||
ModelName: "new-model",
|
||||
TimeoutMS: 120000,
|
||||
},
|
||||
}, &dto.AuthPrincipal{UserID: 1, Username: "admin"})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateAIConfig() error = %v", err)
|
||||
}
|
||||
|
||||
var updated models.AIConfig
|
||||
if err := db.First(&updated, item.ID).Error; err != nil {
|
||||
t.Fatalf("get updated ai config error = %v", err)
|
||||
}
|
||||
if updated.APIKey != "sk-existing" {
|
||||
t.Fatalf("expected api key to be preserved, got %q", updated.APIKey)
|
||||
}
|
||||
}
|
||||
|
||||
func setupAIConfigServiceTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
|
||||
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{
|
||||
NamingStrategy: schema.NamingStrategy{
|
||||
TablePrefix: "t_",
|
||||
SingularTable: true,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite error = %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
sqlDB, err := db.DB()
|
||||
if err == nil {
|
||||
_ = sqlDB.Close()
|
||||
}
|
||||
})
|
||||
if err := db.AutoMigrate(&models.AIConfig{}); err != nil {
|
||||
t.Fatalf("auto migrate error = %v", err)
|
||||
}
|
||||
sqls.SetDB(db)
|
||||
return db
|
||||
}
|
||||
@@ -88,7 +88,7 @@ function buildForm(item: AIConfig | null): EditForm {
|
||||
name: item.name,
|
||||
provider: item.provider,
|
||||
baseUrl: item.baseUrl,
|
||||
apiKey: item.apiKey,
|
||||
apiKey: "",
|
||||
modelType: item.modelType,
|
||||
modelName: item.modelName,
|
||||
dimension: String(item.dimension),
|
||||
|
||||
@@ -78,13 +78,6 @@ function getModelTypeLabel(value: AIModelType, t: TFunction) {
|
||||
);
|
||||
}
|
||||
|
||||
function maskAPIKey(value: string) {
|
||||
const text = value.trim();
|
||||
if (!text) return "-";
|
||||
if (text.length <= 8) return "****";
|
||||
return `${text.slice(0, 4)}****${text.slice(-4)}`;
|
||||
}
|
||||
|
||||
function getNextStatus(item: AIConfig) {
|
||||
return item.status === Status.Ok ? Status.Disabled : Status.Ok;
|
||||
}
|
||||
@@ -178,7 +171,7 @@ export default function DashboardAIConfigsPage() {
|
||||
<div className="space-y-1 text-sm">
|
||||
<div className="line-clamp-1">{item.baseUrl}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t("aiConfig.apiKey", { key: maskAPIKey(item.apiKey) })}
|
||||
{t("aiConfig.apiKey", { key: item.hasApiKey ? "****" : "-" })}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
|
||||
@@ -1181,7 +1181,7 @@ export type AIConfig = {
|
||||
name: string
|
||||
provider: string
|
||||
baseUrl: string
|
||||
apiKey: string
|
||||
hasApiKey: boolean
|
||||
modelType: string
|
||||
modelName: string
|
||||
dimension: number
|
||||
|
||||
Reference in New Issue
Block a user