mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-04 09:06:38 +08:00
fix(backend): improve captcha validation and forward service sync (#219)
## Summary - Add cloudflare site/secret key validation for captcha enabled check to prevent incomplete captcha config - Fix forward create to use UpdateService with tolerateExists for idempotent service sync - Introduce isAlreadyExistsMessage helper that correctly excludes "address already in use" errors from being tolerated - Add contract tests for forward toggle (pause/resume), address-in-use rollback, and captcha compatibility ## Test Plan - Contract tests added: `TestForwardCreateThenPauseResumeContract`, `TestForwardCreateRollbackWhenServiceDispatchReturnsAddressInUseContract`, `TestIsAlreadyExistsMessage` - Captcha login flow tests updated for cloudflare key validation
This commit is contained in:
@@ -311,7 +311,7 @@ func (h *Handler) sendNodeCommand(nodeID int64, commandType string, data interfa
|
|||||||
}
|
}
|
||||||
msg := strings.ToLower(strings.TrimSpace(err.Error()))
|
msg := strings.ToLower(strings.TrimSpace(err.Error()))
|
||||||
if tolerateExists {
|
if tolerateExists {
|
||||||
if strings.Contains(msg, "exists") || strings.Contains(msg, "already") || strings.Contains(msg, "已存在") {
|
if isAlreadyExistsMessage(msg) {
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -907,6 +907,17 @@ func isNotFoundError(err error) bool {
|
|||||||
return strings.Contains(msg, "not found") || strings.Contains(msg, "不存在")
|
return strings.Contains(msg, "not found") || strings.Contains(msg, "不存在")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isAlreadyExistsMessage(message string) bool {
|
||||||
|
msg := strings.ToLower(strings.TrimSpace(message))
|
||||||
|
if msg == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if strings.Contains(msg, "address already in use") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.Contains(msg, "already exists") || strings.Contains(msg, "已存在")
|
||||||
|
}
|
||||||
|
|
||||||
func buildForwardServiceConfigs(baseName string, forward *forwardRecord, tunnel *tunnelRecord, node *nodeRecord, port int, limiterID *int64, tunnelTLSProtocol bool) []map[string]interface{} {
|
func buildForwardServiceConfigs(baseName string, forward *forwardRecord, tunnel *tunnelRecord, node *nodeRecord, port int, limiterID *int64, tunnelTLSProtocol bool) []map[string]interface{} {
|
||||||
protocols := []string{"tcp", "udp"}
|
protocols := []string{"tcp", "udp"}
|
||||||
services := make([]map[string]interface{}, 0, 2)
|
services := make([]map[string]interface{}, 0, 2)
|
||||||
|
|||||||
@@ -53,3 +53,15 @@ func TestShouldTryLegacySingleService(t *testing.T) {
|
|||||||
t.Fatalf("DeleteService should not require legacy fallback")
|
t.Fatalf("DeleteService should not require legacy fallback")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIsAlreadyExistsMessage(t *testing.T) {
|
||||||
|
if !isAlreadyExistsMessage("service demo already exists") {
|
||||||
|
t.Fatalf("expected already exists message to be tolerated")
|
||||||
|
}
|
||||||
|
if !isAlreadyExistsMessage("服务已存在") {
|
||||||
|
t.Fatalf("expected Chinese already exists message to be tolerated")
|
||||||
|
}
|
||||||
|
if isAlreadyExistsMessage("listen tcp [::]:10001: bind: address already in use") {
|
||||||
|
t.Fatalf("address already in use must not be treated as already exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -989,10 +989,27 @@ func (h *Handler) captchaEnabled() (bool, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
if cfg == nil {
|
if cfg == nil || !strings.EqualFold(strings.TrimSpace(cfg.Value), "true") {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
return strings.EqualFold(cfg.Value, "true"), nil
|
|
||||||
|
siteCfg, err := h.repo.GetConfigByName("cloudflare_site_key")
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if siteCfg == nil || strings.TrimSpace(siteCfg.Value) == "" {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
secretCfg, err := h.repo.GetConfigByName("cloudflare_secret_key")
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if secretCfg == nil || strings.TrimSpace(secretCfg.Value) == "" {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) apiClientCaptchaBypassEnabled(r *http.Request) bool {
|
func (h *Handler) apiClientCaptchaBypassEnabled(r *http.Request) bool {
|
||||||
|
|||||||
@@ -1183,7 +1183,7 @@ func (h *Handler) forwardCreate(w http.ResponseWriter, r *http.Request) {
|
|||||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := h.syncForwardServices(createdForward, "AddService", false); err != nil {
|
if err := h.syncForwardServices(createdForward, "UpdateService", true); err != nil {
|
||||||
_ = h.deleteForwardByID(forwardID)
|
_ = h.deleteForwardByID(forwardID)
|
||||||
response.WriteJSON(w, response.ErrDefault(err.Error()))
|
response.WriteJSON(w, response.ErrDefault(err.Error()))
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -610,6 +610,96 @@ func TestForwardSpeedIDWriteAndClearContracts(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestForwardCreateThenPauseResumeContract(t *testing.T) {
|
||||||
|
secret := "contract-jwt-secret"
|
||||||
|
router, repo := setupContractRouter(t, secret)
|
||||||
|
|
||||||
|
adminToken, err := auth.GenerateToken(1, "admin_user", 0, secret)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("generate admin token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().UnixMilli()
|
||||||
|
if err := repo.DB().Exec(`
|
||||||
|
INSERT INTO tunnel(name, traffic_ratio, type, protocol, flow, created_time, updated_time, status, in_ip, inx)
|
||||||
|
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`, "forward-toggle-tunnel", 1.0, 1, "tls", 99999, now, now, 1, nil, 0).Error; err != nil {
|
||||||
|
t.Fatalf("insert tunnel: %v", err)
|
||||||
|
}
|
||||||
|
tunnelID := mustLastInsertID(t, repo, "forward-toggle-tunnel")
|
||||||
|
|
||||||
|
if err := repo.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)
|
||||||
|
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`, "forward-toggle-node", "forward-toggle-secret", "10.31.0.1", "10.31.0.1", "", "41000-41010", "", "v1", 1, 1, 1, now, now, 1, "[::]", "[::]", 0).Error; err != nil {
|
||||||
|
t.Fatalf("insert node: %v", err)
|
||||||
|
}
|
||||||
|
nodeID := mustLastInsertID(t, repo, "forward-toggle-node")
|
||||||
|
|
||||||
|
if err := repo.DB().Exec(`
|
||||||
|
INSERT INTO chain_tunnel(tunnel_id, chain_type, node_id, port, strategy, inx, protocol)
|
||||||
|
VALUES(?, 1, ?, 41001, 'round', 1, 'tls')
|
||||||
|
`, tunnelID, nodeID).Error; err != nil {
|
||||||
|
t.Fatalf("insert chain_tunnel: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
server := httptest.NewServer(router)
|
||||||
|
defer server.Close()
|
||||||
|
stopNode := startMockNodeSession(t, server.URL, "forward-toggle-secret")
|
||||||
|
defer stopNode()
|
||||||
|
|
||||||
|
createPayload := map[string]interface{}{
|
||||||
|
"name": "forward-toggle-target",
|
||||||
|
"tunnelId": tunnelID,
|
||||||
|
"remoteAddr": "1.1.1.1:443",
|
||||||
|
"strategy": "fifo",
|
||||||
|
}
|
||||||
|
createBody, err := json.Marshal(createPayload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal create payload: %v", err)
|
||||||
|
}
|
||||||
|
createReq := httptest.NewRequest(http.MethodPost, "/api/v1/forward/create", bytes.NewReader(createBody))
|
||||||
|
createReq.Header.Set("Authorization", adminToken)
|
||||||
|
createReq.Header.Set("Content-Type", "application/json")
|
||||||
|
createRes := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(createRes, createReq)
|
||||||
|
assertCode(t, createRes, 0)
|
||||||
|
|
||||||
|
forwardID := mustLastInsertID(t, repo, "forward-toggle-target")
|
||||||
|
|
||||||
|
pauseBody, err := json.Marshal(map[string]interface{}{"id": forwardID})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal pause payload: %v", err)
|
||||||
|
}
|
||||||
|
pauseReq := httptest.NewRequest(http.MethodPost, "/api/v1/forward/pause", bytes.NewReader(pauseBody))
|
||||||
|
pauseReq.Header.Set("Authorization", adminToken)
|
||||||
|
pauseReq.Header.Set("Content-Type", "application/json")
|
||||||
|
pauseRes := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(pauseRes, pauseReq)
|
||||||
|
assertCode(t, pauseRes, 0)
|
||||||
|
|
||||||
|
pausedStatus := mustQueryInt(t, repo, `SELECT status FROM forward WHERE id = ?`, forwardID)
|
||||||
|
if pausedStatus != 0 {
|
||||||
|
t.Fatalf("expected status=0 after pause, got %d", pausedStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
resumeBody, err := json.Marshal(map[string]interface{}{"id": forwardID})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal resume payload: %v", err)
|
||||||
|
}
|
||||||
|
resumeReq := httptest.NewRequest(http.MethodPost, "/api/v1/forward/resume", bytes.NewReader(resumeBody))
|
||||||
|
resumeReq.Header.Set("Authorization", adminToken)
|
||||||
|
resumeReq.Header.Set("Content-Type", "application/json")
|
||||||
|
resumeRes := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(resumeRes, resumeReq)
|
||||||
|
assertCode(t, resumeRes, 0)
|
||||||
|
|
||||||
|
resumedStatus := mustQueryInt(t, repo, `SELECT status FROM forward WHERE id = ?`, forwardID)
|
||||||
|
if resumedStatus != 1 {
|
||||||
|
t.Fatalf("expected status=1 after resume, got %d", resumedStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func jsonNumber(v int64) string {
|
func jsonNumber(v int64) string {
|
||||||
return strconv.FormatInt(v, 10)
|
return strconv.FormatInt(v, 10)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,6 +99,77 @@ func TestForwardCreateRollbackWhenLimiterDispatchFailsContract(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestForwardCreateRollbackWhenServiceDispatchReturnsAddressInUseContract(t *testing.T) {
|
||||||
|
secret := "contract-jwt-secret"
|
||||||
|
router, r := setupContractRouter(t, secret)
|
||||||
|
server := httptest.NewServer(router)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
adminToken, err := auth.GenerateToken(1, "admin_user", 0, secret)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("generate admin token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().UnixMilli()
|
||||||
|
if err := r.DB().Exec(`
|
||||||
|
INSERT INTO tunnel(name, traffic_ratio, type, protocol, flow, created_time, updated_time, status, in_ip, inx)
|
||||||
|
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`, "addr-in-use-tunnel", 1.0, 1, "tls", 99999, now, now, 1, nil, 0).Error; err != nil {
|
||||||
|
t.Fatalf("insert tunnel: %v", err)
|
||||||
|
}
|
||||||
|
tunnelID := mustLastInsertID(t, r, "addr-in-use-tunnel")
|
||||||
|
|
||||||
|
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)
|
||||||
|
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`, "addr-in-use-node", "addr-in-use-secret", "10.20.0.11", "10.20.0.11", "", "32100-32110", "", "v1", 1, 1, 1, now, now, 1, "[::]", "[::]", 0).Error; err != nil {
|
||||||
|
t.Fatalf("insert node: %v", err)
|
||||||
|
}
|
||||||
|
nodeID := mustLastInsertID(t, r, "addr-in-use-node")
|
||||||
|
|
||||||
|
if err := r.DB().Exec(`
|
||||||
|
INSERT INTO chain_tunnel(tunnel_id, chain_type, node_id, port, strategy, inx, protocol)
|
||||||
|
VALUES(?, 1, ?, 32101, 'round', 1, 'tls')
|
||||||
|
`, tunnelID, nodeID).Error; err != nil {
|
||||||
|
t.Fatalf("insert chain_tunnel: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stopNode := startMockNodeSessionWithCommandFailures(t, server.URL, "addr-in-use-secret", map[string]string{
|
||||||
|
"updateservice": "listen tcp [::]:32101: bind: address already in use",
|
||||||
|
"addservice": "listen tcp [::]:32101: bind: address already in use",
|
||||||
|
})
|
||||||
|
defer stopNode()
|
||||||
|
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"name": "addr-in-use-forward",
|
||||||
|
"tunnelId": tunnelID,
|
||||||
|
"remoteAddr": "1.1.1.1:443",
|
||||||
|
"strategy": "fifo",
|
||||||
|
}
|
||||||
|
body, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal payload: %v", err)
|
||||||
|
}
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/forward/create", bytes.NewReader(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 create failure on address-in-use service dispatch, got code=0")
|
||||||
|
}
|
||||||
|
|
||||||
|
forwardCount := mustQueryInt(t, r, `SELECT COUNT(1) FROM forward WHERE name = ?`, "addr-in-use-forward")
|
||||||
|
if forwardCount != 0 {
|
||||||
|
t.Fatalf("expected forward rollback delete on address-in-use failure, got count=%d", forwardCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBatchAssignRollbackWhenLimiterDispatchFailsContract(t *testing.T) {
|
func TestBatchAssignRollbackWhenLimiterDispatchFailsContract(t *testing.T) {
|
||||||
secret := "contract-jwt-secret"
|
secret := "contract-jwt-secret"
|
||||||
router, r := setupContractRouter(t, secret)
|
router, r := setupContractRouter(t, secret)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import (
|
|||||||
func TestCaptchaVerifyLoginContract(t *testing.T) {
|
func TestCaptchaVerifyLoginContract(t *testing.T) {
|
||||||
secret := "contract-jwt-secret"
|
secret := "contract-jwt-secret"
|
||||||
router, r := setupContractRouter(t, secret)
|
router, r := setupContractRouter(t, secret)
|
||||||
|
verifiedToken := ""
|
||||||
|
|
||||||
if err := r.DB().Exec(`
|
if err := r.DB().Exec(`
|
||||||
INSERT INTO vite_config(name, value, time)
|
INSERT INTO vite_config(name, value, time)
|
||||||
@@ -34,6 +35,55 @@ func TestCaptchaVerifyLoginContract(t *testing.T) {
|
|||||||
t.Fatalf("enable captcha: %v", err)
|
t.Fatalf("enable captcha: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
t.Run("login allowed when cloudflare keys are missing", func(t *testing.T) {
|
||||||
|
body := bytes.NewBufferString(`{"username":"admin_user","password":"admin_user","captchaId":""}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/user/login", body)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
resp := httptest.NewRecorder()
|
||||||
|
|
||||||
|
router.ServeHTTP(resp, req)
|
||||||
|
|
||||||
|
assertCode(t, resp, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("captcha verify remains compatible without cloudflare secret", func(t *testing.T) {
|
||||||
|
verifyReq := httptest.NewRequest(http.MethodPost, "/api/v1/captcha/verify", bytes.NewBufferString(`{"id":"captcha-token-1","data":"ok"}`))
|
||||||
|
verifyReq.Header.Set("Content-Type", "application/json")
|
||||||
|
verifyResp := httptest.NewRecorder()
|
||||||
|
|
||||||
|
router.ServeHTTP(verifyResp, verifyReq)
|
||||||
|
|
||||||
|
var verifyOut struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Data struct {
|
||||||
|
ValidToken string `json:"validToken"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(verifyResp.Body).Decode(&verifyOut); err != nil {
|
||||||
|
t.Fatalf("decode captcha verify response: %v", err)
|
||||||
|
}
|
||||||
|
if !verifyOut.Success || verifyOut.Data.ValidToken != "captcha-token-1" {
|
||||||
|
t.Fatalf("unexpected captcha verify payload: success=%v token=%q", verifyOut.Success, verifyOut.Data.ValidToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
verifiedToken = verifyOut.Data.ValidToken
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := r.DB().Exec(`
|
||||||
|
INSERT INTO vite_config(name, value, time)
|
||||||
|
VALUES(?, ?, ?)
|
||||||
|
ON CONFLICT(name) DO UPDATE SET value = excluded.value, time = excluded.time
|
||||||
|
`, "cloudflare_site_key", "test-site-key", time.Now().UnixMilli()).Error; err != nil {
|
||||||
|
t.Fatalf("set cloudflare site key: %v", err)
|
||||||
|
}
|
||||||
|
if err := r.DB().Exec(`
|
||||||
|
INSERT INTO vite_config(name, value, time)
|
||||||
|
VALUES(?, ?, ?)
|
||||||
|
ON CONFLICT(name) DO UPDATE SET value = excluded.value, time = excluded.time
|
||||||
|
`, "cloudflare_secret_key", "test-secret-key", time.Now().UnixMilli()).Error; err != nil {
|
||||||
|
t.Fatalf("set cloudflare secret key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
t.Run("login denied without verified captcha token", func(t *testing.T) {
|
t.Run("login denied without verified captcha token", func(t *testing.T) {
|
||||||
body := bytes.NewBufferString(`{"username":"admin_user","password":"admin_user","captchaId":""}`)
|
body := bytes.NewBufferString(`{"username":"admin_user","password":"admin_user","captchaId":""}`)
|
||||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/user/login", body)
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/user/login", body)
|
||||||
@@ -58,33 +108,18 @@ func TestCaptchaVerifyLoginContract(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("captcha token is one-time and consumed by login", func(t *testing.T) {
|
t.Run("captcha token is one-time and consumed by login", func(t *testing.T) {
|
||||||
verifyReq := httptest.NewRequest(http.MethodPost, "/api/v1/captcha/verify", bytes.NewBufferString(`{"id":"captcha-token-1","data":"ok"}`))
|
if strings.TrimSpace(verifiedToken) == "" {
|
||||||
verifyReq.Header.Set("Content-Type", "application/json")
|
t.Fatalf("expected verified token from compatibility captcha verify")
|
||||||
verifyResp := httptest.NewRecorder()
|
|
||||||
|
|
||||||
router.ServeHTTP(verifyResp, verifyReq)
|
|
||||||
|
|
||||||
var verifyOut struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
Data struct {
|
|
||||||
ValidToken string `json:"validToken"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(verifyResp.Body).Decode(&verifyOut); err != nil {
|
|
||||||
t.Fatalf("decode captcha verify response: %v", err)
|
|
||||||
}
|
|
||||||
if !verifyOut.Success || verifyOut.Data.ValidToken != "captcha-token-1" {
|
|
||||||
t.Fatalf("unexpected captcha verify payload: success=%v token=%q", verifyOut.Success, verifyOut.Data.ValidToken)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loginBody := bytes.NewBufferString(`{"username":"admin_user","password":"admin_user","captchaId":"captcha-token-1"}`)
|
loginBody := bytes.NewBufferString(`{"username":"admin_user","password":"admin_user","captchaId":"` + verifiedToken + `"}`)
|
||||||
loginReq := httptest.NewRequest(http.MethodPost, "/api/v1/user/login", loginBody)
|
loginReq := httptest.NewRequest(http.MethodPost, "/api/v1/user/login", loginBody)
|
||||||
loginReq.Header.Set("Content-Type", "application/json")
|
loginReq.Header.Set("Content-Type", "application/json")
|
||||||
loginResp := httptest.NewRecorder()
|
loginResp := httptest.NewRecorder()
|
||||||
router.ServeHTTP(loginResp, loginReq)
|
router.ServeHTTP(loginResp, loginReq)
|
||||||
assertCode(t, loginResp, 0)
|
assertCode(t, loginResp, 0)
|
||||||
|
|
||||||
replayBody := bytes.NewBufferString(`{"username":"admin_user","password":"admin_user","captchaId":"captcha-token-1"}`)
|
replayBody := bytes.NewBufferString(`{"username":"admin_user","password":"admin_user","captchaId":"` + verifiedToken + `"}`)
|
||||||
replayReq := httptest.NewRequest(http.MethodPost, "/api/v1/user/login", replayBody)
|
replayReq := httptest.NewRequest(http.MethodPost, "/api/v1/user/login", replayBody)
|
||||||
replayReq.Header.Set("Content-Type", "application/json")
|
replayReq.Header.Set("Content-Type", "application/json")
|
||||||
replayResp := httptest.NewRecorder()
|
replayResp := httptest.NewRecorder()
|
||||||
|
|||||||
@@ -2349,7 +2349,6 @@ export default function ForwardPage() {
|
|||||||
|
|
||||||
<Select
|
<Select
|
||||||
label="限速规则"
|
label="限速规则"
|
||||||
placeholder="请选择限速规则"
|
|
||||||
selectedKeys={
|
selectedKeys={
|
||||||
selectedSpeedId !== null
|
selectedSpeedId !== null
|
||||||
? [selectedSpeedId.toString()]
|
? [selectedSpeedId.toString()]
|
||||||
|
|||||||
Reference in New Issue
Block a user