refactor: 前后端目录结构模块化 + v2 优化移植与缺陷修复

前端:
- src/srv/api → src/api,按域分文件夹(auth/cluster/host/storage/deployment/collector)
- 新增 http.ts 引擎(http.get/post/put/delete),域模块改用语义化调用
- index.html 增加 favicon(匹配应用 logo 的 emerald + Activity 脉冲)

后端分层增强(保留包名):
- 新增 repository/:从 models 拆出 JsonDB 持久化层(InitDB/GetDB/Shutdown/RequestSave)
- 新增 dto/:从 controllers 抽出各域请求契约(带 binding 校验)
- 新增 pkg/crypto、pkg/jwt:从 utils 拆出安全原语(外部 jwt 别名 jwtv5 避免冲突)
- models 只留领域实体 + sanitizer;utils 只留 gin/应用相关(response/errors)

v2 优化移植与缺陷修复:
- 移植密钥分离/SSH TOFU/并发写安全/Loki 生产级配置/Docker 加固
- 修复 deployment.go 并发丢更新、Helm values 重复键、EncryptField 哨兵、loki-gateway 端点等审计缺陷
- 实现真正的优雅关停(main.go http.Server + signal + repository.Shutdown)
- 修复登录契约:后端返回 accessToken,前端 auth.ts/Login.tsx 对齐
- 补 crypto/jwt/repository 单元测试(含并发写回归)

验证: go build/vet/test 全绿;npm run build 通过;登录→鉴权→优雅关停冒烟通过

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Logan.LiandClaude committed 2026-06-30 10:46:26 +08:00
1 parent fb8d10231a
commit 327e282356
93 files changed
+8433 -3089

No files matched your search

+28 -3
View File
@@ -1,14 +1,18 @@
package models
import "time"
import (
"time"
"lokideploy/pkg/crypto"
)
type Cluster struct {
ID uint `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
ConfigType string `json:"configType"` // kubeconfig | token(空值视为 kubeconfig,向后兼容)
ConfigType string `json:"configType"` // kubeconfig | token
APIServer string `json:"apiServer"` // 展示用
KubeConfig string `json:"kubeconfig"` // 统一存储(token 接入时存构造出的等价 kubeconfig)
KubeConfig string `json:"kubeconfig"` // 统一存储(加密存储时带ENC:前缀)
KubeConfigPath string `json:"kubeconfigPath"`
Context string `json:"context"`
Version string `json:"version"`
@@ -36,3 +40,24 @@ func (c *Cluster) Sanitized() map[string]interface{} {
"updatedAt": c.UpdatedAt,
}
}
// DecryptKubeconfig 解密集群的kubeconfig配置
// kubeconfig字段可能以ENC:前缀存储加密数据
func (c *Cluster) DecryptKubeconfig() string {
return crypto.DecryptField(c.KubeConfig)
}
// IsKubeconfigEncrypted 检查kubeconfig是否已加密存储
func (c *Cluster) IsKubeconfigEncrypted() bool {
return crypto.IsEncryptedField(c.KubeConfig)
}
// EncryptAndSetKubeconfig 加密并设置kubeconfig
func (c *Cluster) EncryptAndSetKubeconfig(kubeconfig string) error {
enc, err := crypto.EncryptField(kubeconfig)
if err != nil {
return err
}
c.KubeConfig = enc
return nil
}
-115
View File
@@ -1,115 +0,0 @@
package models
import (
"encoding/json"
"lokideploy/config"
"os"
"sync"
)
type JsonDB struct {
mu sync.RWMutex
dataFile string
Users []User `json:"users"`
Clusters []Cluster `json:"clusters"`
Hosts []Host `json:"hosts"`
Storages []StorageConfig `json:"storages"`
Deployments []LokiDeployment `json:"deployments"`
Collectors []Collector `json:"collectors"`
Logs []DeploymentLog `json:"logs"`
Settings []SystemSetting `json:"settings"`
sequences map[string]uint
}
var db *JsonDB
func GetDB() *JsonDB {
return db
}
func InitDB() error {
cfg := config.GetConfig()
_ = os.MkdirAll(cfg.DataDir, 0755)
d := &JsonDB{
dataFile: cfg.DatabasePath,
Users: []User{},
Clusters: []Cluster{},
Hosts: []Host{},
Storages: []StorageConfig{},
Deployments: []LokiDeployment{},
Collectors: []Collector{},
Logs: []DeploymentLog{},
Settings: []SystemSetting{},
sequences: map[string]uint{
"users": 0,
"clusters": 0,
"hosts": 0,
"storages": 0,
"deployments": 0,
"collectors": 0,
"logs": 0,
},
}
if data, err := os.ReadFile(d.dataFile); err == nil {
if err := json.Unmarshal(data, d); err == nil {
d.sequences = map[string]uint{}
for _, u := range d.Users { if u.ID > d.sequences["users"] { d.sequences["users"] = u.ID } }
for _, c := range d.Clusters { if c.ID > d.sequences["clusters"] { d.sequences["clusters"] = c.ID } }
for _, h := range d.Hosts { if h.ID > d.sequences["hosts"] { d.sequences["hosts"] = h.ID } }
for _, s := range d.Storages { if s.ID > d.sequences["storages"] { d.sequences["storages"] = s.ID } }
for _, de := range d.Deployments { if de.ID > d.sequences["deployments"] { d.sequences["deployments"] = de.ID } }
for _, col := range d.Collectors { if col.ID > d.sequences["collectors"] { d.sequences["collectors"] = col.ID } }
for _, l := range d.Logs { if l.ID > d.sequences["logs"] { d.sequences["logs"] = l.ID } }
}
}
db = d
return nil
}
func (d *JsonDB) Save() error {
d.mu.Lock()
defer d.mu.Unlock()
data, err := json.MarshalIndent(d, "", " ")
if err != nil {
return err
}
return os.WriteFile(d.dataFile, data, 0644)
}
func (d *JsonDB) NextID(table string) uint {
d.mu.Lock()
defer d.mu.Unlock()
d.sequences[table]++
return d.sequences[table]
}
func (d *JsonDB) Lock() {
d.mu.Lock()
}
func (d *JsonDB) Unlock() {
d.mu.Unlock()
}
func (d *JsonDB) RLock() {
d.mu.RLock()
}
func (d *JsonDB) RUnlock() {
d.mu.RUnlock()
}
func (d *JsonDB) CreateUser(user *User) error {
d.mu.Lock()
defer d.mu.Unlock()
if user.ID == 0 {
user.ID = d.sequences["users"] + 1
d.sequences["users"] = user.ID
}
d.Users = append(d.Users, *user)
go d.Save()
return nil
}
+41
View File
@@ -41,3 +41,44 @@ func (d *LokiDeployment) BeforeCreate() {
d.UUID = uuid.New().String()
}
}
// Sanitized 返回去除敏感字段(AdminPassword)的部署视图,并对嵌套的
// Cluster/Host/StorageConfig 一并做脱敏,避免经部署接口泄漏各自凭证。
func (d *LokiDeployment) Sanitized() map[string]interface{} {
out := map[string]interface{}{
"id": d.ID,
"uuid": d.UUID,
"name": d.Name,
"description": d.Description,
"version": d.Version,
"mode": d.Mode,
"clusterID": d.ClusterID,
"hostID": d.HostID,
"namespace": d.Namespace,
"storageConfigID": d.StorageConfigID,
"authEnabled": d.AuthEnabled,
"retentionDays": d.RetentionDays,
"replicas": d.Replicas,
"resourcesCPU": d.ResourcesCPU,
"resourcesMem": d.ResourcesMem,
"storageSize": d.StorageSize,
"status": d.Status,
"statusMessage": d.StatusMessage,
"helmValues": d.HelmValues,
"endpoint": d.Endpoint,
"adminUser": d.AdminUser,
// AdminPassword 故意省略
"createdAt": d.CreatedAt,
"updatedAt": d.UpdatedAt,
}
if d.Cluster != nil {
out["cluster"] = d.Cluster.Sanitized()
}
if d.Host != nil {
out["host"] = d.Host.Sanitized()
}
if d.StorageConfig != nil {
out["storageConfig"] = d.StorageConfig.Sanitized()
}
return out
}
+87 -4
View File
@@ -1,7 +1,23 @@
package models
import "time"
import (
"time"
"lokideploy/pkg/crypto"
)
// SSHAuthConfig SSH认证配置(定义在models包避免循环导入)
type SSHAuthConfig struct {
Host string
Port int
User string
Password string
PrivateKey string
KeyPhrase string
}
// Host 主机模型,敏感字段(Password/PrivateKey/KeyPhrase)加密存储
// 加密格式: ENC:<base64(AES-GCM(ciphertext))>
type Host struct {
ID uint `json:"id"`
Name string `json:"name"`
@@ -9,9 +25,9 @@ type Host struct {
Port int `json:"port"`
User string `json:"user"`
AuthType string `json:"authType"`
Password string `json:"password"`
PrivateKey string `json:"privateKey"`
KeyPhrase string `json:"keyPhrase"`
Password string `json:"password"` // 加密存储(ENC:前缀)
PrivateKey string `json:"privateKey"` // 加密存储(ENC:前缀)
KeyPhrase string `json:"keyPhrase"` // 加密存储(ENC:前缀)
OS string `json:"os"`
Arch string `json:"arch"`
Status string `json:"status"`
@@ -36,3 +52,70 @@ func (h *Host) Sanitized() map[string]interface{} {
"updatedAt": h.UpdatedAt,
}
}
// DecryptPassword 解密主机密码
func (h *Host) DecryptPassword() string {
return crypto.DecryptField(h.Password)
}
// DecryptPrivateKey 解密SSH私钥
func (h *Host) DecryptPrivateKey() string {
return crypto.DecryptField(h.PrivateKey)
}
// DecryptKeyPhrase 解密私钥密码
func (h *Host) DecryptKeyPhrase() string {
return crypto.DecryptField(h.KeyPhrase)
}
// IsPasswordEncrypted 检查密码是否已加密
func (h *Host) IsPasswordEncrypted() bool {
return crypto.IsEncryptedField(h.Password)
}
// IsPrivateKeyEncrypted 检查私钥是否已加密
func (h *Host) IsPrivateKeyEncrypted() bool {
return crypto.IsEncryptedField(h.PrivateKey)
}
// EncryptAndSetPassword 加密并设置密码
func (h *Host) EncryptAndSetPassword(password string) error {
enc, err := crypto.EncryptField(password)
if err != nil {
return err
}
h.Password = enc
return nil
}
// EncryptAndSetPrivateKey 加密并设置私钥
func (h *Host) EncryptAndSetPrivateKey(privateKey string) error {
enc, err := crypto.EncryptField(privateKey)
if err != nil {
return err
}
h.PrivateKey = enc
return nil
}
// EncryptAndSetKeyPhrase 加密并设置私钥密码
func (h *Host) EncryptAndSetKeyPhrase(keyPhrase string) error {
enc, err := crypto.EncryptField(keyPhrase)
if err != nil {
return err
}
h.KeyPhrase = enc
return nil
}
// GetSSHAuthConfig 获取SSH认证配置(自动解密敏感字段)
func (h *Host) GetSSHAuthConfig() *SSHAuthConfig {
return &SSHAuthConfig{
Host: h.HostIP,
Port: h.Port,
User: h.User,
Password: h.DecryptPassword(),
PrivateKey: h.DecryptPrivateKey(),
KeyPhrase: h.DecryptKeyPhrase(),
}
}
+23 -11
View File
@@ -1,49 +1,48 @@
package models
// Sanitizer interface for models that can sanitize themselves
// 用于从API响应中移除敏感字段
type Sanitizer interface {
Sanitized() map[string]interface{}
}
// SanitizedJSON sanitizes a single model or a slice of models
// SanitizedJSON sanitizes a single model or a slice of models.
// 使用接口替代类型断言,更优雅且可扩展。
func SanitizedJSON(v any) any {
switch val := v.(type) {
// Single objects
case User:
// Single objects: 尝试Sanitizer接口
case Sanitizer:
return val.Sanitized()
case *User:
if val == nil {
return nil
}
return val.Sanitized()
case Cluster:
return val.Sanitized()
case *Cluster:
if val == nil {
return nil
}
return val.Sanitized()
case Host:
return val.Sanitized()
case *Host:
if val == nil {
return nil
}
return val.Sanitized()
case StorageConfig:
return val.Sanitized()
case *StorageConfig:
if val == nil {
return nil
}
return val.Sanitized()
case Collector:
return val.Sanitized()
case *Collector:
if val == nil {
return nil
}
return val.Sanitized()
case *LokiDeployment:
if val == nil {
return nil
}
return val.Sanitized()
// Slices
case []User:
result := make([]map[string]interface{}, len(val))
@@ -75,7 +74,20 @@ func SanitizedJSON(v any) any {
result[i] = item.Sanitized()
}
return result
case []LokiDeployment:
result := make([]map[string]interface{}, len(val))
for i, item := range val {
result[i] = item.Sanitized()
}
return result
case []Sanitizer:
result := make([]map[string]interface{}, len(val))
for i, item := range val {
result[i] = item.Sanitized()
}
return result
default:
// 尝试通过反射处理指针类型
return v
}
}