1706552b33
API now returns:
{
"logs": [...], // ConnectionLog array (same fields as before)
"_diag": [...] // status.log paths/existence/parse status per instance
}
Frontend updated to handle both array (legacy) and object format.
This makes it easy to diagnose:
- Which status.log paths the backend looked at
- Whether files exist and their sizes
- How many entries were parsed
- Any parse errors
User just needs to: git pull && rebuild && restart on production.
739 lines
20 KiB
Go
739 lines
20 KiB
Go
package api
|
|
|
|
import (
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-contrib/cors"
|
|
"github.com/gin-gonic/gin"
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
"openvpn-manager/internal/config"
|
|
"openvpn-manager/internal/middleware"
|
|
"openvpn-manager/internal/model"
|
|
"openvpn-manager/internal/service"
|
|
)
|
|
|
|
type Server struct {
|
|
Cfg *config.Config
|
|
Svc *service.Service
|
|
}
|
|
|
|
func NewServer(cfg *config.Config, svc *service.Service) *Server {
|
|
return &Server{Cfg: cfg, Svc: svc}
|
|
}
|
|
|
|
func (s *Server) Router(distDir string) *gin.Engine {
|
|
gin.SetMode(gin.ReleaseMode)
|
|
r := gin.New()
|
|
r.Use(gin.Recovery())
|
|
r.Use(cors.New(cors.Config{
|
|
AllowAllOrigins: true,
|
|
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
|
AllowHeaders: []string{"Authorization", "Content-Type"},
|
|
MaxAge: 12 * time.Hour,
|
|
}))
|
|
|
|
// 公共
|
|
r.POST("/api/login", s.login)
|
|
r.GET("/api/health", func(c *gin.Context) { c.JSON(200, gin.H{"ok": true}) })
|
|
// OpenVPN auth-user-pass-verify 回调(无 JWT,仅 localhost 可用)
|
|
r.POST("/api/vpn/verify", s.verifyVPNUser)
|
|
|
|
auth := r.Group("/api", middleware.JWTAuth(s.Cfg.JWTSecret))
|
|
{
|
|
auth.GET("/me", s.me)
|
|
auth.POST("/logout", s.logout)
|
|
auth.GET("/dashboard", s.dashboard)
|
|
|
|
auth.GET("/instances", s.listInstances)
|
|
auth.POST("/instances", s.createInstance)
|
|
auth.GET("/instances/:id", s.getInstance)
|
|
auth.PUT("/instances/:id", s.updateInstance)
|
|
auth.DELETE("/instances/:id", s.deleteInstance)
|
|
auth.POST("/instances/:id/start", s.startInstance)
|
|
auth.POST("/instances/:id/stop", s.stopInstance)
|
|
auth.GET("/instances/:id/online", s.onlineClients)
|
|
|
|
auth.GET("/instances/:id/users", s.listUsers)
|
|
auth.POST("/instances/:id/users", s.createUser)
|
|
auth.PUT("/instances/:id/users/:uid", s.updateUser)
|
|
auth.POST("/instances/:id/users/:uid/revoke", s.revokeUser)
|
|
auth.POST("/instances/:id/users/:uid/password", s.resetVPNPassword)
|
|
auth.DELETE("/instances/:id/users/:uid", s.deleteUser)
|
|
auth.GET("/instances/:id/users/:uid/ovpn", s.downloadOVPN)
|
|
|
|
auth.GET("/certs", s.listCerts)
|
|
|
|
auth.GET("/connlogs", s.listConnLogs)
|
|
|
|
auth.GET("/backups", s.listBackups)
|
|
auth.POST("/backups", s.createBackup)
|
|
auth.POST("/backups/:id/restore", s.restoreBackup)
|
|
auth.DELETE("/backups/:id", s.deleteBackup)
|
|
|
|
auth.GET("/audits", s.listAudits)
|
|
|
|
// Admin 账号管理(self)
|
|
auth.POST("/me/password", s.changeMyPassword)
|
|
|
|
// Admin 账号管理(列表/创建/重置密码/启停/删除 - 仅 admin role)
|
|
auth.GET("/admins", s.listAdmins)
|
|
auth.POST("/admins", s.createAdmin)
|
|
auth.POST("/admins/:id/password", s.resetAdminPassword)
|
|
auth.POST("/admins/:id/status", s.setAdminStatus)
|
|
auth.DELETE("/admins/:id", s.deleteAdmin)
|
|
}
|
|
|
|
// 静态前端
|
|
if distDir != "" {
|
|
if _, err := os.Stat(distDir); err == nil {
|
|
r.NoRoute(func(c *gin.Context) {
|
|
path := filepath.Join(distDir, c.Request.URL.Path)
|
|
if !fileExists(path) || strings.HasSuffix(c.Request.URL.Path, "/") {
|
|
c.File(filepath.Join(distDir, "index.html"))
|
|
return
|
|
}
|
|
c.File(path)
|
|
})
|
|
}
|
|
}
|
|
return r
|
|
}
|
|
|
|
func fileExists(p string) bool {
|
|
fi, err := os.Stat(p)
|
|
return err == nil && !fi.IsDir()
|
|
}
|
|
|
|
// ---------- handlers ----------
|
|
|
|
func (s *Server) login(c *gin.Context) {
|
|
var req struct {
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
|
|
return
|
|
}
|
|
// 从 admin 表验证;若表为空则用环境变量默认账号首次 seed
|
|
admin, err := s.Svc.Store.GetAdminByUsername(req.Username)
|
|
if err != nil {
|
|
// 兼容:如果 db.json 还没有 admins(首次启动),允许用 env 默认账号登录,
|
|
// 登录成功后由 SeedDefaultIfEmpty 自动 seed 到 db.json
|
|
if s.Svc.Store.AdminCount() == 0 && req.Username == s.Cfg.AdminUser && req.Password == s.Cfg.AdminPass {
|
|
if err := s.Svc.SeedDefaultAdminIfEmpty(); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "seed admin: " + err.Error()})
|
|
return
|
|
}
|
|
admin, err = s.Svc.Store.GetAdminByUsername(req.Username)
|
|
if err != nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"})
|
|
return
|
|
}
|
|
} else {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"})
|
|
return
|
|
}
|
|
}
|
|
if admin.Status != "active" {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "账号已被禁用"})
|
|
return
|
|
}
|
|
if err := bcrypt.CompareHashAndPassword([]byte(admin.PasswordHash), []byte(req.Password)); err != nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"})
|
|
return
|
|
}
|
|
// 记录登录时间
|
|
now := time.Now()
|
|
admin.LastLoginAt = &now
|
|
admin.LastLoginIP = c.ClientIP()
|
|
_ = s.Svc.Store.UpsertAdmin(*admin)
|
|
// 颁发 token
|
|
tok, err := middleware.IssueToken(s.Cfg.JWTSecret, admin.ID, admin.Username, admin.Role, 12*time.Hour)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "issue token failed"})
|
|
return
|
|
}
|
|
c.JSON(200, gin.H{
|
|
"token": tok,
|
|
"username": admin.Username,
|
|
"role": admin.Role,
|
|
"must_change_password": admin.MustChangePassword,
|
|
})
|
|
}
|
|
|
|
func (s *Server) logout(c *gin.Context) { c.JSON(200, gin.H{"ok": true}) }
|
|
|
|
func (s *Server) me(c *gin.Context) {
|
|
userID, _ := c.Get("user_id")
|
|
uid, _ := userID.(string)
|
|
username, _ := c.Get("user")
|
|
role, _ := c.Get("role")
|
|
must := false
|
|
if uid != "" {
|
|
if a, err := s.Svc.Store.GetAdmin(uid); err == nil {
|
|
must = a.MustChangePassword
|
|
}
|
|
}
|
|
c.JSON(200, gin.H{
|
|
"user_id": uid,
|
|
"username": username,
|
|
"role": role,
|
|
"must_change_password": must,
|
|
})
|
|
}
|
|
|
|
// dashboard 汇总统计
|
|
func (s *Server) dashboard(c *gin.Context) {
|
|
instances := s.Svc.Store.ListInstances()
|
|
users := s.Svc.Store.ListUsers("")
|
|
certs, _ := s.Svc.CertInfos()
|
|
expiring := 0
|
|
for _, ct := range certs {
|
|
if ct.DaysLeft <= 30 {
|
|
expiring++
|
|
}
|
|
}
|
|
online := 0
|
|
for _, in := range instances {
|
|
cl, _ := s.Svc.ListOnline(in.ID)
|
|
online += len(cl)
|
|
}
|
|
whitelistN := 0
|
|
overview := []map[string]any{}
|
|
for _, in := range instances {
|
|
if in.AccessMode == model.AccessWhitelist {
|
|
whitelistN++
|
|
}
|
|
overview = append(overview, map[string]any{
|
|
"name": in.Name,
|
|
"access_mode": in.AccessMode,
|
|
"allow_networks": in.AllowNetworks,
|
|
})
|
|
}
|
|
// 实时连接日志:从 status.log 读取在线用户
|
|
var recentConnLogs []model.ConnectionLog
|
|
for _, in := range instances {
|
|
statusPath := filepath.Join(s.Svc.Cfg.InstanceDir(in.Name), "status.log")
|
|
entries, _ := s.Svc.Ovm.ParseStatus(statusPath)
|
|
for _, e := range entries {
|
|
recentConnLogs = append(recentConnLogs, model.ConnectionLog{
|
|
InstanceID: in.ID, CommonName: e.CommonName,
|
|
RealIP: e.RealAddress, VPNIP: e.VPNAddress,
|
|
BytesIn: e.BytesRecv, BytesOut: e.BytesSent,
|
|
ConnectedAt: e.ConnectedAt,
|
|
})
|
|
}
|
|
}
|
|
// 补充历史已断开的
|
|
history := s.Svc.Store.ListConnLogs("", 20)
|
|
liveSet := map[string]bool{}
|
|
for _, l := range recentConnLogs {
|
|
liveSet[l.CommonName] = true
|
|
}
|
|
for _, h := range history {
|
|
if h.DisconnectedAt == nil && liveSet[h.CommonName] {
|
|
continue
|
|
}
|
|
recentConnLogs = append(recentConnLogs, h)
|
|
}
|
|
|
|
c.JSON(200, gin.H{
|
|
"instances": len(instances),
|
|
"running": countByStatus(instances, "running"),
|
|
"whitelist_instances": whitelistN,
|
|
"users": len(users),
|
|
"active_users": countEnabled(users),
|
|
"online": online,
|
|
"expiring_certs": expiring,
|
|
"recent_audits": s.Svc.Store.ListAudits(20),
|
|
"recent_conn_logs": recentConnLogs,
|
|
"access_overview": overview,
|
|
})
|
|
}
|
|
|
|
// ---- instances ----
|
|
|
|
func (s *Server) listInstances(c *gin.Context) {
|
|
c.JSON(200, s.Svc.Store.ListInstances())
|
|
}
|
|
|
|
func (s *Server) getInstance(c *gin.Context) {
|
|
in, err := s.Svc.Store.GetInstance(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(404, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(200, in)
|
|
}
|
|
|
|
func (s *Server) createInstance(c *gin.Context) {
|
|
var in model.Instance
|
|
if err := c.ShouldBindJSON(&in); err != nil {
|
|
c.JSON(400, gin.H{"error": "bad request"})
|
|
return
|
|
}
|
|
out, err := s.Svc.CreateInstance(in)
|
|
if err != nil {
|
|
s.Svc.AuditForAPI(c, "create_instance", in.Name, err.Error(), "failed")
|
|
c.JSON(400, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
s.Svc.AuditForAPI(c, "create_instance", in.Name, "port="+itoa(out.Port), "ok")
|
|
c.JSON(200, out)
|
|
}
|
|
|
|
func (s *Server) updateInstance(c *gin.Context) {
|
|
var in model.Instance
|
|
if err := c.ShouldBindJSON(&in); err != nil {
|
|
c.JSON(400, gin.H{"error": "bad request"})
|
|
return
|
|
}
|
|
in.ID = c.Param("id")
|
|
if err := s.Svc.UpdateInstance(in); err != nil {
|
|
c.JSON(400, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
s.Svc.AuditForAPI(c, "update_instance", in.Name, "", "ok")
|
|
c.JSON(200, in)
|
|
}
|
|
|
|
func (s *Server) deleteInstance(c *gin.Context) {
|
|
id := c.Param("id")
|
|
if err := s.Svc.DeleteInstance(id); err != nil {
|
|
c.JSON(400, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
s.Svc.AuditForAPI(c, "delete_instance", id, "", "ok")
|
|
c.JSON(200, gin.H{"ok": true})
|
|
}
|
|
|
|
func (s *Server) startInstance(c *gin.Context) {
|
|
id := c.Param("id")
|
|
if err := s.Svc.StartInstance(id); err != nil {
|
|
s.Svc.AuditForAPI(c, "start_instance", id, err.Error(), "failed")
|
|
c.JSON(400, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
s.Svc.AuditForAPI(c, "start_instance", id, "", "ok")
|
|
c.JSON(200, gin.H{"ok": true})
|
|
}
|
|
|
|
func (s *Server) stopInstance(c *gin.Context) {
|
|
id := c.Param("id")
|
|
if err := s.Svc.StopInstance(id); err != nil {
|
|
c.JSON(400, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
s.Svc.AuditForAPI(c, "stop_instance", id, "", "ok")
|
|
c.JSON(200, gin.H{"ok": true})
|
|
}
|
|
|
|
func (s *Server) onlineClients(c *gin.Context) {
|
|
cl, err := s.Svc.ListOnline(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(400, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(200, cl)
|
|
}
|
|
|
|
// ---- users ----
|
|
|
|
func (s *Server) listUsers(c *gin.Context) {
|
|
c.JSON(200, s.Svc.Store.ListUsers(c.Param("id")))
|
|
}
|
|
|
|
func (s *Server) createUser(c *gin.Context) {
|
|
var u model.VPNUser
|
|
if err := c.ShouldBindJSON(&u); err != nil {
|
|
c.JSON(400, gin.H{"error": "bad request"})
|
|
return
|
|
}
|
|
u.InstanceID = c.Param("id")
|
|
out, err := s.Svc.CreateUser(u)
|
|
if err != nil {
|
|
s.Svc.AuditForAPI(c, "create_user", u.Username, err.Error(), "failed")
|
|
c.JSON(400, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
s.Svc.AuditForAPI(c, "create_user", u.Username, "instance="+u.InstanceID, "ok")
|
|
c.JSON(200, out)
|
|
}
|
|
|
|
func (s *Server) updateUser(c *gin.Context) {
|
|
var u model.VPNUser
|
|
if err := c.ShouldBindJSON(&u); err != nil {
|
|
c.JSON(400, gin.H{"error": "bad request"})
|
|
return
|
|
}
|
|
u.ID = c.Param("uid")
|
|
u.InstanceID = c.Param("id")
|
|
if err := s.Svc.UpdateUser(u); err != nil {
|
|
s.Svc.AuditForAPI(c, "update_user", u.Username, err.Error(), "failed")
|
|
c.JSON(400, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
s.Svc.AuditForAPI(c, "update_user", u.Username, "", "ok")
|
|
c.JSON(200, u)
|
|
}
|
|
|
|
func (s *Server) revokeUser(c *gin.Context) {
|
|
uid := c.Param("uid")
|
|
if err := s.Svc.RevokeUser(uid); err != nil {
|
|
c.JSON(400, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
s.Svc.AuditForAPI(c, "revoke_user", uid, "", "ok")
|
|
c.JSON(200, gin.H{"ok": true})
|
|
}
|
|
|
|
func (s *Server) resetVPNPassword(c *gin.Context) {
|
|
var body struct {
|
|
NewPassword string `json:"new_password"`
|
|
}
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
c.JSON(400, gin.H{"error": "bad request"})
|
|
return
|
|
}
|
|
uid := c.Param("uid")
|
|
if err := s.Svc.ResetVPNPassword(uid, body.NewPassword); err != nil {
|
|
s.Svc.AuditForAPI(c, "reset_vpn_password", uid, err.Error(), "failed")
|
|
c.JSON(400, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
s.Svc.AuditForAPI(c, "reset_vpn_password", uid, "", "ok")
|
|
c.JSON(200, gin.H{"ok": true})
|
|
}
|
|
|
|
func (s *Server) deleteUser(c *gin.Context) {
|
|
uid := c.Param("uid")
|
|
if err := s.Svc.DeleteUser(uid); err != nil {
|
|
c.JSON(400, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
s.Svc.AuditForAPI(c, "delete_user", uid, "", "ok")
|
|
c.JSON(200, gin.H{"ok": true})
|
|
}
|
|
|
|
func (s *Server) downloadOVPN(c *gin.Context) {
|
|
host := c.Query("host")
|
|
if host == "" {
|
|
// 从 Host 头取 hostname(strip 端口号)
|
|
// 用户应在前端 ?host=vpn.example.com 传入真实地址
|
|
host = c.Request.Host
|
|
if h, _, err := net.SplitHostPort(host); err == nil {
|
|
host = h
|
|
}
|
|
}
|
|
p, err := s.Svc.GenerateOVPN(c.Param("uid"), host)
|
|
if err != nil {
|
|
c.JSON(400, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.Header("Content-Disposition", "attachment; filename="+filepath.Base(p))
|
|
c.File(p)
|
|
}
|
|
|
|
// verifyVPNUser OpenVPN auth-user-pass-verify 回调。
|
|
// 由 OpenVPN 通过 verify.sh 脚本调用(curl POST)。
|
|
// 安全:仅允许 127.0.0.1 来源,其他 IP 拒绝。
|
|
func (s *Server) verifyVPNUser(c *gin.Context) {
|
|
// 仅允许 localhost
|
|
remoteIP := c.ClientIP()
|
|
if remoteIP != "127.0.0.1" && remoteIP != "::1" {
|
|
c.JSON(403, gin.H{"error": "forbidden"})
|
|
return
|
|
}
|
|
var req struct {
|
|
Instance string `json:"instance"`
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(400, gin.H{"error": "bad request"})
|
|
return
|
|
}
|
|
// 查找实例
|
|
inst, err := s.Svc.Store.GetInstanceByName(req.Instance)
|
|
if err != nil {
|
|
c.JSON(401, gin.H{"error": "auth failed"})
|
|
return
|
|
}
|
|
// 查找用户
|
|
user, err := s.Svc.Store.GetUserByCN(inst.ID, req.Username)
|
|
if err != nil || !user.Enabled {
|
|
c.JSON(401, gin.H{"error": "auth failed"})
|
|
return
|
|
}
|
|
// bcrypt 验证密码
|
|
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil {
|
|
c.JSON(401, gin.H{"error": "auth failed"})
|
|
return
|
|
}
|
|
c.JSON(200, gin.H{"ok": true})
|
|
}
|
|
|
|
// ---- certs ----
|
|
|
|
func (s *Server) listCerts(c *gin.Context) {
|
|
certs, err := s.Svc.CertInfos()
|
|
if err != nil {
|
|
c.JSON(400, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(200, certs)
|
|
}
|
|
|
|
// ---- conn logs ----
|
|
func (s *Server) listConnLogs(c *gin.Context) {
|
|
// 诊断信息:返回每个实例 status.log 的存在情况和原始内容前 200 字节
|
|
instanceID := c.Query("instance")
|
|
|
|
// 1) 数据库里的历史日志(有断开时间的老连接)
|
|
history := s.Svc.Store.ListConnLogs(instanceID, 200)
|
|
|
|
// 2) 实时解析所有实例的 status.log,生成当前在线用户
|
|
var live []model.ConnectionLog
|
|
var diag []map[string]any
|
|
instances := s.Svc.Store.ListInstances()
|
|
for _, in := range instances {
|
|
if instanceID != "" && instanceID != in.ID {
|
|
continue
|
|
}
|
|
statusPath := filepath.Join(s.Svc.Cfg.InstanceDir(in.Name), "status.log")
|
|
fi, statErr := os.Stat(statusPath)
|
|
d := map[string]any{
|
|
"instance": in.Name, "path": statusPath,
|
|
"exists": fi != nil, "size": int64(0),
|
|
}
|
|
if statErr == nil {
|
|
d["size"] = fi.Size()
|
|
}
|
|
entries, err := s.Svc.Ovm.ParseStatus(statusPath)
|
|
d["parsed"] = len(entries)
|
|
if err != nil {
|
|
d["error"] = err.Error()
|
|
}
|
|
diag = append(diag, d)
|
|
for _, e := range entries {
|
|
live = append(live, model.ConnectionLog{
|
|
InstanceID: in.ID,
|
|
CommonName: e.CommonName,
|
|
RealIP: e.RealAddress,
|
|
VPNIP: e.VPNAddress,
|
|
BytesIn: e.BytesRecv,
|
|
BytesOut: e.BytesSent,
|
|
ConnectedAt: e.ConnectedAt,
|
|
})
|
|
}
|
|
}
|
|
|
|
// 3) 合并:先输出在线的,再输出历史的(去重:跳过 history 中与 live CN 重复的未断开记录)
|
|
liveSet := map[string]bool{}
|
|
for _, l := range live {
|
|
liveSet[l.CommonName] = true
|
|
}
|
|
var merged []model.ConnectionLog
|
|
merged = append(merged, live...)
|
|
for _, h := range history {
|
|
if h.DisconnectedAt == nil && liveSet[h.CommonName] {
|
|
continue // 已用实时数据替代
|
|
}
|
|
merged = append(merged, h)
|
|
}
|
|
// 同时返回诊断信息(方便排查),前端忽略 _diag 字段
|
|
c.JSON(200, gin.H{
|
|
"logs": merged,
|
|
"_diag": diag,
|
|
})
|
|
}
|
|
|
|
// ---- backups ----
|
|
|
|
func (s *Server) listBackups(c *gin.Context) {
|
|
c.JSON(200, s.Svc.Store.ListBackups())
|
|
}
|
|
|
|
func (s *Server) createBackup(c *gin.Context) {
|
|
var req struct{ Note string `json:"note"` }
|
|
_ = c.ShouldBindJSON(&req)
|
|
b, err := s.Svc.Backup(req.Note)
|
|
if err != nil {
|
|
c.JSON(400, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
s.Svc.AuditForAPI(c, "create_backup", b.ID, "", "ok")
|
|
c.JSON(200, b)
|
|
}
|
|
|
|
func (s *Server) restoreBackup(c *gin.Context) {
|
|
id := c.Param("id")
|
|
if err := s.Svc.Restore(id); err != nil {
|
|
s.Svc.AuditForAPI(c, "restore_backup", id, err.Error(), "failed")
|
|
c.JSON(400, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
s.Svc.AuditForAPI(c, "restore_backup", id, "", "ok")
|
|
c.JSON(200, gin.H{"ok": true})
|
|
}
|
|
|
|
func (s *Server) deleteBackup(c *gin.Context) {
|
|
id := c.Param("id")
|
|
if err := s.Svc.DeleteBackup(id); err != nil {
|
|
c.JSON(400, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(200, gin.H{"ok": true})
|
|
}
|
|
|
|
// ---- audits ----
|
|
|
|
func (s *Server) listAudits(c *gin.Context) {
|
|
c.JSON(200, s.Svc.Store.ListAudits(500))
|
|
}
|
|
|
|
// ---- helpers ----
|
|
|
|
func countByStatus(in []model.Instance, status string) int {
|
|
n := 0
|
|
for _, x := range in {
|
|
if x.Status == status {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
func countEnabled(u []model.VPNUser) int {
|
|
n := 0
|
|
for _, x := range u {
|
|
if x.Enabled {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
func itoa(n int) string {
|
|
if n == 0 {
|
|
return "0"
|
|
}
|
|
neg := false
|
|
if n < 0 {
|
|
neg = true
|
|
n = -n
|
|
}
|
|
var buf [20]byte
|
|
i := len(buf)
|
|
for n > 0 {
|
|
i--
|
|
buf[i] = byte('0' + n%10)
|
|
n /= 10
|
|
}
|
|
if neg {
|
|
i--
|
|
buf[i] = '-'
|
|
}
|
|
return string(buf[i:])
|
|
}
|
|
|
|
// ---------- Admin 账号管理 ----------
|
|
|
|
func (s *Server) changeMyPassword(c *gin.Context) {
|
|
var req struct {
|
|
OldPassword string `json:"old_password"`
|
|
NewPassword string `json:"new_password"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
|
|
return
|
|
}
|
|
userID, _ := c.Get("user_id")
|
|
uid, _ := userID.(string)
|
|
if err := s.Svc.ChangePassword(uid, req.OldPassword, req.NewPassword); err != nil {
|
|
s.Svc.AuditForAPI(c, "change_password", uid, err.Error(), "failed")
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
s.Svc.AuditForAPI(c, "change_password", uid, "", "ok")
|
|
c.JSON(200, gin.H{"ok": true})
|
|
}
|
|
|
|
func (s *Server) listAdmins(c *gin.Context) {
|
|
c.JSON(200, s.Svc.Store.ListAdmins())
|
|
}
|
|
|
|
func (s *Server) createAdmin(c *gin.Context) {
|
|
var req struct {
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
Role string `json:"role"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
|
|
return
|
|
}
|
|
a, err := s.Svc.CreateAdmin(req.Username, req.Password, req.Role)
|
|
if err != nil {
|
|
s.Svc.AuditForAPI(c, "create_admin", req.Username, err.Error(), "failed")
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
s.Svc.AuditForAPI(c, "create_admin", req.Username, "role="+a.Role, "ok")
|
|
c.JSON(200, a)
|
|
}
|
|
|
|
func (s *Server) resetAdminPassword(c *gin.Context) {
|
|
id := c.Param("id")
|
|
var req struct {
|
|
NewPassword string `json:"new_password"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
|
|
return
|
|
}
|
|
if err := s.Svc.ResetPassword(id, req.NewPassword); err != nil {
|
|
s.Svc.AuditForAPI(c, "reset_password", id, err.Error(), "failed")
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
s.Svc.AuditForAPI(c, "reset_password", id, "", "ok")
|
|
c.JSON(200, gin.H{"ok": true})
|
|
}
|
|
|
|
func (s *Server) setAdminStatus(c *gin.Context) {
|
|
id := c.Param("id")
|
|
var req struct {
|
|
Status string `json:"status"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
|
|
return
|
|
}
|
|
if err := s.Svc.SetAdminStatus(id, req.Status); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
s.Svc.AuditForAPI(c, "set_admin_status", id, req.Status, "ok")
|
|
c.JSON(200, gin.H{"ok": true})
|
|
}
|
|
|
|
func (s *Server) deleteAdmin(c *gin.Context) {
|
|
id := c.Param("id")
|
|
userID, _ := c.Get("user_id")
|
|
uid, _ := userID.(string)
|
|
if err := s.Svc.DeleteAdmin(uid, id); err != nil {
|
|
s.Svc.AuditForAPI(c, "delete_admin", id, err.Error(), "failed")
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
s.Svc.AuditForAPI(c, "delete_admin", id, "", "ok")
|
|
c.JSON(200, gin.H{"ok": true})
|
|
} |