1
This commit is contained in:
parent
6ac175038e
commit
13f2845201
@ -1,7 +1,7 @@
|
||||
"""
|
||||
认证API
|
||||
"""
|
||||
from fastapi import APIRouter, HTTPException, Depends, Request
|
||||
from fastapi import APIRouter, HTTPException, Depends, Request, Response
|
||||
from app.models.auth import (
|
||||
SendCodeRequest, SendCodeResponse,
|
||||
LoginRequest, LoginResponse,
|
||||
@ -12,11 +12,54 @@ from app.services.auth_service import auth_service
|
||||
from app.services.jwt_service import jwt_service
|
||||
from app.services.db_service import db_service
|
||||
from app.middleware.auth_middleware import get_current_user, get_client_ip
|
||||
from app.middleware.console_auth import (
|
||||
CONSOLE_AUTH_COOKIE,
|
||||
create_console_access_token,
|
||||
require_console_access,
|
||||
)
|
||||
from app.config import get_settings
|
||||
from app.utils.logger import logger
|
||||
|
||||
router = APIRouter(prefix="/api/auth", tags=["认证"])
|
||||
|
||||
|
||||
@router.post("/console-login")
|
||||
async def console_login(request: Request, response: Response):
|
||||
try:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
body = {}
|
||||
|
||||
password = str(body.get("password") or "")
|
||||
settings = get_settings()
|
||||
if not password or password != settings.console_access_password:
|
||||
raise HTTPException(status_code=403, detail="访问密码错误")
|
||||
|
||||
token = create_console_access_token()
|
||||
max_age = max(1, int(settings.console_access_expire_days or 30)) * 24 * 60 * 60
|
||||
response.set_cookie(
|
||||
key=CONSOLE_AUTH_COOKIE,
|
||||
value=token,
|
||||
max_age=max_age,
|
||||
httponly=True,
|
||||
samesite="lax",
|
||||
secure=False,
|
||||
path="/",
|
||||
)
|
||||
return {"success": True, "message": "总控台登录成功"}
|
||||
|
||||
|
||||
@router.post("/console-logout")
|
||||
async def console_logout(response: Response):
|
||||
response.delete_cookie(CONSOLE_AUTH_COOKIE, path="/")
|
||||
return {"success": True, "message": "已退出总控台"}
|
||||
|
||||
|
||||
@router.get("/console-session")
|
||||
async def get_console_session(_: dict = Depends(require_console_access)):
|
||||
return {"success": True, "authenticated": True}
|
||||
|
||||
|
||||
@router.post("/send-code", response_model=SendCodeResponse)
|
||||
async def send_verification_code(
|
||||
request_data: SendCodeRequest,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
"""
|
||||
交易 API
|
||||
"""
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from fastapi import APIRouter, HTTPException, Query, Depends
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel
|
||||
@ -13,6 +13,7 @@ from app.services.db_service import db_service
|
||||
from app.services.runtime_status_service import get_runtime_status
|
||||
from app.utils.logger import logger
|
||||
from app.crypto_agent.crypto_agent import get_crypto_agent
|
||||
from app.middleware.console_auth import require_console_access
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/trading", tags=["交易"])
|
||||
@ -249,7 +250,7 @@ async def get_platform_halts():
|
||||
|
||||
|
||||
@router.get("/execution-controls")
|
||||
async def get_execution_controls():
|
||||
async def get_execution_controls(_: dict = Depends(require_console_access)):
|
||||
"""获取目标级自动交易开关状态"""
|
||||
try:
|
||||
agent = get_crypto_agent()
|
||||
@ -263,7 +264,7 @@ async def get_execution_controls():
|
||||
|
||||
|
||||
@router.post("/execution-controls")
|
||||
async def set_execution_controls(request: ExecutionControlRequest):
|
||||
async def set_execution_controls(request: ExecutionControlRequest, _: dict = Depends(require_console_access)):
|
||||
"""设置目标级自动交易开关"""
|
||||
try:
|
||||
agent = get_crypto_agent()
|
||||
@ -286,7 +287,7 @@ async def set_execution_controls(request: ExecutionControlRequest):
|
||||
|
||||
|
||||
@router.post("/platform-halts/resume")
|
||||
async def resume_platform(request: ResumePlatformRequest):
|
||||
async def resume_platform(request: ResumePlatformRequest, _: dict = Depends(require_console_access)):
|
||||
"""手动恢复指定平台执行"""
|
||||
try:
|
||||
agent = get_crypto_agent()
|
||||
|
||||
@ -2,10 +2,11 @@
|
||||
系统状态 API
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from typing import Dict, Any
|
||||
import numpy as np
|
||||
from app.config import get_settings
|
||||
from app.middleware.console_auth import require_console_access
|
||||
from app.utils.logger import logger
|
||||
from app.utils.system_status import get_system_monitor
|
||||
from app.crypto_agent.crypto_agent import get_crypto_agent
|
||||
@ -339,7 +340,7 @@ async def get_agent_status(agent_id: str):
|
||||
|
||||
|
||||
@router.get("/console", response_model=Dict[str, Any])
|
||||
async def get_console_snapshot():
|
||||
async def get_console_snapshot(_: dict = Depends(require_console_access)):
|
||||
"""
|
||||
获取总控台快照
|
||||
|
||||
|
||||
@ -64,6 +64,8 @@ class Settings(BaseSettings):
|
||||
# 安全配置
|
||||
secret_key: str = "change-this-secret-key-in-production"
|
||||
rate_limit: str = "100/minute"
|
||||
console_access_password: str = "223388" # 总控台访问密码
|
||||
console_access_expire_days: int = 30 # 总控台访问会话有效期
|
||||
|
||||
# JWT配置
|
||||
jwt_algorithm: str = "HS256"
|
||||
|
||||
@ -2447,20 +2447,24 @@ class CryptoAgent:
|
||||
if setup_type in {'trend_continuation_pullback', 'deep_pullback_continuation', 'breakout_pullback'}:
|
||||
if entry_type != 'limit':
|
||||
return False, f"{setup_type} 应使用 limit 等待回踩/反抽"
|
||||
if location_tag == 'far_from_trade_zone':
|
||||
return False, f"{setup_type} 当前远离有效回踩交易区"
|
||||
if pullback_quality != 'healthy_pullback':
|
||||
return False, f"{setup_type} 缺少健康缩量回调证据"
|
||||
if location_tag in {'far_from_trade_zone', 'middle_of_range'}:
|
||||
return False, f"{setup_type} 当前不在有效回踩交易区"
|
||||
return True, f"{setup_type} 缺少健康缩量回调证据,降级执行"
|
||||
if location_tag == 'middle_of_range':
|
||||
return True, f"{setup_type} 当前位于区间中部,降级执行"
|
||||
|
||||
if setup_type == 'range_reversal':
|
||||
if location_tag == 'far_from_trade_zone':
|
||||
return False, "区间反转 setup 远离关键交易区"
|
||||
if location_tag not in {'near_range_support', 'near_range_resistance'}:
|
||||
return False, "区间反转 setup 不在区间边界"
|
||||
return True, "区间反转 setup 不在区间边界,降级执行"
|
||||
if rejection_signal not in {'bullish_rejection', 'bearish_rejection'} and entry_type == 'market':
|
||||
return False, "区间反转现价执行缺少明确拒绝信号"
|
||||
|
||||
if setup_type == 'trend_reversal':
|
||||
if rejection_signal not in {'bullish_rejection', 'bearish_rejection'}:
|
||||
return False, "趋势反转 setup 缺少拒绝/结构切换证据"
|
||||
return True, "趋势反转 setup 缺少明确拒绝信号,降级执行"
|
||||
|
||||
if exhaustion_risk in {'upside_climax', 'downside_climax'} and setup_type != 'trend_reversal':
|
||||
return False, "当前量价处于高潮风险,非反转 setup 不执行"
|
||||
@ -3995,6 +3999,9 @@ class CryptoAgent:
|
||||
setup_passed, setup_reason = self._check_setup_execution_constraints(signal)
|
||||
if not setup_passed:
|
||||
return False, f"setup 过滤: {setup_reason}"
|
||||
if setup_reason != "setup 约束通过":
|
||||
signal["_setup_execution_warning"] = setup_reason
|
||||
logger.info(f"[{platform_name}] ⚠️ setup 降级执行: {setup_reason}")
|
||||
|
||||
current_leverage = account.get('current_total_leverage', 0)
|
||||
max_leverage = account.get('max_total_leverage', 10)
|
||||
|
||||
39
backend/app/middleware/console_auth.py
Normal file
39
backend/app/middleware/console_auth.py
Normal file
@ -0,0 +1,39 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict
|
||||
|
||||
from fastapi import Cookie, HTTPException
|
||||
from jose import JWTError, jwt
|
||||
|
||||
from app.config import get_settings
|
||||
|
||||
|
||||
CONSOLE_AUTH_COOKIE = "console_access_token"
|
||||
|
||||
|
||||
def create_console_access_token() -> str:
|
||||
settings = get_settings()
|
||||
expire = datetime.utcnow() + timedelta(days=max(1, int(settings.console_access_expire_days or 30)))
|
||||
payload = {
|
||||
"scope": "console_access",
|
||||
"exp": expire,
|
||||
"iat": datetime.utcnow(),
|
||||
}
|
||||
return jwt.encode(payload, settings.secret_key, algorithm=settings.jwt_algorithm)
|
||||
|
||||
|
||||
def verify_console_access_token(token: str) -> Dict:
|
||||
settings = get_settings()
|
||||
try:
|
||||
payload = jwt.decode(token, settings.secret_key, algorithms=[settings.jwt_algorithm])
|
||||
except JWTError as exc:
|
||||
raise HTTPException(status_code=401, detail="总控台访问已失效,请重新登录") from exc
|
||||
|
||||
if payload.get("scope") != "console_access":
|
||||
raise HTTPException(status_code=401, detail="总控台访问凭证无效")
|
||||
return payload
|
||||
|
||||
|
||||
def require_console_access(console_access_token: str | None = Cookie(default=None, alias=CONSOLE_AUTH_COOKIE)) -> Dict:
|
||||
if not console_access_token:
|
||||
raise HTTPException(status_code=401, detail="请先登录总控台")
|
||||
return verify_console_access_token(console_access_token)
|
||||
@ -52,6 +52,70 @@
|
||||
mask-image: linear-gradient(180deg, rgba(0,0,0,0.45), rgba(0,0,0,0.9));
|
||||
}
|
||||
|
||||
.auth-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
background: rgba(3, 8, 14, 0.82);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.auth-overlay.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
width: min(420px, 100%);
|
||||
padding: 28px;
|
||||
border-radius: 24px;
|
||||
background: rgba(9, 18, 28, 0.96);
|
||||
border: 1px solid rgba(128, 169, 202, 0.24);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.auth-card h2 {
|
||||
margin: 0 0 10px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.auth-card p {
|
||||
margin: 0 0 18px;
|
||||
color: var(--muted);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.auth-field {
|
||||
width: 100%;
|
||||
padding: 14px 16px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
background: rgba(255,255,255,0.04);
|
||||
color: var(--text);
|
||||
font-size: 15px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.auth-field:focus {
|
||||
border-color: rgba(126, 200, 255, 0.36);
|
||||
}
|
||||
|
||||
.auth-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.auth-error {
|
||||
min-height: 20px;
|
||||
margin-top: 12px;
|
||||
color: var(--danger);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.console-shell {
|
||||
width: min(1600px, calc(100vw - 32px));
|
||||
margin: 0 auto;
|
||||
@ -2466,6 +2530,19 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="auth-overlay" id="consoleAuthOverlay">
|
||||
<div class="auth-card">
|
||||
<div class="eyebrow">Console Access</div>
|
||||
<h2>输入访问密码</h2>
|
||||
<p>总控台包含账户权益、持仓、挂单和执行控制,需要先完成访问验证。</p>
|
||||
<input id="consolePasswordInput" class="auth-field" type="password" placeholder="请输入总控台访问密码">
|
||||
<div class="auth-actions">
|
||||
<button class="action-btn action-primary" id="consoleLoginBtn">进入总控台</button>
|
||||
</div>
|
||||
<div class="auth-error" id="consoleAuthError"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="console-shell">
|
||||
<section class="hero">
|
||||
<div class="hero-card hero-main">
|
||||
@ -2691,6 +2768,7 @@
|
||||
let cachedExecutionEvents = [];
|
||||
let cachedConsoleData = null;
|
||||
let revealSensitiveData = false;
|
||||
let consoleAuthenticated = false;
|
||||
const SENSITIVE_VISIBILITY_KEY = 'console_sensitive_visible_v2';
|
||||
const HUB_PREFERENCE_KEY = 'console_hub_active_v1';
|
||||
|
||||
@ -2712,10 +2790,24 @@
|
||||
return Number.isFinite(num) ? `$${formatNumber(num, 2)}` : '-';
|
||||
}
|
||||
|
||||
function parseConsoleDate(value) {
|
||||
if (!value) return null;
|
||||
if (value instanceof Date) return value;
|
||||
const text = String(value).trim();
|
||||
if (!text) return null;
|
||||
|
||||
const normalized = /([zZ]|[+-]\d{2}:\d{2})$/.test(text) ? text : `${text}Z`;
|
||||
const date = new Date(normalized);
|
||||
if (Number.isNaN(date.getTime())) return null;
|
||||
return date;
|
||||
}
|
||||
|
||||
function formatTime(value) {
|
||||
if (!value) return '-';
|
||||
try {
|
||||
return new Date(value).toLocaleString('zh-CN', { hour12: false });
|
||||
const date = parseConsoleDate(value);
|
||||
if (!date) return String(value);
|
||||
return date.toLocaleString('zh-CN', { hour12: false, timeZone: 'Asia/Shanghai' });
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
@ -2723,7 +2815,8 @@
|
||||
|
||||
function relativeTime(value) {
|
||||
if (!value) return '-';
|
||||
const target = new Date(value);
|
||||
const target = parseConsoleDate(value);
|
||||
if (!target) return String(value);
|
||||
if (Number.isNaN(target.getTime())) return String(value);
|
||||
const diff = Date.now() - target.getTime();
|
||||
const sec = Math.max(0, Math.floor(diff / 1000));
|
||||
@ -2772,6 +2865,64 @@
|
||||
`;
|
||||
}
|
||||
|
||||
function showConsoleAuthOverlay(message = '') {
|
||||
document.getElementById('consoleAuthOverlay').classList.remove('hidden');
|
||||
document.getElementById('consoleAuthError').textContent = message;
|
||||
}
|
||||
|
||||
function hideConsoleAuthOverlay() {
|
||||
document.getElementById('consoleAuthOverlay').classList.add('hidden');
|
||||
document.getElementById('consoleAuthError').textContent = '';
|
||||
}
|
||||
|
||||
async function ensureConsoleSession() {
|
||||
const response = await fetch('/api/auth/console-session');
|
||||
if (!response.ok) {
|
||||
consoleAuthenticated = false;
|
||||
showConsoleAuthOverlay(response.status === 401 ? '请输入访问密码后继续。' : '总控台认证失败');
|
||||
return false;
|
||||
}
|
||||
|
||||
consoleAuthenticated = true;
|
||||
hideConsoleAuthOverlay();
|
||||
return true;
|
||||
}
|
||||
|
||||
async function loginConsole() {
|
||||
const input = document.getElementById('consolePasswordInput');
|
||||
const password = input.value.trim();
|
||||
if (!password) {
|
||||
showConsoleAuthOverlay('请输入访问密码');
|
||||
return;
|
||||
}
|
||||
|
||||
const button = document.getElementById('consoleLoginBtn');
|
||||
button.disabled = true;
|
||||
button.textContent = '验证中...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/console-login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password })
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!response.ok || !result.success) {
|
||||
throw new Error(result.detail || result.message || '访问密码错误');
|
||||
}
|
||||
|
||||
input.value = '';
|
||||
consoleAuthenticated = true;
|
||||
hideConsoleAuthOverlay();
|
||||
loadConsole();
|
||||
} catch (error) {
|
||||
showConsoleAuthOverlay(error.message || '登录失败');
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.textContent = '进入总控台';
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeSeverity(severity) {
|
||||
const value = String(severity || '').toLowerCase();
|
||||
if (['danger', 'error', 'critical'].includes(value)) return 'danger';
|
||||
@ -2783,7 +2934,8 @@
|
||||
const cutoff = Date.now() - hours * 3600 * 1000;
|
||||
return (events || []).filter((event) => {
|
||||
const rawTime = event?.timestamp || event?.created_at || event?.opened_at;
|
||||
const time = rawTime ? new Date(rawTime).getTime() : 0;
|
||||
const parsed = parseConsoleDate(rawTime);
|
||||
const time = parsed ? parsed.getTime() : 0;
|
||||
if (!time || Number.isNaN(time) || time < cutoff) return false;
|
||||
return typeof matcher === 'function' ? matcher(event) : true;
|
||||
}).length;
|
||||
@ -2804,7 +2956,7 @@
|
||||
const protectionCoverage = positions.length > 0 ? (completeProtectionCount / positions.length) * 100 : 100;
|
||||
const conversionRate = recentSignal24h > 0 ? (success24h / recentSignal24h) * 100 : 0;
|
||||
const staleEntryOrders = entryOrders.filter((order) => {
|
||||
const created = order.created_at ? new Date(order.created_at).getTime() : 0;
|
||||
const created = order.created_at ? (parseConsoleDate(order.created_at)?.getTime() || 0) : 0;
|
||||
return created && !Number.isNaN(created) && (Date.now() - created) > (30 * 60 * 1000);
|
||||
}).length;
|
||||
|
||||
@ -2937,17 +3089,21 @@
|
||||
}
|
||||
|
||||
function findMatchingEvent(signal, laneKeyword, events, platform) {
|
||||
const signalTime = signal?.created_at ? new Date(signal.created_at).getTime() : 0;
|
||||
const signalTime = signal?.created_at ? (parseConsoleDate(signal.created_at)?.getTime() || 0) : 0;
|
||||
const symbol = signal?.symbol;
|
||||
const candidates = (events || []).filter((event) => {
|
||||
if (event.symbol !== symbol) return false;
|
||||
if (!eventPlatformMatches(event, platform)) return false;
|
||||
if (laneKeyword && event.signal_timeframe_text && !String(event.signal_timeframe_text).includes(laneKeyword)) return false;
|
||||
const eventTime = event?.timestamp ? new Date(event.timestamp).getTime() : 0;
|
||||
const eventTime = event?.timestamp ? (parseConsoleDate(event.timestamp)?.getTime() || 0) : 0;
|
||||
if (!signalTime || !eventTime || Number.isNaN(signalTime) || Number.isNaN(eventTime)) return true;
|
||||
return Math.abs(eventTime - signalTime) <= 12 * 3600 * 1000;
|
||||
});
|
||||
return candidates.sort((a, b) => new Date(b.timestamp || 0) - new Date(a.timestamp || 0))[0] || null;
|
||||
return candidates.sort((a, b) => {
|
||||
const bt = parseConsoleDate(b.timestamp)?.getTime() || 0;
|
||||
const at = parseConsoleDate(a.timestamp)?.getTime() || 0;
|
||||
return bt - at;
|
||||
})[0] || null;
|
||||
}
|
||||
|
||||
function findMatchingPosition(signal, platform, positions) {
|
||||
@ -3948,6 +4104,10 @@
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
if (response.status === 401) {
|
||||
consoleAuthenticated = false;
|
||||
showConsoleAuthOverlay('访问会话已失效,请重新输入密码');
|
||||
}
|
||||
throw new Error(result.detail || result.message || '恢复失败');
|
||||
}
|
||||
|
||||
@ -3978,6 +4138,10 @@
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
if (response.status === 401) {
|
||||
consoleAuthenticated = false;
|
||||
showConsoleAuthOverlay('访问会话已失效,请重新输入密码');
|
||||
}
|
||||
throw new Error(result.detail || result.message || '更新失败');
|
||||
}
|
||||
|
||||
@ -3992,8 +4156,17 @@
|
||||
|
||||
async function loadConsole() {
|
||||
try {
|
||||
if (!consoleAuthenticated) {
|
||||
const ok = await ensureConsoleSession();
|
||||
if (!ok) return;
|
||||
}
|
||||
setFeedback('');
|
||||
const response = await fetch('/api/system/console');
|
||||
if (response.status === 401) {
|
||||
consoleAuthenticated = false;
|
||||
showConsoleAuthOverlay('访问会话已失效,请重新输入密码');
|
||||
return;
|
||||
}
|
||||
const result = await response.json();
|
||||
|
||||
if (result.status !== 'success') {
|
||||
@ -4045,12 +4218,18 @@
|
||||
applyAutoRefreshState();
|
||||
});
|
||||
document.getElementById('toggleSensitiveBtn').addEventListener('click', toggleSensitiveData);
|
||||
document.getElementById('consoleLoginBtn').addEventListener('click', loginConsole);
|
||||
document.getElementById('consolePasswordInput').addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter') loginConsole();
|
||||
});
|
||||
|
||||
loadSensitivePreference();
|
||||
initTabs();
|
||||
initHubNavigation();
|
||||
applyAutoRefreshState();
|
||||
loadConsole();
|
||||
ensureConsoleSession().then((ok) => {
|
||||
if (ok) loadConsole();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user