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"`
|
Name string `json:"name"`
|
||||||
Provider enums.AIProvider `json:"provider"`
|
Provider enums.AIProvider `json:"provider"`
|
||||||
BaseURL string `json:"baseUrl"`
|
BaseURL string `json:"baseUrl"`
|
||||||
APIKey string `json:"apiKey"`
|
HasAPIKey bool `json:"hasApiKey"`
|
||||||
ModelType enums.AIModelType `json:"modelType"`
|
ModelType enums.AIModelType `json:"modelType"`
|
||||||
ModelName string `json:"modelName"`
|
ModelName string `json:"modelName"`
|
||||||
Dimension int `json:"dimension"`
|
Dimension int `json:"dimension"`
|
||||||
@@ -51,7 +51,7 @@ func BuildAIConfigResponse(item *models.AIConfig) AIConfigResponse {
|
|||||||
Name: item.Name,
|
Name: item.Name,
|
||||||
Provider: item.Provider,
|
Provider: item.Provider,
|
||||||
BaseURL: item.BaseURL,
|
BaseURL: item.BaseURL,
|
||||||
APIKey: item.APIKey,
|
HasAPIKey: item.APIKey != "",
|
||||||
ModelType: item.ModelType,
|
ModelType: item.ModelType,
|
||||||
ModelName: item.ModelName,
|
ModelName: item.ModelName,
|
||||||
Dimension: item.Dimension,
|
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 err
|
||||||
}
|
}
|
||||||
|
|
||||||
return repositories.AIConfigRepository.Updates(sqls.DB(), req.ID, map[string]any{
|
columns := map[string]any{
|
||||||
"name": item.Name,
|
"name": item.Name,
|
||||||
"provider": item.Provider,
|
"provider": item.Provider,
|
||||||
"base_url": item.BaseURL,
|
"base_url": item.BaseURL,
|
||||||
"api_key": item.APIKey,
|
|
||||||
"model_type": item.ModelType,
|
"model_type": item.ModelType,
|
||||||
"model_name": item.ModelName,
|
"model_name": item.ModelName,
|
||||||
"dimension": item.Dimension,
|
"dimension": item.Dimension,
|
||||||
@@ -128,7 +127,11 @@ func (s *aIConfigService) UpdateAIConfig(req request.UpdateAIConfigRequest, oper
|
|||||||
"update_user_id": operator.UserID,
|
"update_user_id": operator.UserID,
|
||||||
"update_user_name": operator.Username,
|
"update_user_name": operator.Username,
|
||||||
"updated_at": time.Now(),
|
"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 {
|
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,
|
name: item.name,
|
||||||
provider: item.provider,
|
provider: item.provider,
|
||||||
baseUrl: item.baseUrl,
|
baseUrl: item.baseUrl,
|
||||||
apiKey: item.apiKey,
|
apiKey: "",
|
||||||
modelType: item.modelType,
|
modelType: item.modelType,
|
||||||
modelName: item.modelName,
|
modelName: item.modelName,
|
||||||
dimension: String(item.dimension),
|
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) {
|
function getNextStatus(item: AIConfig) {
|
||||||
return item.status === Status.Ok ? Status.Disabled : Status.Ok;
|
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="space-y-1 text-sm">
|
||||||
<div className="line-clamp-1">{item.baseUrl}</div>
|
<div className="line-clamp-1">{item.baseUrl}</div>
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
{t("aiConfig.apiKey", { key: maskAPIKey(item.apiKey) })}
|
{t("aiConfig.apiKey", { key: item.hasApiKey ? "****" : "-" })}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1181,7 +1181,7 @@ export type AIConfig = {
|
|||||||
name: string
|
name: string
|
||||||
provider: string
|
provider: string
|
||||||
baseUrl: string
|
baseUrl: string
|
||||||
apiKey: string
|
hasApiKey: boolean
|
||||||
modelType: string
|
modelType: string
|
||||||
modelName: string
|
modelName: string
|
||||||
dimension: number
|
dimension: number
|
||||||
|
|||||||
Reference in New Issue
Block a user