Skip to content

Commit f080fe3

Browse files
authored
feat: Add endpoints for account details and error handling (#16)
* feat: Add JSON copy functionality with success animation - Add functionality to copy account data as JSON and show success animation. * feat: Add endpoints for account details and error handling - Add endpoint to retrieve full account details including sensitive information - Add error handling for fetching and copying full account JSON data
1 parent 60cf204 commit f080fe3

File tree

2 files changed

+136
-1
lines changed

2 files changed

+136
-1
lines changed

proxy/handler.go

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1330,6 +1330,9 @@ func (h *Handler) handleAdminAPI(w http.ResponseWriter, r *http.Request) {
13301330
case strings.HasPrefix(path, "/accounts/") && strings.HasSuffix(path, "/models") && r.Method == "GET":
13311331
id := strings.TrimSuffix(strings.TrimPrefix(path, "/accounts/"), "/models")
13321332
h.apiGetAccountModels(w, r, id)
1333+
case strings.HasPrefix(path, "/accounts/") && strings.HasSuffix(path, "/full") && r.Method == "GET":
1334+
id := strings.TrimSuffix(strings.TrimPrefix(path, "/accounts/"), "/full")
1335+
h.apiGetAccountFull(w, r, id)
13331336
case strings.HasPrefix(path, "/accounts/") && r.Method == "DELETE":
13341337
h.apiDeleteAccount(w, r, strings.TrimPrefix(path, "/accounts/"))
13351338
case strings.HasPrefix(path, "/accounts/") && r.Method == "PUT":
@@ -2046,6 +2049,77 @@ func (h *Handler) apiRefreshAccount(w http.ResponseWriter, r *http.Request, id s
20462049
})
20472050
}
20482051

2052+
// apiGetAccountFull 获取单个账号的完整信息(包含敏感字段)
2053+
func (h *Handler) apiGetAccountFull(w http.ResponseWriter, r *http.Request, id string) {
2054+
accounts := config.GetAccounts()
2055+
poolAccounts := h.pool.GetAllAccounts()
2056+
2057+
// 查找指定账号
2058+
var account *config.Account
2059+
for i := range accounts {
2060+
if accounts[i].ID == id {
2061+
account = &accounts[i]
2062+
break
2063+
}
2064+
}
2065+
2066+
if account == nil {
2067+
w.WriteHeader(404)
2068+
json.NewEncoder(w).Encode(map[string]string{"error": "Account not found"})
2069+
return
2070+
}
2071+
2072+
// 获取运行时统计
2073+
var stats config.Account
2074+
for _, a := range poolAccounts {
2075+
if a.ID == id {
2076+
stats = a
2077+
break
2078+
}
2079+
}
2080+
2081+
// 返回完整账号信息(包含敏感字段)
2082+
result := map[string]interface{}{
2083+
"id": account.ID,
2084+
"email": account.Email,
2085+
"userId": account.UserId,
2086+
"nickname": account.Nickname,
2087+
"accessToken": account.AccessToken,
2088+
"refreshToken": account.RefreshToken,
2089+
"clientId": account.ClientID,
2090+
"clientSecret": account.ClientSecret,
2091+
"authMethod": account.AuthMethod,
2092+
"provider": account.Provider,
2093+
"region": account.Region,
2094+
"expiresAt": account.ExpiresAt,
2095+
"machineId": account.MachineId,
2096+
"enabled": account.Enabled,
2097+
"banStatus": account.BanStatus,
2098+
"banReason": account.BanReason,
2099+
"banTime": account.BanTime,
2100+
"subscriptionType": account.SubscriptionType,
2101+
"subscriptionTitle": account.SubscriptionTitle,
2102+
"daysRemaining": account.DaysRemaining,
2103+
"usageCurrent": account.UsageCurrent,
2104+
"usageLimit": account.UsageLimit,
2105+
"usagePercent": account.UsagePercent,
2106+
"nextResetDate": account.NextResetDate,
2107+
"lastRefresh": account.LastRefresh,
2108+
"trialUsageCurrent": account.TrialUsageCurrent,
2109+
"trialUsageLimit": account.TrialUsageLimit,
2110+
"trialUsagePercent": account.TrialUsagePercent,
2111+
"trialStatus": account.TrialStatus,
2112+
"trialExpiresAt": account.TrialExpiresAt,
2113+
"requestCount": stats.RequestCount,
2114+
"errorCount": stats.ErrorCount,
2115+
"totalTokens": stats.TotalTokens,
2116+
"totalCredits": stats.TotalCredits,
2117+
"lastUsed": stats.LastUsed,
2118+
}
2119+
2120+
json.NewEncoder(w).Encode(result)
2121+
}
2122+
20492123
// apiGetAccountModels 获取账户可用模型
20502124
func (h *Handler) apiGetAccountModels(w http.ResponseWriter, r *http.Request, id string) {
20512125
accounts := config.GetAccounts()
@@ -2204,7 +2278,7 @@ func (h *Handler) apiExportAccounts(w http.ResponseWriter, r *http.Request) {
22042278
type ExportCredentials struct {
22052279
AccessToken string `json:"accessToken"`
22062280
CsrfToken string `json:"csrfToken"`
2207-
RefreshToken string `json:"refreshToken,omitempty"`
2281+
RefreshToken string `json:"refreshToken"`
22082282
ClientID string `json:"clientId,omitempty"`
22092283
ClientSecret string `json:"clientSecret,omitempty"`
22102284
Region string `json:"region,omitempty"`

web/index.html

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,11 @@
130130
color: #374151;
131131
}
132132

133+
.btn-success {
134+
background: #10B981;
135+
color: white;
136+
}
137+
133138
.btn-sm {
134139
padding: 8px 12px;
135140
font-size: 13px;
@@ -1087,6 +1092,8 @@ <h1 class="logo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stro
10871092
'accounts.trial': '试用中',
10881093
'accounts.trialExpired': '已过期',
10891094
'accounts.trialToday': '今天到期',
1095+
'accounts.copyJSON': '复制 JSON',
1096+
'accounts.copyJSONSuccess': 'JSON 已复制到剪贴板',
10901097
'accounts.trialDays': '天后到期',
10911098
'time.expired': '已过期',
10921099
'time.minutes': '分钟',
@@ -1274,6 +1281,8 @@ <h1 class="logo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stro
12741281
'accounts.suspended': 'Suspended',
12751282
'accounts.refreshFailed': 'Refresh failed',
12761283
'accounts.confirmDelete': 'Confirm delete?',
1284+
'accounts.copyJSON': 'Copy JSON',
1285+
'accounts.copyJSONSuccess': 'JSON copied to clipboard',
12771286
'time.expired': 'Expired',
12781287
'time.minutes': 'min',
12791288
'time.hours': 'hr',
@@ -1617,6 +1626,7 @@ <h1 class="logo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stro
16171626
'<div class="account-actions">' +
16181627
'<button class="btn btn-sm btn-icon btn-secondary" onclick="refreshAccount(\'' + a.id + '\')" title="' + t('accounts.refresh') + '"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 4v6h-6M1 20v-6h6"/><path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/></svg></button>' +
16191628
'<button class="btn btn-sm btn-icon btn-secondary" onclick="showDetail(\'' + a.id + '\')" title="' + t('accounts.detail') + '"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg></button>' +
1629+
'<button class="btn btn-sm btn-icon btn-secondary" onclick="copyAccountJSON(\'' + a.id + '\', this)" title="' + t('accounts.copyJSON') + '"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg></button>' +
16201630
// 封禁账户不显示启用/禁用按钮
16211631
(a.banStatus && a.banStatus !== 'ACTIVE' ? '' :
16221632
'<button class="btn btn-sm ' + (a.enabled ? 'btn-secondary' : 'btn-primary') + '" onclick="toggleAccount(\'' + a.id + '\',' + !a.enabled + ')">' + (a.enabled ? t('accounts.disable') : t('accounts.enable')) + '</button>') +
@@ -1902,6 +1912,57 @@ <h1 class="logo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stro
19021912
alert(t('common.copied'));
19031913
}
19041914

1915+
async function copyAccountJSON(accountId, buttonElement) {
1916+
try {
1917+
// 从后端获取完整账号信息(包含敏感字段)
1918+
const res = await fetch('/admin/api/accounts/' + accountId + '/full', {
1919+
headers: { 'X-Admin-Password': password }
1920+
});
1921+
1922+
if (!res.ok) {
1923+
throw new Error('Failed to fetch account data');
1924+
}
1925+
1926+
const account = await res.json();
1927+
const jsonString = JSON.stringify(account, null, 2);
1928+
1929+
if (navigator.clipboard && navigator.clipboard.writeText) {
1930+
await navigator.clipboard.writeText(jsonString);
1931+
} else {
1932+
const textarea = document.createElement('textarea');
1933+
textarea.value = jsonString;
1934+
textarea.style.position = 'fixed';
1935+
textarea.style.opacity = '0';
1936+
document.body.appendChild(textarea);
1937+
textarea.select();
1938+
const success = document.execCommand('copy');
1939+
document.body.removeChild(textarea);
1940+
if (!success) throw new Error('execCommand failed');
1941+
}
1942+
1943+
showCopySuccess(buttonElement);
1944+
alert(t('accounts.copyJSONSuccess'));
1945+
} catch (error) {
1946+
console.error('Copy failed:', error);
1947+
alert(t('common.failed'));
1948+
}
1949+
}
1950+
1951+
function showCopySuccess(buttonElement) {
1952+
const originalHTML = buttonElement.innerHTML;
1953+
const originalClass = buttonElement.className;
1954+
1955+
buttonElement.disabled = true;
1956+
buttonElement.className = 'btn btn-sm btn-icon btn-success';
1957+
buttonElement.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>';
1958+
1959+
setTimeout(() => {
1960+
buttonElement.disabled = false;
1961+
buttonElement.className = originalClass;
1962+
buttonElement.innerHTML = originalHTML;
1963+
}, 800);
1964+
}
1965+
19051966
function showModal(type) {
19061967
const modal = document.getElementById('addModal');
19071968
const title = document.getElementById('modalTitle');

0 commit comments

Comments
 (0)