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参数)
This commit is contained in:
Logan.Li committed 2026-07-23 01:42:36 +08:00
1 parent 58c036c34c
commit 1e2f8ef24f
43 files changed
+6783 -460

No files matched your search

+86 -4
View File
@@ -59,6 +59,27 @@ func (s *Service) Validate(cfg map[string]interface{}) (map[string]interface{},
for k, v := range cfg {
out[k] = v
}
// 厂商模板优先填空字段,再回落到通用默认值。
if key := service.Str(out, "vendor_template"); key != "" {
for _, tmpl := range Templates() {
if tmpl.Key != key {
continue
}
if service.Str(out, "ike_version") == "" {
out["ike_version"] = tmpl.IKEVersion
}
if service.Str(out, "ike_proposal") == "" {
out["ike_proposal"] = tmpl.IKEProposal
}
if service.Str(out, "esp_proposal") == "" {
out["esp_proposal"] = tmpl.ESPProposal
}
if service.Str(out, "dpd_action") == "" {
out["dpd_action"] = tmpl.DPDAction
}
break
}
}
if service.Str(out, "ike_version") == "" {
out["ike_version"] = "ikev2"
}
@@ -77,6 +98,12 @@ func (s *Service) Validate(cfg map[string]interface{}) (map[string]interface{},
if service.Str(out, "remote_id") == "" {
out["remote_id"] = "@vpn-remote"
}
if service.Num(out, "ike_lifetime", 0) == 0 {
out["ike_lifetime"] = 86400
}
if service.Str(out, "pfs_group") == "" {
out["pfs_group"] = "modp2048"
}
remoteGateway := service.Str(out, "remote_gateway")
// StrongSwan accepts both literal addresses and DNS names. Keep DNS support
// for peers with dynamic addresses, while rejecting syntax that can alter the
@@ -347,18 +374,73 @@ func (s *Service) Status(t *model.Tunnel) (core.Status, error) {
if bin, ok := service.LookBin("swanctl"); ok {
out, err := service.Run(bin, "--list-sas")
child := fmt.Sprintf("site-%d", t.ID)
if err == nil && strings.Contains(out, child) && strings.Contains(out, "INSTALLED") {
st := service.NowStatus(true, "running", "IKE/Child SA 已建立")
return st, nil
}
ike := fmt.Sprintf("tunnel-%d", t.ID)
if err != nil {
return service.NowStatus(false, "error", "无法读取 strongSwan SA 状态: "+err.Error()), nil
}
if strings.Contains(out, child) && strings.Contains(out, "INSTALLED") {
detail := "IKE/Child SA 已建立"
// 从 swanctl 输出中尽量提取对端地址与字节计数(格式因版本略有差异)。
if peer := extractSwanPeer(out, ike); peer != "" {
detail += ",对端 " + peer
}
if rx, tx, ok := extractSwanBytes(out, child); ok {
st := service.NowStatus(true, "running", detail)
st.RxBytes = rx
st.TxBytes = tx
return st, nil
}
return service.NowStatus(true, "running", detail), nil
}
if strings.Contains(out, ike) {
return service.NowStatus(false, "stopped", "IKE SA 存在但 Child SA 未 INSTALLED"), nil
}
return service.NowStatus(false, "stopped", "IPSec Child SA 未建立"), nil
}
return service.NowStatus(false, "undeployed", "配置已生成,但未检测到 strongSwan(swanctl),尚未部署"), nil
}
func extractSwanPeer(out, ikeName string) string {
// 典型行: tunnel-1: #1, ESTABLISHED, ... 1.2.3.4[... ]...5.6.7.8[...]
for _, line := range strings.Split(out, "\n") {
if !strings.Contains(line, ikeName) {
continue
}
fields := strings.Fields(line)
for _, f := range fields {
if strings.Contains(f, "[") && strings.Count(f, ".") >= 3 {
return strings.Split(f, "[")[0]
}
}
}
return ""
}
func extractSwanBytes(out, childName string) (rx, tx int64, ok bool) {
// 寻找含 child 名与 bytes_i/bytes_o 的行
for _, line := range strings.Split(out, "\n") {
if !strings.Contains(line, childName) {
continue
}
// 兼容 "123 bytes_i, 456 bytes_o" 与 "bytes_i/bytes_o"
parts := strings.Fields(line)
for i, p := range parts {
if strings.Contains(p, "bytes_i") && i > 0 {
fmt.Sscanf(parts[i-1], "%d", &rx)
ok = true
}
if strings.Contains(p, "bytes_o") && i > 0 {
fmt.Sscanf(parts[i-1], "%d", &tx)
ok = true
}
}
if ok {
return
}
}
return 0, 0, false
}
func (s *Service) Delete(t *model.Tunnel) error { return s.Stop(t) }
func (s *Service) unload(bin, confPath string) error {