fix: restore panel list endpoints and bootstrap sqlite state

This commit is contained in:
sagit
2026-02-06 12:23:46 +00:00
parent 0f37017760
commit b6333d81a2
8 changed files with 1094 additions and 1 deletions
+4 -1
View File
@@ -257,4 +257,7 @@ gitee/
doraemon.jks
device.id
commit.sh
sql/
sql/
!go-backend/internal/store/sqlite/sql/
!go-backend/internal/store/sqlite/sql/schema.sql
!go-backend/internal/store/sqlite/sql/data.sql
+1
View File
@@ -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 {
+203
View File
@@ -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
@@ -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())
})
}
+1
View File
@@ -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
}
@@ -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)
}
@@ -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);
@@ -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
);