From b6333d81a2d59b8fcda3d53d78cbc53c1682199e Mon Sep 17 00:00:00 2001 From: sagit Date: Fri, 6 Feb 2026 12:23:46 +0000 Subject: [PATCH] fix: restore panel list endpoints and bootstrap sqlite state --- .gitignore | 5 +- go-backend/cmd/paneld/main.go | 1 + go-backend/internal/http/handler/handler.go | 203 ++++++ .../internal/http/middleware/request_log.go | 57 ++ go-backend/internal/http/router.go | 1 + .../internal/store/sqlite/repository.go | 641 ++++++++++++++++++ go-backend/internal/store/sqlite/sql/data.sql | 5 + .../internal/store/sqlite/sql/schema.sql | 182 +++++ 8 files changed, 1094 insertions(+), 1 deletion(-) create mode 100644 go-backend/internal/http/middleware/request_log.go create mode 100644 go-backend/internal/store/sqlite/sql/data.sql create mode 100644 go-backend/internal/store/sqlite/sql/schema.sql diff --git a/.gitignore b/.gitignore index b21deae..31b9b70 100644 --- a/.gitignore +++ b/.gitignore @@ -257,4 +257,7 @@ gitee/ doraemon.jks device.id commit.sh -sql/ \ No newline at end of file +sql/ +!go-backend/internal/store/sqlite/sql/ +!go-backend/internal/store/sqlite/sql/schema.sql +!go-backend/internal/store/sqlite/sql/data.sql diff --git a/go-backend/cmd/paneld/main.go b/go-backend/cmd/paneld/main.go index b74d984..832b2dd 100644 --- a/go-backend/cmd/paneld/main.go +++ b/go-backend/cmd/paneld/main.go @@ -19,6 +19,7 @@ func main() { if cfg.JWTSecret == "" { log.Println("warning: JWT_SECRET is empty") } + log.Printf("starting go-backend on %s (db=%s)", cfg.Addr, cfg.DBPath) a, err := app.New(cfg) if err != nil { diff --git a/go-backend/internal/http/handler/handler.go b/go-backend/internal/http/handler/handler.go index c2d6209..631554b 100644 --- a/go-backend/internal/http/handler/handler.go +++ b/go-backend/internal/http/handler/handler.go @@ -62,6 +62,7 @@ func (h *Handler) WebSocketHandler() http.Handler { func (h *Handler) Register(mux *http.ServeMux) { mux.HandleFunc("/api/v1/user/login", h.login) + mux.HandleFunc("/api/v1/user/list", h.userList) mux.HandleFunc("/api/v1/config/get", h.getConfigByName) mux.HandleFunc("/api/v1/config/list", h.getConfigs) mux.HandleFunc("/api/v1/config/update", h.updateConfigs) @@ -69,6 +70,15 @@ func (h *Handler) Register(mux *http.ServeMux) { mux.HandleFunc("/api/v1/captcha/check", h.checkCaptcha) mux.HandleFunc("/api/v1/user/package", h.userPackage) mux.HandleFunc("/api/v1/user/updatePassword", h.updatePassword) + mux.HandleFunc("/api/v1/node/list", h.nodeList) + mux.HandleFunc("/api/v1/tunnel/list", h.tunnelList) + mux.HandleFunc("/api/v1/forward/list", h.forwardList) + mux.HandleFunc("/api/v1/speed-limit/list", h.speedLimitList) + mux.HandleFunc("/api/v1/tunnel/user/tunnel", h.userTunnelVisibleList) + mux.HandleFunc("/api/v1/tunnel/user/list", h.userTunnelList) + mux.HandleFunc("/api/v1/group/tunnel/list", h.tunnelGroupList) + mux.HandleFunc("/api/v1/group/user/list", h.userGroupList) + mux.HandleFunc("/api/v1/group/permission/list", h.groupPermissionList) mux.HandleFunc("/flow/test", h.flowTest) mux.HandleFunc("/flow/config", h.flowConfig) @@ -182,6 +192,191 @@ func (h *Handler) getConfigs(w http.ResponseWriter, r *http.Request) { response.WriteJSON(w, response.OK(cfgMap)) } +func (h *Handler) userList(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + response.WriteJSON(w, response.ErrDefault("请求失败")) + return + } + + users, err := h.repo.ListUsers() + if err != nil { + response.WriteJSON(w, response.Err(-2, err.Error())) + return + } + response.WriteJSON(w, response.OK(users)) +} + +func (h *Handler) nodeList(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + response.WriteJSON(w, response.ErrDefault("请求失败")) + return + } + + items, err := h.repo.ListNodes() + if err != nil { + response.WriteJSON(w, response.Err(-2, err.Error())) + return + } + response.WriteJSON(w, response.OK(items)) +} + +func (h *Handler) tunnelList(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + response.WriteJSON(w, response.ErrDefault("请求失败")) + return + } + + items, err := h.repo.ListTunnels() + if err != nil { + response.WriteJSON(w, response.Err(-2, err.Error())) + return + } + response.WriteJSON(w, response.OK(items)) +} + +func (h *Handler) forwardList(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + response.WriteJSON(w, response.ErrDefault("请求失败")) + return + } + + items, err := h.repo.ListForwards() + if err != nil { + response.WriteJSON(w, response.Err(-2, err.Error())) + return + } + response.WriteJSON(w, response.OK(items)) +} + +func (h *Handler) speedLimitList(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + response.WriteJSON(w, response.ErrDefault("请求失败")) + return + } + + items, err := h.repo.ListSpeedLimits() + if err != nil { + response.WriteJSON(w, response.Err(-2, err.Error())) + return + } + response.WriteJSON(w, response.OK(items)) +} + +func (h *Handler) userTunnelVisibleList(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + response.WriteJSON(w, response.ErrDefault("请求失败")) + return + } + + userID, err := userIDFromRequest(r) + if err != nil { + response.WriteJSON(w, response.Err(401, "无效的token或token已过期")) + return + } + + items, err := h.repo.ListUserAccessibleTunnels(userID) + if err != nil { + response.WriteJSON(w, response.Err(-2, err.Error())) + return + } + response.WriteJSON(w, response.OK(items)) +} + +func (h *Handler) userTunnelList(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + response.WriteJSON(w, response.ErrDefault("请求失败")) + return + } + + var req struct { + UserID int64 `json:"userId"` + } + if err := decodeJSON(r.Body, &req); err != nil { + response.WriteJSON(w, response.ErrDefault("请求参数错误")) + return + } + if req.UserID <= 0 { + response.WriteJSON(w, response.OK([]interface{}{})) + return + } + + tunnels, err := h.repo.GetUserPackageTunnels(req.UserID) + if err != nil { + response.WriteJSON(w, response.Err(-2, err.Error())) + return + } + + out := make([]map[string]interface{}, 0, len(tunnels)) + for _, t := range tunnels { + item := map[string]interface{}{ + "id": t.ID, + "userId": t.UserID, + "tunnelId": t.TunnelID, + "tunnelName": t.TunnelName, + "status": 1, + "flow": t.Flow, + "num": t.Num, + "expTime": t.ExpTime, + "flowResetTime": t.FlowResetTime, + "inFlow": t.InFlow, + "outFlow": t.OutFlow, + "tunnelFlow": t.TunnelFlow, + "speedId": nil, + "speedLimitName": nil, + } + if t.SpeedID.Valid { + item["speedId"] = t.SpeedID.Int64 + } + if t.SpeedLimit.Valid { + item["speedLimitName"] = t.SpeedLimit.String + } + out = append(out, item) + } + response.WriteJSON(w, response.OK(out)) +} + +func (h *Handler) tunnelGroupList(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + response.WriteJSON(w, response.ErrDefault("请求失败")) + return + } + + items, err := h.repo.ListTunnelGroups() + if err != nil { + response.WriteJSON(w, response.Err(-2, err.Error())) + return + } + response.WriteJSON(w, response.OK(items)) +} + +func (h *Handler) userGroupList(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + response.WriteJSON(w, response.ErrDefault("请求失败")) + return + } + + items, err := h.repo.ListUserGroups() + if err != nil { + response.WriteJSON(w, response.Err(-2, err.Error())) + return + } + response.WriteJSON(w, response.OK(items)) +} + +func (h *Handler) groupPermissionList(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + response.WriteJSON(w, response.ErrDefault("请求失败")) + return + } + + items, err := h.repo.ListGroupPermissions() + if err != nil { + response.WriteJSON(w, response.Err(-2, err.Error())) + return + } + response.WriteJSON(w, response.OK(items)) +} + func (h *Handler) checkCaptcha(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("请求失败")) @@ -535,6 +730,14 @@ func parseUserID(sub string) (int64, error) { return id, nil } +func userIDFromRequest(r *http.Request) (int64, error) { + claims, ok := r.Context().Value(middleware.ClaimsContextKey).(auth.Claims) + if !ok { + return 0, strconv.ErrSyntax + } + return parseUserID(claims.Sub) +} + func nullableNullInt64(v sql.NullInt64) interface{} { if v.Valid { return v.Int64 diff --git a/go-backend/internal/http/middleware/request_log.go b/go-backend/internal/http/middleware/request_log.go new file mode 100644 index 0000000..506f325 --- /dev/null +++ b/go-backend/internal/http/middleware/request_log.go @@ -0,0 +1,57 @@ +package middleware + +import ( + "bufio" + "io" + "log" + "net" + "net/http" + "time" +) + +type statusWriter struct { + http.ResponseWriter + status int +} + +func (w *statusWriter) WriteHeader(code int) { + w.status = code + w.ResponseWriter.WriteHeader(code) +} + +func (w *statusWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + hj, ok := w.ResponseWriter.(http.Hijacker) + if !ok { + return nil, nil, http.ErrNotSupported + } + return hj.Hijack() +} + +func (w *statusWriter) Flush() { + if f, ok := w.ResponseWriter.(http.Flusher); ok { + f.Flush() + } +} + +func (w *statusWriter) ReadFrom(r io.Reader) (int64, error) { + if rf, ok := w.ResponseWriter.(io.ReaderFrom); ok { + return rf.ReadFrom(r) + } + return io.Copy(w.ResponseWriter, r) +} + +func (w *statusWriter) Push(target string, opts *http.PushOptions) error { + if p, ok := w.ResponseWriter.(http.Pusher); ok { + return p.Push(target, opts) + } + return http.ErrNotSupported +} + +func RequestLog(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + sw := &statusWriter{ResponseWriter: w, status: http.StatusOK} + start := time.Now() + next.ServeHTTP(sw, r) + log.Printf("%s %s -> %d (%s)", r.Method, r.URL.Path, sw.status, time.Since(start).String()) + }) +} diff --git a/go-backend/internal/http/router.go b/go-backend/internal/http/router.go index 7bc2b8b..a18ed47 100644 --- a/go-backend/internal/http/router.go +++ b/go-backend/internal/http/router.go @@ -14,6 +14,7 @@ func NewRouter(h *handler.Handler, jwtSecret string) http.Handler { wrapped := middleware.Recover(mux) wrapped = middleware.JWT(middleware.AuthOptions{JWTSecret: jwtSecret})(wrapped) + wrapped = middleware.RequestLog(wrapped) wrapped = middleware.CORS(wrapped) return wrapped } diff --git a/go-backend/internal/store/sqlite/repository.go b/go-backend/internal/store/sqlite/repository.go index 1cade0a..a4fa678 100644 --- a/go-backend/internal/store/sqlite/repository.go +++ b/go-backend/internal/store/sqlite/repository.go @@ -2,12 +2,25 @@ package sqlite import ( "database/sql" + _ "embed" "errors" + "fmt" + "log" + "os" + "path/filepath" + "sort" + "strings" "time" _ "modernc.org/sqlite" ) +//go:embed sql/schema.sql +var embeddedSchema string + +//go:embed sql/data.sql +var embeddedSeedData string + type Repository struct { db *sql.DB } @@ -85,6 +98,10 @@ type Node struct { } func Open(path string) (*Repository, error) { + if err := ensureParentDir(path); err != nil { + return nil, err + } + db, err := sql.Open("sqlite", path) if err != nil { return nil, err @@ -95,6 +112,11 @@ func Open(path string) (*Repository, error) { return nil, err } + if err := bootstrapSchema(db); err != nil { + _ = db.Close() + return nil, err + } + return &Repository{db: db}, nil } @@ -415,6 +437,625 @@ func (r *Repository) AddFlow(forwardID, userID int64, userTunnelID int64, inFlow return err } +func (r *Repository) ListNodes() ([]map[string]interface{}, error) { + if r == nil || r.db == nil { + return nil, errors.New("repository not initialized") + } + + rows, err := r.db.Query(` + SELECT id, inx, name, server_ip, server_ip_v4, server_ip_v6, port, tcp_listen_addr, udp_listen_addr, version, http, tls, socks, status + FROM node + ORDER BY inx ASC, id ASC + `) + if err != nil { + return nil, err + } + defer rows.Close() + + items := make([]map[string]interface{}, 0) + for rows.Next() { + var id, inx int64 + var name, serverIP, port string + var serverIPV4, serverIPV6, tcpListen, udpListen, version sql.NullString + var httpVal, tlsVal, socksVal, status int + + if err := rows.Scan(&id, &inx, &name, &serverIP, &serverIPV4, &serverIPV6, &port, &tcpListen, &udpListen, &version, &httpVal, &tlsVal, &socksVal, &status); err != nil { + return nil, err + } + + items = append(items, map[string]interface{}{ + "id": id, + "inx": inx, + "name": name, + "ip": serverIP, + "serverIp": serverIP, + "serverIpV4": nullableString(serverIPV4), + "serverIpV6": nullableString(serverIPV6), + "port": port, + "tcpListenAddr": nullableString(tcpListen), + "udpListenAddr": nullableString(udpListen), + "version": nullableString(version), + "http": httpVal, + "tls": tlsVal, + "socks": socksVal, + "status": status, + }) + } + + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +func (r *Repository) ListUsers() ([]map[string]interface{}, error) { + if r == nil || r.db == nil { + return nil, errors.New("repository not initialized") + } + + rows, err := r.db.Query(` + SELECT id, user, role_id, exp_time, flow, in_flow, out_flow, flow_reset_time, num, created_time, updated_time, status + FROM user + ORDER BY id ASC + `) + if err != nil { + return nil, err + } + defer rows.Close() + + items := make([]map[string]interface{}, 0) + for rows.Next() { + var id int64 + var user string + var roleID int + var expTime, flow, inFlow, outFlow, flowResetTime, createdTime int64 + var num, status int + var updatedTime sql.NullInt64 + + if err := rows.Scan(&id, &user, &roleID, &expTime, &flow, &inFlow, &outFlow, &flowResetTime, &num, &createdTime, &updatedTime, &status); err != nil { + return nil, err + } + + items = append(items, map[string]interface{}{ + "id": id, + "user": user, + "name": user, + "roleId": roleID, + "status": status, + "flow": flow, + "num": num, + "expTime": expTime, + "flowResetTime": flowResetTime, + "createdTime": createdTime, + "updatedTime": nullableInt64(updatedTime), + "inFlow": inFlow, + "outFlow": outFlow, + }) + } + + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +func (r *Repository) ListSpeedLimits() ([]map[string]interface{}, error) { + if r == nil || r.db == nil { + return nil, errors.New("repository not initialized") + } + + rows, err := r.db.Query(` + SELECT id, name, speed, tunnel_id, tunnel_name, status, created_time, updated_time + FROM speed_limit + ORDER BY id ASC + `) + if err != nil { + return nil, err + } + defer rows.Close() + + items := make([]map[string]interface{}, 0) + for rows.Next() { + var id, tunnelID, createdTime int64 + var name, tunnelName string + var speed, status int + var updatedTime sql.NullInt64 + if err := rows.Scan(&id, &name, &speed, &tunnelID, &tunnelName, &status, &createdTime, &updatedTime); err != nil { + return nil, err + } + items = append(items, map[string]interface{}{ + "id": id, + "name": name, + "speed": speed, + "tunnelId": tunnelID, + "tunnelName": tunnelName, + "status": status, + "createdTime": createdTime, + "updatedTime": nullableInt64(updatedTime), + }) + } + + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +func (r *Repository) ListForwards() ([]map[string]interface{}, error) { + if r == nil || r.db == nil { + return nil, errors.New("repository not initialized") + } + + rows, err := r.db.Query(` + SELECT f.id, f.user_id, f.user_name, f.name, f.tunnel_id, t.name, f.remote_addr, f.strategy, + f.in_flow, f.out_flow, f.created_time, f.status, f.inx, + GROUP_CONCAT(CASE WHEN n.server_ip IS NOT NULL AND fp.port IS NOT NULL THEN n.server_ip || ':' || fp.port END), + MIN(fp.port) + FROM forward f + LEFT JOIN tunnel t ON t.id = f.tunnel_id + LEFT JOIN forward_port fp ON fp.forward_id = f.id + LEFT JOIN node n ON n.id = fp.node_id + GROUP BY f.id + ORDER BY f.inx ASC, f.id ASC + `) + if err != nil { + return nil, err + } + defer rows.Close() + + items := make([]map[string]interface{}, 0) + for rows.Next() { + var id, userID, tunnelID, inFlow, outFlow, createdTime, inx int64 + var userName, name, tunnelName, remoteAddr, strategy string + var status int + var inIP sql.NullString + var inPort sql.NullInt64 + + if err := rows.Scan(&id, &userID, &userName, &name, &tunnelID, &tunnelName, &remoteAddr, &strategy, &inFlow, &outFlow, &createdTime, &status, &inx, &inIP, &inPort); err != nil { + return nil, err + } + + items = append(items, map[string]interface{}{ + "id": id, + "userId": userID, + "userName": userName, + "name": name, + "tunnelId": tunnelID, + "tunnelName": tunnelName, + "inIp": nullableString(inIP), + "inPort": nullableInt64(inPort), + "remoteAddr": remoteAddr, + "strategy": strategy, + "inFlow": inFlow, + "outFlow": outFlow, + "createdTime": createdTime, + "status": status, + "inx": inx, + }) + } + + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +func (r *Repository) ListUserAccessibleTunnels(userID int64) ([]map[string]interface{}, error) { + if r == nil || r.db == nil { + return nil, errors.New("repository not initialized") + } + + rows, err := r.db.Query(` + SELECT t.id, t.name + FROM user_tunnel ut + JOIN tunnel t ON t.id = ut.tunnel_id + WHERE ut.user_id = ? AND ut.status = 1 + ORDER BY t.inx ASC, t.id ASC + `, userID) + if err != nil { + return nil, err + } + defer rows.Close() + + items := make([]map[string]interface{}, 0) + for rows.Next() { + var id int64 + var name string + if err := rows.Scan(&id, &name); err != nil { + return nil, err + } + items = append(items, map[string]interface{}{"id": id, "name": name}) + } + + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +func (r *Repository) ListTunnels() ([]map[string]interface{}, error) { + if r == nil || r.db == nil { + return nil, errors.New("repository not initialized") + } + + rows, err := r.db.Query(` + SELECT id, inx, name, type, flow, traffic_ratio, status, created_time, in_ip + FROM tunnel + ORDER BY inx ASC, id ASC + `) + if err != nil { + return nil, err + } + defer rows.Close() + + tunnelMap := make(map[int64]map[string]interface{}) + orderedIDs := make([]int64, 0) + + for rows.Next() { + var id, inx, flow, createdTime int64 + var name string + var typ, status int + var trafficRatio float64 + var inIP sql.NullString + if err := rows.Scan(&id, &inx, &name, &typ, &flow, &trafficRatio, &status, &createdTime, &inIP); err != nil { + return nil, err + } + + tunnelMap[id] = map[string]interface{}{ + "id": id, + "inx": inx, + "name": name, + "type": typ, + "flow": flow, + "trafficRatio": trafficRatio, + "status": status, + "createdTime": createdTime, + "inIp": nullableString(inIP), + "inNodeId": make([]map[string]interface{}, 0), + "outNodeId": make([]map[string]interface{}, 0), + "chainNodes": make([][]map[string]interface{}, 0), + } + orderedIDs = append(orderedIDs, id) + } + if err := rows.Err(); err != nil { + return nil, err + } + + nodeIPMap := map[int64]string{} + nRows, err := r.db.Query(`SELECT id, server_ip FROM node`) + if err == nil { + for nRows.Next() { + var id int64 + var ip string + if scanErr := nRows.Scan(&id, &ip); scanErr == nil { + nodeIPMap[id] = ip + } + } + _ = nRows.Close() + } + + chainRows, err := r.db.Query(` + SELECT tunnel_id, chain_type, node_id, protocol, strategy, COALESCE(inx, 0) + FROM chain_tunnel + ORDER BY tunnel_id ASC, chain_type ASC, inx ASC, id ASC + `) + if err != nil { + return nil, err + } + defer chainRows.Close() + + chainBucket := map[int64]map[int][]map[string]interface{}{} + inNodeIPs := map[int64][]string{} + + for chainRows.Next() { + var tunnelID, nodeID, inx int64 + var chainType int + var protocol, strategy sql.NullString + if err := chainRows.Scan(&tunnelID, &chainType, &nodeID, &protocol, &strategy, &inx); err != nil { + return nil, err + } + + t, ok := tunnelMap[tunnelID] + if !ok { + continue + } + + nodeObj := map[string]interface{}{ + "nodeId": nodeID, + "chainType": chainType, + "inx": inx, + } + if protocol.Valid { + nodeObj["protocol"] = protocol.String + } + if strategy.Valid { + nodeObj["strategy"] = strategy.String + } + + switch chainType { + case 1: + t["inNodeId"] = append(t["inNodeId"].([]map[string]interface{}), nodeObj) + if ip, ok := nodeIPMap[nodeID]; ok && ip != "" { + inNodeIPs[tunnelID] = append(inNodeIPs[tunnelID], ip) + } + case 2: + if _, ok := chainBucket[tunnelID]; !ok { + chainBucket[tunnelID] = map[int][]map[string]interface{}{} + } + chainBucket[tunnelID][int(inx)] = append(chainBucket[tunnelID][int(inx)], nodeObj) + case 3: + t["outNodeId"] = append(t["outNodeId"].([]map[string]interface{}), nodeObj) + } + } + if err := chainRows.Err(); err != nil { + return nil, err + } + + for tunnelID, groups := range chainBucket { + t := tunnelMap[tunnelID] + if t == nil { + continue + } + keys := make([]int, 0, len(groups)) + for k := range groups { + keys = append(keys, k) + } + sort.Ints(keys) + ordered := make([][]map[string]interface{}, 0, len(keys)) + for _, k := range keys { + ordered = append(ordered, groups[k]) + } + t["chainNodes"] = ordered + + if s, ok := t["inIp"].(string); !ok || strings.TrimSpace(s) == "" { + if ips := inNodeIPs[tunnelID]; len(ips) > 0 { + t["inIp"] = strings.Join(ips, ",") + } + } + } + + result := make([]map[string]interface{}, 0, len(orderedIDs)) + for _, id := range orderedIDs { + if t, ok := tunnelMap[id]; ok { + result = append(result, t) + } + } + return result, nil +} + +func (r *Repository) ListTunnelGroups() ([]map[string]interface{}, error) { + if r == nil || r.db == nil { + return nil, errors.New("repository not initialized") + } + + rows, err := r.db.Query(`SELECT id, name, status, created_time FROM tunnel_group ORDER BY id ASC`) + if err != nil { + return nil, err + } + defer rows.Close() + + result := make([]map[string]interface{}, 0) + for rows.Next() { + var id, createdTime int64 + var name string + var status int + if err := rows.Scan(&id, &name, &status, &createdTime); err != nil { + return nil, err + } + + ids, names, err := r.listTunnelGroupMembers(id) + if err != nil { + return nil, err + } + + result = append(result, map[string]interface{}{ + "id": id, + "name": name, + "status": status, + "tunnelIds": ids, + "tunnelNames": names, + "createdTime": createdTime, + }) + } + if err := rows.Err(); err != nil { + return nil, err + } + return result, nil +} + +func (r *Repository) ListUserGroups() ([]map[string]interface{}, error) { + if r == nil || r.db == nil { + return nil, errors.New("repository not initialized") + } + + rows, err := r.db.Query(`SELECT id, name, status, created_time FROM user_group ORDER BY id ASC`) + if err != nil { + return nil, err + } + defer rows.Close() + + result := make([]map[string]interface{}, 0) + for rows.Next() { + var id, createdTime int64 + var name string + var status int + if err := rows.Scan(&id, &name, &status, &createdTime); err != nil { + return nil, err + } + + ids, names, err := r.listUserGroupMembers(id) + if err != nil { + return nil, err + } + + result = append(result, map[string]interface{}{ + "id": id, + "name": name, + "status": status, + "userIds": ids, + "userNames": names, + "createdTime": createdTime, + }) + } + if err := rows.Err(); err != nil { + return nil, err + } + return result, nil +} + +func (r *Repository) ListGroupPermissions() ([]map[string]interface{}, error) { + if r == nil || r.db == nil { + return nil, errors.New("repository not initialized") + } + + rows, err := r.db.Query(` + SELECT gp.id, gp.user_group_id, ug.name, gp.tunnel_group_id, tg.name, gp.created_time + FROM group_permission gp + LEFT JOIN user_group ug ON ug.id = gp.user_group_id + LEFT JOIN tunnel_group tg ON tg.id = gp.tunnel_group_id + ORDER BY gp.id ASC + `) + if err != nil { + return nil, err + } + defer rows.Close() + + result := make([]map[string]interface{}, 0) + for rows.Next() { + var id, userGroupID, tunnelGroupID, createdTime int64 + var userGroupName, tunnelGroupName sql.NullString + if err := rows.Scan(&id, &userGroupID, &userGroupName, &tunnelGroupID, &tunnelGroupName, &createdTime); err != nil { + return nil, err + } + + result = append(result, map[string]interface{}{ + "id": id, + "userGroupId": userGroupID, + "userGroupName": nullableString(userGroupName), + "tunnelGroupId": tunnelGroupID, + "tunnelGroupName": nullableString(tunnelGroupName), + "createdTime": createdTime, + }) + } + if err := rows.Err(); err != nil { + return nil, err + } + return result, nil +} + +func (r *Repository) listTunnelGroupMembers(groupID int64) ([]int64, []string, error) { + rows, err := r.db.Query(` + SELECT t.id, t.name + FROM tunnel_group_tunnel tgt + JOIN tunnel t ON t.id = tgt.tunnel_id + WHERE tgt.tunnel_group_id = ? + ORDER BY t.id ASC + `, groupID) + if err != nil { + return nil, nil, err + } + defer rows.Close() + + ids := make([]int64, 0) + names := make([]string, 0) + for rows.Next() { + var id int64 + var name string + if err := rows.Scan(&id, &name); err != nil { + return nil, nil, err + } + ids = append(ids, id) + names = append(names, name) + } + if err := rows.Err(); err != nil { + return nil, nil, err + } + return ids, names, nil +} + +func (r *Repository) listUserGroupMembers(groupID int64) ([]int64, []string, error) { + rows, err := r.db.Query(` + SELECT u.id, u.user + FROM user_group_user ugu + JOIN user u ON u.id = ugu.user_id + WHERE ugu.user_group_id = ? + ORDER BY u.id ASC + `, groupID) + if err != nil { + return nil, nil, err + } + defer rows.Close() + + ids := make([]int64, 0) + names := make([]string, 0) + for rows.Next() { + var id int64 + var name string + if err := rows.Scan(&id, &name); err != nil { + return nil, nil, err + } + ids = append(ids, id) + names = append(names, name) + } + if err := rows.Err(); err != nil { + return nil, nil, err + } + return ids, names, nil +} + +func nullableString(v sql.NullString) interface{} { + if v.Valid { + return v.String + } + return nil +} + +func nullableInt64(v sql.NullInt64) interface{} { + if v.Valid { + return v.Int64 + } + return nil +} + func unixMilliNow() int64 { return time.Now().UnixMilli() } + +func ensureParentDir(dbPath string) error { + if dbPath == "" { + return fmt.Errorf("empty db path") + } + dir := filepath.Dir(dbPath) + if dir == "" || dir == "." { + return nil + } + return osMkdirAll(dir) +} + +func bootstrapSchema(db *sql.DB) error { + if db == nil { + return errors.New("nil db") + } + + var exists int + err := db.QueryRow(`SELECT COUNT(1) FROM sqlite_master WHERE type='table' AND name='user'`).Scan(&exists) + if err != nil { + return fmt.Errorf("check schema: %w", err) + } + if exists > 0 { + return nil + } + + log.Printf("sqlite schema not found, bootstrapping embedded schema") + if _, err := db.Exec(embeddedSchema); err != nil { + return fmt.Errorf("apply schema.sql: %w", err) + } + if _, err := db.Exec(embeddedSeedData); err != nil { + return fmt.Errorf("apply data.sql: %w", err) + } + return nil +} + +var osMkdirAll = func(path string) error { + return os.MkdirAll(path, 0o755) +} diff --git a/go-backend/internal/store/sqlite/sql/data.sql b/go-backend/internal/store/sqlite/sql/data.sql new file mode 100644 index 0000000..ed78932 --- /dev/null +++ b/go-backend/internal/store/sqlite/sql/data.sql @@ -0,0 +1,5 @@ +INSERT OR IGNORE INTO user (id, user, pwd, role_id, exp_time, flow, in_flow, out_flow, flow_reset_time, num, created_time, updated_time, status) +VALUES (1, 'admin_user', '3c85cdebade1c51cf64ca9f3c09d182d', 0, 2727251700000, 99999, 0, 0, 1, 99999, 1748914865000, 1754011744252, 1); + +INSERT OR IGNORE INTO vite_config (id, name, value, time) +VALUES (1, 'app_name', 'flux', 1755147963000); diff --git a/go-backend/internal/store/sqlite/sql/schema.sql b/go-backend/internal/store/sqlite/sql/schema.sql new file mode 100644 index 0000000..9330f04 --- /dev/null +++ b/go-backend/internal/store/sqlite/sql/schema.sql @@ -0,0 +1,182 @@ +-- SQLite Auto-generated schema +-- This will be executed automatically on startup if tables don't exist + +CREATE TABLE IF NOT EXISTS forward ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + user_name VARCHAR(100) NOT NULL, + name VARCHAR(100) NOT NULL, + tunnel_id INTEGER NOT NULL, + remote_addr TEXT NOT NULL, + strategy VARCHAR(100) NOT NULL DEFAULT 'fifo', + in_flow INTEGER NOT NULL DEFAULT 0, + out_flow INTEGER NOT NULL DEFAULT 0, + created_time INTEGER NOT NULL, + updated_time INTEGER NOT NULL, + status INTEGER NOT NULL, + inx INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS forward_port ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + forward_id INTEGER NOT NULL, + node_id INTEGER NOT NULL, + port INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS node ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(100) NOT NULL, + secret VARCHAR(100) NOT NULL, + server_ip VARCHAR(100) NOT NULL, + server_ip_v4 VARCHAR(100), + server_ip_v6 VARCHAR(100), + port TEXT NOT NULL, + interface_name VARCHAR(200), + version VARCHAR(100), + http INTEGER NOT NULL DEFAULT 0, + tls INTEGER NOT NULL DEFAULT 0, + socks INTEGER NOT NULL DEFAULT 0, + created_time INTEGER NOT NULL, + updated_time INTEGER, + status INTEGER NOT NULL, + tcp_listen_addr VARCHAR(100) NOT NULL DEFAULT '[::]', + udp_listen_addr VARCHAR(100) NOT NULL DEFAULT '[::]', + inx INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS speed_limit ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(100) NOT NULL, + speed INTEGER NOT NULL, + tunnel_id INTEGER NOT NULL, + tunnel_name VARCHAR(100) NOT NULL, + created_time INTEGER NOT NULL, + updated_time INTEGER, + status INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS statistics_flow ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + flow INTEGER NOT NULL, + total_flow INTEGER NOT NULL, + time VARCHAR(100) NOT NULL, + created_time INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS tunnel ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(100) NOT NULL, + traffic_ratio REAL NOT NULL DEFAULT 1.0, + type INTEGER NOT NULL, + protocol VARCHAR(10) NOT NULL DEFAULT 'tls', + flow INTEGER NOT NULL, + created_time INTEGER NOT NULL, + updated_time INTEGER NOT NULL, + status INTEGER NOT NULL, + in_ip TEXT, + inx INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS chain_tunnel ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + tunnel_id INTEGER NOT NULL , + chain_type VARCHAR(10) NOT NULL, + node_id INTEGER NOT NULL , + port INTEGER, + strategy VARCHAR(10), + inx INTEGER, + protocol VARCHAR(10) +); + + +CREATE TABLE IF NOT EXISTS user ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user VARCHAR(100) NOT NULL, + pwd VARCHAR(100) NOT NULL, + role_id INTEGER NOT NULL, + exp_time INTEGER NOT NULL, + flow INTEGER NOT NULL, + in_flow INTEGER NOT NULL DEFAULT 0, + out_flow INTEGER NOT NULL DEFAULT 0, + flow_reset_time INTEGER NOT NULL, + num INTEGER NOT NULL, + created_time INTEGER NOT NULL, + updated_time INTEGER, + status INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS user_tunnel ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + tunnel_id INTEGER NOT NULL, + speed_id INTEGER, + num INTEGER NOT NULL, + flow INTEGER NOT NULL, + in_flow INTEGER NOT NULL DEFAULT 0, + out_flow INTEGER NOT NULL DEFAULT 0, + flow_reset_time INTEGER NOT NULL, + exp_time INTEGER NOT NULL, + status INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS tunnel_group ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(100) NOT NULL, + created_time INTEGER NOT NULL, + updated_time INTEGER NOT NULL, + status INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS user_group ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(100) NOT NULL, + created_time INTEGER NOT NULL, + updated_time INTEGER NOT NULL, + status INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS tunnel_group_tunnel ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + tunnel_group_id INTEGER NOT NULL, + tunnel_id INTEGER NOT NULL, + created_time INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS user_group_user ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_group_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + created_time INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS group_permission ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_group_id INTEGER NOT NULL, + tunnel_group_id INTEGER NOT NULL, + created_time INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS group_permission_grant ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_group_id INTEGER NOT NULL, + tunnel_group_id INTEGER NOT NULL, + user_tunnel_id INTEGER NOT NULL, + created_by_group INTEGER NOT NULL DEFAULT 0, + created_time INTEGER NOT NULL +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_tunnel_group_name ON tunnel_group(name); +CREATE UNIQUE INDEX IF NOT EXISTS idx_user_group_name ON user_group(name); +CREATE UNIQUE INDEX IF NOT EXISTS idx_tunnel_group_tunnel_unique ON tunnel_group_tunnel(tunnel_group_id, tunnel_id); +CREATE UNIQUE INDEX IF NOT EXISTS idx_user_group_user_unique ON user_group_user(user_group_id, user_id); +CREATE UNIQUE INDEX IF NOT EXISTS idx_group_permission_unique ON group_permission(user_group_id, tunnel_group_id); +CREATE UNIQUE INDEX IF NOT EXISTS idx_group_permission_grant_unique ON group_permission_grant(user_group_id, tunnel_group_id, user_tunnel_id); + +CREATE TABLE IF NOT EXISTS vite_config ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(200) NOT NULL UNIQUE, + value VARCHAR(200) NOT NULL, + time INTEGER NOT NULL +);