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