Files
DinQuant/backend_api_python/app/routes/credentials.py
T
dienakdz 87f2845483 Refactor code for improved readability and consistency
- Cleaned up whitespace and formatting in various files including http.py, language.py, logger.py, safe_exec.py, and SQL migration scripts.
- Consolidated import statements and removed unnecessary blank lines.
- Updated logging configuration for better clarity.
- Enhanced the safe execution code with improved error handling and logging.
- Removed commented-out code and unnecessary variables in backfill_zero_trades.py and other scripts.
- Added a pyproject.toml for Ruff and Vulture configuration.
- Introduced requirements-dev.txt for development dependencies.
- Removed commented-out stock entries in init.sql for cleaner migration scripts.
2026-04-09 14:30:51 +07:00

287 lines
9.6 KiB
Python

"""
Exchange credentials vault.
encrypted_config stores Fernet ciphertext derived from SECRET_KEY (see app.utils.credential_crypto).
"""
import json
import traceback
import requests as rq
from flask import Blueprint, g, jsonify, request
from app.utils.auth import login_required
from app.utils.credential_crypto import decrypt_credential_blob, encrypt_credential_blob
from app.utils.db import get_db_connection
from app.utils.logger import get_logger
logger = get_logger(__name__)
credentials_bp = Blueprint("credentials", __name__)
def _api_key_hint(api_key: str) -> str:
if not api_key:
return ""
s = str(api_key)
if len(s) <= 8:
return s[:2] + "***"
return f"{s[:4]}...{s[-4:]}"
@credentials_bp.route("/list", methods=["GET"])
@login_required
def list_credentials():
"""List all credentials for the current user."""
try:
user_id = g.user_id
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
"""
SELECT id, user_id, name, exchange_id, api_key_hint, encrypted_config, created_at, updated_at
FROM qd_exchange_credentials
WHERE user_id = %s
ORDER BY id DESC
""",
(user_id,),
)
rows = cur.fetchall() or []
cur.close()
items = []
for row in rows:
item = dict(row or {})
item["enable_demo_trading"] = False
try:
plain = decrypt_credential_blob(item.get("encrypted_config"))
cfg = json.loads(plain) if plain else {}
item["enable_demo_trading"] = bool(cfg.get("enable_demo_trading") or cfg.get("enableDemoTrading"))
except Exception:
item["enable_demo_trading"] = False
item.pop("encrypted_config", None)
items.append(item)
return jsonify({"code": 1, "msg": "success", "data": {"items": items}})
except Exception as e:
logger.error(f"list_credentials failed: {str(e)}")
logger.error(traceback.format_exc())
return jsonify({"code": 0, "msg": str(e), "data": {"items": []}}), 500
CRYPTO_EXCHANGES = [
"binance",
"okx",
"bitget",
"bybit",
"coinbaseexchange",
"kraken",
"kucoin",
"gate",
"bitfinex",
"deepcoin",
"htx",
]
def _egress_ipify(url: str) -> str:
try:
r = rq.get(url, timeout=8)
if r.status_code != 200:
return ""
j = r.json()
if not isinstance(j, dict):
return ""
return str(j.get("ip") or "").strip()
except Exception:
return ""
@credentials_bp.route("/egress-ip", methods=["GET"])
@login_required
def get_egress_ip():
"""
Public egress IPv4/IPv6 of this API server (for exchange API key IP whitelist).
Uses ipify's v4-only / v6-only endpoints so each family is detected independently.
"""
ipv4 = _egress_ipify("https://api4.ipify.org?format=json")
ipv6 = _egress_ipify("https://api6.ipify.org?format=json")
return jsonify(
{
"code": 1,
"msg": "success",
"data": {
"ipv4": ipv4 or None,
"ipv6": ipv6 or None,
# Compatible with old frontends: IPv4 first, otherwise IPv6
"ip": ipv4 or ipv6 or None,
},
}
)
@credentials_bp.route("/create", methods=["POST"])
@login_required
def create_credential():
"""Create a new credential for the current user.
Supports crypto exchanges, IBKR (US stocks) and MT5 (Forex).
"""
try:
user_id = g.user_id
data = request.get_json() or {}
name = (data.get("name") or "").strip()
exchange_id = (data.get("exchange_id") or "").strip().lower()
if not exchange_id:
return jsonify({"code": 0, "msg": "Missing exchange_id", "data": None}), 400
config = {"exchange_id": exchange_id}
hint = ""
if exchange_id == "ibkr":
# Interactive Brokers (US stocks)
config.update(
{
"ibkr_host": (data.get("ibkr_host") or "127.0.0.1").strip(),
"ibkr_port": int(data.get("ibkr_port") or 7497),
"ibkr_client_id": int(data.get("ibkr_client_id") or 1),
"ibkr_account": (data.get("ibkr_account") or "").strip(),
}
)
hint = f"{config['ibkr_host']}:{config['ibkr_port']}"
elif exchange_id == "mt5":
# MetaTrader 5 (Forex)
mt5_server = (data.get("mt5_server") or "").strip()
mt5_login = str(data.get("mt5_login") or "").strip()
mt5_password = (data.get("mt5_password") or "").strip()
if not mt5_server or not mt5_login or not mt5_password:
return jsonify({"code": 0, "msg": "Missing mt5_server/mt5_login/mt5_password", "data": None}), 400
config.update(
{
"mt5_server": mt5_server,
"mt5_login": mt5_login,
"mt5_password": mt5_password,
"mt5_terminal_path": (data.get("mt5_terminal_path") or "").strip(),
}
)
hint = f"{mt5_server}/{mt5_login}"
elif exchange_id in CRYPTO_EXCHANGES:
# Crypto exchanges
api_key = (data.get("api_key") or "").strip()
secret_key = (data.get("secret_key") or "").strip()
if not api_key or not secret_key:
return jsonify({"code": 0, "msg": "Missing api_key/secret_key", "data": None}), 400
config.update(
{
"api_key": api_key,
"secret_key": secret_key,
"passphrase": (data.get("passphrase") or "").strip(),
"enable_demo_trading": bool(data.get("enable_demo_trading", False)),
}
)
hint = _api_key_hint(api_key)
else:
return jsonify({"code": 0, "msg": f"Unsupported exchange: {exchange_id}", "data": None}), 400
plaintext_config = json.dumps(config, ensure_ascii=False)
stored_blob = encrypt_credential_blob(plaintext_config)
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
"""
INSERT INTO qd_exchange_credentials (user_id, name, exchange_id, api_key_hint, encrypted_config, created_at, updated_at)
VALUES (%s, %s, %s, %s, %s, NOW(), NOW())
RETURNING id
""",
(user_id, name, exchange_id, hint, stored_blob),
)
row = cur.fetchone()
new_id = (row or {}).get("id")
db.commit()
cur.close()
return jsonify({"code": 1, "msg": "success", "data": {"id": new_id}})
except Exception as e:
logger.error(f"create_credential failed: {str(e)}")
logger.error(traceback.format_exc())
return jsonify({"code": 0, "msg": str(e), "data": None}), 500
@credentials_bp.route("/delete", methods=["DELETE"])
@login_required
def delete_credential():
"""Delete a credential for the current user."""
try:
user_id = g.user_id
cred_id = request.args.get("id", type=int)
if not cred_id:
return jsonify({"code": 0, "msg": "Missing id", "data": None}), 400
with get_db_connection() as db:
cur = db.cursor()
cur.execute("DELETE FROM qd_exchange_credentials WHERE id = %s AND user_id = %s", (cred_id, user_id))
db.commit()
cur.close()
return jsonify({"code": 1, "msg": "success", "data": None})
except Exception as e:
logger.error(f"delete_credential failed: {str(e)}")
logger.error(traceback.format_exc())
return jsonify({"code": 0, "msg": str(e), "data": None}), 500
@credentials_bp.route("/get", methods=["GET"])
@login_required
def get_credential():
"""
Return decrypted credential for form auto-fill.
"""
try:
user_id = g.user_id
cred_id = request.args.get("id", type=int)
if not cred_id:
return jsonify({"code": 0, "msg": "Missing id", "data": None}), 400
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
"""
SELECT id, user_id, name, exchange_id, encrypted_config, api_key_hint, created_at, updated_at
FROM qd_exchange_credentials
WHERE id = %s AND user_id = %s
""",
(cred_id, user_id),
)
row = cur.fetchone()
cur.close()
if not row:
return jsonify({"code": 0, "msg": "Not found", "data": None}), 404
raw = row.get("encrypted_config")
plain = decrypt_credential_blob(raw)
decrypted = json.loads(plain) if plain else {}
# Ensure exchange_id is present
decrypted["exchange_id"] = row.get("exchange_id") or decrypted.get("exchange_id")
return jsonify(
{
"code": 1,
"msg": "success",
"data": {
"id": row.get("id"),
"name": row.get("name"),
"exchange_id": row.get("exchange_id"),
"api_key_hint": row.get("api_key_hint"),
"config": decrypted,
},
}
)
except Exception as e:
logger.error(f"get_credential failed: {str(e)}")
logger.error(traceback.format_exc())
return jsonify({"code": 0, "msg": str(e), "data": None}), 500