mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-28 07:36:38 +08:00
fix(backend): sync user tunnel status and relax forward speedId permission check
- Return actual user_tunnel.status in admin permission list instead of hardcoded 1 - Allow non-admin users to update forwards when keeping the same speedId selection - Add contract tests for user tunnel status mapping and forward permission edge case Entire-Checkpoint: deb90fb942ee
This commit is contained in:
@@ -571,7 +571,7 @@ func (h *Handler) userTunnelList(w http.ResponseWriter, r *http.Request) {
|
|||||||
"userId": t.UserID,
|
"userId": t.UserID,
|
||||||
"tunnelId": t.TunnelID,
|
"tunnelId": t.TunnelID,
|
||||||
"tunnelName": t.TunnelName,
|
"tunnelName": t.TunnelName,
|
||||||
"status": 1,
|
"status": t.Status,
|
||||||
"flow": t.Flow,
|
"flow": t.Flow,
|
||||||
"num": t.Num,
|
"num": t.Num,
|
||||||
"expTime": t.ExpTime,
|
"expTime": t.ExpTime,
|
||||||
|
|||||||
@@ -1279,13 +1279,13 @@ func (h *Handler) forwardUpdate(w http.ResponseWriter, r *http.Request) {
|
|||||||
if strategy == "" {
|
if strategy == "" {
|
||||||
strategy = forward.Strategy
|
strategy = forward.Strategy
|
||||||
}
|
}
|
||||||
if actorRole != 0 {
|
rawSpeedID, hasSpeedID := req["speedId"]
|
||||||
if speedIDVal, ok := req["speedId"]; ok && speedIDVal != nil {
|
requestedSpeedID := asAnyToInt64Ptr(rawSpeedID)
|
||||||
|
if actorRole != 0 && hasSpeedID && requestedSpeedID != nil && !sameSpeedLimitSelection(forward.SpeedID, requestedSpeedID) {
|
||||||
response.WriteJSON(w, response.Err(-1, "普通用户无法修改限速规则"))
|
response.WriteJSON(w, response.Err(-1, "普通用户无法修改限速规则"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
speedID := requestedSpeedID
|
||||||
speedID := asAnyToInt64Ptr(req["speedId"])
|
|
||||||
speedID, err = h.normalizeSpeedLimitReference(speedID)
|
speedID, err = h.normalizeSpeedLimitReference(speedID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||||
@@ -3380,6 +3380,14 @@ func (h *Handler) normalizeSpeedLimitReference(speedID *int64) (*int64, error) {
|
|||||||
return speedID, nil
|
return speedID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sameSpeedLimitSelection(current sql.NullInt64, requested *int64) bool {
|
||||||
|
if requested == nil {
|
||||||
|
return !current.Valid
|
||||||
|
}
|
||||||
|
|
||||||
|
return current.Valid && current.Int64 == *requested
|
||||||
|
}
|
||||||
|
|
||||||
func asAnySlice(v interface{}) []interface{} {
|
func asAnySlice(v interface{}) []interface{} {
|
||||||
if v == nil {
|
if v == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -573,6 +573,7 @@ type UserTunnelDetail struct {
|
|||||||
UserID int64
|
UserID int64
|
||||||
TunnelID int64
|
TunnelID int64
|
||||||
TunnelName string
|
TunnelName string
|
||||||
|
Status int
|
||||||
TunnelFlow int
|
TunnelFlow int
|
||||||
Flow int64
|
Flow int64
|
||||||
InFlow int64
|
InFlow int64
|
||||||
|
|||||||
@@ -447,7 +447,7 @@ func (r *Repository) GetUserPackageTunnels(userID int64) ([]model.UserTunnelDeta
|
|||||||
}
|
}
|
||||||
var items []model.UserTunnelDetail
|
var items []model.UserTunnelDetail
|
||||||
err := r.db.Model(&model.UserTunnel{}).
|
err := r.db.Model(&model.UserTunnel{}).
|
||||||
Select("user_tunnel.id, user_tunnel.user_id, user_tunnel.tunnel_id, tunnel.name AS tunnel_name, tunnel.flow AS tunnel_flow, user_tunnel.flow, user_tunnel.in_flow, user_tunnel.out_flow, user_tunnel.num, user_tunnel.flow_reset_time, user_tunnel.exp_time, user_tunnel.speed_id, speed_limit.name AS speed_limit, speed_limit.speed").
|
Select("user_tunnel.id, user_tunnel.user_id, user_tunnel.tunnel_id, tunnel.name AS tunnel_name, user_tunnel.status, tunnel.flow AS tunnel_flow, user_tunnel.flow, user_tunnel.in_flow, user_tunnel.out_flow, user_tunnel.num, user_tunnel.flow_reset_time, user_tunnel.exp_time, user_tunnel.speed_id, speed_limit.name AS speed_limit, speed_limit.speed").
|
||||||
Joins("LEFT JOIN tunnel ON tunnel.id = user_tunnel.tunnel_id").
|
Joins("LEFT JOIN tunnel ON tunnel.id = user_tunnel.tunnel_id").
|
||||||
Joins("LEFT JOIN speed_limit ON speed_limit.id = user_tunnel.speed_id").
|
Joins("LEFT JOIN speed_limit ON speed_limit.id = user_tunnel.speed_id").
|
||||||
Where("user_tunnel.user_id = ?", userID).
|
Where("user_tunnel.user_id = ?", userID).
|
||||||
|
|||||||
@@ -1310,6 +1310,30 @@ func TestNonAdminCannotSetSpeedIdOrPort(t *testing.T) {
|
|||||||
assertCode(t, res, 0)
|
assertCode(t, res, 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("non-admin can update when request keeps existing speedId", func(t *testing.T) {
|
||||||
|
if err := repo.DB().Exec(`UPDATE forward SET speed_id = ? WHERE id = ?`, speedID, forwardID).Error; err != nil {
|
||||||
|
t.Fatalf("assign forward speed limit: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePayload := map[string]interface{}{
|
||||||
|
"id": forwardID,
|
||||||
|
"name": "perm-forward-keep-speed",
|
||||||
|
"tunnelId": tunnelID,
|
||||||
|
"remoteAddr": "9.10.11.12:443",
|
||||||
|
"speedId": speedID,
|
||||||
|
}
|
||||||
|
updateBody, err := json.Marshal(updatePayload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal update payload: %v", err)
|
||||||
|
}
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/forward/update", bytes.NewReader(updateBody))
|
||||||
|
req.Header.Set("Authorization", userToken)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
res := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(res, req)
|
||||||
|
assertCode(t, res, 0)
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("non-admin can create with speedId null and inPort 0", func(t *testing.T) {
|
t.Run("non-admin can create with speedId null and inPort 0", func(t *testing.T) {
|
||||||
createPayload := map[string]interface{}{
|
createPayload := map[string]interface{}{
|
||||||
"name": "perm-forward-null-values",
|
"name": "perm-forward-null-values",
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package contract_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go-backend/internal/auth"
|
||||||
|
"go-backend/internal/http/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUserTunnelListReturnsStoredStatusContract(t *testing.T) {
|
||||||
|
secret := "contract-jwt-secret"
|
||||||
|
router, repo := setupContractRouter(t, secret)
|
||||||
|
now := time.Now().UnixMilli()
|
||||||
|
|
||||||
|
adminToken, err := auth.GenerateToken(1, "admin_user", 0, secret)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("generate admin token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := repo.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(201, 'user_tunnel_status_user', 'pwd', 1, 2727251700000, 99999, 0, 0, 1, 99999, ?, ?, 1)
|
||||||
|
`, now, now).Error; err != nil {
|
||||||
|
t.Fatalf("insert user: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := repo.DB().Exec(`
|
||||||
|
INSERT INTO tunnel(id, name, traffic_ratio, type, protocol, flow, created_time, updated_time, status, in_ip, inx)
|
||||||
|
VALUES(301, 'user-tunnel-status-enabled', 1.0, 1, 'tls', 1, ?, ?, 1, NULL, 0)
|
||||||
|
`, now, now).Error; err != nil {
|
||||||
|
t.Fatalf("insert tunnel enabled: %v", err)
|
||||||
|
}
|
||||||
|
if err := repo.DB().Exec(`
|
||||||
|
INSERT INTO tunnel(id, name, traffic_ratio, type, protocol, flow, created_time, updated_time, status, in_ip, inx)
|
||||||
|
VALUES(302, 'user-tunnel-status-disabled', 1.0, 1, 'tls', 1, ?, ?, 1, NULL, 1)
|
||||||
|
`, now, now).Error; err != nil {
|
||||||
|
t.Fatalf("insert tunnel disabled: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := repo.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(401, 201, 301, NULL, 10, 500, 0, 0, 1, 2727251700000, 1)
|
||||||
|
`).Error; err != nil {
|
||||||
|
t.Fatalf("insert enabled user_tunnel: %v", err)
|
||||||
|
}
|
||||||
|
if err := repo.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(402, 201, 302, NULL, 10, 500, 0, 0, 1, 2727251700000, 0)
|
||||||
|
`).Error; err != nil {
|
||||||
|
t.Fatalf("insert disabled user_tunnel: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := bytes.NewBufferString(`{"userId":201}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/tunnel/user/list", body)
|
||||||
|
req.Header.Set("Authorization", adminToken)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
res := httptest.NewRecorder()
|
||||||
|
|
||||||
|
router.ServeHTTP(res, req)
|
||||||
|
|
||||||
|
var out response.R
|
||||||
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
||||||
|
t.Fatalf("decode response: %v", err)
|
||||||
|
}
|
||||||
|
if out.Code != 0 {
|
||||||
|
t.Fatalf("expected code 0, got %d (%s)", out.Code, out.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
items, ok := out.Data.([]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected array data, got %T", out.Data)
|
||||||
|
}
|
||||||
|
if len(items) != 2 {
|
||||||
|
t.Fatalf("expected 2 items, got %d", len(items))
|
||||||
|
}
|
||||||
|
|
||||||
|
statusByTunnelID := make(map[int64]int, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
obj, ok := item.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected object item, got %T", item)
|
||||||
|
}
|
||||||
|
tunnelID, ok := obj["tunnelId"].(float64)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected tunnelId to be float64, got %T", obj["tunnelId"])
|
||||||
|
}
|
||||||
|
status, ok := obj["status"].(float64)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected status to be float64, got %T", obj["status"])
|
||||||
|
}
|
||||||
|
statusByTunnelID[int64(tunnelID)] = int(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if statusByTunnelID[301] != 1 {
|
||||||
|
t.Fatalf("expected enabled tunnel status 1, got %d", statusByTunnelID[301])
|
||||||
|
}
|
||||||
|
if statusByTunnelID[302] != 0 {
|
||||||
|
t.Fatalf("expected disabled tunnel status 0, got %d", statusByTunnelID[302])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# 018 User Tunnel Disable Status Sync
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [x] Inspect the user tunnel permission edit flow and identify why disabling an assigned tunnel appears ineffective.
|
||||||
|
- [x] Return the real `user_tunnel.status` value from the admin permission list API instead of a hardcoded enabled state.
|
||||||
|
- [x] Add contract coverage for the user tunnel permission list status mapping and run focused backend verification.
|
||||||
|
|
||||||
|
## Test Record
|
||||||
|
|
||||||
|
- Command: `cd go-backend && go test ./tests/contract/...`
|
||||||
|
- Result: passed.
|
||||||
Reference in New Issue
Block a user