refactor(backend): migrate to modular repository pattern with separated concerns

- Extract database layer into model and repo packages
- Split repository into focused modules (control, federation, flow, groups, mutations)
- Remove monolithic db.go and sqlite/repository.go
- Update handlers to use new repository structure
- Migrate contract tests to new patterns
- Add migration plan documentation
This commit is contained in:
Antigravity
2026-02-17 04:47:11 +00:00
parent 98b4d78b4d
commit 66be07750f
44 changed files with 6829 additions and 6188 deletions
+8 -8
View File
@@ -10,30 +10,30 @@ import (
"go-backend/internal/config"
httpserver "go-backend/internal/http"
"go-backend/internal/http/handler"
"go-backend/internal/store/sqlite"
"go-backend/internal/store/repo"
)
type App struct {
cfg config.Config
server *http.Server
repo *sqlite.Repository
repo *repo.Repository
h *handler.Handler
}
func New(cfg config.Config) (*App, error) {
var (
repo *sqlite.Repository
err error
r *repo.Repository
err error
)
switch strings.ToLower(strings.TrimSpace(cfg.DBType)) {
case "", "sqlite":
repo, err = sqlite.Open(cfg.DBPath)
r, err = repo.Open(cfg.DBPath)
if err != nil {
return nil, fmt.Errorf("open sqlite: %w", err)
}
case "postgres", "postgresql":
repo, err = sqlite.OpenPostgres(cfg.DatabaseURL)
r, err = repo.OpenPostgres(cfg.DatabaseURL)
if err != nil {
return nil, fmt.Errorf("open postgres: %w", err)
}
@@ -41,7 +41,7 @@ func New(cfg config.Config) (*App, error) {
return nil, fmt.Errorf("unsupported DB_TYPE %q", cfg.DBType)
}
h := handler.New(repo, cfg.JWTSecret)
h := handler.New(r, cfg.JWTSecret)
router := httpserver.NewRouter(h, cfg.JWTSecret)
s := &http.Server{
@@ -53,7 +53,7 @@ func New(cfg config.Config) (*App, error) {
IdleTimeout: 60 * time.Second,
}
return &App{cfg: cfg, server: s, repo: repo, h: h}, nil
return &App{cfg: cfg, server: s, repo: r, h: h}, nil
}
func (a *App) Run() error {
+5 -4
View File
@@ -4,7 +4,7 @@
## OVERVIEW
HTTP request handlers for FLVX Admin API. Core business logic layer.
**Stack:** Go 1.23, net/http, raw SQL (no ORM).
**Stack:** Go 1.23, net/http, GORM via Repository pattern.
## STRUCTURE
```
@@ -28,13 +28,14 @@ handler/
| **Background Jobs** | `jobs.go` | Scheduled sync/cleanup tasks |
## CONVENTIONS
- Inherits from parent: raw SQL, no ORM, JWT in Authorization header.
- Inherits from parent: GORM via Repository pattern, JWT in Authorization header.
- Large files expected (`mutations.go` 3716 LOC - central mutation hub).
- Uses `sqlite.Repository` for DB access via `repo.XXX()` methods.
- Uses `repo.Repository` for DB access via `h.repo.XXX()` methods.
- Handlers never call `repo.DB()` directly — all queries go through Repository methods.
- Domain-driven file split: one file per functional area (federation, jobs, etc.).
## ANTI-PATTERNS
- Do NOT add ORM here - uses raw SQL throughout.
- Do NOT let handlers call `repo.DB()` directly — add a Repository method instead.
- Do NOT change handler signatures without updating router.go.
## COMMANDS
+37 -279
View File
@@ -1,7 +1,6 @@
package handler
import (
"database/sql"
"errors"
"fmt"
"net"
@@ -12,61 +11,18 @@ import (
"time"
"go-backend/internal/http/client"
"go-backend/internal/store/model"
"go-backend/internal/ws"
)
var errForwardNotFound = errors.New("forward not found")
type forwardRecord struct {
ID int64
UserID int64
UserName string
Name string
TunnelID int64
RemoteAddr string
Strategy string
Status int
}
type forwardRecord = model.ForwardRecord
type tunnelRecord = model.TunnelRecord
type forwardPortRecord = model.ForwardPortRecord
type nodeRecord = model.NodeRecord
type tunnelRecord struct {
ID int64
Type int
Status int
Flow int64
TrafficRatio float64
}
type forwardPortRecord struct {
NodeID int64
Port int
}
type nodeRecord struct {
ID int64
Name string
ServerIP string
ServerIPv4 string
ServerIPv6 string
Status int
PortRange string
TCPListenAddr string
UDPListenAddr string
InterfaceName string
IsRemote int
RemoteURL string
RemoteToken string
RemoteConfig string
}
type chainNodeRecord struct {
ChainType int
Inx int64
NodeID int64
Port int
NodeName string
Protocol string
Strategy string
}
type chainNodeRecord = model.ChainNodeRecord
type diagnosisTarget struct {
Address string
@@ -101,247 +57,82 @@ func (h *Handler) ensureTunnelPermission(userID int64, roleID int, tunnelID int6
if roleID == 0 {
return nil
}
var count int
err := h.repo.DB().QueryRow(`SELECT COUNT(1) FROM user_tunnel WHERE user_id = ? AND tunnel_id = ? AND status = 1`, userID, tunnelID).Scan(&count)
ok, err := h.repo.UserTunnelExistsByUserAndTunnel(userID, tunnelID)
if err != nil {
return err
}
if count <= 0 {
if !ok {
return errors.New("你没有该隧道的权限")
}
return nil
}
func (h *Handler) getForwardRecord(forwardID int64) (*forwardRecord, error) {
row := h.repo.DB().QueryRow(`
SELECT id, user_id, user_name, name, tunnel_id, remote_addr, COALESCE(strategy, 'fifo'), status
FROM forward WHERE id = ? LIMIT 1
`, forwardID)
var fr forwardRecord
err := row.Scan(&fr.ID, &fr.UserID, &fr.UserName, &fr.Name, &fr.TunnelID, &fr.RemoteAddr, &fr.Strategy, &fr.Status)
fr, err := h.repo.GetForwardRecord(forwardID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, errForwardNotFound
}
return nil, err
}
if strings.TrimSpace(fr.Strategy) == "" {
fr.Strategy = "fifo"
if fr == nil {
return nil, errForwardNotFound
}
return &fr, nil
return fr, nil
}
func (h *Handler) getTunnelRecord(tunnelID int64) (*tunnelRecord, error) {
row := h.repo.DB().QueryRow(`SELECT id, type, status, flow, traffic_ratio FROM tunnel WHERE id = ? LIMIT 1`, tunnelID)
var tr tunnelRecord
err := row.Scan(&tr.ID, &tr.Type, &tr.Status, &tr.Flow, &tr.TrafficRatio)
tr, err := h.repo.GetTunnelRecord(tunnelID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, errors.New("隧道不存在")
}
return nil, err
}
if tr.Flow <= 0 {
tr.Flow = 1
if tr == nil {
return nil, errors.New("隧道不存在")
}
if tr.TrafficRatio <= 0 {
tr.TrafficRatio = 1
}
return &tr, nil
return tr, nil
}
func (h *Handler) listForwardsByTunnel(tunnelID int64) ([]forwardRecord, error) {
rows, err := h.repo.DB().Query(`
SELECT id, user_id, user_name, name, tunnel_id, remote_addr, COALESCE(strategy, 'fifo'), status
FROM forward
WHERE tunnel_id = ?
ORDER BY id ASC
`, tunnelID)
if err != nil {
return nil, err
}
defer rows.Close()
result := make([]forwardRecord, 0)
for rows.Next() {
var fr forwardRecord
if err := rows.Scan(&fr.ID, &fr.UserID, &fr.UserName, &fr.Name, &fr.TunnelID, &fr.RemoteAddr, &fr.Strategy, &fr.Status); err != nil {
return nil, err
}
if strings.TrimSpace(fr.Strategy) == "" {
fr.Strategy = "fifo"
}
result = append(result, fr)
}
if err := rows.Err(); err != nil {
return nil, err
}
return result, nil
return h.repo.ListForwardsByTunnel(tunnelID)
}
func (h *Handler) listForwardPorts(forwardID int64) ([]forwardPortRecord, error) {
rows, err := h.repo.DB().Query(`SELECT node_id, port FROM forward_port WHERE forward_id = ? ORDER BY id ASC`, forwardID)
if err != nil {
return nil, err
}
defer rows.Close()
result := make([]forwardPortRecord, 0)
for rows.Next() {
var item forwardPortRecord
if err := rows.Scan(&item.NodeID, &item.Port); err != nil {
return nil, err
}
result = append(result, item)
}
if err := rows.Err(); err != nil {
return nil, err
}
return result, nil
return h.repo.ListForwardPorts(forwardID)
}
func (h *Handler) isTunnelSelectedTLSProtocol(tunnelID int64) (bool, error) {
row := h.repo.DB().QueryRow(`
SELECT protocol
FROM chain_tunnel
WHERE tunnel_id = ? AND chain_type = '3'
ORDER BY id ASC
LIMIT 1
`, tunnelID)
var protocol sql.NullString
if err := row.Scan(&protocol); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return false, nil
}
protocol, err := h.repo.GetTunnelOutProtocol(tunnelID)
if err != nil {
return false, err
}
return isTLSTunnelProtocol(protocol.String), nil
return isTLSTunnelProtocol(protocol), nil
}
func (h *Handler) getNodeRecord(nodeID int64) (*nodeRecord, error) {
row := h.repo.DB().QueryRow(`
SELECT id, name, server_ip, server_ip_v4, server_ip_v6, status, port, tcp_listen_addr, udp_listen_addr, interface_name, is_remote, remote_url, remote_token, remote_config
FROM node
WHERE id = ?
LIMIT 1
`, nodeID)
var n nodeRecord
var serverIPv4 sql.NullString
var serverIPv6 sql.NullString
var portRange sql.NullString
var tcpListen sql.NullString
var udpListen sql.NullString
var iface sql.NullString
var remoteURL sql.NullString
var remoteToken sql.NullString
var remoteConfig sql.NullString
err := row.Scan(&n.ID, &n.Name, &n.ServerIP, &serverIPv4, &serverIPv6, &n.Status, &portRange, &tcpListen, &udpListen, &iface, &n.IsRemote, &remoteURL, &remoteToken, &remoteConfig)
n, err := h.repo.GetNodeRecord(nodeID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, errors.New("节点不存在")
}
return nil, err
}
n.ServerIPv4 = strings.TrimSpace(serverIPv4.String)
n.ServerIPv6 = strings.TrimSpace(serverIPv6.String)
n.PortRange = strings.TrimSpace(portRange.String)
n.TCPListenAddr = strings.TrimSpace(tcpListen.String)
n.UDPListenAddr = strings.TrimSpace(udpListen.String)
n.InterfaceName = strings.TrimSpace(iface.String)
n.RemoteURL = strings.TrimSpace(remoteURL.String)
n.RemoteToken = strings.TrimSpace(remoteToken.String)
n.RemoteConfig = strings.TrimSpace(remoteConfig.String)
if n.TCPListenAddr == "" {
n.TCPListenAddr = "[::]"
if n == nil {
return nil, errors.New("节点不存在")
}
if n.UDPListenAddr == "" {
n.UDPListenAddr = "[::]"
}
if strings.TrimSpace(n.Name) == "" {
n.Name = fmt.Sprintf("node_%d", n.ID)
}
return &n, nil
return n, nil
}
func (h *Handler) resolveUserTunnelAndLimiter(userID, tunnelID int64) (int64, *int64, *int, error) {
row := h.repo.DB().QueryRow(`
SELECT ut.id, sl.id, sl.speed
FROM user_tunnel ut
LEFT JOIN speed_limit sl ON sl.id = ut.speed_id
WHERE ut.user_id = ? AND ut.tunnel_id = ?
ORDER BY ut.id ASC
LIMIT 1
`, userID, tunnelID)
var userTunnelID int64
var limiterID sql.NullInt64
var speed sql.NullInt64
err := row.Scan(&userTunnelID, &limiterID, &speed)
info, err := h.repo.ResolveUserTunnelAndLimiter(userID, tunnelID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return 0, nil, nil, nil
}
return 0, nil, nil, err
}
if !limiterID.Valid || limiterID.Int64 <= 0 {
return userTunnelID, nil, nil, nil
if info == nil {
return 0, nil, nil, nil
}
v := limiterID.Int64
s := int(speed.Int64)
return userTunnelID, &v, &s, nil
return info.UserTunnelID, info.LimiterID, info.Speed, nil
}
func (h *Handler) listUserTunnelIDs(userID, tunnelID int64) ([]int64, error) {
rows, err := h.repo.DB().Query(`
SELECT id
FROM user_tunnel
WHERE user_id = ? AND tunnel_id = ?
ORDER BY id ASC
`, userID, tunnelID)
if err != nil {
return nil, err
}
defer rows.Close()
out := make([]int64, 0)
for rows.Next() {
var id int64
if err := rows.Scan(&id); err != nil {
return nil, err
}
out = append(out, id)
}
if err := rows.Err(); err != nil {
return nil, err
}
return out, nil
return h.repo.ListUserTunnelIDs(userID, tunnelID)
}
func (h *Handler) listUserTunnelIDsByUser(userID int64) ([]int64, error) {
rows, err := h.repo.DB().Query(`
SELECT id
FROM user_tunnel
WHERE user_id = ?
ORDER BY id ASC
`, userID)
if err != nil {
return nil, err
}
defer rows.Close()
out := make([]int64, 0)
for rows.Next() {
var id int64
if err := rows.Scan(&id); err != nil {
return nil, err
}
out = append(out, id)
}
if err := rows.Err(); err != nil {
return nil, err
}
return out, nil
return h.repo.ListUserTunnelIDsByUser(userID)
}
func (h *Handler) syncForwardServices(forward *forwardRecord, method string, allowFallbackAdd bool) error {
@@ -663,13 +454,13 @@ func (h *Handler) diagnoseTunnelRuntime(tunnelID int64) (map[string]interface{},
return nil, err
}
var tunnelName string
if err := h.repo.DB().QueryRow(`SELECT name FROM tunnel WHERE id = ?`, tunnelID).Scan(&tunnelName); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, errors.New("隧道不存在")
}
tunnelName, err := h.repo.GetTunnelName(tunnelID)
if err != nil {
return nil, err
}
if tunnelName == "" {
return nil, errors.New("隧道不存在")
}
chainRows, err := h.listChainNodesForTunnel(tunnelID)
if err != nil {
@@ -960,40 +751,7 @@ func firstPortFromRange(portRange string) int {
}
func (h *Handler) listChainNodesForTunnel(tunnelID int64) ([]chainNodeRecord, error) {
rows, err := h.repo.DB().Query(`
SELECT CAST(ct.chain_type AS INTEGER), COALESCE(ct.inx, 0), ct.node_id, COALESCE(ct.port, 0), n.name, ct.protocol, ct.strategy
FROM chain_tunnel ct
LEFT JOIN node n ON n.id = ct.node_id
WHERE ct.tunnel_id = ?
ORDER BY CAST(ct.chain_type AS INTEGER) ASC, COALESCE(ct.inx, 0) ASC, ct.id ASC
`, tunnelID)
if err != nil {
return nil, err
}
defer rows.Close()
result := make([]chainNodeRecord, 0)
for rows.Next() {
var item chainNodeRecord
var name sql.NullString
var protocol sql.NullString
var strategy sql.NullString
if err := rows.Scan(&item.ChainType, &item.Inx, &item.NodeID, &item.Port, &name, &protocol, &strategy); err != nil {
return nil, err
}
if strings.TrimSpace(name.String) == "" {
item.NodeName = fmt.Sprintf("node_%d", item.NodeID)
} else {
item.NodeName = name.String
}
item.Protocol = defaultString(protocol.String, "tls")
item.Strategy = defaultString(strategy.String, "round")
result = append(result, item)
}
if err := rows.Err(); err != nil {
return nil, err
}
return result, nil
return h.repo.ListChainNodesForTunnel(tunnelID)
}
func (h *Handler) tcpPingViaNode(nodeID int64, ip string, port int) (map[string]interface{}, error) {
+49 -141
View File
@@ -1,7 +1,6 @@
package handler
import (
"database/sql"
"encoding/json"
"fmt"
"net"
@@ -14,7 +13,7 @@ import (
"go-backend/internal/http/client"
"go-backend/internal/http/response"
"go-backend/internal/store/sqlite"
"go-backend/internal/store/repo"
)
type federationTunnelRequest struct {
@@ -108,7 +107,7 @@ type peerShareUsedPort struct {
}
type peerShareListItem struct {
sqlite.PeerShare
repo.PeerShare
UsedPorts []int `json:"usedPorts"`
UsedPortDetails []peerShareUsedPort `json:"usedPortDetails"`
ActiveRuntimeNum int `json:"activeRuntimeNum"`
@@ -264,7 +263,7 @@ func (h *Handler) federationShareCreate(w http.ResponseWriter, r *http.Request)
now := time.Now().UnixMilli()
token := randomToken(32)
share := &sqlite.PeerShare{
share := &repo.PeerShare{
Name: req.Name,
NodeID: req.NodeID,
Token: token,
@@ -430,40 +429,25 @@ func (h *Handler) federationRemoteUsageList(w http.ResponseWriter, r *http.Reque
return
}
rows, err := h.repo.DB().Query(`
SELECT id, name, remote_url, remote_token, remote_config
FROM node
WHERE is_remote = 1
ORDER BY id DESC
`)
remoteNodes, err := h.repo.ListRemoteNodes()
if err != nil {
response.WriteJSON(w, response.Err(-2, err.Error()))
return
}
defer rows.Close()
fc := client.NewFederationClient()
localDomain := h.federationLocalDomain()
items := make([]remoteUsageNodeItem, 0)
for rows.Next() {
var (
nodeID int64
nodeName string
remoteURL sql.NullString
remoteToken sql.NullString
remoteConfig sql.NullString
)
if err := rows.Scan(&nodeID, &nodeName, &remoteURL, &remoteToken, &remoteConfig); err != nil {
response.WriteJSON(w, response.Err(-2, err.Error()))
return
}
for _, node := range remoteNodes {
nodeID := node.ID
nodeName := node.Name
shareID, maxBandwidth, currentFlow, expiryTime, portRangeStart, portRangeEnd := parseRemoteShareUsageConfig(remoteConfig.String)
shareID, maxBandwidth, currentFlow, expiryTime, portRangeStart, portRangeEnd := parseRemoteShareUsageConfig(node.RemoteConfig.String)
var syncError string
url := strings.TrimSpace(remoteURL.String)
token := strings.TrimSpace(remoteToken.String)
url := strings.TrimSpace(node.RemoteURL.String)
token := strings.TrimSpace(node.RemoteToken.String)
if url != "" && token != "" {
info, connectErr := fc.Connect(url, token, localDomain)
if connectErr != nil {
@@ -484,42 +468,34 @@ func (h *Handler) federationRemoteUsageList(w http.ResponseWriter, r *http.Reque
"portRangeStart": info.PortRangeStart,
"portRangeEnd": info.PortRangeEnd,
})
_, _ = h.repo.DB().Exec(`UPDATE node SET remote_config = ? WHERE id = ?`, string(configData), nodeID)
_ = h.repo.UpdateNodeRemoteConfig(nodeID, string(configData))
}
}
bindingRows, err := h.repo.DB().Query(`
SELECT fb.id, fb.tunnel_id, COALESCE(t.name, ''), fb.chain_type, fb.hop_inx, fb.allocated_port, fb.resource_key, fb.remote_binding_id, fb.updated_time
FROM federation_tunnel_binding fb
LEFT JOIN tunnel t ON t.id = fb.tunnel_id
WHERE fb.node_id = ? AND fb.status = 1
ORDER BY fb.allocated_port ASC, fb.id ASC
`, nodeID)
bindingRows, err := h.repo.ListActiveBindingsForNode(nodeID)
if err != nil {
response.WriteJSON(w, response.Err(-2, err.Error()))
return
}
usedSet := make(map[int]struct{})
bindings := make([]remoteUsageBindingItem, 0)
for bindingRows.Next() {
var item remoteUsageBindingItem
if err := bindingRows.Scan(&item.BindingID, &item.TunnelID, &item.TunnelName, &item.ChainType, &item.HopInx, &item.AllocatedPort, &item.ResourceKey, &item.RemoteBindingID, &item.UpdatedTime); err != nil {
_ = bindingRows.Close()
response.WriteJSON(w, response.Err(-2, err.Error()))
return
}
bindings = append(bindings, item)
if item.AllocatedPort > 0 {
usedSet[item.AllocatedPort] = struct{}{}
bindings := make([]remoteUsageBindingItem, 0, len(bindingRows))
for _, b := range bindingRows {
bindings = append(bindings, remoteUsageBindingItem{
BindingID: b.ID,
TunnelID: b.TunnelID,
TunnelName: b.TunnelName,
ChainType: b.ChainType,
HopInx: b.HopInx,
AllocatedPort: b.AllocatedPort,
ResourceKey: b.ResourceKey,
RemoteBindingID: b.RemoteBindingID,
UpdatedTime: b.UpdatedTime,
})
if b.AllocatedPort > 0 {
usedSet[b.AllocatedPort] = struct{}{}
}
}
if err := bindingRows.Err(); err != nil {
_ = bindingRows.Close()
response.WriteJSON(w, response.Err(-2, err.Error()))
return
}
_ = bindingRows.Close()
usedPorts := make([]int, 0, len(usedSet))
for port := range usedSet {
@@ -543,10 +519,6 @@ func (h *Handler) federationRemoteUsageList(w http.ResponseWriter, r *http.Reque
SyncError: syncError,
})
}
if err := rows.Err(); err != nil {
response.WriteJSON(w, response.Err(-2, err.Error()))
return
}
response.WriteJSON(w, response.OK(items))
}
@@ -639,31 +611,21 @@ func (h *Handler) nodeImport(w http.ResponseWriter, r *http.Request) {
portRange = fmt.Sprintf("%d-%d", info.PortRangeStart, info.PortRangeEnd)
}
db := h.repo.DB()
inx := nextIndex(db, "node")
inx := h.repo.NextIndex("node")
now := time.Now().UnixMilli()
_, err = db.Exec(`
INSERT INTO node(name, secret, server_ip, server_ip_v4, server_ip_v6, port, interface_name, version, http, tls, socks, created_time, updated_time, status, tcp_listen_addr, udp_listen_addr, inx, is_remote, remote_url, remote_token, remote_config)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, 0, 0, 0, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?)
`,
if err = h.repo.CreateRemoteNode(
fmt.Sprintf("%s (Remote)", info.NodeName),
randomToken(16), // Dummy secret
randomToken(16),
info.ServerIP,
"", "", // v4/v6 unknown, use server_ip
portRange,
"",
"",
now, now,
now,
info.Status,
"[::]", "[::]",
inx,
req.RemoteURL,
req.Token,
string(configBytes),
)
if err != nil {
); err != nil {
response.WriteJSON(w, response.Err(-2, "Database error: "+err.Error()))
return
}
@@ -755,11 +717,7 @@ func (h *Handler) federationConnect(w http.ResponseWriter, r *http.Request) {
return
}
var nodeName string
var serverIP string
var status int
err = h.repo.DB().QueryRow("SELECT name, server_ip, status FROM node WHERE id = ?", share.NodeID).Scan(&nodeName, &serverIP, &status)
nodeInfo, err := h.repo.GetNodeBasicInfo(share.NodeID)
if err != nil {
response.WriteJSON(w, response.Err(-2, "Node not found"))
return
@@ -769,9 +727,9 @@ func (h *Handler) federationConnect(w http.ResponseWriter, r *http.Request) {
"shareId": share.ID,
"shareName": share.Name,
"nodeId": share.NodeID,
"nodeName": nodeName,
"serverIp": serverIP,
"status": status,
"nodeName": nodeInfo.Name,
"serverIp": nodeInfo.ServerIP,
"status": nodeInfo.Status,
"maxBandwidth": share.MaxBandwidth,
"currentFlow": share.CurrentFlow,
"expiryTime": share.ExpiryTime,
@@ -808,45 +766,20 @@ func (h *Handler) federationTunnelCreate(w http.ResponseWriter, r *http.Request)
return
}
tunnelType := 1
tx, err := h.repo.DB().Begin()
if err != nil {
response.WriteJSON(w, response.Err(-2, err.Error()))
return
}
defer tx.Rollback()
now := time.Now().UnixMilli()
tunnelID, err := tx.ExecReturningID(`INSERT INTO tunnel (name, type, protocol, flow, created_time, updated_time, status, in_ip) VALUES (?, ?, ?, 0, ?, ?, 1, ?)`,
tunnelID, err := h.repo.CreateFederationTunnel(
fmt.Sprintf("Share-%d-Port-%d", share.ID, req.RemotePort),
tunnelType,
1,
req.Protocol,
now,
now,
"",
)
if err != nil {
response.WriteJSON(w, response.Err(-2, err.Error()))
return
}
_, err = tx.Exec(`INSERT INTO chain_tunnel (tunnel_id, chain_type, node_id, port, strategy, inx, protocol) VALUES (?, '1', ?, ?, 'fifo', 0, ?)`,
tunnelID,
share.NodeID,
req.RemotePort,
req.Protocol,
)
if err != nil {
response.WriteJSON(w, response.Err(-2, err.Error()))
return
}
if err := tx.Commit(); err != nil {
response.WriteJSON(w, response.Err(-2, err.Error()))
return
}
h.wsServer.SendCommand(share.NodeID, "reload", nil, time.Second*5)
response.WriteJSON(w, response.OK(map[string]interface{}{
@@ -927,7 +860,7 @@ func (h *Handler) federationRuntimeReservePort(w http.ResponseWriter, r *http.Re
return
}
runtime := &sqlite.PeerShareRuntime{
runtime := &repo.PeerShareRuntime{
ShareID: share.ID,
NodeID: share.NodeID,
ReservationID: randomToken(24),
@@ -981,7 +914,7 @@ func (h *Handler) federationRuntimeApplyRole(w http.ResponseWriter, r *http.Requ
return
}
var runtime *sqlite.PeerShareRuntime
var runtime *repo.PeerShareRuntime
if strings.TrimSpace(req.ReservationID) != "" {
runtime, err = h.repo.GetPeerShareRuntimeByReservationID(share.ID, strings.TrimSpace(req.ReservationID))
} else {
@@ -1152,7 +1085,7 @@ func (h *Handler) federationRuntimeReleaseRole(w http.ResponseWriter, r *http.Re
return
}
var runtime *sqlite.PeerShareRuntime
var runtime *repo.PeerShareRuntime
if strings.TrimSpace(req.BindingID) != "" {
runtime, err = h.repo.GetPeerShareRuntimeByBindingID(share.ID, strings.TrimSpace(req.BindingID))
} else if strings.TrimSpace(req.ReservationID) != "" {
@@ -1299,7 +1232,7 @@ func isFederationServiceCommand(commandType string) bool {
}
}
func validateFederationCommandPorts(share *sqlite.PeerShare, data interface{}) error {
func validateFederationCommandPorts(share *repo.PeerShare, data interface{}) error {
if share == nil || (share.PortRangeStart <= 0 && share.PortRangeEnd <= 0) {
return nil
}
@@ -1339,7 +1272,7 @@ func validateFederationCommandPorts(share *sqlite.PeerShare, data interface{}) e
return nil
}
func (h *Handler) pickPeerSharePort(share *sqlite.PeerShare, requestedPort int) (int, error) {
func (h *Handler) pickPeerSharePort(share *repo.PeerShare, requestedPort int) (int, error) {
if share == nil {
return 0, fmt.Errorf("share not found")
}
@@ -1349,29 +1282,13 @@ func (h *Handler) pickPeerSharePort(share *sqlite.PeerShare, requestedPort int)
used := make(map[int]struct{})
rows, err := h.repo.DB().Query(`SELECT port FROM chain_tunnel WHERE node_id = ? AND port IS NOT NULL AND port > 0`, share.NodeID)
nodePorts, err := h.repo.ListUsedPortsOnNode(share.NodeID)
if err != nil {
return 0, err
}
for rows.Next() {
var p sql.NullInt64
if scanErr := rows.Scan(&p); scanErr == nil && p.Valid && p.Int64 > 0 {
used[int(p.Int64)] = struct{}{}
}
for _, p := range nodePorts {
used[p] = struct{}{}
}
_ = rows.Close()
rows, err = h.repo.DB().Query(`SELECT port FROM forward_port WHERE node_id = ? AND port > 0`, share.NodeID)
if err != nil {
return 0, err
}
for rows.Next() {
var p sql.NullInt64
if scanErr := rows.Scan(&p); scanErr == nil && p.Valid && p.Int64 > 0 {
used[int(p.Int64)] = struct{}{}
}
}
_ = rows.Close()
ports, err := h.repo.ListActivePeerShareRuntimePorts(share.ID, share.NodeID)
if err != nil {
@@ -1412,7 +1329,7 @@ func extractBearerToken(r *http.Request) string {
return ""
}
func isPeerShareFlowExceeded(share *sqlite.PeerShare) bool {
func isPeerShareFlowExceeded(share *repo.PeerShare) bool {
if share == nil {
return false
}
@@ -1655,19 +1572,10 @@ func (h *Handler) cleanupFederationTunnels(shareID int64) {
return
}
namePrefix := fmt.Sprintf("Share-%d-Port-", shareID)
rows, err := h.repo.DB().Query(`SELECT id FROM tunnel WHERE name LIKE ?`, namePrefix+"%")
if err != nil {
tunnelIDs, err := h.repo.ListTunnelIDsByNamePrefix(namePrefix)
if err != nil || len(tunnelIDs) == 0 {
return
}
defer rows.Close()
var tunnelIDs []int64
for rows.Next() {
var id int64
if err := rows.Scan(&id); err == nil {
tunnelIDs = append(tunnelIDs, id)
}
}
for _, tid := range tunnelIDs {
_ = h.deleteTunnelByID(tid)
@@ -10,33 +10,33 @@ import (
"time"
"go-backend/internal/http/response"
"go-backend/internal/store/sqlite"
"go-backend/internal/store/repo"
)
func TestPickPeerSharePortUsesRuntimeReservations(t *testing.T) {
repo, err := sqlite.Open(filepath.Join(t.TempDir(), "panel.db"))
r, err := repo.Open(filepath.Join(t.TempDir(), "panel.db"))
if err != nil {
t.Fatalf("open repo: %v", err)
}
defer repo.Close()
defer r.Close()
h := &Handler{repo: repo}
h := &Handler{repo: r}
now := time.Now().UnixMilli()
if _, err := repo.DB().Exec(`INSERT INTO chain_tunnel(tunnel_id, chain_type, node_id, port, strategy, inx, protocol) VALUES(?, ?, ?, ?, ?, ?, ?)`, 1, 2, 1, 3000, "round", 1, "tls"); err != nil {
if err := r.DB().Exec(`INSERT INTO chain_tunnel(tunnel_id, chain_type, node_id, port, strategy, inx, protocol) VALUES(?, ?, ?, ?, ?, ?, ?)`, 1, 2, 1, 3000, "round", 1, "tls").Error; err != nil {
t.Fatalf("insert chain_tunnel: %v", err)
}
if _, err := repo.DB().Exec(`INSERT INTO forward_port(forward_id, node_id, port) VALUES(?, ?, ?)`, 1, 1, 3001); err != nil {
if err := r.DB().Exec(`INSERT INTO forward_port(forward_id, node_id, port) VALUES(?, ?, ?)`, 1, 1, 3001).Error; err != nil {
t.Fatalf("insert forward_port: %v", err)
}
if _, err := repo.DB().Exec(`
if err := r.DB().Exec(`
INSERT INTO peer_share_runtime(share_id, node_id, reservation_id, resource_key, binding_id, role, chain_name, service_name, protocol, strategy, port, target, applied, status, created_time, updated_time)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, 77, 1, "res-1", "rk-1", "b-1", "exit", "", "fed_svc_1", "tls", "round", 3002, "", 1, 1, now, now); err != nil {
`, 77, 1, "res-1", "rk-1", "b-1", "exit", "", "fed_svc_1", "tls", "round", 3002, "", 1, 1, now, now).Error; err != nil {
t.Fatalf("insert peer_share_runtime: %v", err)
}
share := &sqlite.PeerShare{
share := &repo.PeerShare{
ID: 77,
NodeID: 1,
PortRangeStart: 3000,
@@ -57,13 +57,13 @@ func TestPickPeerSharePortUsesRuntimeReservations(t *testing.T) {
}
func TestApplyTunnelRuntimeSkipsRemoteChainAndOutNodes(t *testing.T) {
repo, err := sqlite.Open(filepath.Join(t.TempDir(), "rt-skip.db"))
r, err := repo.Open(filepath.Join(t.TempDir(), "rt-skip.db"))
if err != nil {
t.Fatalf("open repo: %v", err)
}
defer repo.Close()
defer r.Close()
h := &Handler{repo: repo}
h := &Handler{repo: r}
now := time.Now().UnixMilli()
for _, n := range []struct {
id int64
@@ -73,10 +73,10 @@ func TestApplyTunnelRuntimeSkipsRemoteChainAndOutNodes(t *testing.T) {
{12, "remote-chain", "10.99.0.2"},
{13, "remote-out", "10.99.0.3"},
} {
if _, err := repo.DB().Exec(`
if err := r.DB().Exec(`
INSERT INTO node(id, name, secret, server_ip, server_ip_v4, server_ip_v6, port, interface_name, version, http, tls, socks, created_time, updated_time, status, tcp_listen_addr, udp_listen_addr, inx, is_remote, remote_url, remote_token)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, n.id, n.name, n.name+"-secret", n.ip, n.ip, "", "40000-40010", "", "v1", 1, 1, 1, now, now, 1, "[::]", "[::]", 0, 1, "http://remote-peer", "remote-token"); err != nil {
`, n.id, n.name, n.name+"-secret", n.ip, n.ip, "", "40000-40010", "", "v1", 1, 1, 1, now, now, 1, "[::]", "[::]", 0, 1, "http://remote-peer", "remote-token").Error; err != nil {
t.Fatalf("insert node %s: %v", n.name, err)
}
}
@@ -112,25 +112,24 @@ func TestApplyTunnelRuntimeSkipsRemoteChainAndOutNodes(t *testing.T) {
}
func TestPrepareTunnelCreateStateRemoteAutoPortDefersToFederation(t *testing.T) {
repo, err := sqlite.Open(filepath.Join(t.TempDir(), "panel.db"))
r, err := repo.Open(filepath.Join(t.TempDir(), "panel.db"))
if err != nil {
t.Fatalf("open repo: %v", err)
}
defer repo.Close()
defer r.Close()
h := &Handler{repo: repo}
h := &Handler{repo: r}
now := time.Now().UnixMilli()
insertNode := func(name string, status int, portRange string, isRemote int) int64 {
res, execErr := repo.DB().Exec(`
if execErr := r.DB().Exec(`
INSERT INTO node(name, secret, server_ip, server_ip_v4, server_ip_v6, port, interface_name, version, http, tls, socks, created_time, updated_time, status, tcp_listen_addr, udp_listen_addr, inx, is_remote, remote_url, remote_token, remote_config)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, name, name+"-secret", "10.0.0.1", "10.0.0.1", "", portRange, "", "v1", 1, 1, 1, now, now, status, "[::]", "[::]", 0, isRemote, "http://peer", "peer-token", `{"shareId":1}`)
if execErr != nil {
`, name, name+"-secret", "10.0.0.1", "10.0.0.1", "", portRange, "", "v1", 1, 1, 1, now, now, status, "[::]", "[::]", 0, isRemote, "http://peer", "peer-token", `{"shareId":1}`).Error; execErr != nil {
t.Fatalf("insert node %s: %v", name, execErr)
}
id, idErr := res.LastInsertId()
if idErr != nil {
var id int64
if idErr := r.DB().Raw("SELECT last_insert_rowid()").Row().Scan(&id); idErr != nil {
t.Fatalf("node id %s: %v", name, idErr)
}
return id
@@ -139,12 +138,12 @@ func TestPrepareTunnelCreateStateRemoteAutoPortDefersToFederation(t *testing.T)
entryID := insertNode("entry", 1, "31000-31010", 0)
remoteOutID := insertNode("remote-out", 1, "30000", 1)
if _, err := repo.DB().Exec(`INSERT INTO forward_port(forward_id, node_id, port) VALUES(?, ?, ?)`, 1, remoteOutID, 30000); err != nil {
if err := r.DB().Exec(`INSERT INTO forward_port(forward_id, node_id, port) VALUES(?, ?, ?)`, 1, remoteOutID, 30000).Error; err != nil {
t.Fatalf("insert forward_port: %v", err)
}
tx, err := repo.DB().Begin()
if err != nil {
tx := r.DB().Begin()
if tx.Error != nil {
t.Fatalf("begin tx: %v", err)
}
defer tx.Rollback()
@@ -173,25 +172,24 @@ func TestPrepareTunnelCreateStateRemoteAutoPortDefersToFederation(t *testing.T)
}
func TestPrepareTunnelCreateStateAllowsOfflineRemoteMiddleNode(t *testing.T) {
repo, err := sqlite.Open(filepath.Join(t.TempDir(), "panel.db"))
r, err := repo.Open(filepath.Join(t.TempDir(), "panel.db"))
if err != nil {
t.Fatalf("open repo: %v", err)
}
defer repo.Close()
defer r.Close()
h := &Handler{repo: repo}
h := &Handler{repo: r}
now := time.Now().UnixMilli()
insertNode := func(name string, status int, portRange string, isRemote int) int64 {
res, execErr := repo.DB().Exec(`
if execErr := r.DB().Exec(`
INSERT INTO node(name, secret, server_ip, server_ip_v4, server_ip_v6, port, interface_name, version, http, tls, socks, created_time, updated_time, status, tcp_listen_addr, udp_listen_addr, inx, is_remote, remote_url, remote_token, remote_config)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, name, name+"-secret", "10.0.0.1", "10.0.0.1", "", portRange, "", "v1", 1, 1, 1, now, now, status, "[::]", "[::]", 0, isRemote, "http://peer", "peer-token", `{"shareId":2}`)
if execErr != nil {
`, name, name+"-secret", "10.0.0.1", "10.0.0.1", "", portRange, "", "v1", 1, 1, 1, now, now, status, "[::]", "[::]", 0, isRemote, "http://peer", "peer-token", `{"shareId":2}`).Error; execErr != nil {
t.Fatalf("insert node %s: %v", name, execErr)
}
id, idErr := res.LastInsertId()
if idErr != nil {
var id int64
if idErr := r.DB().Raw("SELECT last_insert_rowid()").Row().Scan(&id); idErr != nil {
t.Fatalf("node id %s: %v", name, idErr)
}
return id
@@ -201,8 +199,8 @@ func TestPrepareTunnelCreateStateAllowsOfflineRemoteMiddleNode(t *testing.T) {
remoteMiddleID := insertNode("middle-remote", 0, "33000-33010", 1)
outID := insertNode("out-local", 1, "34000-34010", 0)
tx, err := repo.DB().Begin()
if err != nil {
tx := r.DB().Begin()
if tx.Error != nil {
t.Fatalf("begin tx: %v", err)
}
defer tx.Rollback()
@@ -238,16 +236,16 @@ func TestPrepareTunnelCreateStateAllowsOfflineRemoteMiddleNode(t *testing.T) {
}
func TestFederationRuntimeReservePortRejectsWhenShareFlowExceeded(t *testing.T) {
repo, err := sqlite.Open(filepath.Join(t.TempDir(), "panel.db"))
r, err := repo.Open(filepath.Join(t.TempDir(), "panel.db"))
if err != nil {
t.Fatalf("open repo: %v", err)
}
defer repo.Close()
defer r.Close()
h := &Handler{repo: repo}
h := &Handler{repo: r}
now := time.Now().UnixMilli()
if err := repo.CreatePeerShare(&sqlite.PeerShare{
if err := r.CreatePeerShare(&repo.PeerShare{
Name: "limited-share",
NodeID: 1,
Token: "limited-token",
@@ -12,28 +12,27 @@ import (
"time"
"go-backend/internal/http/response"
"go-backend/internal/store/sqlite"
"go-backend/internal/store/repo"
)
func TestFederationShareCreateRejectsRemoteNode(t *testing.T) {
repo, err := sqlite.Open(filepath.Join(t.TempDir(), "panel.db"))
r, err := repo.Open(filepath.Join(t.TempDir(), "panel.db"))
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
t.Cleanup(func() { _ = repo.Close() })
t.Cleanup(func() { _ = r.Close() })
h := New(repo, "test-jwt-secret")
h := New(r, "test-jwt-secret")
now := time.Now().UnixMilli()
insertRes, err := repo.DB().Exec(`
if err := r.DB().Exec(`
INSERT INTO node(name, secret, server_ip, server_ip_v4, server_ip_v6, port, interface_name, version, http, tls, socks, created_time, updated_time, status, tcp_listen_addr, udp_listen_addr, inx, is_remote, remote_url, remote_token, remote_config)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, "remote-share-node", "remote-share-secret", "10.10.10.1", "10.10.10.1", "", "20000-20010", "", "v1", 1, 1, 1, now, now, 1, "[::]", "[::]", 0, 1, "http://peer.example", "peer-token", `{"shareId":1}`)
if err != nil {
`, "remote-share-node", "remote-share-secret", "10.10.10.1", "10.10.10.1", "", "20000-20010", "", "v1", 1, 1, 1, now, now, 1, "[::]", "[::]", 0, 1, "http://peer.example", "peer-token", `{"shareId":1}`).Error; err != nil {
t.Fatalf("insert remote node: %v", err)
}
remoteNodeID, err := insertRes.LastInsertId()
if err != nil {
var remoteNodeID int64
if err := r.DB().Raw("SELECT last_insert_rowid()").Row().Scan(&remoteNodeID); err != nil {
t.Fatalf("get remote node id: %v", err)
}
@@ -71,7 +70,7 @@ func TestFederationShareCreateRejectsRemoteNode(t *testing.T) {
}
var shareCount int
if err := repo.DB().QueryRow(`SELECT COUNT(1) FROM peer_share WHERE node_id = ?`, remoteNodeID).Scan(&shareCount); err != nil {
if err := r.DB().Raw(`SELECT COUNT(1) FROM peer_share WHERE node_id = ?`, remoteNodeID).Row().Scan(&shareCount); err != nil {
t.Fatalf("query peer_share count: %v", err)
}
if shareCount != 0 {
@@ -80,24 +79,23 @@ func TestFederationShareCreateRejectsRemoteNode(t *testing.T) {
}
func TestFederationShareCreateRejectsInvalidAllowedIPs(t *testing.T) {
repo, err := sqlite.Open(filepath.Join(t.TempDir(), "panel.db"))
r, err := repo.Open(filepath.Join(t.TempDir(), "panel.db"))
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
t.Cleanup(func() { _ = repo.Close() })
t.Cleanup(func() { _ = r.Close() })
h := New(repo, "test-jwt-secret")
h := New(r, "test-jwt-secret")
now := time.Now().UnixMilli()
insertRes, err := repo.DB().Exec(`
if err := r.DB().Exec(`
INSERT INTO node(name, secret, server_ip, server_ip_v4, server_ip_v6, port, interface_name, version, http, tls, socks, created_time, updated_time, status, tcp_listen_addr, udp_listen_addr, inx, is_remote, remote_url, remote_token, remote_config)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, "local-share-node", "local-share-secret", "10.20.30.40", "10.20.30.40", "", "21000-21010", "", "v1", 1, 1, 1, now, now, 1, "[::]", "[::]", 0, 0, "", "", "")
if err != nil {
`, "local-share-node", "local-share-secret", "10.20.30.40", "10.20.30.40", "", "21000-21010", "", "v1", 1, 1, 1, now, now, 1, "[::]", "[::]", 0, 0, "", "", "").Error; err != nil {
t.Fatalf("insert local node: %v", err)
}
localNodeID, err := insertRes.LastInsertId()
if err != nil {
var localNodeID int64
if err := r.DB().Raw("SELECT last_insert_rowid()").Row().Scan(&localNodeID); err != nil {
t.Fatalf("get local node id: %v", err)
}
@@ -136,7 +134,7 @@ func TestFederationShareCreateRejectsInvalidAllowedIPs(t *testing.T) {
}
var shareCount int
if err := repo.DB().QueryRow(`SELECT COUNT(1) FROM peer_share WHERE node_id = ?`, localNodeID).Scan(&shareCount); err != nil {
if err := r.DB().Raw(`SELECT COUNT(1) FROM peer_share WHERE node_id = ?`, localNodeID).Row().Scan(&shareCount); err != nil {
t.Fatalf("query peer_share count: %v", err)
}
if shareCount != 0 {
@@ -145,16 +143,16 @@ func TestFederationShareCreateRejectsInvalidAllowedIPs(t *testing.T) {
}
func TestFederationShareListIncludesRemoteUsedPorts(t *testing.T) {
repo, err := sqlite.Open(filepath.Join(t.TempDir(), "panel.db"))
r, err := repo.Open(filepath.Join(t.TempDir(), "panel.db"))
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
t.Cleanup(func() { _ = repo.Close() })
t.Cleanup(func() { _ = r.Close() })
h := New(repo, "test-jwt-secret")
h := New(r, "test-jwt-secret")
now := time.Now().UnixMilli()
if err := repo.CreatePeerShare(&sqlite.PeerShare{
if err := r.CreatePeerShare(&repo.PeerShare{
Name: "provider-share",
NodeID: 9,
Token: "share-list-token",
@@ -169,12 +167,12 @@ func TestFederationShareListIncludesRemoteUsedPorts(t *testing.T) {
t.Fatalf("create peer share: %v", err)
}
share, err := repo.GetPeerShareByToken("share-list-token")
share, err := r.GetPeerShareByToken("share-list-token")
if err != nil || share == nil {
t.Fatalf("load peer share: %v", err)
}
if _, err := repo.DB().Exec(`
if err := r.DB().Exec(`
INSERT INTO peer_share_runtime(share_id, node_id, reservation_id, resource_key, binding_id, role, chain_name, service_name, protocol, strategy, port, target, applied, status, created_time, updated_time)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?),
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?),
@@ -183,7 +181,7 @@ func TestFederationShareListIncludesRemoteUsedPorts(t *testing.T) {
share.ID, share.NodeID, "r-1", "rk-1", "b-1", "middle", "fed_chain_1", "fed_svc_1", "tls", "round", 22001, "", 1, 1, now, now,
share.ID, share.NodeID, "r-2", "rk-2", "b-2", "exit", "", "fed_svc_2", "tls", "round", 22002, "", 1, 1, now, now,
share.ID, share.NodeID, "r-3", "rk-3", "", "", "", "", "tls", "round", 22003, "", 0, 0, now, now,
); err != nil {
).Error; err != nil {
t.Fatalf("insert peer_share_runtime rows: %v", err)
}
@@ -238,16 +236,16 @@ func TestFederationShareListIncludesRemoteUsedPorts(t *testing.T) {
}
func TestFederationShareDeleteCleansUpRuntimes(t *testing.T) {
repo, err := sqlite.Open(filepath.Join(t.TempDir(), "panel.db"))
r, err := repo.Open(filepath.Join(t.TempDir(), "panel.db"))
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
t.Cleanup(func() { _ = repo.Close() })
t.Cleanup(func() { _ = r.Close() })
h := New(repo, "test-jwt-secret")
h := New(r, "test-jwt-secret")
now := time.Now().UnixMilli()
if err := repo.CreatePeerShare(&sqlite.PeerShare{
if err := r.CreatePeerShare(&repo.PeerShare{
Name: "delete-cleanup-share",
NodeID: 99,
Token: "delete-cleanup-token",
@@ -261,24 +259,24 @@ func TestFederationShareDeleteCleansUpRuntimes(t *testing.T) {
t.Fatalf("create peer share: %v", err)
}
share, err := repo.GetPeerShareByToken("delete-cleanup-token")
share, err := r.GetPeerShareByToken("delete-cleanup-token")
if err != nil || share == nil {
t.Fatalf("load peer share: %v", err)
}
if _, err := repo.DB().Exec(`
if err := r.DB().Exec(`
INSERT INTO peer_share_runtime(share_id, node_id, reservation_id, resource_key, binding_id, role, chain_name, service_name, protocol, strategy, port, target, applied, status, created_time, updated_time)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?),
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`,
share.ID, 99, "dc-r1", "dc-rk1", "dc-b1", "exit", "", "fed_svc_dc1", "tls", "round", 40001, "", 1, 1, now, now,
share.ID, 99, "dc-r2", "dc-rk2", "dc-b2", "middle", "fed_chain_dc2", "fed_svc_dc2", "tls", "round", 40002, "", 1, 1, now, now,
); err != nil {
).Error; err != nil {
t.Fatalf("insert peer_share_runtime rows: %v", err)
}
var runtimeCount int
if err := repo.DB().QueryRow(`SELECT COUNT(1) FROM peer_share_runtime WHERE share_id = ? AND status = 1`, share.ID).Scan(&runtimeCount); err != nil {
if err := r.DB().Raw(`SELECT COUNT(1) FROM peer_share_runtime WHERE share_id = ? AND status = 1`, share.ID).Row().Scan(&runtimeCount); err != nil {
t.Fatalf("count active runtimes before: %v", err)
}
if runtimeCount != 2 {
@@ -307,7 +305,7 @@ func TestFederationShareDeleteCleansUpRuntimes(t *testing.T) {
}
var shareCount int
if err := repo.DB().QueryRow(`SELECT COUNT(1) FROM peer_share WHERE id = ?`, share.ID).Scan(&shareCount); err != nil {
if err := r.DB().Raw(`SELECT COUNT(1) FROM peer_share WHERE id = ?`, share.ID).Row().Scan(&shareCount); err != nil {
t.Fatalf("count peer_share after: %v", err)
}
if shareCount != 0 {
@@ -315,7 +313,7 @@ func TestFederationShareDeleteCleansUpRuntimes(t *testing.T) {
}
var runtimeCountAfter int
if err := repo.DB().QueryRow(`SELECT COUNT(1) FROM peer_share_runtime WHERE share_id = ?`, share.ID).Scan(&runtimeCountAfter); err != nil {
if err := r.DB().Raw(`SELECT COUNT(1) FROM peer_share_runtime WHERE share_id = ?`, share.ID).Row().Scan(&runtimeCountAfter); err != nil {
t.Fatalf("count peer_share_runtime after: %v", err)
}
if runtimeCountAfter != 0 {
@@ -324,19 +322,19 @@ func TestFederationShareDeleteCleansUpRuntimes(t *testing.T) {
}
func TestFederationRemoteUsageListSyncErrorFallback(t *testing.T) {
repo, err := sqlite.Open(filepath.Join(t.TempDir(), "panel.db"))
r, err := repo.Open(filepath.Join(t.TempDir(), "panel.db"))
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
t.Cleanup(func() { _ = repo.Close() })
t.Cleanup(func() { _ = r.Close() })
h := New(repo, "test-jwt-secret")
h := New(r, "test-jwt-secret")
now := time.Now().UnixMilli()
if _, err := repo.DB().Exec(`
if err := r.DB().Exec(`
INSERT INTO node(name, secret, server_ip, server_ip_v4, server_ip_v6, port, interface_name, version, http, tls, socks, created_time, updated_time, status, tcp_listen_addr, udp_listen_addr, inx, is_remote, remote_url, remote_token, remote_config)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, "sync-error-node", "sync-error-secret", "10.50.60.70", "10.50.60.70", "", "32000-32010", "", "v1", 1, 1, 1, now, now, 1, "[::]", "[::]", 0, 1, "http://unreachable.invalid:9999", "bad-token", `{"shareId":42,"maxBandwidth":5368709120,"currentFlow":999999,"portRangeStart":32000,"portRangeEnd":32010}`); err != nil {
`, "sync-error-node", "sync-error-secret", "10.50.60.70", "10.50.60.70", "", "32000-32010", "", "v1", 1, 1, 1, now, now, 1, "[::]", "[::]", 0, 1, "http://unreachable.invalid:9999", "bad-token", `{"shareId":42,"maxBandwidth":5368709120,"currentFlow":999999,"portRangeStart":32000,"portRangeEnd":32010}`).Error; err != nil {
t.Fatalf("insert remote node: %v", err)
}
@@ -380,15 +378,15 @@ func TestFederationRemoteUsageListSyncErrorFallback(t *testing.T) {
}
func TestFederationShareResetFlow(t *testing.T) {
repo, err := sqlite.Open(filepath.Join(t.TempDir(), "panel.db"))
r, err := repo.Open(filepath.Join(t.TempDir(), "panel.db"))
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
t.Cleanup(func() { _ = repo.Close() })
t.Cleanup(func() { _ = r.Close() })
h := New(repo, "test-jwt-secret")
h := New(r, "test-jwt-secret")
now := time.Now().UnixMilli()
if err := repo.CreatePeerShare(&sqlite.PeerShare{
if err := r.CreatePeerShare(&repo.PeerShare{
Name: "reset-flow-share",
NodeID: 11,
Token: "reset-flow-token",
@@ -402,7 +400,7 @@ func TestFederationShareResetFlow(t *testing.T) {
}); err != nil {
t.Fatalf("create peer share: %v", err)
}
share, err := repo.GetPeerShareByToken("reset-flow-token")
share, err := r.GetPeerShareByToken("reset-flow-token")
if err != nil || share == nil {
t.Fatalf("load peer share: %v", err)
}
@@ -428,7 +426,7 @@ func TestFederationShareResetFlow(t *testing.T) {
t.Fatalf("expected response code 0, got %d (%s)", payload.Code, payload.Msg)
}
updated, err := repo.GetPeerShare(share.ID)
updated, err := r.GetPeerShare(share.ID)
if err != nil || updated == nil {
t.Fatalf("reload peer share: %v", err)
}
@@ -438,47 +436,50 @@ func TestFederationShareResetFlow(t *testing.T) {
}
func TestFederationRemoteUsageList(t *testing.T) {
repo, err := sqlite.Open(filepath.Join(t.TempDir(), "panel.db"))
r, err := repo.Open(filepath.Join(t.TempDir(), "panel.db"))
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
t.Cleanup(func() { _ = repo.Close() })
t.Cleanup(func() { _ = r.Close() })
h := New(repo, "test-jwt-secret")
h := New(r, "test-jwt-secret")
now := time.Now().UnixMilli()
resNode, err := repo.DB().Exec(`
if err := r.DB().Exec(`
INSERT INTO node(name, secret, server_ip, server_ip_v4, server_ip_v6, port, interface_name, version, http, tls, socks, created_time, updated_time, status, tcp_listen_addr, udp_listen_addr, inx, is_remote, remote_url, remote_token, remote_config)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, "remote-consumer-node", "remote-consumer-secret", "10.30.40.50", "10.30.40.50", "", "31000-31010", "", "v1", 1, 1, 1, now, now, 1, "[::]", "[::]", 0, 1, "http://peer.example", "peer-token", `{"shareId":88,"maxBandwidth":2147483648,"currentFlow":1073741824,"portRangeStart":31000,"portRangeEnd":31010}`)
if err != nil {
`, "remote-consumer-node", "remote-consumer-secret", "10.30.40.50", "10.30.40.50", "", "31000-31010", "", "v1", 1, 1, 1, now, now, 1, "[::]", "[::]", 0, 1, "http://peer.example", "peer-token", `{"shareId":88,"maxBandwidth":2147483648,"currentFlow":1073741824,"portRangeStart":31000,"portRangeEnd":31010}`).Error; err != nil {
t.Fatalf("insert remote node: %v", err)
}
nodeID, err := resNode.LastInsertId()
if err != nil {
var nodeID int64
if err := r.DB().Raw("SELECT last_insert_rowid()").Row().Scan(&nodeID); err != nil {
t.Fatalf("remote node id: %v", err)
}
resTunnelA, err := repo.DB().Exec(`INSERT INTO tunnel(name, type, protocol, flow, created_time, updated_time, status, in_ip, inx) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)`, "consumer-tunnel-a", 2, "tls", 1, now, now, 1, "", 0)
if err != nil {
if err := r.DB().Exec(`INSERT INTO tunnel(name, type, protocol, flow, created_time, updated_time, status, in_ip, inx) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)`, "consumer-tunnel-a", 2, "tls", 1, now, now, 1, "", 0).Error; err != nil {
t.Fatalf("insert tunnel a: %v", err)
}
tunnelAID, _ := resTunnelA.LastInsertId()
var tunnelAID int64
if err := r.DB().Raw("SELECT last_insert_rowid()").Row().Scan(&tunnelAID); err != nil {
t.Fatal(err)
}
resTunnelB, err := repo.DB().Exec(`INSERT INTO tunnel(name, type, protocol, flow, created_time, updated_time, status, in_ip, inx) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)`, "consumer-tunnel-b", 2, "tls", 1, now, now, 1, "", 0)
if err != nil {
if err := r.DB().Exec(`INSERT INTO tunnel(name, type, protocol, flow, created_time, updated_time, status, in_ip, inx) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)`, "consumer-tunnel-b", 2, "tls", 1, now, now, 1, "", 0).Error; err != nil {
t.Fatalf("insert tunnel b: %v", err)
}
tunnelBID, _ := resTunnelB.LastInsertId()
var tunnelBID int64
if err := r.DB().Raw("SELECT last_insert_rowid()").Row().Scan(&tunnelBID); err != nil {
t.Fatal(err)
}
if _, err := repo.DB().Exec(`
if err := r.DB().Exec(`
INSERT INTO federation_tunnel_binding(tunnel_id, node_id, chain_type, hop_inx, remote_url, resource_key, remote_binding_id, allocated_port, status, created_time, updated_time)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?),
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`,
tunnelAID, nodeID, 2, 1, "http://peer.example", "rk-a", "rb-a", 31001, 1, now, now,
tunnelBID, nodeID, 3, 0, "http://peer.example", "rk-b", "rb-b", 31002, 1, now, now,
); err != nil {
).Error; err != nil {
t.Fatalf("insert federation bindings: %v", err)
}
@@ -532,13 +533,13 @@ func TestFederationRemoteUsageList(t *testing.T) {
}
func TestAuthPeerAllowedIPs(t *testing.T) {
repo, err := sqlite.Open(filepath.Join(t.TempDir(), "panel.db"))
r, err := repo.Open(filepath.Join(t.TempDir(), "panel.db"))
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
t.Cleanup(func() { _ = repo.Close() })
t.Cleanup(func() { _ = r.Close() })
h := New(repo, "test-jwt-secret")
h := New(r, "test-jwt-secret")
now := time.Now().UnixMilli()
tests := []struct {
@@ -585,7 +586,7 @@ func TestAuthPeerAllowedIPs(t *testing.T) {
for idx, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
token := fmt.Sprintf("share-token-%d", idx)
if err := repo.CreatePeerShare(&sqlite.PeerShare{
if err := r.CreatePeerShare(&repo.PeerShare{
Name: "share-" + tt.name,
NodeID: 1,
Token: token,
+20 -69
View File
@@ -1,7 +1,6 @@
package handler
import (
"database/sql"
"encoding/json"
"strconv"
"strings"
@@ -207,22 +206,18 @@ func (h *Handler) getUserTunnelPolicy(userTunnelID int64) (*userTunnelPolicy, er
if userTunnelID <= 0 {
return nil, nil
}
row := h.repo.DB().QueryRow(`
SELECT id, user_id, tunnel_id, flow, in_flow, out_flow, exp_time, status
FROM user_tunnel
WHERE id = ?
LIMIT 1
`, userTunnelID)
var policy userTunnelPolicy
if err := row.Scan(&policy.ID, &policy.UserID, &policy.TunnelID, &policy.Flow, &policy.InFlow, &policy.OutFlow, &policy.ExpTime, &policy.Status); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
ut, err := h.repo.GetUserTunnelByID(userTunnelID)
if err != nil {
return nil, err
}
return &policy, nil
if ut == nil {
return nil, nil
}
return &userTunnelPolicy{
ID: ut.ID, UserID: ut.UserID, TunnelID: ut.TunnelID,
Flow: ut.Flow, InFlow: ut.InFlow, OutFlow: ut.OutFlow,
ExpTime: ut.ExpTime, Status: ut.Status,
}, nil
}
func (h *Handler) pauseUserForwards(userID int64, now int64) {
@@ -245,60 +240,20 @@ func (h *Handler) pauseForwardRecords(forwards []forwardRecord, now int64) {
for i := range forwards {
forward := forwards[i]
_ = h.controlForwardServices(&forward, "PauseService", false)
_, _ = h.repo.DB().Exec(`UPDATE forward SET status = 0, updated_time = ? WHERE id = ?`, now, forward.ID)
_ = h.repo.UpdateForwardStatus(forward.ID, 0, now)
}
}
func (h *Handler) listActiveForwardsByUser(userID int64) ([]forwardRecord, error) {
rows, err := h.repo.DB().Query(`
SELECT id, user_id, user_name, name, tunnel_id, remote_addr, COALESCE(strategy, 'fifo'), status
FROM forward
WHERE user_id = ? AND status = 1
ORDER BY id ASC
`, userID)
if err != nil {
return nil, err
}
defer rows.Close()
return scanForwardRecords(rows)
return h.repo.ListActiveForwardsByUser(userID)
}
func (h *Handler) listActiveForwardsByUserTunnel(userID int64, tunnelID int64) ([]forwardRecord, error) {
rows, err := h.repo.DB().Query(`
SELECT id, user_id, user_name, name, tunnel_id, remote_addr, COALESCE(strategy, 'fifo'), status
FROM forward
WHERE user_id = ? AND tunnel_id = ? AND status = 1
ORDER BY id ASC
`, userID, tunnelID)
if err != nil {
return nil, err
}
defer rows.Close()
return scanForwardRecords(rows)
}
func scanForwardRecords(rows *sql.Rows) ([]forwardRecord, error) {
out := make([]forwardRecord, 0)
for rows.Next() {
var record forwardRecord
if err := rows.Scan(&record.ID, &record.UserID, &record.UserName, &record.Name, &record.TunnelID, &record.RemoteAddr, &record.Strategy, &record.Status); err != nil {
return nil, err
}
if strings.TrimSpace(record.Strategy) == "" {
record.Strategy = "fifo"
}
out = append(out, record)
}
if err := rows.Err(); err != nil {
return nil, err
}
return out, nil
return h.repo.ListActiveForwardsByUserTunnel(userID, tunnelID)
}
func (h *Handler) cleanNodeConfigs(nodeID int64, rawConfig string) {
if h == nil || h.repo == nil || h.repo.DB() == nil || nodeID <= 0 {
if h == nil || h.repo == nil || nodeID <= 0 {
return
}
if strings.TrimSpace(rawConfig) == "" {
@@ -383,15 +338,13 @@ func (h *Handler) cleanOrphanedLimiters(nodeID int64, limiters []namedConfigItem
}
func (h *Handler) tunnelExists(tunnelID int64) bool {
var count int
err := h.repo.DB().QueryRow(`SELECT COUNT(1) FROM tunnel WHERE id = ?`, tunnelID).Scan(&count)
return err == nil && count > 0
ok, _ := h.repo.TunnelExists(tunnelID)
return ok
}
func (h *Handler) forwardExists(forwardID int64) bool {
var count int
err := h.repo.DB().QueryRow(`SELECT COUNT(1) FROM forward WHERE id = ?`, forwardID).Scan(&count)
return err == nil && count > 0
ok, _ := h.repo.ForwardExists(forwardID)
return ok
}
func (h *Handler) speedLimiterExists(name string) bool {
@@ -402,8 +355,6 @@ func (h *Handler) speedLimiterExists(name string) bool {
if err != nil || id <= 0 {
return false
}
var count int
err = h.repo.DB().QueryRow(`SELECT COUNT(1) FROM speed_limit WHERE id = ?`, id).Scan(&count)
return err == nil && count > 0
ok, _ := h.repo.SpeedLimitExists(id)
return ok
}
@@ -5,18 +5,18 @@ import (
"testing"
"time"
"go-backend/internal/store/sqlite"
"go-backend/internal/store/repo"
)
func TestProcessFlowItemTracksPeerShareFlowAndEnforcesLimit(t *testing.T) {
repo, err := sqlite.Open(filepath.Join(t.TempDir(), "panel.db"))
r, err := repo.Open(filepath.Join(t.TempDir(), "panel.db"))
if err != nil {
t.Fatalf("open repo: %v", err)
}
defer repo.Close()
defer r.Close()
now := time.Now().UnixMilli()
if err := repo.CreatePeerShare(&sqlite.PeerShare{
if err := r.CreatePeerShare(&repo.PeerShare{
Name: "flow-share",
NodeID: 1,
Token: "flow-share-token",
@@ -30,22 +30,22 @@ func TestProcessFlowItemTracksPeerShareFlowAndEnforcesLimit(t *testing.T) {
}); err != nil {
t.Fatalf("create peer share: %v", err)
}
share, err := repo.GetPeerShareByToken("flow-share-token")
share, err := r.GetPeerShareByToken("flow-share-token")
if err != nil || share == nil {
t.Fatalf("load peer share: %v", err)
}
if _, err := repo.DB().Exec(`
if err := r.DB().Exec(`
INSERT INTO peer_share_runtime(id, share_id, node_id, reservation_id, resource_key, binding_id, role, chain_name, service_name, protocol, strategy, port, target, applied, status, created_time, updated_time)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, 17, share.ID, share.NodeID, "res-17", "rk-17", "17", "exit", "", "fed_svc_17", "tls", "round", 32001, "", 1, 1, now, now); err != nil {
`, 17, share.ID, share.NodeID, "res-17", "rk-17", "17", "exit", "", "fed_svc_17", "tls", "round", 32001, "", 1, 1, now, now).Error; err != nil {
t.Fatalf("insert peer_share_runtime: %v", err)
}
h := &Handler{repo: repo}
h := &Handler{repo: r}
h.processFlowItem(flowItem{N: "fed_svc_17", U: 1200, D: 900})
updatedShare, err := repo.GetPeerShare(share.ID)
updatedShare, err := r.GetPeerShare(share.ID)
if err != nil || updatedShare == nil {
t.Fatalf("reload share: %v", err)
}
@@ -53,7 +53,7 @@ func TestProcessFlowItemTracksPeerShareFlowAndEnforcesLimit(t *testing.T) {
t.Fatalf("expected current_flow=3100, got %d", updatedShare.CurrentFlow)
}
runtime, err := repo.GetPeerShareRuntimeByID(17)
runtime, err := r.GetPeerShareRuntimeByID(17)
if err != nil || runtime == nil {
t.Fatalf("reload runtime: %v", err)
}
+12 -18
View File
@@ -18,12 +18,12 @@ import (
"go-backend/internal/http/middleware"
"go-backend/internal/http/response"
"go-backend/internal/security"
"go-backend/internal/store/sqlite"
"go-backend/internal/store/repo"
"go-backend/internal/ws"
)
type Handler struct {
repo *sqlite.Repository
repo *repo.Repository
jwtSecret string
wsServer *ws.Server
@@ -69,7 +69,7 @@ type flowItem struct {
D int64 `json:"d"`
}
func New(repo *sqlite.Repository, jwtSecret string) *Handler {
func New(repo *repo.Repository, jwtSecret string) *Handler {
return &Handler{
repo: repo,
jwtSecret: jwtSecret,
@@ -426,7 +426,7 @@ func (h *Handler) openAPISubStore(w http.ResponseWriter, r *http.Request) {
response.WriteJSON(w, response.ErrDefault("请求失败"))
return
}
if h == nil || h.repo == nil || h.repo.DB() == nil {
if h == nil || h.repo == nil {
response.WriteJSON(w, response.Err(-2, "database unavailable"))
return
}
@@ -469,27 +469,21 @@ func (h *Handler) openAPISubStore(w http.ResponseWriter, r *http.Request) {
return
}
var userID int64
var inFlow int64
var outFlow int64
var flow int64
var expTime int64
err = h.repo.DB().QueryRow(`SELECT user_id, in_flow, out_flow, flow, exp_time FROM user_tunnel WHERE id = ? LIMIT 1`, tunnelID).
Scan(&userID, &inFlow, &outFlow, &flow, &expTime)
ut, err := h.repo.GetUserTunnelByID(tunnelID)
if err != nil {
if err == sql.ErrNoRows {
response.WriteJSON(w, response.ErrDefault("隧道不存在"))
return
}
response.WriteJSON(w, response.Err(-2, err.Error()))
return
}
if userID != user.ID {
if ut == nil {
response.WriteJSON(w, response.ErrDefault("隧道不存在"))
return
}
if ut.UserID != user.ID {
response.WriteJSON(w, response.ErrDefault("隧道不存在"))
return
}
headerValue = buildSubscriptionHeader(outFlow, inFlow, flow*giga, expTime/1000)
headerValue = buildSubscriptionHeader(ut.OutFlow, ut.InFlow, ut.Flow*giga, ut.ExpTime/1000)
}
w.Header().Set("subscription-userinfo", headerValue)
@@ -1190,7 +1184,7 @@ func (h *Handler) backupExport(w http.ResponseWriter, r *http.Request) {
type backupImportRequest struct {
Types []string `json:"types"`
sqlite.BackupData
repo.BackupData
}
func (h *Handler) backupImport(w http.ResponseWriter, r *http.Request) {
+15 -107
View File
@@ -2,12 +2,11 @@ package handler
import (
"context"
"database/sql"
"time"
)
func (h *Handler) StartBackgroundJobs() {
if h == nil || h.repo == nil || h.repo.DB() == nil {
if h == nil || h.repo == nil {
return
}
@@ -97,47 +96,28 @@ func durationUntilNextDailyMaintenance(now time.Time) time.Duration {
}
func (h *Handler) runStatisticsFlowJob(now time.Time) {
if h == nil || h.repo == nil || h.repo.DB() == nil {
if h == nil || h.repo == nil {
return
}
db := h.repo.DB()
nowMs := now.UnixMilli()
cutoffMs := nowMs - int64((48*time.Hour)/time.Millisecond)
_, _ = db.Exec(`DELETE FROM statistics_flow WHERE created_time < ?`, cutoffMs)
_ = h.repo.PurgeOldStatisticsFlows(cutoffMs)
hourMark := now.Truncate(time.Hour)
hourText := hourMark.Format("15:04")
createdTime := hourMark.UnixMilli()
rows, err := db.Query(`SELECT id, in_flow, out_flow FROM user ORDER BY id ASC`)
users, err := h.repo.ListAllUserFlowSnapshots()
if err != nil {
return
}
type userFlowSnapshot struct {
userID int64
inFlow int64
outFlow int64
}
users := make([]userFlowSnapshot, 0)
for rows.Next() {
var userID int64
var inFlow int64
var outFlow int64
if err := rows.Scan(&userID, &inFlow, &outFlow); err != nil {
continue
}
users = append(users, userFlowSnapshot{userID: userID, inFlow: inFlow, outFlow: outFlow})
}
_ = rows.Close()
for _, user := range users {
currentTotal := user.inFlow + user.outFlow
currentTotal := user.InFlow + user.OutFlow
increment := currentTotal
var lastTotal sql.NullInt64
err := db.QueryRow(`SELECT total_flow FROM statistics_flow WHERE user_id = ? ORDER BY id DESC LIMIT 1`, user.userID).Scan(&lastTotal)
lastTotal, err := h.repo.GetLastStatisticsFlowTotal(user.UserID)
if err == nil && lastTotal.Valid {
increment = currentTotal - lastTotal.Int64
if increment < 0 {
@@ -145,15 +125,12 @@ func (h *Handler) runStatisticsFlowJob(now time.Time) {
}
}
_, _ = db.Exec(`
INSERT INTO statistics_flow(user_id, flow, total_flow, time, created_time)
VALUES(?, ?, ?, ?, ?)
`, user.userID, increment, currentTotal, hourText, createdTime)
_ = h.repo.CreateStatisticsFlow(user.UserID, increment, currentTotal, hourText, createdTime)
}
}
func (h *Handler) runResetAndExpiryJob(now time.Time) {
if h == nil || h.repo == nil || h.repo.DB() == nil {
if h == nil || h.repo == nil {
return
}
@@ -163,108 +140,39 @@ func (h *Handler) runResetAndExpiryJob(now time.Time) {
}
func (h *Handler) resetMonthlyFlow(now time.Time) {
db := h.repo.DB()
currentDay := now.Day()
lastDay := time.Date(now.Year(), now.Month()+1, 0, 0, 0, 0, 0, now.Location()).Day()
if currentDay == lastDay {
_, _ = db.Exec(`
UPDATE user
SET in_flow = 0, out_flow = 0
WHERE flow_reset_time != 0
AND (flow_reset_time = ? OR flow_reset_time > ?)
`, currentDay, lastDay)
_, _ = db.Exec(`
UPDATE user_tunnel
SET in_flow = 0, out_flow = 0
WHERE flow_reset_time != 0
AND (flow_reset_time = ? OR flow_reset_time > ?)
`, currentDay, lastDay)
return
}
_, _ = db.Exec(`
UPDATE user
SET in_flow = 0, out_flow = 0
WHERE flow_reset_time != 0
AND flow_reset_time = ?
`, currentDay)
_, _ = db.Exec(`
UPDATE user_tunnel
SET in_flow = 0, out_flow = 0
WHERE flow_reset_time != 0
AND flow_reset_time = ?
`, currentDay)
_ = h.repo.ResetUserMonthlyFlow(currentDay, lastDay)
_ = h.repo.ResetUserTunnelMonthlyFlow(currentDay, lastDay)
}
func (h *Handler) disableExpiredUsers(nowMs int64) {
db := h.repo.DB()
rows, err := db.Query(`
SELECT id
FROM user
WHERE role_id != 0
AND status = 1
AND exp_time IS NOT NULL
AND exp_time < ?
`, nowMs)
userIDs, err := h.repo.ListExpiredActiveUserIDs(nowMs)
if err != nil {
return
}
userIDs := make([]int64, 0)
for rows.Next() {
var userID int64
if err := rows.Scan(&userID); err != nil {
continue
}
userIDs = append(userIDs, userID)
}
_ = rows.Close()
for _, userID := range userIDs {
forwards, err := h.listActiveForwardsByUser(userID)
if err == nil {
h.pauseForwardRecords(forwards, nowMs)
}
_, _ = db.Exec(`UPDATE user SET status = 0 WHERE id = ?`, userID)
_ = h.repo.DisableUser(userID)
}
}
func (h *Handler) disableExpiredUserTunnels(nowMs int64) {
db := h.repo.DB()
rows, err := db.Query(`
SELECT id, user_id, tunnel_id
FROM user_tunnel
WHERE status = 1
AND exp_time IS NOT NULL
AND exp_time < ?
`, nowMs)
items, err := h.repo.ListExpiredActiveUserTunnels(nowMs)
if err != nil {
return
}
type expiredUserTunnel struct {
userTunnelID int64
userID int64
tunnelID int64
}
items := make([]expiredUserTunnel, 0)
for rows.Next() {
var userTunnelID int64
var userID int64
var tunnelID int64
if err := rows.Scan(&userTunnelID, &userID, &tunnelID); err != nil {
continue
}
items = append(items, expiredUserTunnel{userTunnelID: userTunnelID, userID: userID, tunnelID: tunnelID})
}
_ = rows.Close()
for _, item := range items {
forwards, err := h.listActiveForwardsByUserTunnel(item.userID, item.tunnelID)
forwards, err := h.listActiveForwardsByUserTunnel(item.UserID, item.TunnelID)
if err == nil {
h.pauseForwardRecords(forwards, nowMs)
}
_, _ = db.Exec(`UPDATE user_tunnel SET status = 0 WHERE id = ?`, item.userTunnelID)
_ = h.repo.DisableUserTunnel(item.ID)
}
}
+23 -23
View File
@@ -5,36 +5,36 @@ import (
"testing"
"time"
"go-backend/internal/store/sqlite"
"go-backend/internal/store/repo"
)
func TestRunStatisticsFlowJobTracksIncrementAndPrunes(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "jobs-stats.db")
repo, err := sqlite.Open(dbPath)
r, err := repo.Open(dbPath)
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
t.Cleanup(func() { _ = repo.Close() })
t.Cleanup(func() { _ = r.Close() })
h := New(repo, "secret")
h := New(r, "secret")
now := time.Date(2026, 2, 7, 12, 0, 0, 0, time.UTC)
nowMs := now.UnixMilli()
if _, err := repo.DB().Exec(`UPDATE user SET in_flow = 100, out_flow = 200 WHERE id = 1`); err != nil {
if err := r.DB().Exec(`UPDATE user SET in_flow = 100, out_flow = 200 WHERE id = 1`).Error; err != nil {
t.Fatalf("seed user flow: %v", err)
}
if _, err := repo.DB().Exec(`INSERT INTO statistics_flow(user_id, flow, total_flow, time, created_time) VALUES(1, 250, 250, '11:00', ?)`, now.Add(-time.Hour).UnixMilli()); err != nil {
if err := r.DB().Exec(`INSERT INTO statistics_flow(user_id, flow, total_flow, time, created_time) VALUES(1, 250, 250, '11:00', ?)`, now.Add(-time.Hour).UnixMilli()).Error; err != nil {
t.Fatalf("seed recent statistics row: %v", err)
}
if _, err := repo.DB().Exec(`INSERT INTO statistics_flow(user_id, flow, total_flow, time, created_time) VALUES(1, 10, 10, '00:00', ?)`, now.Add(-49*time.Hour).UnixMilli()); err != nil {
if err := r.DB().Exec(`INSERT INTO statistics_flow(user_id, flow, total_flow, time, created_time) VALUES(1, 10, 10, '00:00', ?)`, now.Add(-49*time.Hour).UnixMilli()).Error; err != nil {
t.Fatalf("seed stale statistics row: %v", err)
}
h.runStatisticsFlowJob(now)
var staleCount int
if err := repo.DB().QueryRow(`SELECT COUNT(1) FROM statistics_flow WHERE created_time < ?`, nowMs-int64((48*time.Hour)/time.Millisecond)).Scan(&staleCount); err != nil {
if err := r.DB().Raw(`SELECT COUNT(1) FROM statistics_flow WHERE created_time < ?`, nowMs-int64((48*time.Hour)/time.Millisecond)).Row().Scan(&staleCount); err != nil {
t.Fatalf("query stale statistics rows: %v", err)
}
if staleCount != 0 {
@@ -44,7 +44,7 @@ func TestRunStatisticsFlowJobTracksIncrementAndPrunes(t *testing.T) {
var flow int64
var total int64
var hour string
if err := repo.DB().QueryRow(`SELECT flow, total_flow, time FROM statistics_flow WHERE user_id = 1 ORDER BY id DESC LIMIT 1`).Scan(&flow, &total, &hour); err != nil {
if err := r.DB().Raw(`SELECT flow, total_flow, time FROM statistics_flow WHERE user_id = 1 ORDER BY id DESC LIMIT 1`).Row().Scan(&flow, &total, &hour); err != nil {
t.Fatalf("query latest statistics row: %v", err)
}
if flow != 50 {
@@ -60,41 +60,41 @@ func TestRunStatisticsFlowJobTracksIncrementAndPrunes(t *testing.T) {
func TestRunResetAndExpiryJobResetsFlowAndDisablesExpiredRecords(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "jobs-reset.db")
repo, err := sqlite.Open(dbPath)
r, err := repo.Open(dbPath)
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
t.Cleanup(func() { _ = repo.Close() })
t.Cleanup(func() { _ = r.Close() })
h := New(repo, "secret")
h := New(r, "secret")
now := time.Date(2026, 3, 15, 0, 0, 5, 0, time.UTC)
nowMs := now.UnixMilli()
if _, err := repo.DB().Exec(`
if err := r.DB().Exec(`
INSERT INTO user(id, user, pwd, role_id, exp_time, flow, in_flow, out_flow, flow_reset_time, num, created_time, updated_time, status)
VALUES(2, 'expired_user', 'x', 1, ?, 100, 1000, 2000, 15, 1, ?, ?, 1)
`, nowMs-1000, nowMs, nowMs); err != nil {
`, nowMs-1000, nowMs, nowMs).Error; err != nil {
t.Fatalf("insert expired user: %v", err)
}
if _, err := repo.DB().Exec(`
if err := r.DB().Exec(`
INSERT INTO tunnel(id, name, traffic_ratio, type, protocol, flow, created_time, updated_time, status, in_ip, inx)
VALUES(1, 't1', 1.0, 1, 'tls', 1, ?, ?, 1, NULL, 0)
`, nowMs, nowMs); err != nil {
`, nowMs, nowMs).Error; err != nil {
t.Fatalf("insert tunnel: %v", err)
}
if _, err := repo.DB().Exec(`
if err := r.DB().Exec(`
INSERT INTO user_tunnel(id, user_id, tunnel_id, speed_id, num, flow, in_flow, out_flow, flow_reset_time, exp_time, status)
VALUES(10, 2, 1, NULL, 1, 1, 300, 400, 15, ?, 1)
`, nowMs-1000); err != nil {
`, nowMs-1000).Error; err != nil {
t.Fatalf("insert expired user_tunnel: %v", err)
}
if _, err := repo.DB().Exec(`
if err := r.DB().Exec(`
INSERT INTO forward(id, user_id, user_name, name, tunnel_id, remote_addr, strategy, in_flow, out_flow, created_time, updated_time, status, inx)
VALUES(20, 2, 'expired_user', 'f1', 1, '1.1.1.1:443', 'fifo', 0, 0, ?, ?, 1, 0)
`, nowMs, nowMs); err != nil {
`, nowMs, nowMs).Error; err != nil {
t.Fatalf("insert forward: %v", err)
}
@@ -102,7 +102,7 @@ func TestRunResetAndExpiryJobResetsFlowAndDisablesExpiredRecords(t *testing.T) {
var userIn, userOut int64
var userStatus int
if err := repo.DB().QueryRow(`SELECT in_flow, out_flow, status FROM user WHERE id = 2`).Scan(&userIn, &userOut, &userStatus); err != nil {
if err := r.DB().Raw(`SELECT in_flow, out_flow, status FROM user WHERE id = 2`).Row().Scan(&userIn, &userOut, &userStatus); err != nil {
t.Fatalf("query user after maintenance: %v", err)
}
if userIn != 0 || userOut != 0 || userStatus != 0 {
@@ -111,7 +111,7 @@ func TestRunResetAndExpiryJobResetsFlowAndDisablesExpiredRecords(t *testing.T) {
var utIn, utOut int64
var utStatus int
if err := repo.DB().QueryRow(`SELECT in_flow, out_flow, status FROM user_tunnel WHERE id = 10`).Scan(&utIn, &utOut, &utStatus); err != nil {
if err := r.DB().Raw(`SELECT in_flow, out_flow, status FROM user_tunnel WHERE id = 10`).Row().Scan(&utIn, &utOut, &utStatus); err != nil {
t.Fatalf("query user_tunnel after maintenance: %v", err)
}
if utIn != 0 || utOut != 0 || utStatus != 0 {
@@ -119,7 +119,7 @@ func TestRunResetAndExpiryJobResetsFlowAndDisablesExpiredRecords(t *testing.T) {
}
var forwardStatus int
if err := repo.DB().QueryRow(`SELECT status FROM forward WHERE id = 20`).Scan(&forwardStatus); err != nil {
if err := r.DB().Raw(`SELECT status FROM forward WHERE id = 20`).Row().Scan(&forwardStatus); err != nil {
t.Fatalf("query forward after maintenance: %v", err)
}
if forwardStatus != 0 {
File diff suppressed because it is too large Load Diff
-466
View File
@@ -1,466 +0,0 @@
// Package store provides a thin dialect-aware wrapper around database/sql,
// enabling transparent use of both SQLite and PostgreSQL.
package store
import (
"database/sql"
"strconv"
"strings"
)
// Dialect identifies the underlying database engine.
type Dialect int
const (
DialectSQLite Dialect = iota
DialectPostgres
)
// String returns a human-readable dialect name.
func (d Dialect) String() string {
switch d {
case DialectSQLite:
return "sqlite"
case DialectPostgres:
return "postgres"
default:
return "unknown"
}
}
// DB wraps *sql.DB with dialect awareness.
type DB struct {
raw *sql.DB
dialect Dialect
}
// Wrap creates a new dialect-aware DB from an existing *sql.DB.
func Wrap(raw *sql.DB, dialect Dialect) *DB {
return &DB{raw: raw, dialect: dialect}
}
// Dialect returns the database dialect.
func (db *DB) Dialect() Dialect {
if db == nil {
return DialectSQLite
}
return db.dialect
}
// RawDB returns the underlying *sql.DB.
func (db *DB) RawDB() *sql.DB {
if db == nil {
return nil
}
return db.raw
}
// Close closes the underlying connection.
func (db *DB) Close() error {
if db == nil || db.raw == nil {
return nil
}
return db.raw.Close()
}
// Ping verifies the connection is alive.
func (db *DB) Ping() error {
return db.raw.Ping()
}
// Exec executes a query with transparent placeholder and syntax rewriting.
func (db *DB) Exec(query string, args ...any) (sql.Result, error) {
return db.raw.Exec(db.rewrite(query), args...)
}
// Query executes a query that returns rows, with transparent rewriting.
func (db *DB) Query(query string, args ...any) (*sql.Rows, error) {
return db.raw.Query(db.rewrite(query), args...)
}
// QueryRow executes a query that returns at most one row, with transparent rewriting.
func (db *DB) QueryRow(query string, args ...any) *sql.Row {
return db.raw.QueryRow(db.rewrite(query), args...)
}
// Begin starts a transaction, returning a dialect-aware Tx.
func (db *DB) Begin() (*Tx, error) {
tx, err := db.raw.Begin()
if err != nil {
return nil, err
}
return &Tx{raw: tx, dialect: db.dialect}, nil
}
// ExecReturningID executes an INSERT and returns the auto-generated id.
// - SQLite: uses LastInsertId()
// - PostgreSQL: appends RETURNING id and uses QueryRow().Scan()
func (db *DB) ExecReturningID(query string, args ...any) (int64, error) {
q := db.rewrite(query)
if db.dialect == DialectPostgres {
q = ensureReturningID(q)
var id int64
if err := db.raw.QueryRow(q, args...).Scan(&id); err != nil {
return 0, err
}
return id, nil
}
res, err := db.raw.Exec(q, args...)
if err != nil {
return 0, err
}
return res.LastInsertId()
}
// Tx wraps *sql.Tx with dialect awareness.
type Tx struct {
raw *sql.Tx
dialect Dialect
}
// Exec executes a query inside the transaction with transparent rewriting.
func (tx *Tx) Exec(query string, args ...any) (sql.Result, error) {
return tx.raw.Exec(rewriteQuery(tx.dialect, query), args...)
}
// Query executes a query that returns rows inside the transaction.
func (tx *Tx) Query(query string, args ...any) (*sql.Rows, error) {
return tx.raw.Query(rewriteQuery(tx.dialect, query), args...)
}
// QueryRow executes a query that returns at most one row inside the transaction.
func (tx *Tx) QueryRow(query string, args ...any) *sql.Row {
return tx.raw.QueryRow(rewriteQuery(tx.dialect, query), args...)
}
// Commit commits the transaction.
func (tx *Tx) Commit() error { return tx.raw.Commit() }
// Rollback aborts the transaction.
func (tx *Tx) Rollback() error { return tx.raw.Rollback() }
// ExecReturningID executes an INSERT inside the transaction and returns the id.
func (tx *Tx) ExecReturningID(query string, args ...any) (int64, error) {
q := rewriteQuery(tx.dialect, query)
if tx.dialect == DialectPostgres {
q = ensureReturningID(q)
var id int64
if err := tx.raw.QueryRow(q, args...).Scan(&id); err != nil {
return 0, err
}
return id, nil
}
res, err := tx.raw.Exec(q, args...)
if err != nil {
return 0, err
}
return res.LastInsertId()
}
func (db *DB) rewrite(query string) string {
return rewriteQuery(db.dialect, query)
}
func rewriteQuery(dialect Dialect, query string) string {
if dialect != DialectPostgres {
return query
}
query = rewriteUserIdentifier(query)
query = rewriteInsertOrIgnore(query)
query = rewritePlaceholders(query)
return query
}
func rewriteUserIdentifier(query string) string {
var buf strings.Builder
buf.Grow(len(query) + 16)
i := 0
for i < len(query) {
if end, ok := skipSQLProtectedSegment(query, i); ok {
buf.WriteString(query[i:end])
i = end
continue
}
ch := query[i]
if isIdentifierChar(ch) {
j := i + 1
for j < len(query) && isIdentifierChar(query[j]) {
j++
}
tok := query[i:j]
if strings.EqualFold(tok, "user") {
buf.WriteString(`"user"`)
} else {
buf.WriteString(tok)
}
i = j
continue
}
buf.WriteByte(ch)
i++
}
return buf.String()
}
func isIdentifierChar(ch byte) bool {
if ch >= 'a' && ch <= 'z' {
return true
}
if ch >= 'A' && ch <= 'Z' {
return true
}
if ch >= '0' && ch <= '9' {
return true
}
return ch == '_'
}
func rewriteInsertOrIgnore(query string) string {
start, end, ok := findKeywordSequenceOutside(query, []string{"INSERT", "OR", "IGNORE", "INTO"}, 0)
if !ok {
return query
}
rewritten := query[:start] + "INSERT INTO" + query[end:]
rewritten = strings.TrimRight(rewritten, "; \t\n")
insertIntoEnd := start + len("INSERT INTO")
if _, _, hasOnConflict := findKeywordSequenceOutside(rewritten, []string{"ON", "CONFLICT"}, insertIntoEnd); hasOnConflict {
return rewritten
}
if retStart, _, hasReturning := findKeywordSequenceOutside(rewritten, []string{"RETURNING"}, insertIntoEnd); hasReturning {
prefix := strings.TrimRight(rewritten[:retStart], " \t\n")
suffix := strings.TrimLeft(rewritten[retStart:], " \t\n")
return prefix + " ON CONFLICT DO NOTHING " + suffix
}
return rewritten + " ON CONFLICT DO NOTHING"
}
func rewritePlaceholders(query string) string {
var buf strings.Builder
buf.Grow(len(query) + 16)
n := 1
for i := 0; i < len(query); i++ {
if end, ok := skipSQLProtectedSegment(query, i); ok {
buf.WriteString(query[i:end])
i = end - 1
continue
}
ch := query[i]
if ch == '?' {
buf.WriteByte('$')
buf.WriteString(strconv.Itoa(n))
n++
continue
}
buf.WriteByte(ch)
}
return buf.String()
}
func ensureReturningID(query string) string {
trimmed := strings.TrimRight(query, "; \t\n")
if _, _, ok := findKeywordSequenceOutside(trimmed, []string{"RETURNING"}, 0); ok {
return trimmed
}
return trimmed + " RETURNING id"
}
func findKeywordSequenceOutside(query string, keywords []string, from int) (int, int, bool) {
if len(keywords) == 0 {
return 0, 0, false
}
if from < 0 {
from = 0
}
if from >= len(query) {
return 0, 0, false
}
matched := 0
seqStart := -1
for i := from; i < len(query); {
if end, ok := skipSQLProtectedSegment(query, i); ok {
i = end
continue
}
ch := query[i]
if isIdentifierChar(ch) {
j := i + 1
for j < len(query) && isIdentifierChar(query[j]) {
j++
}
tok := query[i:j]
if strings.EqualFold(tok, keywords[matched]) {
if matched == 0 {
seqStart = i
}
matched++
if matched == len(keywords) {
return seqStart, j, true
}
} else if strings.EqualFold(tok, keywords[0]) {
seqStart = i
matched = 1
} else {
matched = 0
seqStart = -1
}
i = j
continue
}
if !isSQLSpace(ch) {
matched = 0
seqStart = -1
}
i++
}
return 0, 0, false
}
func skipSQLProtectedSegment(query string, i int) (int, bool) {
if i < 0 || i >= len(query) {
return 0, false
}
switch query[i] {
case '\'':
return skipSingleQuotedLiteral(query, i), true
case '"':
return skipDoubleQuotedIdentifier(query, i), true
case '-':
if i+1 < len(query) && query[i+1] == '-' {
return skipLineComment(query, i), true
}
case '/':
if i+1 < len(query) && query[i+1] == '*' {
return skipBlockComment(query, i), true
}
case '$':
if end, ok := skipDollarQuotedLiteral(query, i); ok {
return end, true
}
}
return 0, false
}
func skipSingleQuotedLiteral(query string, i int) int {
for j := i + 1; j < len(query); j++ {
if query[j] != '\'' {
continue
}
if j+1 < len(query) && query[j+1] == '\'' {
j++
continue
}
return j + 1
}
return len(query)
}
func skipDoubleQuotedIdentifier(query string, i int) int {
for j := i + 1; j < len(query); j++ {
if query[j] != '"' {
continue
}
if j+1 < len(query) && query[j+1] == '"' {
j++
continue
}
return j + 1
}
return len(query)
}
func skipLineComment(query string, i int) int {
for j := i + 2; j < len(query); j++ {
if query[j] == '\n' {
return j
}
}
return len(query)
}
func skipBlockComment(query string, i int) int {
depth := 1
for j := i + 2; j < len(query)-1; j++ {
if query[j] == '/' && query[j+1] == '*' {
depth++
j++
continue
}
if query[j] == '*' && query[j+1] == '/' {
depth--
j++
if depth == 0 {
return j + 1
}
}
}
return len(query)
}
func skipDollarQuotedLiteral(query string, i int) (int, bool) {
if i < 0 || i >= len(query) || query[i] != '$' {
return 0, false
}
if i+1 >= len(query) {
return 0, false
}
var endTag int
if query[i+1] == '$' {
endTag = i + 1
} else {
if !isDollarTagStart(query[i+1]) {
return 0, false
}
j := i + 2
for j < len(query) && isDollarTagChar(query[j]) {
j++
}
if j >= len(query) || query[j] != '$' {
return 0, false
}
endTag = j
}
tag := query[i : endTag+1]
if closeIdx := strings.Index(query[endTag+1:], tag); closeIdx >= 0 {
return endTag + 1 + closeIdx + len(tag), true
}
return len(query), true
}
func isDollarTagStart(ch byte) bool {
return ch == '_' || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')
}
func isDollarTagChar(ch byte) bool {
if isDollarTagStart(ch) {
return true
}
return ch >= '0' && ch <= '9'
}
func isSQLSpace(ch byte) bool {
switch ch {
case ' ', '\t', '\n', '\r', '\f':
return true
default:
return false
}
}
-116
View File
@@ -1,116 +0,0 @@
package store
import "testing"
func TestRewritePlaceholdersSkipsProtectedSegments(t *testing.T) {
q := `SELECT ?, '?', "id?", $$body ? $$, $tag$X?$tag$, col -- comment ?
FROM t /* block ? */ WHERE id = ?`
got := rewritePlaceholders(q)
want := `SELECT $1, '?', "id?", $$body ? $$, $tag$X?$tag$, col -- comment ?
FROM t /* block ? */ WHERE id = $2`
if got != want {
t.Fatalf("unexpected rewrite\nwant: %s\ngot: %s", want, got)
}
}
func TestRewriteInsertOrIgnoreBasic(t *testing.T) {
q := `INSERT OR IGNORE INTO user_group_user(user_group_id, user_id, created_time) VALUES(?, ?, ?)`
got := rewriteInsertOrIgnore(q)
want := `INSERT INTO user_group_user(user_group_id, user_id, created_time) VALUES(?, ?, ?) ON CONFLICT DO NOTHING`
if got != want {
t.Fatalf("unexpected rewrite\nwant: %s\ngot: %s", want, got)
}
}
func TestRewriteInsertOrIgnoreBeforeReturning(t *testing.T) {
q := `INSERT OR IGNORE INTO x(a) VALUES(?) RETURNING id`
got := rewriteInsertOrIgnore(q)
want := `INSERT INTO x(a) VALUES(?) ON CONFLICT DO NOTHING RETURNING id`
if got != want {
t.Fatalf("unexpected rewrite\nwant: %s\ngot: %s", want, got)
}
}
func TestRewriteInsertOrIgnoreNotDuplicatingOnConflict(t *testing.T) {
q := `INSERT OR IGNORE INTO x(a) VALUES(?) ON CONFLICT(a) DO UPDATE SET a=excluded.a`
got := rewriteInsertOrIgnore(q)
want := `INSERT INTO x(a) VALUES(?) ON CONFLICT(a) DO UPDATE SET a=excluded.a`
if got != want {
t.Fatalf("unexpected rewrite\nwant: %s\ngot: %s", want, got)
}
}
func TestEnsureReturningID(t *testing.T) {
if got := ensureReturningID(`INSERT INTO x(a) VALUES($1)`); got != `INSERT INTO x(a) VALUES($1) RETURNING id` {
t.Fatalf("missing RETURNING append: %s", got)
}
if got := ensureReturningID(`INSERT INTO x(a) VALUES($1) RETURNING other_id`); got != `INSERT INTO x(a) VALUES($1) RETURNING other_id` {
t.Fatalf("RETURNING should not be duplicated: %s", got)
}
}
func TestRewriteUserIdentifierSafety(t *testing.T) {
q := `SELECT user, user_id, 'user', "user", note FROM user -- user
WHERE owner='user'`
got := rewriteUserIdentifier(q)
want := `SELECT "user", user_id, 'user', "user", note FROM "user" -- user
WHERE owner='user'`
if got != want {
t.Fatalf("unexpected rewrite\nwant: %s\ngot: %s", want, got)
}
}
func TestRewriteQueryPostgresPipeline(t *testing.T) {
q := `INSERT OR IGNORE INTO user(name, note) VALUES(?, '?')`
got := rewriteQuery(DialectPostgres, q)
want := `INSERT INTO "user"(name, note) VALUES($1, '?') ON CONFLICT DO NOTHING`
if got != want {
t.Fatalf("unexpected rewrite\nwant: %s\ngot: %s", want, got)
}
}
func TestRewriteInsertOrIgnoreSkipsStringLiteral(t *testing.T) {
q := `SELECT 'INSERT OR IGNORE INTO t(a) VALUES(?)' AS q`
got := rewriteInsertOrIgnore(q)
if got != q {
t.Fatalf("string literal should stay unchanged\nwant: %s\ngot: %s", q, got)
}
}
func TestRewriteInsertOrIgnoreSkipsCommentedKeyword(t *testing.T) {
q := `-- INSERT OR IGNORE INTO ignored(a) VALUES(?)
INSERT OR IGNORE INTO real_t(a) VALUES(?)`
got := rewriteInsertOrIgnore(q)
want := `-- INSERT OR IGNORE INTO ignored(a) VALUES(?)
INSERT INTO real_t(a) VALUES(?) ON CONFLICT DO NOTHING`
if got != want {
t.Fatalf("unexpected rewrite\nwant: %s\ngot: %s", want, got)
}
}
func TestRewritePlaceholdersSkipsNestedBlockComment(t *testing.T) {
q := `SELECT ? /* outer ? /* inner ? */ still_outer ? */ FROM t WHERE id = ?`
got := rewritePlaceholders(q)
want := `SELECT $1 /* outer ? /* inner ? */ still_outer ? */ FROM t WHERE id = $2`
if got != want {
t.Fatalf("unexpected rewrite\nwant: %s\ngot: %s", want, got)
}
}
func TestRewritePlaceholdersSkipsUnterminatedBlockComment(t *testing.T) {
q := `SELECT ? /* unterminated ? comment`
got := rewritePlaceholders(q)
want := `SELECT $1 /* unterminated ? comment`
if got != want {
t.Fatalf("unexpected rewrite\nwant: %s\ngot: %s", want, got)
}
}
func TestRewriteUserIdentifierSkipsDollarQuotedAndComment(t *testing.T) {
q := `SELECT user, $$user ?$$ AS body, col FROM user /* user */ -- user`
got := rewriteUserIdentifier(q)
want := `SELECT "user", $$user ?$$ AS body, col FROM "user" /* user */ -- user`
if got != want {
t.Fatalf("unexpected rewrite\nwant: %s\ngot: %s", want, got)
}
}
+591
View File
@@ -0,0 +1,591 @@
// Package model defines GORM model structs for all database tables,
// providing a single source of truth for the schema that works
// transparently with both SQLite and PostgreSQL.
package model
import "database/sql"
// ─── Core Business Tables ────────────────────────────────────────────
// User maps to the "user" table. PostgreSQL treats "user" as a reserved
// word, so TableName() is required for correct quoting.
type User struct {
ID int64 `gorm:"primaryKey;autoIncrement"`
User string `gorm:"column:user;type:varchar(100);not null"`
Pwd string `gorm:"type:varchar(100);not null"`
RoleID int `gorm:"column:role_id;not null"`
ExpTime int64 `gorm:"column:exp_time;not null"`
Flow int64 `gorm:"not null"`
InFlow int64 `gorm:"column:in_flow;not null;default:0"`
OutFlow int64 `gorm:"column:out_flow;not null;default:0"`
FlowResetTime int64 `gorm:"column:flow_reset_time;not null"`
Num int `gorm:"not null"`
CreatedTime int64 `gorm:"column:created_time;not null"`
UpdatedTime sql.NullInt64 `gorm:"column:updated_time"`
Status int `gorm:"not null"`
}
func (User) TableName() string { return "user" }
// Forward maps to the "forward" table.
type Forward struct {
ID int64 `gorm:"primaryKey;autoIncrement"`
UserID int64 `gorm:"column:user_id;not null"`
UserName string `gorm:"column:user_name;type:varchar(100);not null"`
Name string `gorm:"type:varchar(100);not null"`
TunnelID int64 `gorm:"column:tunnel_id;not null"`
RemoteAddr string `gorm:"column:remote_addr;type:text;not null"`
Strategy string `gorm:"type:varchar(100);not null;default:'fifo'"`
InFlow int64 `gorm:"column:in_flow;not null;default:0"`
OutFlow int64 `gorm:"column:out_flow;not null;default:0"`
CreatedTime int64 `gorm:"column:created_time;not null"`
UpdatedTime int64 `gorm:"column:updated_time;not null"`
Status int `gorm:"not null"`
Inx int `gorm:"not null;default:0"`
}
func (Forward) TableName() string { return "forward" }
type ForwardPort struct {
ID int64 `gorm:"primaryKey;autoIncrement"`
ForwardID int64 `gorm:"column:forward_id;not null"`
NodeID int64 `gorm:"column:node_id;not null"`
Port int `gorm:"not null"`
}
func (ForwardPort) TableName() string { return "forward_port" }
type Node struct {
ID int64 `gorm:"primaryKey;autoIncrement"`
Name string `gorm:"type:varchar(100);not null"`
Secret string `gorm:"type:varchar(100);not null"`
ServerIP string `gorm:"column:server_ip;type:varchar(100);not null"`
ServerIPV4 sql.NullString `gorm:"column:server_ip_v4;type:varchar(100)"`
ServerIPV6 sql.NullString `gorm:"column:server_ip_v6;type:varchar(100)"`
Port string `gorm:"type:text;not null"`
InterfaceName sql.NullString `gorm:"column:interface_name;type:varchar(200)"`
Version sql.NullString `gorm:"type:varchar(100)"`
HTTP int `gorm:"column:http;not null;default:0"`
TLS int `gorm:"column:tls;not null;default:0"`
Socks int `gorm:"not null;default:0"`
CreatedTime int64 `gorm:"column:created_time;not null"`
UpdatedTime sql.NullInt64 `gorm:"column:updated_time"`
Status int `gorm:"not null"`
TCPListenAddr string `gorm:"column:tcp_listen_addr;type:varchar(100);not null;default:'[::]'"`
UDPListenAddr string `gorm:"column:udp_listen_addr;type:varchar(100);not null;default:'[::]'"`
Inx int `gorm:"not null;default:0"`
IsRemote int `gorm:"column:is_remote;default:0"`
RemoteURL sql.NullString `gorm:"column:remote_url;type:text"`
RemoteToken sql.NullString `gorm:"column:remote_token;type:text"`
RemoteConfig sql.NullString `gorm:"column:remote_config;type:text"`
}
func (Node) TableName() string { return "node" }
type SpeedLimit struct {
ID int64 `gorm:"primaryKey;autoIncrement"`
Name string `gorm:"type:varchar(100);not null"`
Speed int `gorm:"not null"`
TunnelID int64 `gorm:"column:tunnel_id;not null"`
TunnelName string `gorm:"column:tunnel_name;type:varchar(100);not null"`
CreatedTime int64 `gorm:"column:created_time;not null"`
UpdatedTime sql.NullInt64 `gorm:"column:updated_time"`
Status int `gorm:"not null"`
}
func (SpeedLimit) TableName() string { return "speed_limit" }
type StatisticsFlow struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
UserID int64 `gorm:"column:user_id;not null" json:"userId"`
Flow int64 `gorm:"not null" json:"flow"`
TotalFlow int64 `gorm:"column:total_flow;not null" json:"totalFlow"`
Time string `gorm:"type:varchar(100);not null" json:"time"`
CreatedTime int64 `gorm:"column:created_time;not null" json:"-"`
}
func (StatisticsFlow) TableName() string { return "statistics_flow" }
type Tunnel struct {
ID int64 `gorm:"primaryKey;autoIncrement"`
Name string `gorm:"type:varchar(100);not null"`
TrafficRatio float64 `gorm:"column:traffic_ratio;not null;default:1.0"`
Type int `gorm:"not null"`
Protocol string `gorm:"type:varchar(10);not null;default:'tls'"`
Flow int64 `gorm:"not null"`
CreatedTime int64 `gorm:"column:created_time;not null"`
UpdatedTime int64 `gorm:"column:updated_time;not null"`
Status int `gorm:"not null"`
InIP sql.NullString `gorm:"column:in_ip;type:text"`
Inx int `gorm:"not null;default:0"`
IPPreference string `gorm:"column:ip_preference;type:varchar(10);not null;default:''"`
}
func (Tunnel) TableName() string { return "tunnel" }
type ChainTunnel struct {
ID int64 `gorm:"primaryKey;autoIncrement"`
TunnelID int64 `gorm:"column:tunnel_id;not null"`
ChainType string `gorm:"column:chain_type;type:varchar(10);not null"`
NodeID int64 `gorm:"column:node_id;not null"`
Port sql.NullInt64 `gorm:"column:port"`
Strategy sql.NullString `gorm:"type:varchar(10)"`
Inx sql.NullInt64 `gorm:"column:inx"`
Protocol sql.NullString `gorm:"type:varchar(10)"`
}
func (ChainTunnel) TableName() string { return "chain_tunnel" }
type UserTunnel struct {
ID int64 `gorm:"primaryKey;autoIncrement"`
UserID int64 `gorm:"column:user_id;not null;uniqueIndex:idx_user_tunnel_unique"`
TunnelID int64 `gorm:"column:tunnel_id;not null;uniqueIndex:idx_user_tunnel_unique"`
SpeedID sql.NullInt64 `gorm:"column:speed_id"`
Num int `gorm:"not null"`
Flow int64 `gorm:"not null"`
InFlow int64 `gorm:"column:in_flow;not null;default:0"`
OutFlow int64 `gorm:"column:out_flow;not null;default:0"`
FlowResetTime int64 `gorm:"column:flow_reset_time;not null"`
ExpTime int64 `gorm:"column:exp_time;not null"`
Status int `gorm:"not null"`
}
func (UserTunnel) TableName() string { return "user_tunnel" }
type TunnelGroup struct {
ID int64 `gorm:"primaryKey;autoIncrement"`
Name string `gorm:"type:varchar(100);not null;uniqueIndex:idx_tunnel_group_name"`
CreatedTime int64 `gorm:"column:created_time;not null"`
UpdatedTime int64 `gorm:"column:updated_time;not null"`
Status int `gorm:"not null"`
}
func (TunnelGroup) TableName() string { return "tunnel_group" }
type UserGroup struct {
ID int64 `gorm:"primaryKey;autoIncrement"`
Name string `gorm:"type:varchar(100);not null;uniqueIndex:idx_user_group_name"`
CreatedTime int64 `gorm:"column:created_time;not null"`
UpdatedTime int64 `gorm:"column:updated_time;not null"`
Status int `gorm:"not null"`
}
func (UserGroup) TableName() string { return "user_group" }
type TunnelGroupTunnel struct {
ID int64 `gorm:"primaryKey;autoIncrement"`
TunnelGroupID int64 `gorm:"column:tunnel_group_id;not null;uniqueIndex:idx_tunnel_group_tunnel_unique"`
TunnelID int64 `gorm:"column:tunnel_id;not null;uniqueIndex:idx_tunnel_group_tunnel_unique"`
CreatedTime int64 `gorm:"column:created_time;not null"`
}
func (TunnelGroupTunnel) TableName() string { return "tunnel_group_tunnel" }
type UserGroupUser struct {
ID int64 `gorm:"primaryKey;autoIncrement"`
UserGroupID int64 `gorm:"column:user_group_id;not null;uniqueIndex:idx_user_group_user_unique"`
UserID int64 `gorm:"column:user_id;not null;uniqueIndex:idx_user_group_user_unique"`
CreatedTime int64 `gorm:"column:created_time;not null"`
}
func (UserGroupUser) TableName() string { return "user_group_user" }
type GroupPermission struct {
ID int64 `gorm:"primaryKey;autoIncrement"`
UserGroupID int64 `gorm:"column:user_group_id;not null;uniqueIndex:idx_group_permission_unique"`
TunnelGroupID int64 `gorm:"column:tunnel_group_id;not null;uniqueIndex:idx_group_permission_unique"`
CreatedTime int64 `gorm:"column:created_time;not null"`
}
func (GroupPermission) TableName() string { return "group_permission" }
type GroupPermissionGrant struct {
ID int64 `gorm:"primaryKey;autoIncrement"`
UserGroupID int64 `gorm:"column:user_group_id;not null;uniqueIndex:idx_group_permission_grant_unique"`
TunnelGroupID int64 `gorm:"column:tunnel_group_id;not null;uniqueIndex:idx_group_permission_grant_unique"`
UserTunnelID int64 `gorm:"column:user_tunnel_id;not null;uniqueIndex:idx_group_permission_grant_unique"`
CreatedByGroup int `gorm:"column:created_by_group;not null;default:0"`
CreatedTime int64 `gorm:"column:created_time;not null"`
}
func (GroupPermissionGrant) TableName() string { return "group_permission_grant" }
type ViteConfig struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
Name string `gorm:"type:varchar(200);not null;uniqueIndex" json:"name"`
Value string `gorm:"type:varchar(200);not null" json:"value"`
Time int64 `gorm:"not null" json:"time"`
}
func (ViteConfig) TableName() string { return "vite_config" }
type Announcement struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
Content string `gorm:"type:text;not null" json:"content"`
Enabled int `gorm:"not null;default:1" json:"enabled"`
CreatedTime int64 `gorm:"column:created_time;not null" json:"created_time"`
UpdatedTime sql.NullInt64 `gorm:"column:updated_time" json:"updated_time,omitempty"`
}
func (Announcement) TableName() string { return "announcement" }
type SchemaVersion struct {
Version int `gorm:"not null;default:0"`
}
func (SchemaVersion) TableName() string { return "schema_version" }
type PeerShare struct {
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
Name string `gorm:"type:text;not null" json:"name"`
NodeID int64 `gorm:"column:node_id;not null" json:"nodeId"`
Token string `gorm:"type:text;not null;uniqueIndex" json:"token"`
MaxBandwidth int64 `gorm:"column:max_bandwidth;default:0" json:"maxBandwidth"`
ExpiryTime int64 `gorm:"column:expiry_time;default:0" json:"expiryTime"`
PortRangeStart int `gorm:"column:port_range_start;default:0" json:"portRangeStart"`
PortRangeEnd int `gorm:"column:port_range_end;default:0" json:"portRangeEnd"`
CurrentFlow int64 `gorm:"column:current_flow;default:0" json:"currentFlow"`
IsActive int `gorm:"column:is_active;default:1" json:"isActive"`
CreatedTime int64 `gorm:"column:created_time;not null" json:"createdTime"`
UpdatedTime int64 `gorm:"column:updated_time;not null" json:"updatedTime"`
AllowedDomains string `gorm:"column:allowed_domains;type:text;default:''" json:"allowedDomains"`
AllowedIPs string `gorm:"column:allowed_ips;type:text;default:''" json:"allowedIps"`
}
func (PeerShare) TableName() string { return "peer_share" }
type PeerShareRuntime struct {
ID int64 `gorm:"primaryKey;autoIncrement"`
ShareID int64 `gorm:"column:share_id;not null;index:idx_peer_share_runtime_share_node_status"`
NodeID int64 `gorm:"column:node_id;not null;index:idx_peer_share_runtime_share_node_status"`
ReservationID string `gorm:"column:reservation_id;type:text;not null;uniqueIndex"`
ResourceKey string `gorm:"column:resource_key;type:text;not null;uniqueIndex"`
BindingID string `gorm:"column:binding_id;type:text;not null;default:'';index:idx_peer_share_runtime_binding_id"`
Role string `gorm:"type:text;not null;default:''"`
ChainName string `gorm:"column:chain_name;type:text;not null;default:''"`
ServiceName string `gorm:"column:service_name;type:text;not null;default:''"`
Protocol string `gorm:"type:text;not null;default:'tls'"`
Strategy string `gorm:"type:text;not null;default:'round'"`
Port int `gorm:"not null;default:0"`
Target string `gorm:"type:text;not null;default:''"`
Applied int `gorm:"not null;default:0"`
Status int `gorm:"not null;default:1;index:idx_peer_share_runtime_share_node_status"`
CreatedTime int64 `gorm:"column:created_time;not null"`
UpdatedTime int64 `gorm:"column:updated_time;not null"`
}
func (PeerShareRuntime) TableName() string { return "peer_share_runtime" }
type FederationTunnelBinding struct {
ID int64 `gorm:"primaryKey;autoIncrement"`
TunnelID int64 `gorm:"column:tunnel_id;not null;uniqueIndex:idx_federation_tunnel_binding_unique;index:idx_federation_tunnel_binding_tunnel"`
NodeID int64 `gorm:"column:node_id;not null;uniqueIndex:idx_federation_tunnel_binding_unique"`
ChainType int `gorm:"column:chain_type;not null;uniqueIndex:idx_federation_tunnel_binding_unique"`
HopInx int `gorm:"column:hop_inx;not null;default:0;uniqueIndex:idx_federation_tunnel_binding_unique"`
RemoteURL string `gorm:"column:remote_url;type:text;not null"`
ResourceKey string `gorm:"column:resource_key;type:text;not null;uniqueIndex"`
RemoteBindingID string `gorm:"column:remote_binding_id;type:text;not null"`
AllocatedPort int `gorm:"column:allocated_port;not null"`
Status int `gorm:"not null;default:1;index:idx_federation_tunnel_binding_tunnel"`
CreatedTime int64 `gorm:"column:created_time;not null"`
UpdatedTime int64 `gorm:"column:updated_time;not null"`
}
func (FederationTunnelBinding) TableName() string { return "federation_tunnel_binding" }
// ─── Backup / Import-Export Structs ──────────────────────────────────
// These are not GORM models; they define the JSON wire format for the
// backup/restore API and MUST keep their existing json tags unchanged.
// BackupData represents the full backup structure.
type BackupData struct {
Version string `json:"version"`
ExportedAt int64 `json:"exportedAt"`
Users []UserBackup `json:"users,omitempty"`
Nodes []NodeBackup `json:"nodes,omitempty"`
Tunnels []TunnelBackup `json:"tunnels,omitempty"`
Forwards []ForwardBackup `json:"forwards,omitempty"`
UserTunnels []UserTunnelBackup `json:"userTunnels,omitempty"`
SpeedLimits []SpeedLimitBackup `json:"speedLimits,omitempty"`
TunnelGroups []TunnelGroupBackup `json:"tunnelGroups,omitempty"`
UserGroups []UserGroupBackup `json:"userGroups,omitempty"`
Permissions []PermissionBackup `json:"permissions,omitempty"`
Configs map[string]string `json:"configs,omitempty"`
}
type UserBackup struct {
ID int64 `json:"id"`
User string `json:"user"`
Pwd string `json:"pwd"`
RoleID int `json:"roleId"`
ExpTime int64 `json:"expTime"`
Flow int64 `json:"flow"`
InFlow int64 `json:"inFlow"`
OutFlow int64 `json:"outFlow"`
FlowResetTime int64 `json:"flowResetTime"`
Num int `json:"num"`
CreatedTime int64 `json:"createdTime"`
UpdatedTime int64 `json:"updatedTime,omitempty"`
Status int `json:"status"`
}
type NodeBackup struct {
ID int64 `json:"id"`
Name string `json:"name"`
Secret string `json:"secret"`
ServerIP string `json:"serverIp"`
ServerIPv4 string `json:"serverIpV4,omitempty"`
ServerIPv6 string `json:"serverIpV6,omitempty"`
Port string `json:"port"`
InterfaceName string `json:"interfaceName,omitempty"`
Version string `json:"version,omitempty"`
HTTP int `json:"http"`
TLS int `json:"tls"`
Socks int `json:"socks"`
CreatedTime int64 `json:"createdTime"`
UpdatedTime int64 `json:"updatedTime,omitempty"`
Status int `json:"status"`
TCPListenAddr string `json:"tcpListenAddr"`
UDPListenAddr string `json:"udpListenAddr"`
Inx int `json:"inx"`
IsRemote int `json:"isRemote"`
RemoteURL string `json:"remoteUrl,omitempty"`
RemoteToken string `json:"remoteToken,omitempty"`
RemoteConfig string `json:"remoteConfig,omitempty"`
}
type TunnelBackup struct {
ID int64 `json:"id"`
Name string `json:"name"`
TrafficRatio float64 `json:"trafficRatio"`
Type int `json:"type"`
Protocol string `json:"protocol"`
Flow int64 `json:"flow"`
CreatedTime int64 `json:"createdTime"`
UpdatedTime int64 `json:"updatedTime"`
Status int `json:"status"`
InIP string `json:"inIp,omitempty"`
Inx int `json:"inx"`
IPPreference string `json:"ipPreference,omitempty"`
ChainTunnels []ChainTunnelBackup `json:"chainTunnels,omitempty"`
}
type ChainTunnelBackup struct {
ID int64 `json:"id"`
TunnelID int64 `json:"tunnelId"`
ChainType string `json:"chainType"`
NodeID int64 `json:"nodeId"`
Port int `json:"port,omitempty"`
Strategy string `json:"strategy,omitempty"`
Inx int `json:"inx,omitempty"`
Protocol string `json:"protocol,omitempty"`
}
type ForwardBackup struct {
ID int64 `json:"id"`
UserID int64 `json:"userId"`
UserName string `json:"userName"`
Name string `json:"name"`
TunnelID int64 `json:"tunnelId"`
RemoteAddr string `json:"remoteAddr"`
Strategy string `json:"strategy"`
InFlow int64 `json:"inFlow"`
OutFlow int64 `json:"outFlow"`
CreatedTime int64 `json:"createdTime"`
UpdatedTime int64 `json:"updatedTime"`
Status int `json:"status"`
Inx int `json:"inx"`
ForwardPorts *[]ForwardPortBackup `json:"forwardPorts,omitempty"`
}
type ForwardPortBackup struct {
NodeID int64 `json:"nodeId"`
Port int `json:"port"`
}
type UserTunnelBackup struct {
ID int64 `json:"id"`
UserID int64 `json:"userId"`
TunnelID int64 `json:"tunnelId"`
SpeedID int64 `json:"speedId,omitempty"`
Num int `json:"num"`
Flow int64 `json:"flow"`
InFlow int64 `json:"inFlow"`
OutFlow int64 `json:"outFlow"`
FlowResetTime int64 `json:"flowResetTime"`
ExpTime int64 `json:"expTime"`
Status int `json:"status"`
}
type SpeedLimitBackup struct {
ID int64 `json:"id"`
Name string `json:"name"`
Speed int64 `json:"speed"`
TunnelID int64 `json:"tunnelId"`
TunnelName string `json:"tunnelName"`
CreatedTime int64 `json:"createdTime"`
UpdatedTime int64 `json:"updatedTime,omitempty"`
Status int `json:"status"`
}
type TunnelGroupBackup struct {
ID int64 `json:"id"`
Name string `json:"name"`
CreatedTime int64 `json:"createdTime"`
UpdatedTime int64 `json:"updatedTime"`
Status int `json:"status"`
Tunnels []int64 `json:"tunnels,omitempty"`
}
type UserGroupBackup struct {
ID int64 `json:"id"`
Name string `json:"name"`
CreatedTime int64 `json:"createdTime"`
UpdatedTime int64 `json:"updatedTime"`
Status int `json:"status"`
Users []int64 `json:"users,omitempty"`
}
type PermissionBackup struct {
ID int64 `json:"id"`
UserGroupID int64 `json:"userGroupId"`
TunnelGroupID int64 `json:"tunnelGroupId"`
CreatedTime int64 `json:"createdTime"`
CreatedByGroup int `json:"createdByGroup"`
Grants []PermissionGrantBackup `json:"grants,omitempty"`
}
type PermissionGrantBackup struct {
ID int64 `json:"id"`
UserGroupID int64 `json:"userGroupId"`
TunnelGroupID int64 `json:"tunnelGroupId"`
UserTunnelID int64 `json:"userTunnelId"`
CreatedTime int64 `json:"createdTime"`
CreatedByGroup int `json:"createdByGroup"`
}
// ImportResult contains the result of an import operation.
type ImportResult struct {
UsersImported int `json:"usersImported"`
NodesImported int `json:"nodesImported"`
TunnelsImported int `json:"tunnelsImported"`
ForwardsImported int `json:"forwardsImported"`
UserTunnelsImported int `json:"userTunnelsImported"`
SpeedLimitsImported int `json:"speedLimitsImported"`
TunnelGroupsImported int `json:"tunnelGroupsImported"`
UserGroupsImported int `json:"userGroupsImported"`
PermissionsImported int `json:"permissionsImported"`
ConfigsImported int `json:"configsImported"`
AutoBackup *BackupData `json:"autoBackup,omitempty"`
}
// ─── View Structs (used by Repository, not GORM models) ─────────────
// These are used for JOIN query results that don't map 1:1 to a table.
// ForwardRecord is a minimal forward view used by control plane and flow policy.
type ForwardRecord struct {
ID int64
UserID int64
UserName string
Name string
TunnelID int64
RemoteAddr string
Strategy string
Status int
}
// TunnelRecord is a minimal tunnel view used by control plane.
type TunnelRecord struct {
ID int64
Type int
Status int
Flow int64
TrafficRatio float64
}
// ForwardPortRecord is a forward port mapping used by control plane.
type ForwardPortRecord struct {
NodeID int64
Port int
}
// NodeRecord is a node view used by control plane.
type NodeRecord struct {
ID int64
Name string
ServerIP string
ServerIPv4 string
ServerIPv6 string
Status int
PortRange string
TCPListenAddr string
UDPListenAddr string
InterfaceName string
IsRemote int
RemoteURL string
RemoteToken string
RemoteConfig string
}
type ChainNodeRecord struct {
ChainType int
Inx int64
NodeID int64
Port int
NodeName string
Protocol string
Strategy string
}
type UserTunnelLimiterInfo struct {
UserTunnelID int64
LimiterID *int64
Speed *int
}
// UserFlowSnapshot holds a user's current flow counters (used by stats job).
type UserFlowSnapshot struct {
UserID int64
InFlow int64
OutFlow int64
}
// ExpiredUserTunnel holds minimal info for an expired user_tunnel row.
type ExpiredUserTunnel struct {
ID int64
UserID int64
TunnelID int64
}
// UserTunnelDetail is a joined view of user_tunnel + tunnel + speed_limit.
type UserTunnelDetail struct {
ID int64
UserID int64
TunnelID int64
TunnelName string
TunnelFlow int
Flow int64
InFlow int64
OutFlow int64
Num int
FlowResetTime int64
ExpTime int64
SpeedID sql.NullInt64
SpeedLimit sql.NullString
Speed sql.NullInt64
}
// UserForwardDetail is a joined view of forward + tunnel.
type UserForwardDetail struct {
ID int64
Name string
TunnelID int64
TunnelName string
InIP string
InPort sql.NullInt64
RemoteAddr string
InFlow int64
OutFlow int64
Status int
CreatedAt int64
}
@@ -1,9 +0,0 @@
package postgres
import _ "embed"
//go:embed sql/schema.sql
var EmbeddedSchema string
//go:embed sql/data.sql
var EmbeddedSeedData string
@@ -1,18 +0,0 @@
INSERT 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)
ON CONFLICT DO NOTHING;
INSERT INTO vite_config (id, name, value, time)
VALUES (1, 'app_name', 'flux', 1755147963000)
ON CONFLICT DO NOTHING;
DO $$
BEGIN
IF to_regclass('public.user_id_seq') IS NOT NULL THEN
PERFORM setval('user_id_seq', (SELECT COALESCE(MAX(id), 0) FROM "user"));
END IF;
IF to_regclass('public.vite_config_id_seq') IS NOT NULL THEN
PERFORM setval('vite_config_id_seq', (SELECT COALESCE(MAX(id), 0) FROM vite_config));
END IF;
END
$$;
@@ -1,250 +0,0 @@
CREATE TABLE IF NOT EXISTS forward (
id SERIAL PRIMARY KEY,
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 BIGINT NOT NULL DEFAULT 0,
out_flow BIGINT NOT NULL DEFAULT 0,
created_time BIGINT NOT NULL,
updated_time BIGINT NOT NULL,
status INTEGER NOT NULL,
inx INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS forward_port (
id SERIAL PRIMARY KEY,
forward_id INTEGER NOT NULL,
node_id INTEGER NOT NULL,
port INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS node (
id SERIAL PRIMARY KEY,
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 BIGINT NOT NULL,
updated_time BIGINT,
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,
is_remote INTEGER DEFAULT 0,
remote_url TEXT,
remote_token TEXT,
remote_config TEXT
);
CREATE TABLE IF NOT EXISTS speed_limit (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
speed INTEGER NOT NULL,
tunnel_id INTEGER NOT NULL,
tunnel_name VARCHAR(100) NOT NULL,
created_time BIGINT NOT NULL,
updated_time BIGINT,
status INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS statistics_flow (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
flow BIGINT NOT NULL,
total_flow BIGINT NOT NULL,
time VARCHAR(100) NOT NULL,
created_time BIGINT NOT NULL
);
CREATE TABLE IF NOT EXISTS tunnel (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
traffic_ratio DOUBLE PRECISION NOT NULL DEFAULT 1.0,
type INTEGER NOT NULL,
protocol VARCHAR(10) NOT NULL DEFAULT 'tls',
flow BIGINT NOT NULL,
created_time BIGINT NOT NULL,
updated_time BIGINT NOT NULL,
status INTEGER NOT NULL,
in_ip TEXT,
inx INTEGER NOT NULL DEFAULT 0,
ip_preference VARCHAR(10) NOT NULL DEFAULT ''
);
CREATE TABLE IF NOT EXISTS chain_tunnel (
id SERIAL PRIMARY KEY,
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 SERIAL PRIMARY KEY,
"user" VARCHAR(100) NOT NULL,
pwd VARCHAR(100) NOT NULL,
role_id INTEGER NOT NULL,
exp_time BIGINT NOT NULL,
flow BIGINT NOT NULL,
in_flow BIGINT NOT NULL DEFAULT 0,
out_flow BIGINT NOT NULL DEFAULT 0,
flow_reset_time BIGINT NOT NULL,
num INTEGER NOT NULL,
created_time BIGINT NOT NULL,
updated_time BIGINT,
status INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS user_tunnel (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
tunnel_id INTEGER NOT NULL,
speed_id INTEGER,
num INTEGER NOT NULL,
flow BIGINT NOT NULL,
in_flow BIGINT NOT NULL DEFAULT 0,
out_flow BIGINT NOT NULL DEFAULT 0,
flow_reset_time BIGINT NOT NULL,
exp_time BIGINT NOT NULL,
status INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS tunnel_group (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
created_time BIGINT NOT NULL,
updated_time BIGINT NOT NULL,
status INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS user_group (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
created_time BIGINT NOT NULL,
updated_time BIGINT NOT NULL,
status INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS tunnel_group_tunnel (
id SERIAL PRIMARY KEY,
tunnel_group_id INTEGER NOT NULL,
tunnel_id INTEGER NOT NULL,
created_time BIGINT NOT NULL
);
CREATE TABLE IF NOT EXISTS user_group_user (
id SERIAL PRIMARY KEY,
user_group_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
created_time BIGINT NOT NULL
);
CREATE TABLE IF NOT EXISTS group_permission (
id SERIAL PRIMARY KEY,
user_group_id INTEGER NOT NULL,
tunnel_group_id INTEGER NOT NULL,
created_time BIGINT NOT NULL
);
CREATE TABLE IF NOT EXISTS group_permission_grant (
id SERIAL PRIMARY KEY,
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 BIGINT 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 UNIQUE INDEX IF NOT EXISTS idx_user_tunnel_unique ON user_tunnel(user_id, tunnel_id);
CREATE TABLE IF NOT EXISTS vite_config (
id SERIAL PRIMARY KEY,
name VARCHAR(200) NOT NULL UNIQUE,
value VARCHAR(200) NOT NULL,
time BIGINT NOT NULL
);
CREATE TABLE IF NOT EXISTS peer_share (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
node_id INTEGER NOT NULL,
token TEXT NOT NULL UNIQUE,
max_bandwidth INTEGER DEFAULT 0,
expiry_time BIGINT DEFAULT 0,
port_range_start INTEGER DEFAULT 0,
port_range_end INTEGER DEFAULT 0,
current_flow BIGINT DEFAULT 0,
is_active INTEGER DEFAULT 1,
created_time BIGINT NOT NULL,
updated_time BIGINT NOT NULL,
allowed_domains TEXT DEFAULT '',
allowed_ips TEXT DEFAULT ''
);
CREATE TABLE IF NOT EXISTS peer_share_runtime (
id SERIAL PRIMARY KEY,
share_id INTEGER NOT NULL,
node_id INTEGER NOT NULL,
reservation_id TEXT NOT NULL UNIQUE,
resource_key TEXT NOT NULL UNIQUE,
binding_id TEXT NOT NULL DEFAULT '',
role TEXT NOT NULL DEFAULT '',
chain_name TEXT NOT NULL DEFAULT '',
service_name TEXT NOT NULL DEFAULT '',
protocol TEXT NOT NULL DEFAULT 'tls',
strategy TEXT NOT NULL DEFAULT 'round',
port INTEGER NOT NULL DEFAULT 0,
target TEXT NOT NULL DEFAULT '',
applied INTEGER NOT NULL DEFAULT 0,
status INTEGER NOT NULL DEFAULT 1,
created_time BIGINT NOT NULL,
updated_time BIGINT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_peer_share_runtime_share_node_status ON peer_share_runtime(share_id, node_id, status);
CREATE INDEX IF NOT EXISTS idx_peer_share_runtime_binding_id ON peer_share_runtime(binding_id);
CREATE TABLE IF NOT EXISTS federation_tunnel_binding (
id SERIAL PRIMARY KEY,
tunnel_id INTEGER NOT NULL,
node_id INTEGER NOT NULL,
chain_type INTEGER NOT NULL,
hop_inx INTEGER NOT NULL DEFAULT 0,
remote_url TEXT NOT NULL,
resource_key TEXT NOT NULL UNIQUE,
remote_binding_id TEXT NOT NULL,
allocated_port INTEGER NOT NULL,
status INTEGER NOT NULL DEFAULT 1,
created_time BIGINT NOT NULL,
updated_time BIGINT NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_federation_tunnel_binding_unique ON federation_tunnel_binding(tunnel_id, node_id, chain_type, hop_inx);
CREATE INDEX IF NOT EXISTS idx_federation_tunnel_binding_tunnel ON federation_tunnel_binding(tunnel_id, status);
CREATE TABLE IF NOT EXISTS announcement (
id SERIAL PRIMARY KEY,
content TEXT NOT NULL,
enabled INTEGER NOT NULL DEFAULT 1,
created_time BIGINT NOT NULL,
updated_time BIGINT
);
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,308 @@
package repo
import (
"database/sql"
"errors"
"fmt"
"strconv"
"strings"
"gorm.io/gorm"
"go-backend/internal/store/model"
)
func (r *Repository) UserTunnelExistsByUserAndTunnel(userID, tunnelID int64) (bool, error) {
if r == nil || r.db == nil {
return false, errors.New("repository not initialized")
}
var count int64
err := r.db.Model(&model.UserTunnel{}).
Where("user_id = ? AND tunnel_id = ? AND status = 1", userID, tunnelID).
Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
func (r *Repository) ListForwardsByTunnel(tunnelID int64) ([]model.ForwardRecord, error) {
if r == nil || r.db == nil {
return nil, errors.New("repository not initialized")
}
var forwards []model.Forward
err := r.db.Where("tunnel_id = ?", tunnelID).Order("id ASC").Find(&forwards).Error
if err != nil {
return nil, err
}
rows := make([]model.ForwardRecord, 0, len(forwards))
for _, f := range forwards {
rows = append(rows, model.ForwardRecord{
ID: f.ID,
UserID: f.UserID,
UserName: f.UserName,
Name: f.Name,
TunnelID: f.TunnelID,
RemoteAddr: f.RemoteAddr,
Strategy: f.Strategy,
Status: f.Status,
})
}
for i := range rows {
if strings.TrimSpace(rows[i].Strategy) == "" {
rows[i].Strategy = "fifo"
}
}
return rows, nil
}
func (r *Repository) ListForwardPorts(forwardID int64) ([]model.ForwardPortRecord, error) {
if r == nil || r.db == nil {
return nil, errors.New("repository not initialized")
}
var ports []model.ForwardPort
err := r.db.Where("forward_id = ?", forwardID).Order("id ASC").Find(&ports).Error
if err != nil {
return nil, err
}
rows := make([]model.ForwardPortRecord, 0, len(ports))
for _, p := range ports {
rows = append(rows, model.ForwardPortRecord{NodeID: p.NodeID, Port: p.Port})
}
return rows, nil
}
func (r *Repository) GetTunnelOutProtocol(tunnelID int64) (string, error) {
if r == nil || r.db == nil {
return "", errors.New("repository not initialized")
}
var ct model.ChainTunnel
err := r.db.Select("protocol").
Where("tunnel_id = ? AND chain_type = ?", tunnelID, "3").
Order("id ASC").
Take(&ct).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return "", nil
}
return "", err
}
if ct.Protocol.Valid {
return ct.Protocol.String, nil
}
return "", nil
}
func (r *Repository) GetNodeRecord(nodeID int64) (*model.NodeRecord, error) {
if r == nil || r.db == nil {
return nil, errors.New("repository not initialized")
}
var n model.Node
err := r.db.Where("id = ?", nodeID).First(&n).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
if err != nil {
return nil, err
}
return nodeRecordFromModel(&n), nil
}
func (r *Repository) GetNodeRecordTx(tx *gorm.DB, nodeID int64) (*model.NodeRecord, error) {
if tx == nil {
return nil, errors.New("database unavailable")
}
var n model.Node
err := tx.Where("id = ?", nodeID).First(&n).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
if err != nil {
return nil, err
}
return nodeRecordFromModel(&n), nil
}
func nodeRecordFromModel(n *model.Node) *model.NodeRecord {
if n == nil {
return nil
}
rec := &model.NodeRecord{
ID: n.ID,
Name: n.Name,
ServerIP: n.ServerIP,
Status: n.Status,
PortRange: n.Port,
TCPListenAddr: n.TCPListenAddr, UDPListenAddr: n.UDPListenAddr,
IsRemote: n.IsRemote,
}
if n.ServerIPV4.Valid {
rec.ServerIPv4 = strings.TrimSpace(n.ServerIPV4.String)
}
if n.ServerIPV6.Valid {
rec.ServerIPv6 = strings.TrimSpace(n.ServerIPV6.String)
}
if n.InterfaceName.Valid {
rec.InterfaceName = strings.TrimSpace(n.InterfaceName.String)
}
if n.RemoteURL.Valid {
rec.RemoteURL = strings.TrimSpace(n.RemoteURL.String)
}
if n.RemoteToken.Valid {
rec.RemoteToken = strings.TrimSpace(n.RemoteToken.String)
}
if n.RemoteConfig.Valid {
rec.RemoteConfig = strings.TrimSpace(n.RemoteConfig.String)
}
if rec.TCPListenAddr == "" {
rec.TCPListenAddr = "[::]"
}
if rec.UDPListenAddr == "" {
rec.UDPListenAddr = "[::]"
}
if strings.TrimSpace(rec.Name) == "" {
rec.Name = fmt.Sprintf("node_%d", rec.ID)
}
return rec
}
func (r *Repository) ResolveUserTunnelAndLimiter(userID, tunnelID int64) (*model.UserTunnelLimiterInfo, error) {
if r == nil || r.db == nil {
return nil, errors.New("repository not initialized")
}
type row struct {
UserTunnelID int64 `gorm:"column:user_tunnel_id"`
LimiterID sql.NullInt64 `gorm:"column:limiter_id"`
Speed sql.NullInt64 `gorm:"column:speed"`
}
var rec row
err := r.db.Model(&model.UserTunnel{}).
Select("user_tunnel.id AS user_tunnel_id, speed_limit.id AS limiter_id, speed_limit.speed AS speed").
Joins("LEFT JOIN speed_limit ON speed_limit.id = user_tunnel.speed_id").
Where("user_tunnel.user_id = ? AND user_tunnel.tunnel_id = ?", userID, tunnelID).
Order("user_tunnel.id ASC").
Limit(1).
Take(&rec).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return &model.UserTunnelLimiterInfo{}, nil
}
return nil, err
}
info := &model.UserTunnelLimiterInfo{UserTunnelID: rec.UserTunnelID}
if rec.LimiterID.Valid && rec.LimiterID.Int64 > 0 {
v := rec.LimiterID.Int64
info.LimiterID = &v
s := int(rec.Speed.Int64)
info.Speed = &s
}
return info, nil
}
func (r *Repository) ListUserTunnelIDs(userID, tunnelID int64) ([]int64, error) {
if r == nil || r.db == nil {
return nil, errors.New("repository not initialized")
}
var ids []int64
err := r.db.Model(&model.UserTunnel{}).
Where("user_id = ? AND tunnel_id = ?", userID, tunnelID).
Order("id ASC").Pluck("id", &ids).Error
if err != nil {
return nil, err
}
return ids, nil
}
func (r *Repository) ListUserTunnelIDsByUser(userID int64) ([]int64, error) {
if r == nil || r.db == nil {
return nil, errors.New("repository not initialized")
}
var ids []int64
err := r.db.Model(&model.UserTunnel{}).
Where("user_id = ?", userID).
Order("id ASC").Pluck("id", &ids).Error
if err != nil {
return nil, err
}
return ids, nil
}
func (r *Repository) GetTunnelName(tunnelID int64) (string, error) {
if r == nil || r.db == nil {
return "", errors.New("repository not initialized")
}
var name string
err := r.db.Model(&model.Tunnel{}).Where("id = ?", tunnelID).Pluck("name", &name).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return "", nil
}
return "", err
}
return name, nil
}
func (r *Repository) ListChainNodesForTunnel(tunnelID int64) ([]model.ChainNodeRecord, error) {
if r == nil || r.db == nil {
return nil, errors.New("repository not initialized")
}
type row struct {
ChainType string
Inx sql.NullInt64
NodeID int64
Port sql.NullInt64
Name sql.NullString
Protocol sql.NullString
Strategy sql.NullString
}
var rows []row
err := r.db.Model(&model.ChainTunnel{}).
Select("chain_tunnel.chain_type, chain_tunnel.inx, chain_tunnel.node_id, chain_tunnel.port, node.name, chain_tunnel.protocol, chain_tunnel.strategy").
Joins("LEFT JOIN node ON node.id = chain_tunnel.node_id").
Where("chain_tunnel.tunnel_id = ?", tunnelID).
Order("chain_tunnel.chain_type ASC, chain_tunnel.inx ASC, chain_tunnel.id ASC").
Find(&rows).Error
if err != nil {
return nil, err
}
result := make([]model.ChainNodeRecord, 0, len(rows))
for _, row := range rows {
chainType := 0
if v := strings.TrimSpace(row.ChainType); v != "" {
if parsed, parseErr := strconv.Atoi(v); parseErr == nil {
chainType = parsed
}
}
inx := int64(0)
if row.Inx.Valid {
inx = row.Inx.Int64
}
port := 0
if row.Port.Valid {
port = int(row.Port.Int64)
}
item := model.ChainNodeRecord{
ChainType: chainType,
Inx: inx,
NodeID: row.NodeID,
Port: port,
}
if strings.TrimSpace(row.Name.String) == "" {
item.NodeName = fmt.Sprintf("node_%d", row.NodeID)
} else {
item.NodeName = row.Name.String
}
if strings.TrimSpace(row.Protocol.String) == "" {
item.Protocol = "tls"
} else {
item.Protocol = row.Protocol.String
}
if strings.TrimSpace(row.Strategy.String) == "" {
item.Strategy = "round"
} else {
item.Strategy = row.Strategy.String
}
result = append(result, item)
}
return result, nil
}
@@ -0,0 +1,269 @@
package repo
import (
"database/sql"
"errors"
"go-backend/internal/store/model"
"gorm.io/gorm"
)
// RemoteNodeRow holds the columns fetched for a remote node listing.
type RemoteNodeRow struct {
ID int64
Name string
RemoteURL sql.NullString
RemoteToken sql.NullString
RemoteConfig sql.NullString
}
// NodeBasicInfo holds name, server_ip, and status for a node.
type NodeBasicInfo struct {
Name string
ServerIP string
Status int
}
// FederationBindingRow holds the columns for an active federation tunnel binding.
type FederationBindingRow struct {
ID int64
TunnelID int64
TunnelName string
ChainType int
HopInx int
AllocatedPort int
ResourceKey string
RemoteBindingID string
UpdatedTime int64
}
// ListRemoteNodes returns all nodes with is_remote=1, ordered by id desc.
func (r *Repository) ListRemoteNodes() ([]RemoteNodeRow, error) {
if r == nil || r.db == nil {
return nil, errors.New("repository not initialized")
}
var result []RemoteNodeRow
err := r.db.Model(&model.Node{}).
Select("id, name, remote_url, remote_token, remote_config").
Where("is_remote = 1").
Order("id DESC").
Find(&result).Error
if err != nil {
return nil, err
}
if result == nil {
result = make([]RemoteNodeRow, 0)
}
return result, nil
}
// UpdateNodeRemoteConfig sets the remote_config JSON for a given node.
func (r *Repository) UpdateNodeRemoteConfig(nodeID int64, configJSON string) error {
if r == nil || r.db == nil {
return errors.New("repository not initialized")
}
return r.db.Model(&model.Node{}).Where("id = ?", nodeID).Update("remote_config", configJSON).Error
}
// ListActiveBindingsForNode returns active federation tunnel bindings for a node.
func (r *Repository) ListActiveBindingsForNode(nodeID int64) ([]FederationBindingRow, error) {
if r == nil || r.db == nil {
return nil, errors.New("repository not initialized")
}
var result []FederationBindingRow
err := r.db.Model(&model.FederationTunnelBinding{}).
Select("federation_tunnel_binding.id, federation_tunnel_binding.tunnel_id, COALESCE(tunnel.name, '') AS tunnel_name, federation_tunnel_binding.chain_type, federation_tunnel_binding.hop_inx, federation_tunnel_binding.allocated_port, federation_tunnel_binding.resource_key, federation_tunnel_binding.remote_binding_id, federation_tunnel_binding.updated_time").
Joins("LEFT JOIN tunnel ON tunnel.id = federation_tunnel_binding.tunnel_id").
Where("federation_tunnel_binding.node_id = ? AND federation_tunnel_binding.status = 1", nodeID).
Order("federation_tunnel_binding.allocated_port ASC, federation_tunnel_binding.id ASC").
Find(&result).Error
if err != nil {
return nil, err
}
if result == nil {
result = make([]FederationBindingRow, 0)
}
return result, nil
}
// GetNodeBasicInfo returns the name, server_ip, and status for a given node.
func (r *Repository) GetNodeBasicInfo(nodeID int64) (*NodeBasicInfo, error) {
if r == nil || r.db == nil {
return nil, errors.New("repository not initialized")
}
var n model.Node
err := r.db.Select("name", "server_ip", "status").Where("id = ?", nodeID).First(&n).Error
if err != nil {
return nil, err
}
return &NodeBasicInfo{Name: n.Name, ServerIP: n.ServerIP, Status: n.Status}, nil
}
// CreateFederationTunnel creates a tunnel and chain_tunnel entry in a transaction,
// returning the new tunnel ID.
func (r *Repository) CreateFederationTunnel(name string, tunnelType int, protocol string, now int64, nodeID int64, remotePort int) (int64, error) {
if r == nil || r.db == nil {
return 0, errors.New("repository not initialized")
}
tunnel := model.Tunnel{
Name: name,
Type: tunnelType,
Protocol: protocol,
Flow: 0,
CreatedTime: now,
UpdatedTime: now,
Status: 1,
InIP: sql.NullString{String: "", Valid: false},
}
err := r.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&tunnel).Error; err != nil {
return err
}
ct := model.ChainTunnel{
TunnelID: tunnel.ID,
ChainType: "1",
NodeID: nodeID,
Port: sql.NullInt64{Int64: int64(remotePort), Valid: true},
Strategy: sql.NullString{String: "fifo", Valid: true},
Inx: sql.NullInt64{Int64: 0, Valid: true},
Protocol: sql.NullString{String: protocol, Valid: true},
}
if err := tx.Create(&ct).Error; err != nil {
return err
}
return nil
})
if err != nil {
return 0, err
}
return tunnel.ID, nil
}
// ListUsedPortsOnNode returns all ports in use on a given node from chain_tunnel and forward_port tables.
func (r *Repository) ListUsedPortsOnNode(nodeID int64) ([]int, error) {
if r == nil || r.db == nil {
return nil, errors.New("repository not initialized")
}
used := make(map[int]struct{})
var chainPorts []int
err := r.db.Model(&model.ChainTunnel{}).
Where("node_id = ? AND port > 0", nodeID).
Pluck("port", &chainPorts).Error
if err != nil {
return nil, err
}
for _, p := range chainPorts {
if p > 0 {
used[p] = struct{}{}
}
}
var forwardPorts []int
err = r.db.Model(&model.ForwardPort{}).
Where("node_id = ? AND port > 0", nodeID).
Pluck("port", &forwardPorts).Error
if err != nil {
return nil, err
}
for _, p := range forwardPorts {
if p > 0 {
used[p] = struct{}{}
}
}
result := make([]int, 0, len(used))
for p := range used {
result = append(result, p)
}
return result, nil
}
// ListTunnelIDsByNamePrefix returns all tunnel IDs whose name starts with the given prefix.
func (r *Repository) ListTunnelIDsByNamePrefix(prefix string) ([]int64, error) {
if r == nil || r.db == nil {
return nil, errors.New("repository not initialized")
}
var ids []int64
err := r.db.Model(&model.Tunnel{}).
Where("name LIKE ?", prefix+"%").
Order("id ASC").
Pluck("id", &ids).Error
if err != nil {
return nil, err
}
if ids == nil {
ids = make([]int64, 0)
}
return ids, nil
}
// NextIndex returns COALESCE(MAX(inx), -1) + 1 for the given table.
func (r *Repository) NextIndex(table string) int {
if r == nil || r.db == nil {
return 0
}
var modelRef interface{}
switch table {
case "node":
modelRef = &model.Node{}
case "tunnel":
modelRef = &model.Tunnel{}
case "forward":
modelRef = &model.Forward{}
default:
return 0
}
type inxRow struct {
Inx int
}
var row inxRow
err := r.db.Model(modelRef).
Select("inx").
Order("inx DESC").
Limit(1).
Take(&row).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return 0
}
if err != nil {
return 0
}
if row.Inx < 0 {
return 0
}
return row.Inx + 1
}
// CreateRemoteNode inserts a new remote node.
func (r *Repository) CreateRemoteNode(name, secret, serverIP, portRange string, now int64, status int, inx int, remoteURL, remoteToken, remoteConfigJSON string) error {
if r == nil || r.db == nil {
return errors.New("repository not initialized")
}
node := model.Node{
Name: name,
Secret: secret,
ServerIP: serverIP,
ServerIPV4: sql.NullString{},
ServerIPV6: sql.NullString{},
Port: portRange,
InterfaceName: sql.NullString{},
Version: sql.NullString{},
HTTP: 0,
TLS: 0,
Socks: 0,
CreatedTime: now,
UpdatedTime: sql.NullInt64{Int64: now, Valid: true},
Status: status,
TCPListenAddr: "[::]",
UDPListenAddr: "[::]",
Inx: inx,
IsRemote: 1,
RemoteURL: sql.NullString{String: remoteURL, Valid: remoteURL != ""},
RemoteToken: sql.NullString{String: remoteToken, Valid: remoteToken != ""},
RemoteConfig: sql.NullString{String: remoteConfigJSON, Valid: remoteConfigJSON != ""},
}
return r.db.Create(&node).Error
}
@@ -0,0 +1,171 @@
package repo
import (
"errors"
"strings"
"gorm.io/gorm"
"go-backend/internal/store/model"
)
func (r *Repository) UpdateForwardStatus(forwardID int64, status int, now int64) error {
if r == nil || r.db == nil {
return errors.New("repository not initialized")
}
return r.db.Model(&model.Forward{}).Where("id = ?", forwardID).Updates(map[string]interface{}{
"status": status, "updated_time": now,
}).Error
}
func (r *Repository) ListActiveForwardsByUser(userID int64) ([]model.ForwardRecord, error) {
if r == nil || r.db == nil {
return nil, errors.New("repository not initialized")
}
var forwards []model.Forward
err := r.db.Where("user_id = ? AND status = 1", userID).Order("id ASC").Find(&forwards).Error
if err != nil {
return nil, err
}
rows := make([]model.ForwardRecord, 0, len(forwards))
for _, f := range forwards {
rows = append(rows, model.ForwardRecord{
ID: f.ID,
UserID: f.UserID,
UserName: f.UserName,
Name: f.Name,
TunnelID: f.TunnelID,
RemoteAddr: f.RemoteAddr,
Strategy: f.Strategy,
Status: f.Status,
})
}
for i := range rows {
if strings.TrimSpace(rows[i].Strategy) == "" {
rows[i].Strategy = "fifo"
}
}
return rows, nil
}
func (r *Repository) ListActiveForwardsByUserTunnel(userID, tunnelID int64) ([]model.ForwardRecord, error) {
if r == nil || r.db == nil {
return nil, errors.New("repository not initialized")
}
var forwards []model.Forward
err := r.db.Where("user_id = ? AND tunnel_id = ? AND status = 1", userID, tunnelID).Order("id ASC").Find(&forwards).Error
if err != nil {
return nil, err
}
rows := make([]model.ForwardRecord, 0, len(forwards))
for _, f := range forwards {
rows = append(rows, model.ForwardRecord{
ID: f.ID,
UserID: f.UserID,
UserName: f.UserName,
Name: f.Name,
TunnelID: f.TunnelID,
RemoteAddr: f.RemoteAddr,
Strategy: f.Strategy,
Status: f.Status,
})
}
for i := range rows {
if strings.TrimSpace(rows[i].Strategy) == "" {
rows[i].Strategy = "fifo"
}
}
return rows, nil
}
func (r *Repository) GetForwardRecord(forwardID int64) (*model.ForwardRecord, error) {
if r == nil || r.db == nil {
return nil, errors.New("repository not initialized")
}
var f model.Forward
err := r.db.Where("id = ?", forwardID).First(&f).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
fr := model.ForwardRecord{
ID: f.ID,
UserID: f.UserID,
UserName: f.UserName,
Name: f.Name,
TunnelID: f.TunnelID,
RemoteAddr: f.RemoteAddr,
Strategy: f.Strategy,
Status: f.Status,
}
if strings.TrimSpace(fr.Strategy) == "" {
fr.Strategy = "fifo"
}
return &fr, nil
}
func (r *Repository) GetTunnelRecord(tunnelID int64) (*model.TunnelRecord, error) {
if r == nil || r.db == nil {
return nil, errors.New("repository not initialized")
}
var t model.Tunnel
err := r.db.Where("id = ?", tunnelID).First(&t).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
tr := model.TunnelRecord{
ID: t.ID,
Type: t.Type,
Status: t.Status,
Flow: t.Flow,
TrafficRatio: t.TrafficRatio,
}
if tr.Flow <= 0 {
tr.Flow = 1
}
if tr.TrafficRatio <= 0 {
tr.TrafficRatio = 1
}
return &tr, nil
}
func (r *Repository) TunnelExists(tunnelID int64) (bool, error) {
if r == nil || r.db == nil {
return false, errors.New("repository not initialized")
}
var count int64
err := r.db.Model(&model.Tunnel{}).Where("id = ?", tunnelID).Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
func (r *Repository) ForwardExists(forwardID int64) (bool, error) {
if r == nil || r.db == nil {
return false, errors.New("repository not initialized")
}
var count int64
err := r.db.Model(&model.Forward{}).Where("id = ?", forwardID).Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
func (r *Repository) SpeedLimitExists(id int64) (bool, error) {
if r == nil || r.db == nil {
return false, errors.New("repository not initialized")
}
var count int64
err := r.db.Model(&model.SpeedLimit{}).Where("id = ?", id).Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
@@ -0,0 +1,69 @@
package repo
import (
"errors"
"go-backend/internal/store/model"
)
// ─── Semantic Group Queries (replacing QueryInt64List/QueryPairs passthrough) ─
// ListUserIDsByUserGroup returns all user IDs belonging to a user group.
func (r *Repository) ListUserIDsByUserGroup(userGroupID int64) ([]int64, error) {
if r == nil || r.db == nil {
return nil, errors.New("repository not initialized")
}
var ids []int64
err := r.db.Model(&model.UserGroupUser{}).
Where("user_group_id = ?", userGroupID).
Pluck("user_id", &ids).Error
return ids, err
}
// ListTunnelIDsByTunnelGroup returns all tunnel IDs belonging to a tunnel group.
func (r *Repository) ListTunnelIDsByTunnelGroup(tunnelGroupID int64) ([]int64, error) {
if r == nil || r.db == nil {
return nil, errors.New("repository not initialized")
}
var ids []int64
err := r.db.Model(&model.TunnelGroupTunnel{}).
Where("tunnel_group_id = ?", tunnelGroupID).
Pluck("tunnel_id", &ids).Error
return ids, err
}
// ListGroupPermissionPairsByUserGroup returns [userGroupID, tunnelGroupID] pairs
// for all group permissions associated with a user group.
func (r *Repository) ListGroupPermissionPairsByUserGroup(userGroupID int64) ([][2]int64, error) {
if r == nil || r.db == nil {
return nil, errors.New("repository not initialized")
}
var perms []model.GroupPermission
err := r.db.Where("user_group_id = ?", userGroupID).Find(&perms).Error
if err != nil {
return nil, err
}
result := make([][2]int64, len(perms))
for i, p := range perms {
result[i] = [2]int64{p.UserGroupID, p.TunnelGroupID}
}
return result, err
}
// ListGroupPermissionPairsByTunnelGroup returns [userGroupID, tunnelGroupID] pairs
// for all group permissions associated with a tunnel group.
func (r *Repository) ListGroupPermissionPairsByTunnelGroup(tunnelGroupID int64) ([][2]int64, error) {
if r == nil || r.db == nil {
return nil, errors.New("repository not initialized")
}
var perms []model.GroupPermission
err := r.db.Where("tunnel_group_id = ?", tunnelGroupID).Find(&perms).Error
if err != nil {
return nil, err
}
result := make([][2]int64, len(perms))
for i, p := range perms {
result[i] = [2]int64{p.UserGroupID, p.TunnelGroupID}
}
return result, err
}
@@ -1,35 +1,38 @@
package sqlite
package repo
import (
"database/sql"
"errors"
"testing"
"go-backend/internal/store"
_ "modernc.org/sqlite"
gsqlite "github.com/glebarez/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func TestMigrateSchemaRunsPostgresIDRepairEvenAtCurrentVersion(t *testing.T) {
raw, err := sql.Open("sqlite", ":memory:")
db, err := gorm.Open(gsqlite.Open(":memory:"), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
t.Cleanup(func() {
_ = raw.Close()
sqlDB, _ := db.DB()
if sqlDB != nil {
_ = sqlDB.Close()
}
})
db := store.Wrap(raw, store.DialectPostgres)
if _, err := db.Exec(`CREATE TABLE schema_version (version INTEGER NOT NULL DEFAULT 0)`); err != nil {
if err := db.Exec(`CREATE TABLE schema_version (version INTEGER NOT NULL DEFAULT 0)`).Error; err != nil {
t.Fatalf("create schema_version: %v", err)
}
if _, err := db.Exec(`INSERT INTO schema_version(version) VALUES(?)`, currentSchemaVersion); err != nil {
if err := db.Exec(`INSERT INTO schema_version(version) VALUES(?)`, currentSchemaVersion).Error; err != nil {
t.Fatalf("seed schema_version: %v", err)
}
called := 0
original := ensurePostgresIDDefaultsFn
ensurePostgresIDDefaultsFn = func(db *store.DB) error {
ensurePostgresIDDefaultsFn = func(db *gorm.DB) error {
called++
return nil
}
@@ -46,25 +49,29 @@ func TestMigrateSchemaRunsPostgresIDRepairEvenAtCurrentVersion(t *testing.T) {
}
func TestMigrateSchemaReturnsPostgresIDRepairError(t *testing.T) {
raw, err := sql.Open("sqlite", ":memory:")
db, err := gorm.Open(gsqlite.Open(":memory:"), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
t.Cleanup(func() {
_ = raw.Close()
sqlDB, _ := db.DB()
if sqlDB != nil {
_ = sqlDB.Close()
}
})
db := store.Wrap(raw, store.DialectPostgres)
if _, err := db.Exec(`CREATE TABLE schema_version (version INTEGER NOT NULL DEFAULT 0)`); err != nil {
if err := db.Exec(`CREATE TABLE schema_version (version INTEGER NOT NULL DEFAULT 0)`).Error; err != nil {
t.Fatalf("create schema_version: %v", err)
}
if _, err := db.Exec(`INSERT INTO schema_version(version) VALUES(?)`, currentSchemaVersion); err != nil {
if err := db.Exec(`INSERT INTO schema_version(version) VALUES(?)`, currentSchemaVersion).Error; err != nil {
t.Fatalf("seed schema_version: %v", err)
}
wantErr := errors.New("repair failed")
original := ensurePostgresIDDefaultsFn
ensurePostgresIDDefaultsFn = func(db *store.DB) error {
ensurePostgresIDDefaultsFn = func(db *gorm.DB) error {
return wantErr
}
t.Cleanup(func() {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,5 +0,0 @@
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);
@@ -1,254 +0,0 @@
-- 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,
is_remote INTEGER DEFAULT 0,
remote_url TEXT,
remote_token TEXT,
remote_config TEXT
);
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,
ip_preference VARCHAR(10) NOT NULL DEFAULT ''
);
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 UNIQUE INDEX IF NOT EXISTS idx_user_tunnel_unique ON user_tunnel(user_id, 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
);
CREATE TABLE IF NOT EXISTS peer_share (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
node_id INTEGER NOT NULL,
token TEXT NOT NULL UNIQUE,
max_bandwidth INTEGER DEFAULT 0,
expiry_time INTEGER DEFAULT 0,
port_range_start INTEGER DEFAULT 0,
port_range_end INTEGER DEFAULT 0,
current_flow INTEGER DEFAULT 0,
is_active INTEGER DEFAULT 1,
created_time INTEGER NOT NULL,
updated_time INTEGER NOT NULL,
allowed_domains TEXT DEFAULT '',
allowed_ips TEXT DEFAULT ''
);
CREATE TABLE IF NOT EXISTS peer_share_runtime (
id INTEGER PRIMARY KEY AUTOINCREMENT,
share_id INTEGER NOT NULL,
node_id INTEGER NOT NULL,
reservation_id TEXT NOT NULL UNIQUE,
resource_key TEXT NOT NULL UNIQUE,
binding_id TEXT NOT NULL DEFAULT '',
role TEXT NOT NULL DEFAULT '',
chain_name TEXT NOT NULL DEFAULT '',
service_name TEXT NOT NULL DEFAULT '',
protocol TEXT NOT NULL DEFAULT 'tls',
strategy TEXT NOT NULL DEFAULT 'round',
port INTEGER NOT NULL DEFAULT 0,
target TEXT NOT NULL DEFAULT '',
applied INTEGER NOT NULL DEFAULT 0,
status INTEGER NOT NULL DEFAULT 1,
created_time INTEGER NOT NULL,
updated_time INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_peer_share_runtime_share_node_status ON peer_share_runtime(share_id, node_id, status);
CREATE INDEX IF NOT EXISTS idx_peer_share_runtime_binding_id ON peer_share_runtime(binding_id);
CREATE TABLE IF NOT EXISTS federation_tunnel_binding (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tunnel_id INTEGER NOT NULL,
node_id INTEGER NOT NULL,
chain_type INTEGER NOT NULL,
hop_inx INTEGER NOT NULL DEFAULT 0,
remote_url TEXT NOT NULL,
resource_key TEXT NOT NULL UNIQUE,
remote_binding_id TEXT NOT NULL,
allocated_port INTEGER NOT NULL,
status INTEGER NOT NULL DEFAULT 1,
created_time INTEGER NOT NULL,
updated_time INTEGER NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_federation_tunnel_binding_unique ON federation_tunnel_binding(tunnel_id, node_id, chain_type, hop_inx);
CREATE INDEX IF NOT EXISTS idx_federation_tunnel_binding_tunnel ON federation_tunnel_binding(tunnel_id, status);
CREATE TABLE IF NOT EXISTS announcement (
id INTEGER PRIMARY KEY AUTOINCREMENT,
content TEXT NOT NULL,
enabled INTEGER NOT NULL DEFAULT 1,
created_time INTEGER NOT NULL,
updated_time INTEGER
);
+3 -3
View File
@@ -15,7 +15,7 @@ import (
"go-backend/internal/auth"
"go-backend/internal/security"
"go-backend/internal/store/sqlite"
"go-backend/internal/store/repo"
)
type encryptedMessage struct {
@@ -68,7 +68,7 @@ type CommandResult struct {
}
type Server struct {
repo *sqlite.Repository
repo *repo.Repository
jwtSecret string
upgrader websocket.Upgrader
@@ -79,7 +79,7 @@ type Server struct {
pending map[string]pendingRequest
}
func NewServer(repo *sqlite.Repository, jwtSecret string) *Server {
func NewServer(repo *repo.Repository, jwtSecret string) *Server {
return &Server{
repo: repo,
jwtSecret: jwtSecret,