icebear0828 Claude Opus 4.6 commited on
Commit
af3e19d
·
1 Parent(s): ffcac87

feat: proxy pool add form with separate protocol/host/port/auth fields

Browse files

Replace single URL input with structured fields (protocol dropdown,
host, port, username, password) for easier configuration of
authenticated proxies. Backend still accepts raw url for API compat.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

shared/hooks/use-proxies.ts CHANGED
@@ -1,13 +1,22 @@
1
  import { useState, useEffect, useCallback } from "preact/hooks";
2
  import type { ProxyEntry, ProxyAssignment } from "../types";
3
 
 
 
 
 
 
 
 
 
 
4
  export interface ProxiesState {
5
  proxies: ProxyEntry[];
6
  assignments: ProxyAssignment[];
7
  healthCheckIntervalMinutes: number;
8
  loading: boolean;
9
  refresh: () => Promise<void>;
10
- addProxy: (name: string, url: string) => Promise<string | null>;
11
  removeProxy: (id: string) => Promise<string | null>;
12
  checkProxy: (id: string) => Promise<void>;
13
  checkAll: () => Promise<void>;
@@ -43,12 +52,19 @@ export function useProxies(): ProxiesState {
43
  }, [refresh]);
44
 
45
  const addProxy = useCallback(
46
- async (name: string, url: string): Promise<string | null> => {
47
  try {
48
  const resp = await fetch("/api/proxies", {
49
  method: "POST",
50
  headers: { "Content-Type": "application/json" },
51
- body: JSON.stringify({ name, url }),
 
 
 
 
 
 
 
52
  });
53
  const data = await resp.json();
54
  if (!resp.ok) return data.error || "Failed to add proxy";
 
1
  import { useState, useEffect, useCallback } from "preact/hooks";
2
  import type { ProxyEntry, ProxyAssignment } from "../types";
3
 
4
+ export interface AddProxyFields {
5
+ name: string;
6
+ protocol: string;
7
+ host: string;
8
+ port: string;
9
+ username: string;
10
+ password: string;
11
+ }
12
+
13
  export interface ProxiesState {
14
  proxies: ProxyEntry[];
15
  assignments: ProxyAssignment[];
16
  healthCheckIntervalMinutes: number;
17
  loading: boolean;
18
  refresh: () => Promise<void>;
19
+ addProxy: (fields: AddProxyFields) => Promise<string | null>;
20
  removeProxy: (id: string) => Promise<string | null>;
21
  checkProxy: (id: string) => Promise<void>;
22
  checkAll: () => Promise<void>;
 
52
  }, [refresh]);
53
 
54
  const addProxy = useCallback(
55
+ async (fields: AddProxyFields): Promise<string | null> => {
56
  try {
57
  const resp = await fetch("/api/proxies", {
58
  method: "POST",
59
  headers: { "Content-Type": "application/json" },
60
+ body: JSON.stringify({
61
+ name: fields.name,
62
+ protocol: fields.protocol,
63
+ host: fields.host,
64
+ port: fields.port,
65
+ username: fields.username,
66
+ password: fields.password,
67
+ }),
68
  });
69
  const data = await resp.json();
70
  if (!resp.ok) return data.error || "Failed to add proxy";
shared/i18n/translations.ts CHANGED
@@ -77,6 +77,12 @@ export const translations = {
77
  addProxy: "Add Proxy",
78
  proxyName: "Name",
79
  proxyUrl: "Proxy URL",
 
 
 
 
 
 
80
  noProxies: "No proxies configured. Add one to route accounts through different IPs.",
81
  proxyActive: "Active",
82
  proxyUnreachable: "Unreachable",
@@ -177,6 +183,12 @@ export const translations = {
177
  addProxy: "\u6dfb\u52a0\u4ee3\u7406",
178
  proxyName: "\u540d\u79f0",
179
  proxyUrl: "\u4ee3\u7406 URL",
 
 
 
 
 
 
180
  noProxies: "\u672a\u914d\u7f6e\u4ee3\u7406\u3002\u6dfb\u52a0\u4e00\u4e2a\u4ee5\u901a\u8fc7\u4e0d\u540c IP \u8def\u7531\u8d26\u53f7\u3002",
181
  proxyActive: "\u6d3b\u8dc3",
182
  proxyUnreachable: "\u4e0d\u53ef\u8fbe",
 
77
  addProxy: "Add Proxy",
78
  proxyName: "Name",
79
  proxyUrl: "Proxy URL",
80
+ proxyProtocol: "Protocol",
81
+ proxyHost: "Host",
82
+ proxyPort: "Port",
83
+ proxyUsername: "Username",
84
+ proxyPassword: "Password",
85
+ proxyOptional: "optional",
86
  noProxies: "No proxies configured. Add one to route accounts through different IPs.",
87
  proxyActive: "Active",
88
  proxyUnreachable: "Unreachable",
 
183
  addProxy: "\u6dfb\u52a0\u4ee3\u7406",
184
  proxyName: "\u540d\u79f0",
185
  proxyUrl: "\u4ee3\u7406 URL",
186
+ proxyProtocol: "\u534f\u8bae",
187
+ proxyHost: "\u4e3b\u673a",
188
+ proxyPort: "\u7aef\u53e3",
189
+ proxyUsername: "\u7528\u6237\u540d",
190
+ proxyPassword: "\u5bc6\u7801",
191
+ proxyOptional: "\u53ef\u9009",
192
  noProxies: "\u672a\u914d\u7f6e\u4ee3\u7406\u3002\u6dfb\u52a0\u4e00\u4e2a\u4ee5\u901a\u8fc7\u4e0d\u540c IP \u8def\u7531\u8d26\u53f7\u3002",
193
  proxyActive: "\u6d3b\u8dc3",
194
  proxyUnreachable: "\u4e0d\u53ef\u8fbe",
src/routes/proxies.ts CHANGED
@@ -29,14 +29,28 @@ export function createProxyRoutes(proxyPool: ProxyPool): Hono {
29
  });
30
  });
31
 
32
- // Add proxy
33
  app.post("/api/proxies", async (c) => {
34
- const body = await c.req.json<{ name?: string; url?: string }>();
35
- const url = body.url?.trim();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
  if (!url) {
38
  c.status(400);
39
- return c.json({ error: "url is required" });
40
  }
41
 
42
  // URL validation + scheme check
@@ -174,3 +188,23 @@ export function createProxyRoutes(proxyPool: ProxyPool): Hono {
174
 
175
  return app;
176
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  });
30
  });
31
 
32
+ // Add proxy — accepts { name, url } OR { name, protocol, host, port, username?, password? }
33
  app.post("/api/proxies", async (c) => {
34
+ const body = await c.req.json<{
35
+ name?: string;
36
+ url?: string;
37
+ protocol?: string;
38
+ host?: string;
39
+ port?: string | number;
40
+ username?: string;
41
+ password?: string;
42
+ }>();
43
+
44
+ let url = body.url?.trim();
45
+
46
+ // Compose URL from separate fields if raw url not provided
47
+ if (!url && body.host) {
48
+ url = composeProxyUrl(body.protocol, body.host, body.port, body.username, body.password);
49
+ }
50
 
51
  if (!url) {
52
  c.status(400);
53
+ return c.json({ error: "url or host is required" });
54
  }
55
 
56
  // URL validation + scheme check
 
188
 
189
  return app;
190
  }
191
+
192
+ /** Compose a proxy URL from separate fields. */
193
+ function composeProxyUrl(
194
+ protocol: string | undefined,
195
+ host: string,
196
+ port: string | number | undefined,
197
+ username: string | undefined,
198
+ password: string | undefined,
199
+ ): string {
200
+ const scheme = protocol || "http";
201
+ const trimmedHost = host.trim();
202
+ let auth = "";
203
+ if (username) {
204
+ auth = password
205
+ ? `${encodeURIComponent(username)}:${encodeURIComponent(password)}@`
206
+ : `${encodeURIComponent(username)}@`;
207
+ }
208
+ const portSuffix = port ? `:${port}` : "";
209
+ return `${scheme}://${auth}${trimmedHost}${portSuffix}`;
210
+ }
web/src/components/ProxyPool.tsx CHANGED
@@ -3,6 +3,8 @@ import { useT } from "../../../shared/i18n/context";
3
  import type { TranslationKey } from "../../../shared/i18n/translations";
4
  import type { ProxiesState } from "../../../shared/hooks/use-proxies";
5
 
 
 
6
  const statusStyles: Record<string, [string, TranslationKey]> = {
7
  active: [
8
  "bg-green-100 text-green-700 border-green-200 dark:bg-[#11281d] dark:text-primary dark:border-[#1a442e]",
@@ -18,6 +20,8 @@ const statusStyles: Record<string, [string, TranslationKey]> = {
18
  ],
19
  };
20
 
 
 
21
  interface ProxyPoolProps {
22
  proxies: ProxiesState;
23
  }
@@ -26,26 +30,46 @@ export function ProxyPool({ proxies }: ProxyPoolProps) {
26
  const t = useT();
27
  const [showAdd, setShowAdd] = useState(false);
28
  const [newName, setNewName] = useState("");
29
- const [newUrl, setNewUrl] = useState("");
 
 
 
 
30
  const [addError, setAddError] = useState("");
31
  const [checking, setChecking] = useState<string | null>(null);
32
  const [checkingAll, setCheckingAll] = useState(false);
33
 
 
 
 
 
 
 
 
 
 
 
34
  const handleAdd = useCallback(async () => {
35
  setAddError("");
36
- if (!newUrl.trim()) {
37
- setAddError("URL is required");
38
  return;
39
  }
40
- const err = await proxies.addProxy(newName || newUrl, newUrl);
 
 
 
 
 
 
 
41
  if (err) {
42
  setAddError(err);
43
  } else {
44
- setNewName("");
45
- setNewUrl("");
46
  setShowAdd(false);
47
  }
48
- }, [newName, newUrl, proxies]);
49
 
50
  const handleCheck = useCallback(
51
  async (id: string) => {
@@ -102,31 +126,69 @@ export function ProxyPool({ proxies }: ProxyPoolProps) {
102
  {/* Add form */}
103
  {showAdd && (
104
  <div class="bg-white dark:bg-card-dark border border-gray-200 dark:border-border-dark rounded-xl p-4 mb-4">
105
- <div class="flex flex-col sm:flex-row gap-3">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  <input
107
  type="text"
108
- placeholder={t("proxyName")}
109
  value={newName}
110
  onInput={(e) => setNewName((e.target as HTMLInputElement).value)}
111
- class="flex-1 px-3 py-2 text-sm border border-gray-200 dark:border-border-dark rounded-lg bg-transparent focus:outline-none focus:ring-1 focus:ring-primary"
112
  />
113
  <input
114
  type="text"
115
- placeholder="http://host:port or socks5://host:port"
116
- value={newUrl}
117
- onInput={(e) => setNewUrl((e.target as HTMLInputElement).value)}
118
- class="flex-[2] px-3 py-2 text-sm border border-gray-200 dark:border-border-dark rounded-lg bg-transparent focus:outline-none focus:ring-1 focus:ring-primary font-mono"
 
 
 
 
 
 
 
119
  />
120
- <button
121
- onClick={handleAdd}
122
- class="px-4 py-2 text-sm font-medium rounded-lg bg-primary text-white hover:bg-primary/90 transition-colors whitespace-nowrap"
123
- >
124
- {t("addProxy")}
125
- </button>
126
  </div>
127
- {addError && (
128
- <p class="text-xs text-red-500 mt-2">{addError}</p>
129
- )}
 
 
 
 
 
 
 
 
 
 
130
  </div>
131
  )}
132
 
 
3
  import type { TranslationKey } from "../../../shared/i18n/translations";
4
  import type { ProxiesState } from "../../../shared/hooks/use-proxies";
5
 
6
+ const PROTOCOLS = ["http", "https", "socks5", "socks5h"] as const;
7
+
8
  const statusStyles: Record<string, [string, TranslationKey]> = {
9
  active: [
10
  "bg-green-100 text-green-700 border-green-200 dark:bg-[#11281d] dark:text-primary dark:border-[#1a442e]",
 
20
  ],
21
  };
22
 
23
+ const inputCls = "px-3 py-2 text-sm border border-gray-200 dark:border-border-dark rounded-lg bg-transparent focus:outline-none focus:ring-1 focus:ring-primary";
24
+
25
  interface ProxyPoolProps {
26
  proxies: ProxiesState;
27
  }
 
30
  const t = useT();
31
  const [showAdd, setShowAdd] = useState(false);
32
  const [newName, setNewName] = useState("");
33
+ const [newProtocol, setNewProtocol] = useState("http");
34
+ const [newHost, setNewHost] = useState("");
35
+ const [newPort, setNewPort] = useState("");
36
+ const [newUsername, setNewUsername] = useState("");
37
+ const [newPassword, setNewPassword] = useState("");
38
  const [addError, setAddError] = useState("");
39
  const [checking, setChecking] = useState<string | null>(null);
40
  const [checkingAll, setCheckingAll] = useState(false);
41
 
42
+ const resetForm = useCallback(() => {
43
+ setNewName("");
44
+ setNewProtocol("http");
45
+ setNewHost("");
46
+ setNewPort("");
47
+ setNewUsername("");
48
+ setNewPassword("");
49
+ setAddError("");
50
+ }, []);
51
+
52
  const handleAdd = useCallback(async () => {
53
  setAddError("");
54
+ if (!newHost.trim()) {
55
+ setAddError(t("proxyHost") + " is required");
56
  return;
57
  }
58
+ const err = await proxies.addProxy({
59
+ name: newName,
60
+ protocol: newProtocol,
61
+ host: newHost,
62
+ port: newPort,
63
+ username: newUsername,
64
+ password: newPassword,
65
+ });
66
  if (err) {
67
  setAddError(err);
68
  } else {
69
+ resetForm();
 
70
  setShowAdd(false);
71
  }
72
+ }, [newName, newProtocol, newHost, newPort, newUsername, newPassword, proxies, resetForm, t]);
73
 
74
  const handleCheck = useCallback(
75
  async (id: string) => {
 
126
  {/* Add form */}
127
  {showAdd && (
128
  <div class="bg-white dark:bg-card-dark border border-gray-200 dark:border-border-dark rounded-xl p-4 mb-4">
129
+ <div class="grid grid-cols-[auto_1fr_auto] sm:grid-cols-[auto_1fr_80px] gap-2 items-center">
130
+ {/* Row 1: Protocol + Host + Port */}
131
+ <select
132
+ value={newProtocol}
133
+ onChange={(e) => setNewProtocol((e.target as HTMLSelectElement).value)}
134
+ class={`${inputCls} w-[110px]`}
135
+ >
136
+ {PROTOCOLS.map((p) => (
137
+ <option key={p} value={p}>{p}://</option>
138
+ ))}
139
+ </select>
140
+ <input
141
+ type="text"
142
+ placeholder={t("proxyHost")}
143
+ value={newHost}
144
+ onInput={(e) => setNewHost((e.target as HTMLInputElement).value)}
145
+ class={`${inputCls} font-mono`}
146
+ />
147
+ <input
148
+ type="text"
149
+ placeholder={t("proxyPort")}
150
+ value={newPort}
151
+ onInput={(e) => setNewPort((e.target as HTMLInputElement).value)}
152
+ class={`${inputCls} font-mono`}
153
+ />
154
+ </div>
155
+ <div class="grid grid-cols-2 sm:grid-cols-3 gap-2 mt-2">
156
+ {/* Row 2: Name + Username + Password */}
157
  <input
158
  type="text"
159
+ placeholder={`${t("proxyName")} (${t("proxyOptional")})`}
160
  value={newName}
161
  onInput={(e) => setNewName((e.target as HTMLInputElement).value)}
162
+ class={inputCls}
163
  />
164
  <input
165
  type="text"
166
+ placeholder={`${t("proxyUsername")} (${t("proxyOptional")})`}
167
+ value={newUsername}
168
+ onInput={(e) => setNewUsername((e.target as HTMLInputElement).value)}
169
+ class={inputCls}
170
+ />
171
+ <input
172
+ type="password"
173
+ placeholder={`${t("proxyPassword")} (${t("proxyOptional")})`}
174
+ value={newPassword}
175
+ onInput={(e) => setNewPassword((e.target as HTMLInputElement).value)}
176
+ class={`${inputCls} col-span-2 sm:col-span-1`}
177
  />
 
 
 
 
 
 
178
  </div>
179
+ <div class="flex items-center justify-between mt-3">
180
+ {addError && (
181
+ <p class="text-xs text-red-500">{addError}</p>
182
+ )}
183
+ <div class="ml-auto">
184
+ <button
185
+ onClick={handleAdd}
186
+ class="px-4 py-2 text-sm font-medium rounded-lg bg-primary text-white hover:bg-primary/90 transition-colors whitespace-nowrap"
187
+ >
188
+ {t("addProxy")}
189
+ </button>
190
+ </div>
191
+ </div>
192
  </div>
193
  )}
194