feat: Add account ban handling and UI updates (#11)
- Add ban status and reason fields to account configuration - Add account ban status and details handling in API refresh account function. - Add logic to handle account suspension and authentication errors, updating ban status accordingly. - Add and style badge classes for different account statuses and modify account status display logic.
This commit is contained in:
@@ -51,7 +51,10 @@ type Account struct {
|
||||
MachineId string `json:"machineId,omitempty"` // UUID machine identifier for request tracking
|
||||
|
||||
// Account status
|
||||
Enabled bool `json:"enabled"` // Whether account is active in the pool
|
||||
Enabled bool `json:"enabled"` // Whether account is active in the pool
|
||||
BanStatus string `json:"banStatus,omitempty"` // Ban status: "ACTIVE", "BANNED", "SUSPENDED"
|
||||
BanReason string `json:"banReason,omitempty"` // Reason for ban/suspension
|
||||
BanTime int64 `json:"banTime,omitempty"` // Timestamp when ban was detected
|
||||
|
||||
// Subscription information
|
||||
SubscriptionType string `json:"subscriptionType,omitempty"` // Tier: FREE, PRO, PRO_PLUS, or POWER
|
||||
|
||||
@@ -1401,6 +1401,9 @@ func (h *Handler) apiGetAccounts(w http.ResponseWriter, r *http.Request) {
|
||||
"provider": a.Provider,
|
||||
"region": a.Region,
|
||||
"enabled": a.Enabled,
|
||||
"banStatus": a.BanStatus,
|
||||
"banReason": a.BanReason,
|
||||
"banTime": a.BanTime,
|
||||
"expiresAt": a.ExpiresAt,
|
||||
"hasToken": a.AccessToken != "",
|
||||
"machineId": a.MachineId,
|
||||
@@ -1993,14 +1996,36 @@ func (h *Handler) apiRefreshAccount(w http.ResponseWriter, r *http.Request, id s
|
||||
// 获取账户信息
|
||||
info, err := RefreshAccountInfo(account)
|
||||
if err != nil {
|
||||
// 如果是 403/401,说明 token 无效,尝试刷新后重试
|
||||
// 检查是否为封禁相关错误
|
||||
errMsg := err.Error()
|
||||
if strings.Contains(errMsg, "TEMPORARILY_SUSPENDED") || strings.Contains(errMsg, "Account suspended") {
|
||||
// 封禁状态已在 RefreshAccountInfo 中处理,静默返回成功
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "Account status updated",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 如果是 403/401,说明 token 无效,尝试刷新后重试
|
||||
if strings.Contains(errMsg, "403") || strings.Contains(errMsg, "401") || strings.Contains(errMsg, "invalid") || strings.Contains(errMsg, "expired") {
|
||||
if refreshErr := refreshTokenIfNeeded(); refreshErr == nil {
|
||||
// 重试
|
||||
info, err = RefreshAccountInfo(account)
|
||||
if err != nil {
|
||||
// 重试后仍然失败,检查是否为封禁状态
|
||||
if strings.Contains(err.Error(), "TEMPORARILY_SUSPENDED") || strings.Contains(err.Error(), "Account suspended") {
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "Account status updated",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 其他错误才显示错误信息
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
|
||||
|
||||
@@ -136,9 +136,61 @@ func RefreshAccountInfo(account *config.Account) (*config.AccountInfo, error) {
|
||||
// 获取使用量和订阅信息
|
||||
usage, err := GetUsageLimits(account)
|
||||
if err != nil {
|
||||
// 检测封禁状态
|
||||
errMsg := err.Error()
|
||||
if strings.Contains(errMsg, "TEMPORARILY_SUSPENDED") {
|
||||
// 账户被暂时封禁,自动禁用并标记封禁状态
|
||||
fmt.Printf("[RefreshAccountInfo] Account %s is temporarily suspended: %v\n", account.Email, err)
|
||||
|
||||
// 更新账户封禁状态并自动禁用
|
||||
updatedAccount := *account
|
||||
updatedAccount.Enabled = false
|
||||
updatedAccount.BanStatus = "BANNED"
|
||||
updatedAccount.BanReason = "AWS temporarily suspended - unusual user activity detected"
|
||||
updatedAccount.BanTime = time.Now().Unix()
|
||||
|
||||
// 保存更新后的账户状态
|
||||
if updateErr := config.UpdateAccount(account.ID, updatedAccount); updateErr != nil {
|
||||
fmt.Printf("[RefreshAccountInfo] Failed to update account ban status: %v\n", updateErr)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("Account suspended: %w", err)
|
||||
} else if strings.Contains(errMsg, "403") || strings.Contains(errMsg, "401") ||
|
||||
strings.Contains(errMsg, "invalid") || strings.Contains(errMsg, "expired") {
|
||||
// Token 相关错误,可能需要重新认证
|
||||
fmt.Printf("[RefreshAccountInfo] Authentication error for %s: %v\n", account.Email, err)
|
||||
|
||||
// 更新账户封禁状态为认证失败并自动禁用
|
||||
updatedAccount := *account
|
||||
updatedAccount.Enabled = false
|
||||
updatedAccount.BanStatus = "BANNED"
|
||||
updatedAccount.BanReason = "Authentication failed - token invalid or expired"
|
||||
updatedAccount.BanTime = time.Now().Unix()
|
||||
|
||||
// 保存更新后的账户状态
|
||||
if updateErr := config.UpdateAccount(account.ID, updatedAccount); updateErr != nil {
|
||||
fmt.Printf("[RefreshAccountInfo] Failed to update account ban status: %v\n", updateErr)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("GetUsageLimits: %w", err)
|
||||
}
|
||||
|
||||
// 如果成功获取信息,清除封禁状态(如果之前被标记)
|
||||
if account.BanStatus != "" && account.BanStatus != "ACTIVE" {
|
||||
fmt.Printf("[RefreshAccountInfo] Account %s is now active, clearing ban status\n", account.Email)
|
||||
|
||||
updatedAccount := *account
|
||||
updatedAccount.BanStatus = "ACTIVE"
|
||||
updatedAccount.BanReason = ""
|
||||
updatedAccount.BanTime = 0
|
||||
|
||||
// 保存更新后的账户状态
|
||||
if updateErr := config.UpdateAccount(account.ID, updatedAccount); updateErr != nil {
|
||||
fmt.Printf("[RefreshAccountInfo] Failed to clear account ban status: %v\n", updateErr)
|
||||
}
|
||||
}
|
||||
|
||||
// 解析用户信息
|
||||
if usage.UserInfo != nil {
|
||||
info.Email = usage.UserInfo.Email
|
||||
|
||||
@@ -241,6 +241,21 @@
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-trial {
|
||||
background: #10b981;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-banned {
|
||||
background: #dc2626;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-suspended {
|
||||
background: #f59e0b;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
@@ -1062,6 +1077,9 @@
|
||||
'accounts.expired': '已过期',
|
||||
'accounts.disabled': '已禁用',
|
||||
'accounts.normal': '正常',
|
||||
'accounts.enabled': '已启用',
|
||||
'accounts.banned': '已封禁',
|
||||
'accounts.suspended': '已暂停',
|
||||
'accounts.refreshFailed': '刷新失败',
|
||||
'accounts.confirmDelete': '确定删除?',
|
||||
'accounts.mainQuota': '主配额',
|
||||
@@ -1251,6 +1269,9 @@
|
||||
'accounts.expired': 'Expired',
|
||||
'accounts.disabled': 'Disabled',
|
||||
'accounts.normal': 'Active',
|
||||
'accounts.enabled': 'Enabled',
|
||||
'accounts.banned': 'Banned',
|
||||
'accounts.suspended': 'Suspended',
|
||||
'accounts.refreshFailed': 'Refresh failed',
|
||||
'accounts.confirmDelete': 'Confirm delete?',
|
||||
'time.expired': 'Expired',
|
||||
@@ -1596,7 +1617,9 @@
|
||||
'<div class="account-actions">' +
|
||||
'<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>' +
|
||||
'<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>' +
|
||||
'<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>' +
|
||||
// 封禁账户不显示启用/禁用按钮
|
||||
(a.banStatus && a.banStatus !== 'ACTIVE' ? '' :
|
||||
'<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>') +
|
||||
'<button class="btn btn-sm btn-danger" onclick="deleteAccount(\'' + a.id + '\')">' + t('accounts.delete') + '</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
@@ -1644,10 +1667,42 @@
|
||||
return method;
|
||||
}
|
||||
function getStatusBadge(a) {
|
||||
if (!a.hasToken) return '<span class="badge badge-error">' + t('accounts.noToken') + '</span>';
|
||||
if (a.expiresAt && a.expiresAt < Date.now() / 1000) return '<span class="badge badge-warning">' + t('accounts.expired') + '</span>';
|
||||
if (!a.enabled) return '<span class="badge badge-warning">' + t('accounts.disabled') + '</span>';
|
||||
return '<span class="badge badge-success">' + t('accounts.normal') + '</span>';
|
||||
let badges = [];
|
||||
|
||||
// 检查是否为封禁状态
|
||||
const isBanned = a.banStatus && a.banStatus !== 'ACTIVE';
|
||||
|
||||
if (isBanned) {
|
||||
// 封禁账号:显示"封禁 + 禁用"
|
||||
if (a.banStatus === 'BANNED') {
|
||||
badges.push('<span class="badge badge-banned">' + t('accounts.banned') + '</span>');
|
||||
} else if (a.banStatus === 'SUSPENDED') {
|
||||
badges.push('<span class="badge badge-suspended">' + t('accounts.suspended') + '</span>');
|
||||
}
|
||||
// 封禁账号必定显示禁用状态
|
||||
badges.push('<span class="badge badge-warning">' + t('accounts.disabled') + '</span>');
|
||||
} else {
|
||||
// 正常账号:显示"正常 + 启用/禁用"
|
||||
|
||||
// 检查Token状态
|
||||
if (!a.hasToken) {
|
||||
badges.push('<span class="badge badge-error">' + t('accounts.noToken') + '</span>');
|
||||
} else if (a.expiresAt && a.expiresAt < Date.now() / 1000) {
|
||||
badges.push('<span class="badge badge-warning">' + t('accounts.expired') + '</span>');
|
||||
} else {
|
||||
// 有效Token的正常账号显示"正常"
|
||||
badges.push('<span class="badge badge-success">' + t('accounts.normal') + '</span>');
|
||||
}
|
||||
|
||||
// 显示启用/禁用状态
|
||||
if (a.enabled) {
|
||||
badges.push('<span class="badge badge-info">' + t('accounts.enabled') + '</span>');
|
||||
} else {
|
||||
badges.push('<span class="badge badge-warning">' + t('accounts.disabled') + '</span>');
|
||||
}
|
||||
}
|
||||
|
||||
return badges.join('');
|
||||
}
|
||||
function formatTokenExpiry(ts) {
|
||||
if (!ts) return '-';
|
||||
|
||||
Reference in New Issue
Block a user