Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/dashboard/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,23 @@ export async function handleDashboardApi(method, subpath, body, req, res) {
} else {
return json(res, 400, { error: 'Provide api_key or token' });
}

// Handle optional proxy configuration
if (body.proxy) {
const parsed = parseProxyUrl(body.proxy);
if (!parsed) {
return json(res, 400, { error: 'ERR_PROXY_FORMAT_INVALID' });
}
// Validate proxy host (respect ALLOW_PRIVATE_PROXY_HOSTS config)
if (config.allowPrivateProxyHosts) {
await validateHostFormat(parsed.host);
} else {
await assertPublicUrlHost(parsed.host);
}
setAccountProxy(account.id, parsed);
ensureLsForAccount(account.id).catch(e => log.warn(`LS ensure failed: ${e.message}`));
}

// Fire-and-forget probe so the UI gets tier info shortly after add
probeAccount(account.id).catch(e => log.warn(`Auto-probe failed: ${e.message}`));
return json(res, 200, {
Expand Down
6 changes: 6 additions & 0 deletions src/dashboard/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,11 @@
"label": "Label",
"placeholder": "Optional"
},
"proxy": {
"label": "Proxy",
"placeholder": "http://proxy:8080 or socks5://user:pass@host:port (optional)",
"hint": "Leave empty to use global proxy settings"
},
"batchInput": {
"label": "Batch Import",
"hint": "One account per line. Format: [proxy] email password. Proxy is optional, supports http/socks5. Proxy will be automatically bound to corresponding account."
Expand Down Expand Up @@ -872,6 +877,7 @@
"ERR_FORMAT_INVALID": "Invalid format. Expected: [proxy] email password",
"ERR_IDTOKEN_REQUIRED": "ID token is required",
"ERR_PROXY_PRIVATE_IP": "Proxy cannot point to private/local address",
"ERR_PROXY_FORMAT_INVALID": "Invalid proxy format. Expected: protocol://[user:pass@]host:port",
"ERR_PROXY_HTTP_ERROR": "Proxy returned HTTP error",
"ERR_PROXY_PRIVATE_HOST": "Private/local host is not allowed",
"ERR_CONNECTION_FAILED": "Connection failed",
Expand Down
6 changes: 6 additions & 0 deletions src/dashboard/i18n/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,11 @@
"label": "标签",
"placeholder": "可选"
},
"proxy": {
"label": "代理",
"placeholder": "http://proxy:8080 或 socks5://user:pass@host:port(可选)",
"hint": "留空则使用全局代理设置"
},
"batchInput": {
"label": "批量导入",
"hint": "每行一个账号。格式:[代理] 邮箱 密码。代理可选,支持 http/socks5。会自动绑定代理到对应账号。"
Expand Down Expand Up @@ -872,6 +877,7 @@
"ERR_FORMAT_INVALID": "格式错误 需要 [proxy] email password",
"ERR_IDTOKEN_REQUIRED": "缺少 idToken",
"ERR_PROXY_PRIVATE_IP": "代理地址不能指向内网/本机",
"ERR_PROXY_FORMAT_INVALID": "代理格式错误,应为:protocol://[user:pass@]host:port",
"ERR_PROXY_HTTP_ERROR": "代理返回 HTTP 错误",
"ERR_PROXY_PRIVATE_HOST": "内网/本地主机不允许",
"ERR_CONNECTION_FAILED": "连接失败",
Expand Down
12 changes: 10 additions & 2 deletions src/dashboard/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1764,6 +1764,13 @@ <h1 class="page-title" data-i18n="page.accounts">账号管理</h1>
<input id="acc-label" class="input" data-i18n-placeholder="field.label.placeholder" placeholder="可选">
</div>
</div>
<div class="field-row" style="margin-top:12px">
<div class="field" style="flex:1">
<label class="field-label" data-i18n="field.proxy.label">代理</label>
<input id="acc-proxy" class="input" data-i18n-placeholder="field.proxy.placeholder" placeholder="http://proxy:8080 或 socks5://user:pass@host:port(可选)">
</div>
</div>
<div class="field-hint" style="margin-top:4px" data-i18n="field.proxy.hint">留空则使用全局代理设置</div>
<div class="field-row-actions">
<button class="btn btn-primary" onclick="App.addAccount()" data-i18n="action.add">添加账号</button>
</div>
Expand Down Expand Up @@ -3624,10 +3631,11 @@ <h3 data-i18n="login.title">控制台登录</h3>
const type = document.getElementById('acc-type').value;
const key = document.getElementById('acc-key').value.trim();
const label = document.getElementById('acc-label').value.trim();
const proxy = document.getElementById('acc-proxy').value.trim();
if (!key) return this.toast(I18n.t('toast.enterKeyOrToken'), 'error');
const body = type === 'api_key' ? { api_key: key, label } : { token: key, label };
const body = type === 'api_key' ? { api_key: key, label, proxy } : { token: key, label, proxy };
const r = await this.api('POST', '/accounts', body);
if (r.success) { this.toast(I18n.t('account.addSuccess')); document.getElementById('acc-key').value = ''; document.getElementById('acc-label').value = ''; this.loadAccounts(); }
if (r.success) { this.toast(I18n.t('account.addSuccess')); document.getElementById('acc-key').value = ''; document.getElementById('acc-label').value = ''; document.getElementById('acc-proxy').value = ''; this.loadAccounts(); }
else this.toast(r.error || I18n.t('toast.addFailed'), 'error');
},

Expand Down
Loading