feat: implement VPN platform management and real protocol services
This commit is contained in:
commit
58c036c34c
45 files changed
+9244
No files matched your search
@@ -0,0 +1,781 @@
|
||||
// Package api 提供 REST API 路由与处理器
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"vpn-platform/backend/internal/core"
|
||||
"vpn-platform/backend/internal/model"
|
||||
"vpn-platform/backend/internal/service"
|
||||
"vpn-platform/backend/internal/service/custom"
|
||||
"vpn-platform/backend/internal/service/ipsec"
|
||||
"vpn-platform/backend/internal/service/openvpn"
|
||||
"vpn-platform/backend/internal/service/wireguard"
|
||||
"vpn-platform/backend/internal/store"
|
||||
)
|
||||
|
||||
// Server API 服务器
|
||||
type Server struct {
|
||||
mgr *core.Manager
|
||||
}
|
||||
|
||||
func NewServer(mgr *core.Manager) *Server { return &Server{mgr: mgr} }
|
||||
|
||||
// ---- 简易 Token 会话 ----
|
||||
var (
|
||||
sessions = map[string]sessionInfo{}
|
||||
sessionsMu sync.RWMutex
|
||||
)
|
||||
|
||||
type sessionInfo struct {
|
||||
username string
|
||||
userID uint
|
||||
role string
|
||||
expireAt time.Time
|
||||
}
|
||||
|
||||
func issueToken(user *model.User) string {
|
||||
raw := make([]byte, 32)
|
||||
_, _ = rand.Read(raw)
|
||||
token := base64.RawURLEncoding.EncodeToString(raw)
|
||||
sessionsMu.Lock()
|
||||
sessions[token] = sessionInfo{username: user.Username, userID: user.ID, role: user.Role, expireAt: time.Now().Add(24 * time.Hour)}
|
||||
sessionsMu.Unlock()
|
||||
return token
|
||||
}
|
||||
|
||||
func authMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
token := c.GetHeader("Authorization")
|
||||
token = strings.TrimPrefix(token, "Bearer ")
|
||||
sessionsMu.RLock()
|
||||
sess, ok := sessions[token]
|
||||
sessionsMu.RUnlock()
|
||||
if !ok || time.Now().After(sess.expireAt) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "未登录或会话已过期"})
|
||||
return
|
||||
}
|
||||
c.Set("username", sess.username)
|
||||
c.Set("user_id", sess.userID)
|
||||
c.Set("role", sess.role)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes 注册全部路由
|
||||
func (s *Server) RegisterRoutes(r *gin.Engine) {
|
||||
api := r.Group("/api")
|
||||
api.POST("/login", s.login)
|
||||
api.GET("/health", func(c *gin.Context) { c.JSON(200, gin.H{"ok": true}) })
|
||||
|
||||
auth := api.Group("")
|
||||
auth.Use(authMiddleware())
|
||||
{
|
||||
auth.GET("/overview", s.overview)
|
||||
auth.GET("/protocols", s.listProtocols)
|
||||
auth.POST("/password", s.changePassword)
|
||||
auth.GET("/mfa/status", s.mfaStatus)
|
||||
auth.POST("/mfa/setup", s.mfaSetup)
|
||||
auth.POST("/mfa/enable", s.mfaEnable)
|
||||
auth.POST("/mfa/disable", s.mfaDisable)
|
||||
|
||||
admin := auth.Group("")
|
||||
admin.Use(requireRole("admin"))
|
||||
admin.GET("/users", s.listUsers)
|
||||
admin.POST("/users", s.createUser)
|
||||
admin.PUT("/users/:id", s.updateUser)
|
||||
admin.DELETE("/users/:id", s.deleteUser)
|
||||
admin.GET("/ldap", s.getLDAPConfig)
|
||||
admin.PUT("/ldap", s.saveLDAPConfig)
|
||||
admin.POST("/ldap/test", s.testLDAP)
|
||||
admin.GET("/vpn-users", s.listVPNUsers)
|
||||
admin.POST("/vpn-users", s.createVPNUser)
|
||||
admin.PUT("/vpn-users/:id", s.updateVPNUser)
|
||||
admin.DELETE("/vpn-users/:id", s.deleteVPNUser)
|
||||
admin.POST("/tunnels/:id/openvpn/pki", s.initializeOpenVPNPKI)
|
||||
admin.GET("/vpn-users/:id/config", s.downloadVPNUserConfig)
|
||||
admin.GET("/tunnels/:id/wireguard/peers/:peer_id/config", s.downloadWireGuardPeerConfig)
|
||||
|
||||
auth.GET("/tunnels", s.listTunnels)
|
||||
auth.POST("/tunnels", s.createTunnel)
|
||||
auth.GET("/tunnels/:id", s.getTunnel)
|
||||
auth.PUT("/tunnels/:id", s.updateTunnel)
|
||||
auth.DELETE("/tunnels/:id", s.deleteTunnel)
|
||||
auth.POST("/tunnels/:id/start", s.startTunnel)
|
||||
auth.POST("/tunnels/:id/stop", s.stopTunnel)
|
||||
auth.GET("/tunnels/:id/config", s.downloadConfig)
|
||||
auth.GET("/tunnels/:id/status", s.tunnelStatus)
|
||||
auth.POST("/tunnels/:id/test", s.testConnectivity)
|
||||
|
||||
auth.GET("/ipsec/templates", s.ipsecTemplates)
|
||||
auth.POST("/ipsec/compare", s.ipsecCompare)
|
||||
|
||||
auth.GET("/nodes", s.listNodes)
|
||||
auth.POST("/nodes", s.createNode)
|
||||
auth.DELETE("/nodes/:id", s.deleteNode)
|
||||
|
||||
auth.GET("/plugins", s.listPlugins)
|
||||
auth.POST("/plugins", s.createPlugin)
|
||||
auth.PUT("/plugins/:id", s.updatePlugin)
|
||||
auth.DELETE("/plugins/:id", s.deletePlugin)
|
||||
|
||||
auth.GET("/audit", s.listAudit)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) initializeOpenVPNPKI(c *gin.Context) {
|
||||
t, err := s.mustTunnel(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if t.Protocol != "openvpn" {
|
||||
c.JSON(400, gin.H{"error": "仅 OpenVPN 隧道支持 PKI 初始化"})
|
||||
return
|
||||
}
|
||||
if err := openvpn.New().InitializePKI(t); err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
store.Audit(c.GetString("username"), "initialize_openvpn_pki", t.Name, "")
|
||||
c.JSON(200, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
func (s *Server) downloadVPNUserConfig(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": "非法 ID"})
|
||||
return
|
||||
}
|
||||
var user model.VPNUser
|
||||
if err := store.DB.First(&user, id).Error; err != nil {
|
||||
c.JSON(404, gin.H{"error": "拨号用户不存在"})
|
||||
return
|
||||
}
|
||||
t, err := store.GetTunnel(user.TunnelID)
|
||||
if err != nil {
|
||||
c.JSON(404, gin.H{"error": "OpenVPN 隧道不存在"})
|
||||
return
|
||||
}
|
||||
if t.Protocol != "openvpn" {
|
||||
c.JSON(400, gin.H{"error": "拨号用户不属于 OpenVPN 隧道"})
|
||||
return
|
||||
}
|
||||
conf, err := openvpn.New().GenerateUserConfig(t, &user)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=openvpn-%s.ovpn", user.Username))
|
||||
c.Data(200, "application/x-openvpn-profile", []byte(conf))
|
||||
}
|
||||
|
||||
func (s *Server) downloadWireGuardPeerConfig(c *gin.Context) {
|
||||
t, err := s.mustTunnel(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if t.Protocol != "wireguard" {
|
||||
c.JSON(400, gin.H{"error": "仅 WireGuard 隧道支持 Peer 配置"})
|
||||
return
|
||||
}
|
||||
conf, err := wireguard.New().GeneratePeerClientConfig(t, c.Param("peer_id"))
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=wireguard-%s-%s.conf", t.Name, c.Param("peer_id")))
|
||||
c.Data(200, "text/plain; charset=utf-8", []byte(conf))
|
||||
}
|
||||
|
||||
// ---- 认证 ----
|
||||
|
||||
func (s *Server) login(c *gin.Context) {
|
||||
var req struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
MFACode string `json:"mfa_code"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": "参数错误"})
|
||||
return
|
||||
}
|
||||
user, err := s.authenticateUser(req.Username, req.Password)
|
||||
if err != nil {
|
||||
c.JSON(401, gin.H{"error": "用户名或密码错误"})
|
||||
return
|
||||
}
|
||||
if !user.Enabled {
|
||||
c.JSON(403, gin.H{"error": "用户已禁用"})
|
||||
return
|
||||
}
|
||||
if err := verifyMFA(user, req.MFACode); err != nil {
|
||||
c.JSON(401, gin.H{"error": err.Error(), "mfa_required": true})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"token": issueToken(user), "username": user.Username, "role": user.Role})
|
||||
}
|
||||
|
||||
func (s *Server) changePassword(c *gin.Context) {
|
||||
var req struct {
|
||||
Old string `json:"old_password"`
|
||||
New string `json:"new_password"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil || len(req.New) < 6 {
|
||||
c.JSON(400, gin.H{"error": "新密码至少 6 位"})
|
||||
return
|
||||
}
|
||||
username := c.GetString("username")
|
||||
var user model.User
|
||||
if err := store.DB.Where("username = ?", username).First(&user).Error; err != nil {
|
||||
c.JSON(404, gin.H{"error": "用户不存在"})
|
||||
return
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Old)); err != nil {
|
||||
c.JSON(400, gin.H{"error": "原密码错误"})
|
||||
return
|
||||
}
|
||||
hash, _ := bcrypt.GenerateFromPassword([]byte(req.New), bcrypt.DefaultCost)
|
||||
store.DB.Model(&user).Update("password", string(hash))
|
||||
store.Audit(username, "change_password", username, "修改登录密码")
|
||||
c.JSON(200, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
// ---- 总览 ----
|
||||
|
||||
func (s *Server) overview(c *gin.Context) {
|
||||
var total, running int64
|
||||
store.DB.Model(&model.Tunnel{}).Count(&total)
|
||||
store.DB.Model(&model.Tunnel{}).Where("state = ?", "running").Count(&running)
|
||||
var nodeCount, pluginCount int64
|
||||
store.DB.Model(&model.Node{}).Count(&nodeCount)
|
||||
store.DB.Model(&model.Plugin{}).Where("enabled = ?", true).Count(&pluginCount)
|
||||
|
||||
// 各协议数量分布
|
||||
type protoCount struct {
|
||||
Protocol string
|
||||
Count int64
|
||||
}
|
||||
var dist []protoCount
|
||||
store.DB.Model(&model.Tunnel{}).Select("protocol, count(*) as count").Group("protocol").Scan(&dist)
|
||||
|
||||
// 总流量
|
||||
var sum struct {
|
||||
Rx int64
|
||||
Tx int64
|
||||
}
|
||||
store.DB.Model(&model.Tunnel{}).Select("COALESCE(SUM(rx_bytes),0) as rx, COALESCE(SUM(tx_bytes),0) as tx").Scan(&sum)
|
||||
|
||||
// 即将到期(7 天内)
|
||||
var expiring []model.Tunnel
|
||||
week := time.Now().Add(7 * 24 * time.Hour)
|
||||
store.DB.Where("expire_at IS NOT NULL AND expire_at < ?", week).Order("expire_at asc").Limit(5).Find(&expiring)
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"total": total, "running": running, "nodes": nodeCount, "plugins": pluginCount,
|
||||
"rx_bytes": sum.Rx, "tx_bytes": sum.Tx,
|
||||
"protocol_distribution": dist, "expiring": expiring,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) listProtocols(c *gin.Context) {
|
||||
type item struct {
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Available bool `json:"available"`
|
||||
Dependency string `json:"dependency"`
|
||||
Custom bool `json:"custom"`
|
||||
}
|
||||
var out []item
|
||||
for _, p := range s.mgr.List() {
|
||||
ok, dep := p.Available()
|
||||
out = append(out, item{p.Name(), p.DisplayName(), ok, dep, strings.HasPrefix(p.Name(), "custom:")})
|
||||
}
|
||||
c.JSON(200, out)
|
||||
}
|
||||
|
||||
// ---- 专线管理 ----
|
||||
|
||||
func (s *Server) listTunnels(c *gin.Context) {
|
||||
var tunnels []model.Tunnel
|
||||
q := store.DB.Preload("Node").Order("id desc")
|
||||
if p := c.Query("protocol"); p != "" {
|
||||
q = q.Where("protocol = ?", p)
|
||||
}
|
||||
if kw := c.Query("keyword"); kw != "" {
|
||||
q = q.Where("name LIKE ?", "%"+kw+"%")
|
||||
}
|
||||
q.Find(&tunnels)
|
||||
// 附带实时状态
|
||||
type resp struct {
|
||||
model.Tunnel
|
||||
Runtime *core.Status `json:"runtime,omitempty"`
|
||||
}
|
||||
out := make([]resp, 0, len(tunnels))
|
||||
for _, t := range tunnels {
|
||||
r := resp{Tunnel: t}
|
||||
if st, ok := s.mgr.GetStatus(t.ID); ok {
|
||||
r.Runtime = &st
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
c.JSON(200, out)
|
||||
}
|
||||
|
||||
type tunnelReq struct {
|
||||
Name string `json:"name"`
|
||||
Protocol string `json:"protocol"`
|
||||
Remark string `json:"remark"`
|
||||
Config map[string]interface{} `json:"config"`
|
||||
AutoStart bool `json:"auto_start"`
|
||||
ExpireAt *time.Time `json:"expire_at"`
|
||||
NodeID *uint `json:"node_id"`
|
||||
}
|
||||
|
||||
func (s *Server) createTunnel(c *gin.Context) {
|
||||
var req tunnelReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": "参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
if req.Name == "" || req.Protocol == "" {
|
||||
c.JSON(400, gin.H{"error": "名称与协议类型必填"})
|
||||
return
|
||||
}
|
||||
p, err := s.mgr.Get(req.Protocol)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
cfg, err := p.Validate(req.Config)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
t := model.Tunnel{
|
||||
Name: req.Name, Protocol: req.Protocol, Remark: req.Remark,
|
||||
State: "stopped", AutoStart: req.AutoStart, ExpireAt: req.ExpireAt, NodeID: req.NodeID,
|
||||
}
|
||||
// 先创建不含配置的记录以取得 ID;敏感字段只在内存中交给协议层,
|
||||
// 由 SaveTunnelConfig 拆分并加密后才会写入数据库。
|
||||
t.Config = "{}"
|
||||
if err := store.DB.Create(&t).Error; err != nil {
|
||||
c.JSON(500, gin.H{"error": "保存失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
raw, _ := json.Marshal(cfg)
|
||||
t.Config = string(raw)
|
||||
if err := p.Create(&t); err != nil {
|
||||
store.DB.Delete(&t)
|
||||
_ = service.RemoveInstance(&t)
|
||||
c.JSON(500, gin.H{"error": "生成配置失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
username := c.GetString("username")
|
||||
store.Audit(username, "create_tunnel", fmt.Sprintf("%s(%s)", t.Name, t.Protocol), "")
|
||||
if t.AutoStart {
|
||||
if err := p.Start(&t); err != nil {
|
||||
t.LastError = err.Error()
|
||||
t.State = "error"
|
||||
} else {
|
||||
t.State = "running"
|
||||
}
|
||||
store.DB.Save(&t)
|
||||
}
|
||||
c.JSON(200, t)
|
||||
}
|
||||
|
||||
func (s *Server) getTunnel(c *gin.Context) {
|
||||
t, err := s.mustTunnel(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// 客户端配置预览
|
||||
var clientConf string
|
||||
if p, err := s.mgr.Get(t.Protocol); err == nil {
|
||||
clientConf, _ = p.GenerateClientConfig(t)
|
||||
}
|
||||
c.JSON(200, gin.H{"tunnel": t, "client_config": clientConf})
|
||||
}
|
||||
|
||||
func (s *Server) updateTunnel(c *gin.Context) {
|
||||
t, err := s.mustTunnel(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var req tunnelReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": "参数错误"})
|
||||
return
|
||||
}
|
||||
var (
|
||||
p core.Protocol
|
||||
newRaw string
|
||||
hasCfg = req.Config != nil
|
||||
wasRunning = t.State == "running"
|
||||
)
|
||||
if hasCfg {
|
||||
var err error
|
||||
p, err = s.mgr.Get(t.Protocol)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
cfg, err := p.Validate(req.Config)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
raw, _ := json.Marshal(cfg)
|
||||
newRaw = string(raw)
|
||||
if wasRunning {
|
||||
if err := p.Stop(t); err != nil {
|
||||
c.JSON(500, gin.H{"error": "停止旧配置失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
if req.Name != "" {
|
||||
t.Name = req.Name
|
||||
}
|
||||
t.Remark = req.Remark
|
||||
t.AutoStart = req.AutoStart
|
||||
t.ExpireAt = req.ExpireAt
|
||||
// 不通过 Save(t) 写入 Config,避免更新请求中的密钥明文落库。
|
||||
if err := store.DB.Model(t).Updates(map[string]interface{}{
|
||||
"name": t.Name, "remark": t.Remark, "auto_start": t.AutoStart, "expire_at": t.ExpireAt,
|
||||
}).Error; err != nil {
|
||||
c.JSON(500, gin.H{"error": "保存失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
if hasCfg {
|
||||
t.Config = newRaw
|
||||
if err := p.Create(t); err != nil {
|
||||
c.JSON(500, gin.H{"error": "生成配置失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
if wasRunning {
|
||||
if err := p.Start(t); err != nil {
|
||||
t.State = "error"
|
||||
t.LastError = err.Error()
|
||||
} else {
|
||||
t.State = "running"
|
||||
t.LastError = ""
|
||||
}
|
||||
store.DB.Model(t).Updates(map[string]interface{}{"state": t.State, "last_error": t.LastError})
|
||||
}
|
||||
}
|
||||
store.Audit(c.GetString("username"), "update_tunnel", t.Name, "")
|
||||
c.JSON(200, t)
|
||||
}
|
||||
|
||||
func (s *Server) deleteTunnel(c *gin.Context) {
|
||||
t, err := s.mustTunnel(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if p, err := s.mgr.Get(t.Protocol); err == nil {
|
||||
_ = p.Delete(t)
|
||||
}
|
||||
if err := service.RemoveInstance(t); err != nil && !os.IsNotExist(err) {
|
||||
c.JSON(500, gin.H{"error": "清理实例文件失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
s.mgr.RemoveStatus(t.ID)
|
||||
store.DB.Delete(t)
|
||||
store.Audit(c.GetString("username"), "delete_tunnel", t.Name, "")
|
||||
c.JSON(200, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
func (s *Server) startTunnel(c *gin.Context) {
|
||||
t, err := s.mustTunnel(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if t.ExpireAt != nil && time.Now().After(*t.ExpireAt) {
|
||||
c.JSON(400, gin.H{"error": "专线已到期,请先调整到期时间"})
|
||||
return
|
||||
}
|
||||
p, err := s.mgr.Get(t.Protocol)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if t.State == "running" {
|
||||
c.JSON(200, gin.H{"ok": true, "state": "running", "message": "专线已在运行"})
|
||||
return
|
||||
}
|
||||
if err := p.Start(t); err != nil {
|
||||
t.State = "error"
|
||||
t.LastError = err.Error()
|
||||
store.DB.Save(t)
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
t.State = "running"
|
||||
t.LastError = ""
|
||||
store.DB.Save(t)
|
||||
store.Audit(c.GetString("username"), "start_tunnel", t.Name, "")
|
||||
c.JSON(200, gin.H{"ok": true, "state": "running"})
|
||||
}
|
||||
|
||||
func (s *Server) stopTunnel(c *gin.Context) {
|
||||
t, err := s.mustTunnel(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
p, err := s.mgr.Get(t.Protocol)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := p.Stop(t); err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
t.State = "stopped"
|
||||
store.DB.Save(t)
|
||||
store.Audit(c.GetString("username"), "stop_tunnel", t.Name, "")
|
||||
c.JSON(200, gin.H{"ok": true, "state": "stopped"})
|
||||
}
|
||||
|
||||
func (s *Server) downloadConfig(c *gin.Context) {
|
||||
t, err := s.mustTunnel(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
p, err := s.mgr.Get(t.Protocol)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
conf, err := p.GenerateClientConfig(t)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
ext := "conf"
|
||||
switch t.Protocol {
|
||||
case "openvpn":
|
||||
ext = "ovpn"
|
||||
case "frp":
|
||||
ext = "toml"
|
||||
case "ipsec", "l2tp":
|
||||
ext = "txt"
|
||||
}
|
||||
filename := fmt.Sprintf("%s-%s.%s", t.Protocol, strings.ReplaceAll(t.Name, " ", "_"), ext)
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
|
||||
c.Data(200, "text/plain; charset=utf-8", []byte(conf))
|
||||
}
|
||||
|
||||
func (s *Server) tunnelStatus(c *gin.Context) {
|
||||
t, err := s.mustTunnel(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
p, err := s.mgr.Get(t.Protocol)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
st, err := p.Status(t)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, st)
|
||||
}
|
||||
|
||||
// testConnectivity 一键测试与对端网关的连通性
|
||||
func (s *Server) testConnectivity(c *gin.Context) {
|
||||
t, err := s.mustTunnel(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cfg, err := store.TunnelConfig(t)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
target := service.Str(cfg, "remote_gateway")
|
||||
if target == "" {
|
||||
target = service.Str(cfg, "server_addr")
|
||||
}
|
||||
if target == "" {
|
||||
target = service.Str(cfg, "public_endpoint")
|
||||
}
|
||||
if host, _, err := net.SplitHostPort(target); err == nil {
|
||||
target = host
|
||||
}
|
||||
if target == "" {
|
||||
c.JSON(400, gin.H{"error": "该专线未配置对端地址,无法测试"})
|
||||
return
|
||||
}
|
||||
result := map[string]interface{}{"target": target}
|
||||
start := time.Now()
|
||||
// ICMP 不可用时退化为 TCP 常见端口探测
|
||||
conn, err := net.DialTimeout("tcp", net.JoinHostPort(target, "500"), 3*time.Second)
|
||||
if err == nil {
|
||||
conn.Close()
|
||||
result["ike_udp500_note"] = "TCP 探测通过(仅供参考,IPSec 实际走 UDP 500/4500)"
|
||||
result["reachable"] = true
|
||||
} else {
|
||||
conn2, err2 := net.DialTimeout("tcp", net.JoinHostPort(target, "443"), 3*time.Second)
|
||||
if err2 == nil {
|
||||
conn2.Close()
|
||||
result["reachable"] = true
|
||||
result["note"] = "对端可达(TCP 443)"
|
||||
} else {
|
||||
result["reachable"] = false
|
||||
result["note"] = "TCP 探测未通过,可能防火墙仅放行 UDP,建议结合 IPSec SA 状态判断"
|
||||
}
|
||||
}
|
||||
result["latency_ms"] = time.Since(start).Milliseconds()
|
||||
store.Audit(c.GetString("username"), "test_connectivity", t.Name, fmt.Sprintf("target=%s", target))
|
||||
c.JSON(200, result)
|
||||
}
|
||||
|
||||
// ---- IPSec 专用 ----
|
||||
|
||||
func (s *Server) ipsecTemplates(c *gin.Context) {
|
||||
c.JSON(200, ipsec.Templates())
|
||||
}
|
||||
|
||||
// ipsecCompare 双方参数一致性对比检查
|
||||
func (s *Server) ipsecCompare(c *gin.Context) {
|
||||
var req struct {
|
||||
Local map[string]interface{} `json:"local"`
|
||||
Peer map[string]interface{} `json:"peer"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": "参数错误"})
|
||||
return
|
||||
}
|
||||
diffs := ipsec.ComparePeerParams(req.Local, req.Peer)
|
||||
c.JSON(200, gin.H{"consistent": len(diffs) == 0, "differences": diffs})
|
||||
}
|
||||
|
||||
// ---- 节点管理 ----
|
||||
|
||||
func (s *Server) listNodes(c *gin.Context) {
|
||||
var nodes []model.Node
|
||||
store.DB.Find(&nodes)
|
||||
c.JSON(200, nodes)
|
||||
}
|
||||
|
||||
func (s *Server) createNode(c *gin.Context) {
|
||||
var n model.Node
|
||||
if err := c.ShouldBindJSON(&n); err != nil || n.Name == "" {
|
||||
c.JSON(400, gin.H{"error": "节点名称必填"})
|
||||
return
|
||||
}
|
||||
n.ID = 0
|
||||
store.DB.Create(&n)
|
||||
store.Audit(c.GetString("username"), "create_node", n.Name, "")
|
||||
c.JSON(200, n)
|
||||
}
|
||||
|
||||
func (s *Server) deleteNode(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
store.DB.Delete(&model.Node{}, id)
|
||||
c.JSON(200, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
// ---- 自定义协议插件 ----
|
||||
|
||||
func (s *Server) listPlugins(c *gin.Context) {
|
||||
var plugins []model.Plugin
|
||||
store.DB.Find(&plugins)
|
||||
c.JSON(200, plugins)
|
||||
}
|
||||
|
||||
func (s *Server) createPlugin(c *gin.Context) {
|
||||
var p model.Plugin
|
||||
if err := c.ShouldBindJSON(&p); err != nil || p.Name == "" || p.StartHook == "" {
|
||||
c.JSON(400, gin.H{"error": "插件标识与 start 钩子必填"})
|
||||
return
|
||||
}
|
||||
if strings.ContainsAny(p.Name, " /;|&") {
|
||||
c.JSON(400, gin.H{"error": "插件标识不能包含特殊字符"})
|
||||
return
|
||||
}
|
||||
p.ID = 0
|
||||
if p.DisplayName == "" {
|
||||
p.DisplayName = p.Name
|
||||
}
|
||||
if err := store.DB.Create(&p).Error; err != nil {
|
||||
c.JSON(400, gin.H{"error": "插件标识已存在"})
|
||||
return
|
||||
}
|
||||
if p.Enabled {
|
||||
s.mgr.Register(custom.New(&p))
|
||||
}
|
||||
store.Audit(c.GetString("username"), "create_plugin", p.Name, "")
|
||||
c.JSON(200, p)
|
||||
}
|
||||
|
||||
func (s *Server) updatePlugin(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var p model.Plugin
|
||||
if err := store.DB.First(&p, id).Error; err != nil {
|
||||
c.JSON(404, gin.H{"error": "插件不存在"})
|
||||
return
|
||||
}
|
||||
var req model.Plugin
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": "参数错误"})
|
||||
return
|
||||
}
|
||||
p.DisplayName = req.DisplayName
|
||||
p.Description = req.Description
|
||||
p.CreateHook, p.StartHook, p.StopHook = req.CreateHook, req.StartHook, req.StopHook
|
||||
p.StatusHook, p.ConfigHook = req.StatusHook, req.ConfigHook
|
||||
p.Enabled = req.Enabled
|
||||
store.DB.Save(&p)
|
||||
if p.Enabled {
|
||||
s.mgr.Register(custom.New(&p))
|
||||
}
|
||||
c.JSON(200, p)
|
||||
}
|
||||
|
||||
func (s *Server) deletePlugin(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
store.DB.Delete(&model.Plugin{}, id)
|
||||
c.JSON(200, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
// ---- 审计 ----
|
||||
|
||||
func (s *Server) listAudit(c *gin.Context) {
|
||||
var logs []model.AuditLog
|
||||
store.DB.Order("id desc").Limit(100).Find(&logs)
|
||||
c.JSON(200, logs)
|
||||
}
|
||||
|
||||
// ---- 工具 ----
|
||||
|
||||
func (s *Server) mustTunnel(c *gin.Context) (*model.Tunnel, error) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": "非法 ID"})
|
||||
return nil, err
|
||||
}
|
||||
t, err := store.GetTunnel(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(404, gin.H{"error": "专线不存在"})
|
||||
return nil, err
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"vpn-platform/backend/internal/model"
|
||||
"vpn-platform/backend/internal/store"
|
||||
)
|
||||
|
||||
type pendingMFA struct {
|
||||
secret string
|
||||
backups []string
|
||||
expires time.Time
|
||||
}
|
||||
|
||||
var (
|
||||
pendingMFAMu sync.Mutex
|
||||
pendingMFAs = map[uint]pendingMFA{}
|
||||
)
|
||||
|
||||
func requireRole(roles ...string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
role := c.GetString("role")
|
||||
for _, allowed := range roles {
|
||||
if role == allowed {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
}
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "权限不足"})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) authenticateUser(username, password string) (*model.User, error) {
|
||||
var user model.User
|
||||
err := store.DB.Where("username = ?", username).First(&user).Error
|
||||
if err == nil && user.Source != "ldap" {
|
||||
if bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil {
|
||||
return &user, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid password")
|
||||
}
|
||||
if user.Source == "ldap" || err != nil {
|
||||
return s.authenticateLDAP(username, password)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func ldapSettings() (*model.LDAPConfig, error) {
|
||||
var cfg model.LDAPConfig
|
||||
if err := store.DB.First(&cfg).Error; err != nil || !cfg.Enabled {
|
||||
return nil, fmt.Errorf("LDAP 未启用")
|
||||
}
|
||||
plain, err := store.DecryptSecret(cfg.BindPassword)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("LDAP 配置密钥无效")
|
||||
}
|
||||
cfg.BindPassword = plain
|
||||
if cfg.UsernameAttribute == "" {
|
||||
cfg.UsernameAttribute = "uid"
|
||||
}
|
||||
if cfg.UserFilter == "" {
|
||||
cfg.UserFilter = "(objectClass=inetOrgPerson)"
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
func ldapConnect(cfg *model.LDAPConfig) (*ldap.Conn, error) {
|
||||
url := cfg.URL
|
||||
if !strings.HasPrefix(url, "ldap://") && !strings.HasPrefix(url, "ldaps://") {
|
||||
url = "ldap://" + url
|
||||
}
|
||||
conn, err := ldap.DialURL(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cfg.BindDN != "" {
|
||||
if err := conn.Bind(cfg.BindDN, cfg.BindPassword); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (s *Server) authenticateLDAP(username, password string) (*model.User, error) {
|
||||
cfg, err := ldapSettings()
|
||||
if err != nil || password == "" {
|
||||
return nil, fmt.Errorf("LDAP 认证不可用")
|
||||
}
|
||||
conn, err := ldapConnect(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
filter := fmt.Sprintf("(&%s(%s=%s))", cfg.UserFilter, cfg.UsernameAttribute, ldap.EscapeFilter(username))
|
||||
result, err := conn.Search(ldap.NewSearchRequest(cfg.BaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 5, false, filter, []string{"dn", cfg.UsernameAttribute, "cn", "mail"}, nil))
|
||||
if err != nil || len(result.Entries) != 1 {
|
||||
return nil, fmt.Errorf("LDAP 用户不存在")
|
||||
}
|
||||
entry := result.Entries[0]
|
||||
if err := conn.Bind(entry.DN, password); err != nil {
|
||||
return nil, fmt.Errorf("LDAP 密码错误")
|
||||
}
|
||||
var user model.User
|
||||
err = store.DB.Where("username = ?", username).First(&user).Error
|
||||
if err != nil {
|
||||
user = model.User{Username: username, Email: entry.GetAttributeValue("mail"), Source: "ldap", Role: "viewer", Enabled: true}
|
||||
if err := store.DB.Create(&user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func verifyMFA(user *model.User, code string) error {
|
||||
var device model.MFADevice
|
||||
if err := store.DB.Where("user_id = ? AND enabled = ?", user.ID, true).First(&device).Error; err != nil {
|
||||
return nil
|
||||
}
|
||||
secret, err := store.DecryptSecret(device.Secret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("MFA 配置无效")
|
||||
}
|
||||
if totp.Validate(strings.TrimSpace(code), secret) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("需要有效的 MFA 验证码")
|
||||
}
|
||||
|
||||
func (s *Server) mfaStatus(c *gin.Context) {
|
||||
var device model.MFADevice
|
||||
err := store.DB.Where("user_id = ?", c.GetUint("user_id")).First(&device).Error
|
||||
c.JSON(200, gin.H{"enabled": err == nil && device.Enabled})
|
||||
}
|
||||
|
||||
func backupCodes() []string {
|
||||
codes := make([]string, 10)
|
||||
for i := range codes {
|
||||
raw := make([]byte, 5)
|
||||
_, _ = rand.Read(raw)
|
||||
codes[i] = fmt.Sprintf("%X", raw)
|
||||
}
|
||||
return codes
|
||||
}
|
||||
|
||||
func (s *Server) mfaSetup(c *gin.Context) {
|
||||
var user model.User
|
||||
if err := store.DB.First(&user, c.GetUint("user_id")).Error; err != nil {
|
||||
c.JSON(404, gin.H{"error": "用户不存在"})
|
||||
return
|
||||
}
|
||||
key, err := totp.Generate(totp.GenerateOpts{Issuer: "OPSX VPN", AccountName: user.Username})
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
pendingMFAMu.Lock()
|
||||
pendingMFAs[user.ID] = pendingMFA{secret: key.Secret(), backups: backupCodes(), expires: time.Now().Add(15 * time.Minute)}
|
||||
pendingMFAMu.Unlock()
|
||||
c.JSON(200, gin.H{"otpauth_url": key.URL(), "secret": key.Secret(), "expires_in": 900})
|
||||
}
|
||||
|
||||
func (s *Server) mfaEnable(c *gin.Context) {
|
||||
var req struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
c.JSON(400, gin.H{"error": "参数错误"})
|
||||
return
|
||||
}
|
||||
userID := c.GetUint("user_id")
|
||||
pendingMFAMu.Lock()
|
||||
pending, ok := pendingMFAs[userID]
|
||||
delete(pendingMFAs, userID)
|
||||
pendingMFAMu.Unlock()
|
||||
if !ok || time.Now().After(pending.expires) || !totp.Validate(req.Code, pending.secret) {
|
||||
c.JSON(400, gin.H{"error": "验证码无效或设置已过期"})
|
||||
return
|
||||
}
|
||||
secret, _ := store.EncryptSecret(pending.secret)
|
||||
raw, _ := json.Marshal(pending.backups)
|
||||
backups, _ := store.EncryptSecret(string(raw))
|
||||
device := model.MFADevice{UserID: userID, Secret: secret, BackupCodes: backups, Enabled: true}
|
||||
store.DB.Where("user_id = ?", userID).Assign(device).FirstOrCreate(&device)
|
||||
store.Audit(c.GetString("username"), "enable_mfa", c.GetString("username"), "")
|
||||
c.JSON(200, gin.H{"ok": true, "backup_codes": pending.backups})
|
||||
}
|
||||
|
||||
func (s *Server) mfaDisable(c *gin.Context) {
|
||||
store.DB.Where("user_id = ?", c.GetUint("user_id")).Delete(&model.MFADevice{})
|
||||
store.Audit(c.GetString("username"), "disable_mfa", c.GetString("username"), "")
|
||||
c.JSON(200, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
func (s *Server) listUsers(c *gin.Context) {
|
||||
var users []model.User
|
||||
store.DB.Order("id").Find(&users)
|
||||
c.JSON(200, users)
|
||||
}
|
||||
|
||||
func (s *Server) createUser(c *gin.Context) {
|
||||
var req struct {
|
||||
Username, Password, Email, Role string
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
if c.ShouldBindJSON(&req) != nil || req.Username == "" || len(req.Password) < 8 {
|
||||
c.JSON(400, gin.H{"error": "用户名及至少 8 位的密码必填"})
|
||||
return
|
||||
}
|
||||
if req.Role == "" {
|
||||
req.Role = "viewer"
|
||||
}
|
||||
if req.Role != "admin" && req.Role != "operator" && req.Role != "viewer" {
|
||||
c.JSON(400, gin.H{"error": "角色无效"})
|
||||
return
|
||||
}
|
||||
hash, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
user := model.User{Username: req.Username, Password: string(hash), Email: req.Email, Role: req.Role, Source: "local", Enabled: req.Enabled}
|
||||
if err := store.DB.Create(&user).Error; err != nil {
|
||||
c.JSON(400, gin.H{"error": "用户创建失败:" + err.Error()})
|
||||
return
|
||||
}
|
||||
store.Audit(c.GetString("username"), "create_user", user.Username, "")
|
||||
c.JSON(200, user)
|
||||
}
|
||||
|
||||
func (s *Server) updateUser(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var user model.User
|
||||
if store.DB.First(&user, id).Error != nil {
|
||||
c.JSON(404, gin.H{"error": "用户不存在"})
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Email, Role, Password string
|
||||
Enabled *bool `json:"enabled"`
|
||||
}
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
c.JSON(400, gin.H{"error": "参数错误"})
|
||||
return
|
||||
}
|
||||
if req.Email != "" {
|
||||
user.Email = req.Email
|
||||
}
|
||||
if req.Role != "" {
|
||||
user.Role = req.Role
|
||||
}
|
||||
if req.Enabled != nil {
|
||||
user.Enabled = *req.Enabled
|
||||
}
|
||||
if req.Password != "" {
|
||||
if len(req.Password) < 8 {
|
||||
c.JSON(400, gin.H{"error": "密码至少 8 位"})
|
||||
return
|
||||
}
|
||||
hash, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
user.Password = string(hash)
|
||||
}
|
||||
store.DB.Save(&user)
|
||||
c.JSON(200, user)
|
||||
}
|
||||
|
||||
func (s *Server) deleteUser(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if uint(id) == c.GetUint("user_id") {
|
||||
c.JSON(400, gin.H{"error": "不能删除当前用户"})
|
||||
return
|
||||
}
|
||||
store.DB.Delete(&model.User{}, id)
|
||||
c.JSON(200, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
func (s *Server) getLDAPConfig(c *gin.Context) {
|
||||
var cfg model.LDAPConfig
|
||||
if store.DB.First(&cfg).Error != nil {
|
||||
c.JSON(200, gin.H{"enabled": false})
|
||||
return
|
||||
}
|
||||
c.JSON(200, cfg)
|
||||
}
|
||||
|
||||
func (s *Server) saveLDAPConfig(c *gin.Context) {
|
||||
var req struct {
|
||||
URL string `json:"url"`
|
||||
BindDN string `json:"bind_dn"`
|
||||
BindPassword string `json:"bind_password"`
|
||||
BaseDN string `json:"base_dn"`
|
||||
UserFilter string `json:"user_filter"`
|
||||
UsernameAttribute string `json:"username_attribute"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
if c.ShouldBindJSON(&req) != nil || (req.Enabled && (req.URL == "" || req.BaseDN == "")) {
|
||||
c.JSON(400, gin.H{"error": "LDAP 地址与 Base DN 必填"})
|
||||
return
|
||||
}
|
||||
var cfg model.LDAPConfig
|
||||
store.DB.First(&cfg)
|
||||
cfg.URL, cfg.BindDN, cfg.BaseDN, cfg.UserFilter, cfg.UsernameAttribute, cfg.Enabled = req.URL, req.BindDN, req.BaseDN, req.UserFilter, req.UsernameAttribute, req.Enabled
|
||||
if req.BindPassword != "" {
|
||||
cfg.BindPassword, _ = store.EncryptSecret(req.BindPassword)
|
||||
}
|
||||
store.DB.Save(&cfg)
|
||||
c.JSON(200, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
func (s *Server) testLDAP(c *gin.Context) {
|
||||
cfg, err := ldapSettings()
|
||||
if err == nil {
|
||||
conn, connErr := ldapConnect(cfg)
|
||||
if connErr == nil {
|
||||
conn.Close()
|
||||
c.JSON(200, gin.H{"ok": true})
|
||||
return
|
||||
}
|
||||
err = connErr
|
||||
}
|
||||
c.JSON(400, gin.H{"error": "LDAP 连接失败: " + err.Error()})
|
||||
}
|
||||
|
||||
func (s *Server) listVPNUsers(c *gin.Context) {
|
||||
var users []model.VPNUser
|
||||
q := store.DB.Order("id desc")
|
||||
if id := c.Query("tunnel_id"); id != "" {
|
||||
q = q.Where("tunnel_id = ?", id)
|
||||
}
|
||||
q.Find(&users)
|
||||
c.JSON(200, users)
|
||||
}
|
||||
func (s *Server) createVPNUser(c *gin.Context) {
|
||||
var req struct {
|
||||
TunnelID uint `json:"tunnel_id"`
|
||||
Username string `json:"username"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Email string `json:"email"`
|
||||
Source string `json:"source"`
|
||||
LDAPDN string `json:"ldap_dn"`
|
||||
Password string `json:"password"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
}
|
||||
if c.ShouldBindJSON(&req) != nil || req.TunnelID == 0 || req.Username == "" {
|
||||
c.JSON(400, gin.H{"error": "隧道和用户名必填"})
|
||||
return
|
||||
}
|
||||
if req.Source == "" {
|
||||
req.Source = "local"
|
||||
}
|
||||
if req.Source != "local" && req.Source != "ldap" {
|
||||
c.JSON(400, gin.H{"error": "来源必须为 local 或 ldap"})
|
||||
return
|
||||
}
|
||||
user := model.VPNUser{TunnelID: req.TunnelID, Username: req.Username, DisplayName: req.DisplayName, Email: req.Email, Source: req.Source, LDAPDN: req.LDAPDN, Enabled: true}
|
||||
if req.Enabled != nil {
|
||||
user.Enabled = *req.Enabled
|
||||
}
|
||||
if user.Source == "local" {
|
||||
if len(req.Password) < 8 {
|
||||
c.JSON(400, gin.H{"error": "本地拨号用户密码必填"})
|
||||
return
|
||||
}
|
||||
hash, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
user.Password = string(hash)
|
||||
} else if user.LDAPDN == "" {
|
||||
c.JSON(400, gin.H{"error": "LDAP 拨号用户必须填写 LDAP DN"})
|
||||
return
|
||||
}
|
||||
if err := store.DB.Create(&user).Error; err != nil {
|
||||
c.JSON(400, gin.H{"error": "创建失败:" + err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, user)
|
||||
}
|
||||
func (s *Server) updateVPNUser(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var user model.VPNUser
|
||||
if store.DB.First(&user, id).Error != nil {
|
||||
c.JSON(404, gin.H{"error": "拨号用户不存在"})
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
DisplayName, Email, Password string
|
||||
Enabled *bool `json:"enabled"`
|
||||
}
|
||||
if c.ShouldBindJSON(&req) != nil {
|
||||
c.JSON(400, gin.H{"error": "参数错误"})
|
||||
return
|
||||
}
|
||||
user.DisplayName, user.Email = req.DisplayName, req.Email
|
||||
if req.Enabled != nil {
|
||||
user.Enabled = *req.Enabled
|
||||
}
|
||||
if req.Password != "" {
|
||||
hash, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
user.Password = string(hash)
|
||||
}
|
||||
store.DB.Save(&user)
|
||||
c.JSON(200, user)
|
||||
}
|
||||
func (s *Server) deleteVPNUser(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
store.DB.Delete(&model.VPNUser{}, id)
|
||||
c.JSON(200, gin.H{"ok": true})
|
||||
}
|
||||
Reference in new issue
Block a user