diff --git a/backend/api/routes/accounts.py b/backend/api/routes/accounts.py index 81d6c86..9213fd9 100644 --- a/backend/api/routes/accounts.py +++ b/backend/api/routes/accounts.py @@ -57,7 +57,14 @@ async def list_my_accounts(user: Dict[str, Any] = Depends(get_current_user)): else: accounts = UserAccountMembership.get_user_accounts(user["id"]) - # 补充一些运行时信息(可选) + # 补充一些运行时信息(可选),并处理敏感字段 + for acc in accounts: + acc['has_api_key'] = bool(acc.get('api_key_enc')) + acc['has_api_secret'] = bool(acc.get('api_secret_enc')) + # 移除加密字段,不直接暴露给前端 + acc.pop('api_key_enc', None) + acc.pop('api_secret_enc', None) + return accounts except Exception as e: raise HTTPException(status_code=500, detail=str(e)) diff --git a/backend/api/routes/admin.py b/backend/api/routes/admin.py index 18ea835..3bf8ac7 100644 --- a/backend/api/routes/admin.py +++ b/backend/api/routes/admin.py @@ -39,18 +39,17 @@ async def list_users_with_accounts(_admin: Dict[str, Any] = Depends(get_admin_us for u in users: uid = u['id'] - memberships = UserAccountMembership.list_for_user(uid) + memberships = UserAccountMembership.get_user_accounts(uid) user_accounts = [] for m in memberships or []: - aid = int(m.get("account_id")) - a = Account.get(aid) - if a: - user_accounts.append({ - "id": aid, - "name": a.get("name"), - "status": a.get("status"), - "role": m.get("role") - }) + user_accounts.append({ + "id": m.get("id"), + "name": m.get("name"), + "status": m.get("status"), + "role": m.get("role"), + "has_api_key": bool(m.get("api_key_enc")), + "has_api_secret": bool(m.get("api_secret_enc")) + }) out.append({ "id": uid, diff --git a/backend/database/models.py b/backend/database/models.py index ebe47f5..95a44e4 100644 --- a/backend/database/models.py +++ b/backend/database/models.py @@ -67,7 +67,7 @@ class Account: @staticmethod def list_all(): - return db.execute_query("SELECT id, name, status, created_at, updated_at FROM accounts ORDER BY id ASC") + return db.execute_query("SELECT id, name, status, created_at, updated_at, api_key_enc, api_secret_enc, use_testnet FROM accounts ORDER BY id ASC") @staticmethod def create(name: str, api_key: str = "", api_secret: str = "", use_testnet: bool = False, status: str = "active"): @@ -229,7 +229,7 @@ class UserAccountMembership: """获取用户关联的账号列表(包含账号详情)""" return db.execute_query( """ - SELECT a.id, a.name, a.status, a.created_at, a.updated_at, m.role + SELECT a.id, a.name, a.status, a.created_at, a.updated_at, a.api_key_enc, a.api_secret_enc, m.role FROM accounts a JOIN user_account_memberships m ON a.id = m.account_id WHERE m.user_id = %s diff --git a/frontend/src/components/AdminDashboard.css b/frontend/src/components/AdminDashboard.css index 8b2c424..74463b3 100644 --- a/frontend/src/components/AdminDashboard.css +++ b/frontend/src/components/AdminDashboard.css @@ -108,6 +108,16 @@ color: #c62828; } +.status-badge.running { + background-color: #e3f2fd; + color: #1976d2; +} + +.status-badge.stopped { + background-color: #f5f5f5; + color: #616161; +} + .profit { color: #2e7d32; font-weight: 500; @@ -123,6 +133,7 @@ display: flex; flex-direction: column; gap: 16px; + margin-bottom: 40px; } .user-group-card { @@ -253,3 +264,258 @@ .btn-mini.stop { background-color: #f44336; } + +/* Add Account Section in User Group */ +.add-account-section { + margin-top: 16px; + padding-top: 16px; + border-top: 1px dashed #eee; +} + +.add-account-section h4 { + margin-top: 0; + margin-bottom: 12px; + font-size: 14px; + color: #666; +} + +.add-account-form { + display: flex; + gap: 12px; + align-items: center; +} + +.add-account-form select { + padding: 6px 12px; + border: 1px solid #ddd; + border-radius: 4px; + min-width: 150px; +} + +/* Account Manager Section */ +.accounts-manager-section { + margin-top: 40px; +} + +.accounts-manager-section h3 { + margin-bottom: 24px; +} + +.accounts-manager-grid { + display: grid; + grid-template-columns: 350px 1fr; + gap: 24px; + align-items: start; +} + +@media (max-width: 900px) { + .accounts-manager-grid { + grid-template-columns: 1fr; + } +} + +.create-account-card { + height: auto; +} + +.account-list-card { + min-height: 400px; +} + +.form-group { + margin-bottom: 16px; +} + +.form-group label { + display: block; + margin-bottom: 6px; + font-weight: 500; + font-size: 14px; + color: #555; +} + +.form-group input[type="text"], +.form-group input[type="password"], +.form-group select { + width: 100%; + padding: 8px 12px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; + box-sizing: border-box; +} + +.form-group input:focus, +.form-group select:focus { + border-color: #2196f3; + outline: none; +} + +.form-group.checkbox label { + display: flex; + align-items: center; + gap: 8px; + font-weight: normal; + cursor: pointer; +} + +.form-group.checkbox input[type="checkbox"] { + width: auto; + margin: 0; +} + +.btn-primary { + background-color: #2196f3; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: background-color 0.2s; +} + +.btn-primary:hover { + background-color: #1976d2; +} + +.btn-primary:disabled { + background-color: #bbdefb; + cursor: not-allowed; +} + +.btn-primary.full-width { + width: 100%; +} + +.btn-secondary { + background-color: #f5f5f5; + color: #333; + border: 1px solid #ddd; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + font-size: 14px; +} + +.btn-secondary:hover { + background-color: #e0e0e0; +} + +.btn-icon { + width: 28px; + height: 28px; + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid #ddd; + background: white; + border-radius: 4px; + cursor: pointer; + color: #666; + font-size: 14px; +} + +.btn-icon:hover { + background-color: #f5f5f5; + color: #333; +} + +.btn-icon.danger { + color: #d32f2f; + border-color: #ffcdd2; +} + +.btn-icon.danger:hover { + background-color: #ffebee; +} + +.btn-sm { + padding: 4px 8px; + font-size: 12px; + border: 1px solid #ddd; + background: white; + border-radius: 4px; + cursor: pointer; + color: #666; +} + +.btn-sm:hover { + background-color: #f5f5f5; + color: #333; +} + +.btn-sm:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.action-buttons { + display: flex; + gap: 8px; +} + +.message { + padding: 10px 16px; + border-radius: 4px; + margin-bottom: 16px; + font-size: 14px; +} + +.message.success { + background-color: #e8f5e9; + color: #2e7d32; + border: 1px solid #c8e6c9; +} + +.message.error { + background-color: #ffebee; + color: #c62828; + border: 1px solid #ffcdd2; +} + +/* Modal */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0,0,0,0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.modal-content { + background: white; + padding: 24px; + border-radius: 8px; + width: 100%; + max-width: 400px; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); +} + +.modal-content h4 { + margin-top: 0; + margin-bottom: 20px; + font-size: 18px; +} + +.modal-actions { + display: flex; + justify-content: flex-end; + gap: 12px; + margin-top: 24px; +} + +.role-tag { + display: inline-block; + padding: 2px 8px; + border-radius: 12px; + font-size: 11px; + text-transform: uppercase; + background-color: #f0f0f0; + color: #666; +}