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:
1 parent
fb8d10231a
commit
327e282356
93 files changed
+8433
-3089
No files matched your search
@@ -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
|
||||
}
|
||||
@@ -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,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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user