Files
opsx-vpn/backend/internal/store/store.go
T
Logan.Li 1e2f8ef24f feat: 全面优化 — UI/UX重构 + 流量审计与会话管理 + 协议部署
UI/UX 优化:
- 登录页分屏布局 + 可访问性修复 + 内联错误提示
- 侧栏菜单分组(专线运维/资源管理/系统) + 图标修正 + 子路由高亮
- Dashboard 统计卡渐变图标 + 空状态引导 + 统计卡可点击
- 创建向导: 依赖检测门控 + 高级折叠 + 必填校验 + WireGuard推荐标
- 专线列表: 空状态CTA + 操作列「更多」下拉 + 状态筛选 + SSE日志流
- 协议品牌SVG图标(ProtocolIcon组件)替换emoji
- 设计令牌系统(tokens.css)统一颜色/间距/圆角/阴影

功能增强:
- 在线会话管理(sessionmgr): 跨协议会话同步 + 踢下线 + 连接审计日志
- 流量配额引擎(trafficmgr): 配额检查 + 超额动作(block/warn/throttle) + 周期重置
- 异常流量告警: 突发流量检测(>90MB/15s) + 审计记录
- 节点SSH凭证管理: 密码/私钥认证 + 即时测试连接 + AES-GCM加密存储
- 协议二进制管理(binmgr): FRP/OpenVPN GitHub下载 + 系统包安装引导(apt/yum/dnf)
- 右上角修改密码弹窗 + MFA独立验证页 + 密码强度提示
- 用户管理: 批量导入 + LDAP浏览导入 + 平台用户导入VPN拨号用户
- 404路由 + IPSec对接页可达性修复
- MFA备用码持久化弹窗(替代Toast)
- 轮询可见性守卫 + ECharts resize修复

后端:
- 3张新表: VPNSession/ConnectionLog/TrafficPolicy
- monitor增强: 会话同步+配额检查+异常告警+到期审计
- changePassword统一8位+新旧不同校验
- openvpn-otp文案专业化(移除抄袭感)
- SSE日志流(token降级query参数)
2026-07-23 01:42:36 +08:00

174 lines
4.7 KiB
Go

// Package store 负责 SQLite 初始化、敏感字段 AES-GCM 加密存储
package store
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/glebarez/sqlite"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
gormlogger "gorm.io/gorm/logger"
"vpn-platform/backend/internal/model"
)
var DB *gorm.DB
var gcm cipher.AEAD
// Init 打开数据库、迁移、初始化加密密钥与默认账号
func Init(dataDir string) error {
if err := os.MkdirAll(dataDir, 0o700); err != nil {
return err
}
// WAL + busy_timeout:避免 monitor 周期写与 API 写并发时 database is locked
dsn := filepath.Join(dataDir, "vpn-platform.db") +
"?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)&_pragma=foreign_keys(1)"
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{
Logger: gormlogger.Default.LogMode(gormlogger.Warn),
})
if err != nil {
return fmt.Errorf("打开数据库失败: %w", err)
}
if sqlDB, err := db.DB(); err == nil {
sqlDB.SetMaxOpenConns(1) // SQLite 单写者
}
// 兼容旧库:VPNUser 唯一约束曾是 username 单列全局唯一
db.Exec("DROP INDEX IF EXISTS idx_vpn_user")
if err := db.AutoMigrate(&model.User{}, &model.LDAPConfig{}, &model.MFADevice{}, &model.VPNUser{}, &model.Tunnel{}, &model.Node{}, &model.Plugin{}, &model.AuditLog{}, &model.VPNSession{}, &model.ConnectionLog{}, &model.TrafficPolicy{}); err != nil {
return err
}
DB = db
if err := initKey(dataDir); err != nil {
return err
}
// 默认管理员 admin / admin123(首次登录后强制改密) // Logan.Li
var count int64
DB.Model(&model.User{}).Count(&count)
if count == 0 {
hash, _ := bcrypt.GenerateFromPassword([]byte("admin123"), bcrypt.DefaultCost)
DB.Create(&model.User{Username: "admin", Password: string(hash), Role: "admin", Source: "local", Enabled: true, MustChangePassword: true})
} else {
// 兼容早期单管理员数据:管理员升级后保留完整管理权限。
DB.Model(&model.User{}).Where("username = ? AND source = ?", "admin", "local").Update("role", "admin")
}
// 内置本机节点
var nodeCount int64
DB.Model(&model.Node{}).Count(&nodeCount)
if nodeCount == 0 {
DB.Create(&model.Node{Name: "本机节点", Address: "127.0.0.1", Port: 22, Kind: "local", Status: "verified", Remark: "平台所在服务器"})
}
return nil
}
func initKey(dataDir string) error {
keyFile := filepath.Join(dataDir, "secret.key")
raw, err := os.ReadFile(keyFile)
if err != nil {
key := make([]byte, 32)
if _, err := rand.Read(key); err != nil {
return err
}
raw = []byte(hex.EncodeToString(key))
if err := os.WriteFile(keyFile, raw, 0o600); err != nil {
return err
}
}
keyBytes, err := hex.DecodeString(string(raw))
if err != nil || len(keyBytes) != 32 {
sum := sha256.Sum256(raw)
keyBytes = sum[:]
}
block, err := aes.NewCipher(keyBytes)
if err != nil {
return err
}
g, err := cipher.NewGCM(block)
if err != nil {
return err
}
gcm = g
return nil
}
// EncryptSecret 加密敏感字符串
func EncryptSecret(plain string) (string, error) {
if plain == "" {
return "", nil
}
nonce := make([]byte, gcm.NonceSize())
if _, err := rand.Read(nonce); err != nil {
return "", err
}
out := gcm.Seal(nonce, nonce, []byte(plain), nil)
return base64.StdEncoding.EncodeToString(out), nil
}
// DecryptSecret 解密敏感字符串
func DecryptSecret(enc string) (string, error) {
if enc == "" {
return "", nil
}
raw, err := base64.StdEncoding.DecodeString(enc)
if err != nil {
return "", err
}
if len(raw) < gcm.NonceSize() {
return "", fmt.Errorf("密文长度非法")
}
plain, err := gcm.Open(nil, raw[:gcm.NonceSize()], raw[gcm.NonceSize():], nil)
if err != nil {
return "", err
}
return string(plain), nil
}
// Audit 写入操作审计
func Audit(username, action, target, detail string) {
if DB != nil {
DB.Create(&model.AuditLog{Username: username, Action: action, Target: target, Detail: detail})
}
}
// GetTunnel 按 ID 查询专线
func GetTunnel(id uint) (*model.Tunnel, error) {
var t model.Tunnel
if err := DB.Preload("Node").First(&t, id).Error; err != nil {
return nil, err
}
return &t, nil
}
// TunnelConfig 解析专线 Config JSON
func TunnelConfig(t *model.Tunnel) (map[string]interface{}, error) {
cfg := map[string]interface{}{}
if t.Config != "" {
if err := json.Unmarshal([]byte(t.Config), &cfg); err != nil {
return nil, err
}
}
return cfg, nil
}
// TunnelSecrets 解析并解密敏感字段
func TunnelSecrets(t *model.Tunnel) (map[string]interface{}, error) {
out := map[string]interface{}{}
plain, err := DecryptSecret(t.Secrets)
if err != nil || plain == "" {
return out, nil
}
_ = json.Unmarshal([]byte(plain), &out)
return out, nil
}