fix: vá lỗ hổng RBAC + hardcode sàn ở /orders/place, nâng cấp mã hoá và CORS

- orders.place_order: yêu cầu role trader/admin (get_current_trader_user)
  thay vì bất kỳ user đã đăng nhập nào — viewer không còn đặt được lệnh thật
- orders.place_order: resolve exchange theo OrderRequest.exchange thay vì
  hardcode "mexc", fallback về credential active gần nhất nếu không truyền
- security.py: mã hoá API key chuyển AES-256-CBC -> AES-256-GCM (có xác thực
  toàn vẹn), giữ đường giải mã cũ để credential đã lưu trước đây không hỏng
- main_api.py: CORS_ORIGINS rỗng -> deny-all thay vì fallback "*" (kèm
  allow_credentials=True là cấu hình nguy hiểm)
- docker-compose.yml: đồng bộ DB_PASSWORD giữa backend-api/scheduler và db
- frontend: OrderPanel/DashboardPage truyền kèm exchange đang chọn khi đặt lệnh

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Le
2026-07-03 21:27:47 +07:00
parent 88ce9cdd2d
commit 6c1edcda34
7 changed files with 85 additions and 35 deletions
+20 -13
View File
@@ -9,7 +9,7 @@ from fastapi import APIRouter, Depends, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.deps import get_current_user, get_db_session
from app.core.deps import get_current_trader_user, get_current_user, get_db_session
from app.core.exceptions import NotFoundException, ValidationException
from app.core.security import decrypt_api_key
from app.exchange.factory import factory as exchange_factory
@@ -26,12 +26,13 @@ router = APIRouter(prefix="/orders", tags=["orders"])
async def place_order(
req: OrderRequest,
db: AsyncSession = Depends(get_db_session),
current_user: User = Depends(get_current_user),
current_user: User = Depends(get_current_trader_user),
) -> OrderData:
"""Place an order on a connected exchange.
Uses the user's saved API credentials for the exchange.
The exchange name is inferred from the symbol or passed explicitly.
Uses the user's saved API credentials for the exchange. If ``req.exchange``
is given, that exchange's credential is used; otherwise falls back to the
user's only (or first) active credential.
"""
# Currently only market orders are supported via this endpoint
if req.order_type not in ("market", "limit"):
@@ -41,27 +42,33 @@ async def place_order(
if req.amount <= 0:
raise ValidationException(detail="Amount must be positive")
# Infer exchange name from symbol prefix heuristics, or default to mexc
# In a more advanced setup the user would specify exchange_id in the request
exchange_name = "mexc"
# Find the user's active credential for this exchange
result = await db.execute(
# Find the user's active credential — filter by requested exchange if given,
# otherwise fall back to whichever active credential the user has.
query = (
select(ExchangeCredential)
.join(Exchange, Exchange.id == ExchangeCredential.exchange_id)
.where(
ExchangeCredential.user_id == current_user.id,
Exchange.name == exchange_name,
ExchangeCredential.is_active == True,
)
)
cred = result.scalar_one_or_none()
if req.exchange:
query = query.where(Exchange.name == req.exchange.lower())
else:
query = query.order_by(ExchangeCredential.created_at.desc())
result = await db.execute(query)
cred = result.scalars().first()
if cred is None:
target = req.exchange or "any exchange"
raise NotFoundException(
detail=f"No active API key found for {exchange_name}. "
detail=f"No active API key found for {target}. "
f"Go to Profile → API Keys to add one."
)
exchange_result = await db.execute(select(Exchange).where(Exchange.id == cred.exchange_id))
exchange_name = exchange_result.scalar_one().name
# Decrypt the stored API key/secret
try:
# api_key is stored as plaintext (masked in responses), api_secret is encrypted