mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-10 03:07:41 +08:00
feat(proxy): add proxy IP management and account routing
Add admin CRUD and connectivity checks for SOCKS5/SOCKS5H proxy nodes. Allow accounts to bind to a proxy, route DeepSeek requests through the selected node, and expose proxy management in the admin UI.
This commit is contained in:
@@ -26,9 +26,15 @@ func RegisterRoutes(r chi.Router, h *Handler) {
|
||||
pr.Get("/config/export", h.configExport)
|
||||
pr.Post("/keys", h.addKey)
|
||||
pr.Delete("/keys/{key}", h.deleteKey)
|
||||
pr.Get("/proxies", h.listProxies)
|
||||
pr.Post("/proxies", h.addProxy)
|
||||
pr.Put("/proxies/{proxyID}", h.updateProxy)
|
||||
pr.Delete("/proxies/{proxyID}", h.deleteProxy)
|
||||
pr.Post("/proxies/test", h.testProxy)
|
||||
pr.Get("/accounts", h.listAccounts)
|
||||
pr.Post("/accounts", h.addAccount)
|
||||
pr.Delete("/accounts/{identifier}", h.deleteAccount)
|
||||
pr.Put("/accounts/{identifier}/proxy", h.updateAccountProxy)
|
||||
pr.Get("/queue/status", h.queueStatus)
|
||||
pr.Post("/accounts/test", h.testSingleAccount)
|
||||
pr.Post("/accounts/test-all", h.testAllAccounts)
|
||||
|
||||
@@ -68,6 +68,7 @@ func (h *Handler) listAccounts(w http.ResponseWriter, r *http.Request) {
|
||||
"identifier": acc.Identifier(),
|
||||
"email": acc.Email,
|
||||
"mobile": acc.Mobile,
|
||||
"proxy_id": acc.ProxyID,
|
||||
"has_password": acc.Password != "",
|
||||
"has_token": token != "",
|
||||
"token_preview": preview,
|
||||
@@ -86,6 +87,11 @@ func (h *Handler) addAccount(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
err := h.Store.Update(func(c *config.Config) error {
|
||||
if acc.ProxyID != "" {
|
||||
if _, ok := findProxyByID(*c, acc.ProxyID); !ok {
|
||||
return fmt.Errorf("代理不存在")
|
||||
}
|
||||
}
|
||||
mobileKey := config.CanonicalMobileKey(acc.Mobile)
|
||||
for _, a := range c.Accounts {
|
||||
if acc.Email != "" && a.Email == acc.Email {
|
||||
|
||||
@@ -115,10 +115,11 @@ func (h *Handler) testAccount(ctx context.Context, acc config.Account, model, me
|
||||
result["message"] = "登录成功但写入运行时 token 失败: " + err.Error()
|
||||
return result
|
||||
}
|
||||
authCtx := &authn.RequestAuth{UseConfigToken: false, DeepSeekToken: token}
|
||||
sessionID, err := h.DS.CreateSession(ctx, authCtx, 1)
|
||||
authCtx := &authn.RequestAuth{UseConfigToken: false, DeepSeekToken: token, AccountID: identifier, Account: acc}
|
||||
proxyCtx := authn.WithAuth(ctx, authCtx)
|
||||
sessionID, err := h.DS.CreateSession(proxyCtx, authCtx, 1)
|
||||
if err != nil {
|
||||
newToken, loginErr := h.DS.Login(ctx, acc)
|
||||
newToken, loginErr := h.DS.Login(proxyCtx, acc)
|
||||
if loginErr != nil {
|
||||
result["message"] = "创建会话失败: " + err.Error()
|
||||
return result
|
||||
@@ -129,7 +130,7 @@ func (h *Handler) testAccount(ctx context.Context, acc config.Account, model, me
|
||||
result["message"] = "刷新 token 成功但写入运行时 token 失败: " + err.Error()
|
||||
return result
|
||||
}
|
||||
sessionID, err = h.DS.CreateSession(ctx, authCtx, 1)
|
||||
sessionID, err = h.DS.CreateSession(proxyCtx, authCtx, 1)
|
||||
if err != nil {
|
||||
result["message"] = "创建会话失败: " + err.Error()
|
||||
return result
|
||||
@@ -137,7 +138,7 @@ func (h *Handler) testAccount(ctx context.Context, acc config.Account, model, me
|
||||
}
|
||||
|
||||
// 获取会话数量
|
||||
sessionStats, sessionErr := h.DS.GetSessionCountForToken(ctx, token)
|
||||
sessionStats, sessionErr := h.DS.GetSessionCountForToken(proxyCtx, token)
|
||||
if sessionErr == nil && sessionStats != nil {
|
||||
result["session_count"] = sessionStats.FirstPageCount
|
||||
}
|
||||
@@ -153,13 +154,13 @@ func (h *Handler) testAccount(ctx context.Context, acc config.Account, model, me
|
||||
thinking, search = false, false
|
||||
}
|
||||
_ = search
|
||||
pow, err := h.DS.GetPow(ctx, authCtx, 1)
|
||||
pow, err := h.DS.GetPow(proxyCtx, authCtx, 1)
|
||||
if err != nil {
|
||||
result["message"] = "获取 PoW 失败: " + err.Error()
|
||||
return result
|
||||
}
|
||||
payload := map[string]any{"chat_session_id": sessionID, "prompt": deepseek.MessagesPrepare([]map[string]any{{"role": "user", "content": message}}), "ref_file_ids": []any{}, "thinking_enabled": thinking, "search_enabled": search}
|
||||
resp, err := h.DS.CallCompletion(ctx, authCtx, payload, pow, 1)
|
||||
resp, err := h.DS.CallCompletion(proxyCtx, authCtx, payload, pow, 1)
|
||||
if err != nil {
|
||||
result["message"] = "请求失败: " + err.Error()
|
||||
return result
|
||||
@@ -244,25 +245,29 @@ func (h *Handler) deleteAllSessions(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// 每次先登录刷新一次 token,避免使用过期 token。
|
||||
token, err := h.DS.Login(r.Context(), acc)
|
||||
authCtx := &authn.RequestAuth{UseConfigToken: false, AccountID: acc.Identifier(), Account: acc}
|
||||
proxyCtx := authn.WithAuth(r.Context(), authCtx)
|
||||
token, err := h.DS.Login(proxyCtx, acc)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"success": false, "message": "登录失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
_ = h.Store.UpdateAccountToken(acc.Identifier(), token)
|
||||
authCtx.DeepSeekToken = token
|
||||
|
||||
// 删除所有会话
|
||||
err = h.DS.DeleteAllSessionsForToken(r.Context(), token)
|
||||
err = h.DS.DeleteAllSessionsForToken(proxyCtx, token)
|
||||
if err != nil {
|
||||
// token 可能过期,尝试重新登录并重试一次
|
||||
newToken, loginErr := h.DS.Login(r.Context(), acc)
|
||||
newToken, loginErr := h.DS.Login(proxyCtx, acc)
|
||||
if loginErr != nil {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"success": false, "message": "删除失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
token = newToken
|
||||
_ = h.Store.UpdateAccountToken(acc.Identifier(), token)
|
||||
if retryErr := h.DS.DeleteAllSessionsForToken(r.Context(), token); retryErr != nil {
|
||||
authCtx.DeepSeekToken = token
|
||||
if retryErr := h.DS.DeleteAllSessionsForToken(proxyCtx, token); retryErr != nil {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"success": false, "message": "删除失败: " + retryErr.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package admin
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"ds2api/internal/config"
|
||||
)
|
||||
|
||||
func (h *Handler) getConfig(w http.ResponseWriter, _ *http.Request) {
|
||||
@@ -10,6 +12,7 @@ func (h *Handler) getConfig(w http.ResponseWriter, _ *http.Request) {
|
||||
safe := map[string]any{
|
||||
"keys": snap.Keys,
|
||||
"accounts": []map[string]any{},
|
||||
"proxies": []map[string]any{},
|
||||
"env_backed": h.Store.IsEnvBacked(),
|
||||
"env_source_present": h.Store.HasEnvConfigSource(),
|
||||
"env_writeback_enabled": h.Store.IsEnvWritebackEnabled(),
|
||||
@@ -36,12 +39,27 @@ func (h *Handler) getConfig(w http.ResponseWriter, _ *http.Request) {
|
||||
"identifier": acc.Identifier(),
|
||||
"email": acc.Email,
|
||||
"mobile": acc.Mobile,
|
||||
"proxy_id": acc.ProxyID,
|
||||
"has_password": strings.TrimSpace(acc.Password) != "",
|
||||
"has_token": token != "",
|
||||
"token_preview": preview,
|
||||
})
|
||||
}
|
||||
safe["accounts"] = accounts
|
||||
proxies := make([]map[string]any, 0, len(snap.Proxies))
|
||||
for _, proxy := range snap.Proxies {
|
||||
proxy = config.NormalizeProxy(proxy)
|
||||
proxies = append(proxies, map[string]any{
|
||||
"id": proxy.ID,
|
||||
"name": proxy.Name,
|
||||
"type": proxy.Type,
|
||||
"host": proxy.Host,
|
||||
"port": proxy.Port,
|
||||
"username": proxy.Username,
|
||||
"has_password": strings.TrimSpace(proxy.Password) != "",
|
||||
})
|
||||
}
|
||||
safe["proxies"] = proxies
|
||||
writeJSON(w, http.StatusOK, safe)
|
||||
}
|
||||
|
||||
|
||||
189
internal/admin/handler_proxies.go
Normal file
189
internal/admin/handler_proxies.go
Normal file
@@ -0,0 +1,189 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"ds2api/internal/config"
|
||||
"ds2api/internal/deepseek"
|
||||
)
|
||||
|
||||
var proxyConnectivityTester = func(ctx context.Context, proxy config.Proxy) map[string]any {
|
||||
return deepseek.TestProxyConnectivity(ctx, proxy)
|
||||
}
|
||||
|
||||
func validateProxyMutation(cfg *config.Config) error {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
if err := config.ValidateProxyConfig(cfg.Proxies); err != nil {
|
||||
return err
|
||||
}
|
||||
return config.ValidateAccountProxyReferences(cfg.Accounts, cfg.Proxies)
|
||||
}
|
||||
|
||||
func (h *Handler) listProxies(w http.ResponseWriter, _ *http.Request) {
|
||||
proxies := h.Store.Snapshot().Proxies
|
||||
items := make([]map[string]any, 0, len(proxies))
|
||||
for _, proxy := range proxies {
|
||||
proxy = config.NormalizeProxy(proxy)
|
||||
items = append(items, map[string]any{
|
||||
"id": proxy.ID,
|
||||
"name": proxy.Name,
|
||||
"type": proxy.Type,
|
||||
"host": proxy.Host,
|
||||
"port": proxy.Port,
|
||||
"username": proxy.Username,
|
||||
"has_password": strings.TrimSpace(proxy.Password) != "",
|
||||
})
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"items": items, "total": len(items)})
|
||||
}
|
||||
|
||||
func (h *Handler) addProxy(w http.ResponseWriter, r *http.Request) {
|
||||
var req map[string]any
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
proxy := toProxy(req)
|
||||
err := h.Store.Update(func(c *config.Config) error {
|
||||
c.Proxies = append(c.Proxies, proxy)
|
||||
return validateProxyMutation(c)
|
||||
})
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]any{"detail": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"success": true, "proxy": proxy})
|
||||
}
|
||||
|
||||
func (h *Handler) updateProxy(w http.ResponseWriter, r *http.Request) {
|
||||
proxyID := chi.URLParam(r, "proxyID")
|
||||
if decoded, err := url.PathUnescape(proxyID); err == nil {
|
||||
proxyID = decoded
|
||||
}
|
||||
var req map[string]any
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
proxy := toProxy(req)
|
||||
proxy.ID = strings.TrimSpace(proxyID)
|
||||
|
||||
err := h.Store.Update(func(c *config.Config) error {
|
||||
for i, existing := range c.Proxies {
|
||||
existing = config.NormalizeProxy(existing)
|
||||
if existing.ID != proxy.ID {
|
||||
continue
|
||||
}
|
||||
if proxy.Password == "" {
|
||||
proxy.Password = existing.Password
|
||||
}
|
||||
c.Proxies[i] = proxy
|
||||
return validateProxyMutation(c)
|
||||
}
|
||||
return newRequestError("代理不存在")
|
||||
})
|
||||
if err != nil {
|
||||
if detail, ok := requestErrorDetail(err); ok {
|
||||
writeJSON(w, http.StatusNotFound, map[string]any{"detail": detail})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusBadRequest, map[string]any{"detail": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"success": true, "proxy": proxy})
|
||||
}
|
||||
|
||||
func (h *Handler) deleteProxy(w http.ResponseWriter, r *http.Request) {
|
||||
proxyID := chi.URLParam(r, "proxyID")
|
||||
if decoded, err := url.PathUnescape(proxyID); err == nil {
|
||||
proxyID = decoded
|
||||
}
|
||||
err := h.Store.Update(func(c *config.Config) error {
|
||||
idx := -1
|
||||
for i, existing := range c.Proxies {
|
||||
existing = config.NormalizeProxy(existing)
|
||||
if existing.ID == strings.TrimSpace(proxyID) {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if idx < 0 {
|
||||
return newRequestError("代理不存在")
|
||||
}
|
||||
c.Proxies = append(c.Proxies[:idx], c.Proxies[idx+1:]...)
|
||||
for i := range c.Accounts {
|
||||
if strings.TrimSpace(c.Accounts[i].ProxyID) == strings.TrimSpace(proxyID) {
|
||||
c.Accounts[i].ProxyID = ""
|
||||
}
|
||||
}
|
||||
return validateProxyMutation(c)
|
||||
})
|
||||
if err != nil {
|
||||
if detail, ok := requestErrorDetail(err); ok {
|
||||
writeJSON(w, http.StatusNotFound, map[string]any{"detail": detail})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusBadRequest, map[string]any{"detail": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"success": true})
|
||||
}
|
||||
|
||||
func (h *Handler) testProxy(w http.ResponseWriter, r *http.Request) {
|
||||
var req map[string]any
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
proxyID := fieldString(req, "proxy_id")
|
||||
|
||||
var proxy config.Proxy
|
||||
if proxyID != "" {
|
||||
var ok bool
|
||||
proxy, ok = findProxyByID(h.Store.Snapshot(), proxyID)
|
||||
if !ok {
|
||||
writeJSON(w, http.StatusNotFound, map[string]any{"detail": "代理不存在"})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
proxy = toProxy(req)
|
||||
}
|
||||
|
||||
result := proxyConnectivityTester(r.Context(), proxy)
|
||||
writeJSON(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (h *Handler) updateAccountProxy(w http.ResponseWriter, r *http.Request) {
|
||||
identifier := chi.URLParam(r, "identifier")
|
||||
if decoded, err := url.PathUnescape(identifier); err == nil {
|
||||
identifier = decoded
|
||||
}
|
||||
var req map[string]any
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
proxyID := fieldString(req, "proxy_id")
|
||||
|
||||
err := h.Store.Update(func(c *config.Config) error {
|
||||
if proxyID != "" {
|
||||
if _, ok := findProxyByID(*c, proxyID); !ok {
|
||||
return newRequestError("代理不存在")
|
||||
}
|
||||
}
|
||||
for i, acc := range c.Accounts {
|
||||
if !accountMatchesIdentifier(acc, identifier) {
|
||||
continue
|
||||
}
|
||||
c.Accounts[i].ProxyID = proxyID
|
||||
return validateProxyMutation(c)
|
||||
}
|
||||
return newRequestError("账号不存在")
|
||||
})
|
||||
if err != nil {
|
||||
if detail, ok := requestErrorDetail(err); ok {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]any{"detail": detail})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusBadRequest, map[string]any{"detail": err.Error()})
|
||||
return
|
||||
}
|
||||
h.Pool.Reset()
|
||||
writeJSON(w, http.StatusOK, map[string]any{"success": true, "proxy_id": proxyID})
|
||||
}
|
||||
193
internal/admin/handler_proxies_test.go
Normal file
193
internal/admin/handler_proxies_test.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"ds2api/internal/account"
|
||||
"ds2api/internal/config"
|
||||
)
|
||||
|
||||
func newAdminProxyTestHandler(t *testing.T, raw string) *Handler {
|
||||
t.Helper()
|
||||
t.Setenv("DS2API_CONFIG_JSON", raw)
|
||||
store := config.LoadStore()
|
||||
return &Handler{
|
||||
Store: store,
|
||||
Pool: account.NewPool(store),
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddProxyPersistsNormalizedProxy(t *testing.T) {
|
||||
h := newAdminProxyTestHandler(t, `{"accounts":[]}`)
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Post("/admin/proxies", h.addProxy)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/admin/proxies", bytes.NewBufferString(`{
|
||||
"name":" HK Exit ",
|
||||
"type":" SOCKS5H ",
|
||||
"host":" 127.0.0.1 ",
|
||||
"port":1081,
|
||||
"username":" user ",
|
||||
"password":" pass "
|
||||
}`))
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("unexpected status: %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
proxies := h.Store.Snapshot().Proxies
|
||||
if len(proxies) != 1 {
|
||||
t.Fatalf("expected 1 proxy, got %d", len(proxies))
|
||||
}
|
||||
if proxies[0].Name != "HK Exit" {
|
||||
t.Fatalf("unexpected proxy name: %#v", proxies[0])
|
||||
}
|
||||
if proxies[0].Type != "socks5h" {
|
||||
t.Fatalf("unexpected proxy type: %#v", proxies[0])
|
||||
}
|
||||
if proxies[0].Username != "user" || proxies[0].Password != "pass" {
|
||||
t.Fatalf("expected trimmed credentials, got %#v", proxies[0])
|
||||
}
|
||||
if proxies[0].ID == "" {
|
||||
t.Fatalf("expected generated proxy id, got %#v", proxies[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddProxyDoesNotFailOnUnrelatedInvalidRuntimeConfig(t *testing.T) {
|
||||
router := newHTTPAdminHarness(t, `{
|
||||
"keys":["k1"],
|
||||
"runtime":{
|
||||
"account_max_inflight":8,
|
||||
"global_max_inflight":4
|
||||
}
|
||||
}`, &testingDSMock{})
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, adminReq(http.MethodPost, "/proxies", []byte(`{
|
||||
"name":"HK Exit",
|
||||
"type":"socks5h",
|
||||
"host":"127.0.0.1",
|
||||
"port":1080
|
||||
}`)))
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected add proxy success despite unrelated runtime issue, got %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
readRec := httptest.NewRecorder()
|
||||
router.ServeHTTP(readRec, adminReq(http.MethodGet, "/config", nil))
|
||||
if readRec.Code != http.StatusOK {
|
||||
t.Fatalf("config read status=%d body=%s", readRec.Code, readRec.Body.String())
|
||||
}
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal(readRec.Body.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("decode config response: %v", err)
|
||||
}
|
||||
proxies, _ := payload["proxies"].([]any)
|
||||
if len(proxies) != 1 {
|
||||
t.Fatalf("expected proxy to be persisted, got %#v", payload["proxies"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteProxyClearsAssignedAccountProxyID(t *testing.T) {
|
||||
h := newAdminProxyTestHandler(t, `{
|
||||
"proxies":[{"id":"proxy-1","name":"Node 1","type":"socks5","host":"127.0.0.1","port":1080}],
|
||||
"accounts":[{"email":"u@example.com","password":"pwd","proxy_id":"proxy-1"}]
|
||||
}`)
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Delete("/admin/proxies/{proxyID}", h.deleteProxy)
|
||||
|
||||
req := httptest.NewRequest(http.MethodDelete, "/admin/proxies/proxy-1", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("unexpected status: %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
snap := h.Store.Snapshot()
|
||||
if len(snap.Proxies) != 0 {
|
||||
t.Fatalf("expected proxy removed, got %#v", snap.Proxies)
|
||||
}
|
||||
if len(snap.Accounts) != 1 {
|
||||
t.Fatalf("expected account kept, got %#v", snap.Accounts)
|
||||
}
|
||||
if snap.Accounts[0].ProxyID != "" {
|
||||
t.Fatalf("expected proxy assignment cleared, got %#v", snap.Accounts[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateAccountProxyAssignsProxyID(t *testing.T) {
|
||||
h := newAdminProxyTestHandler(t, `{
|
||||
"proxies":[{"id":"proxy-1","name":"Node 1","type":"socks5h","host":"127.0.0.1","port":1080}],
|
||||
"accounts":[{"email":"u@example.com","password":"pwd"}]
|
||||
}`)
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Put("/admin/accounts/{identifier}/proxy", h.updateAccountProxy)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPut, "/admin/accounts/u@example.com/proxy", bytes.NewBufferString(`{"proxy_id":"proxy-1"}`))
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("unexpected status: %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
acc, ok := h.Store.FindAccount("u@example.com")
|
||||
if !ok {
|
||||
t.Fatal("expected account")
|
||||
}
|
||||
if acc.ProxyID != "proxy-1" {
|
||||
t.Fatalf("expected proxy assigned, got %#v", acc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTestProxyUsesStoredProxy(t *testing.T) {
|
||||
h := newAdminProxyTestHandler(t, `{
|
||||
"proxies":[{"id":"proxy-1","name":"Node 1","type":"socks5h","host":"127.0.0.1","port":1080}]
|
||||
}`)
|
||||
|
||||
original := proxyConnectivityTester
|
||||
defer func() { proxyConnectivityTester = original }()
|
||||
|
||||
var got config.Proxy
|
||||
proxyConnectivityTester = func(_ context.Context, proxy config.Proxy) map[string]any {
|
||||
got = proxy
|
||||
return map[string]any{
|
||||
"success": true,
|
||||
"proxy_id": proxy.ID,
|
||||
"proxy_type": proxy.Type,
|
||||
"response_time": 12,
|
||||
}
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Post("/admin/proxies/test", h.testProxy)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/admin/proxies/test", bytes.NewBufferString(`{"proxy_id":"proxy-1"}`))
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("unexpected status: %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if got.ID != "proxy-1" || got.Type != "socks5h" {
|
||||
t.Fatalf("expected stored proxy passed to tester, got %#v", got)
|
||||
}
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if ok, _ := payload["success"].(bool); !ok {
|
||||
t.Fatalf("expected success payload, got %#v", payload)
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,7 @@ func toAccount(m map[string]any) config.Account {
|
||||
Email: email,
|
||||
Mobile: mobile,
|
||||
Password: fieldString(m, "password"),
|
||||
ProxyID: fieldString(m, "proxy_id"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,9 +101,36 @@ func accountMatchesIdentifier(acc config.Account, identifier string) bool {
|
||||
func normalizeAccountForStorage(acc config.Account) config.Account {
|
||||
acc.Email = strings.TrimSpace(acc.Email)
|
||||
acc.Mobile = config.NormalizeMobileForStorage(acc.Mobile)
|
||||
acc.ProxyID = strings.TrimSpace(acc.ProxyID)
|
||||
return acc
|
||||
}
|
||||
|
||||
func toProxy(m map[string]any) config.Proxy {
|
||||
return config.NormalizeProxy(config.Proxy{
|
||||
ID: fieldString(m, "id"),
|
||||
Name: fieldString(m, "name"),
|
||||
Type: fieldString(m, "type"),
|
||||
Host: fieldString(m, "host"),
|
||||
Port: intFrom(m["port"]),
|
||||
Username: fieldString(m, "username"),
|
||||
Password: fieldString(m, "password"),
|
||||
})
|
||||
}
|
||||
|
||||
func findProxyByID(c config.Config, proxyID string) (config.Proxy, bool) {
|
||||
id := strings.TrimSpace(proxyID)
|
||||
if id == "" {
|
||||
return config.Proxy{}, false
|
||||
}
|
||||
for _, proxy := range c.Proxies {
|
||||
proxy = config.NormalizeProxy(proxy)
|
||||
if proxy.ID == id {
|
||||
return proxy, true
|
||||
}
|
||||
}
|
||||
return config.Proxy{}, false
|
||||
}
|
||||
|
||||
func accountDedupeKey(acc config.Account) string {
|
||||
if email := strings.TrimSpace(acc.Email); email != "" {
|
||||
return "email:" + email
|
||||
|
||||
Reference in New Issue
Block a user