Files
DinQuant/backend_api_python/app/routes/auth.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

1129 lines
42 KiB
Python

"""
Authentication API Routes
Handles login, logout, registration, password reset, and OAuth authentication.
Supports both multi-user (database) and single-user (legacy) modes.
"""
import os
from urllib.parse import urlencode
from flask import Blueprint, g, jsonify, redirect, request
from app.config.settings import Config
from app.utils.auth import authenticate_legacy, generate_token, login_required
from app.utils.logger import get_logger
logger = get_logger(__name__)
auth_bp = Blueprint("auth", __name__)
def _build_frontend_login_redirect(frontend_url: str, **params) -> str:
"""
Build a redirect URL to frontend login page for OAuth flows.
Frontend uses Vue Router hash mode (`/#/user/login`), so redirecting to `/user/login`
will 404 on static hosting. Always normalize to `{origin}/#/user/login`.
"""
base = (frontend_url or "").strip().rstrip("/")
if not base:
base = "http://localhost:8080"
if "/#/" in base:
origin = base.split("/#/", 1)[0].rstrip("/")
elif "#" in base:
origin = base.split("#", 1)[0].rstrip("/")
else:
origin = base
login_url = f"{origin}/#/user/login"
qs = urlencode({k: v for k, v in params.items() if v is not None and v != ""})
return f"{login_url}?{qs}" if qs else login_url
def _is_single_user_mode() -> bool:
"""Check if system is in single-user (legacy) mode"""
return os.getenv("SINGLE_USER_MODE", "false").lower() == "true"
def _get_client_ip() -> str:
"""Get client IP address from request"""
# Check for proxy headers
if request.headers.get("X-Forwarded-For"):
return request.headers.get("X-Forwarded-For").split(",")[0].strip()
if request.headers.get("X-Real-IP"):
return request.headers.get("X-Real-IP")
return request.remote_addr or "0.0.0.0"
def _get_user_agent() -> str:
"""Get user agent from request"""
return request.headers.get("User-Agent", "")[:500]
# =============================================================================
# Security Config Endpoint
# =============================================================================
@auth_bp.route("/security-config", methods=["GET"])
def get_security_config():
"""
Get public security configuration for frontend.
Returns:
turnstile_enabled: bool
turnstile_site_key: str
registration_enabled: bool
oauth_google_enabled: bool
oauth_github_enabled: bool
"""
try:
from app.services.security_service import get_security_service
config = get_security_service().get_security_config()
return jsonify({"code": 1, "msg": "success", "data": config})
except Exception as e:
logger.error(f"get_security_config error: {e}")
return jsonify({"code": 0, "msg": str(e), "data": None}), 500
# =============================================================================
# Login Endpoint (Enhanced with security)
# =============================================================================
@auth_bp.route("/login", methods=["POST"])
def login():
"""
User login endpoint.
Request body:
username: str
password: str
turnstile_token: str (optional, required if Turnstile is enabled)
Returns:
token: JWT token
userinfo: User information
"""
ip_address = _get_client_ip()
user_agent = _get_user_agent()
try:
from app.services.security_service import get_security_service
security = get_security_service()
data = request.get_json()
if not data:
return jsonify({"code": 400, "msg": "No data provided", "data": None}), 400
username = data.get("username") or data.get("account")
password = data.get("password")
turnstile_token = data.get("turnstile_token")
if not username or not password:
return jsonify({"code": 400, "msg": "Missing username/email or password", "data": None}), 400
# Step 1: Verify Turnstile (if enabled)
turnstile_ok, turnstile_msg = security.verify_turnstile(turnstile_token, ip_address)
if not turnstile_ok:
return jsonify({"code": 0, "msg": turnstile_msg, "data": None}), 400
# Step 2: Check rate limiting
allowed, block_msg = security.check_login_allowed(username, ip_address)
if not allowed:
return jsonify({"code": 0, "msg": block_msg, "data": {"blocked": True}}), 429
user = None
# Step 3: Authenticate
if not _is_single_user_mode():
try:
from app.services.user_service import get_user_service
user = get_user_service().authenticate(username, password)
# Check if user has no password set (code-login user)
if user and user.get("_no_password"):
user.pop("_no_password", None)
# Record failed attempt
security.record_login_attempt(ip_address, "ip", False, ip_address, user_agent)
security.record_login_attempt(username, "account", False, ip_address, user_agent)
security.log_security_event(
"login_failed",
user.get("id"),
ip_address,
user_agent,
{"username": username, "reason": "no_password_set"},
)
return jsonify(
{
"code": 0,
"msg": "This account was created with email verification code and has no password set. Please use email code login or set a password first in your profile settings.",
"data": None,
}
), 401
except Exception as e:
logger.warning(f"Multi-user auth failed, trying legacy: {e}")
# Fallback to legacy single-user mode
if not user:
user = authenticate_legacy(username, password)
if not user:
# Record failed attempt
security.record_login_attempt(ip_address, "ip", False, ip_address, user_agent)
security.record_login_attempt(username, "account", False, ip_address, user_agent)
security.log_security_event(
"login_failed", None, ip_address, user_agent, {"username": username, "reason": "invalid_credentials"}
)
return jsonify({"code": 0, "msg": "Invalid credentials", "data": None}), 401
# Check user status
if user.get("status") == "disabled":
security.log_security_event(
"login_blocked", user.get("id"), ip_address, user_agent, {"reason": "account_disabled"}
)
return jsonify({"code": 0, "msg": "Account is disabled", "data": None}), 403
if user.get("status") == "pending":
return jsonify({"code": 0, "msg": "Account is pending activation", "data": None}), 403
# Step 4: Increment token_version (invalidates old sessions for single-client login)
user_id = user.get("id") or user.get("user_id", 1)
try:
from app.services.user_service import get_user_service
new_token_version = get_user_service().increment_token_version(user_id)
except Exception as e:
logger.warning(f"Failed to increment token_version: {e}")
new_token_version = 1
# Step 5: Generate token with new token_version
token = generate_token(
user_id=user_id,
username=user.get("username", username),
role=user.get("role", "admin"),
token_version=new_token_version, # Contains new token_version
)
if not token:
return jsonify({"code": 500, "msg": "Token generation error", "data": None}), 500
# Step 6: Record successful login
security.record_login_attempt(ip_address, "ip", True, ip_address, user_agent)
security.record_login_attempt(username, "account", True, ip_address, user_agent)
security.clear_login_attempts(ip_address, "ip")
security.clear_login_attempts(username, "account")
security.log_security_event("login_success", user.get("id"), ip_address, user_agent)
# Build user info for frontend
userinfo = {
"id": user.get("id") or user.get("user_id", 1),
"username": user.get("username", username),
"nickname": user.get("nickname", "User"),
"avatar": user.get("avatar", "/avatar2.jpg"),
"timezone": str(user.get("timezone") or "").strip(),
"role": {"id": user.get("role", "admin"), "permissions": _get_permissions(user.get("role", "admin"))},
}
return jsonify({"code": 1, "msg": "Login successful", "data": {"token": token, "userinfo": userinfo}})
except Exception as e:
logger.error(f"Login error: {e}")
return jsonify({"code": 500, "msg": str(e), "data": None}), 500
# =============================================================================
# Email Code Login
# =============================================================================
@auth_bp.route("/login-code", methods=["POST"])
def login_with_code():
"""
Login with email verification code (quick login / register).
If user doesn't exist, create a new account automatically.
Request body:
email: str
code: str (verification code)
turnstile_token: str (optional)
referral_code: str (optional, referrer's user ID - only for new users)
"""
ip_address = _get_client_ip()
user_agent = _get_user_agent()
try:
from app.services.billing_service import get_billing_service
from app.services.email_service import get_email_service
from app.services.security_service import get_security_service
from app.services.user_service import get_user_service
security = get_security_service()
email_service = get_email_service()
user_service = get_user_service()
billing_service = get_billing_service()
data = request.get_json()
if not data:
return jsonify({"code": 0, "msg": "No data provided", "data": None}), 400
email = (data.get("email") or "").strip().lower()
code = data.get("code", "").strip()
turnstile_token = data.get("turnstile_token")
referral_code = data.get("referral_code", "").strip()
# Validate inputs
if not email or not email_service.is_valid_email(email):
return jsonify({"code": 0, "msg": "Invalid email address", "data": None}), 400
if not code:
return jsonify({"code": 0, "msg": "Verification code is required", "data": None}), 400
# Verify Turnstile
turnstile_ok, turnstile_msg = security.verify_turnstile(turnstile_token, ip_address)
if not turnstile_ok:
return jsonify({"code": 0, "msg": turnstile_msg, "data": None}), 400
# Verify email code
code_valid, code_msg = email_service.verify_code(email, code, "login")
if not code_valid:
return jsonify({"code": 0, "msg": code_msg, "data": None}), 400
# Check if user exists
user = user_service.get_user_by_email(email)
is_new_user = False
if not user:
# Check if registration is enabled
if os.getenv("ENABLE_REGISTRATION", "true").lower() != "true":
return jsonify({"code": 0, "msg": "User not found and registration is disabled", "data": None}), 403
# Auto-create user with email as username
import re
# Generate username from email (before @)
base_username = re.sub(r"[^a-zA-Z0-9_]", "", email.split("@")[0])
if not base_username or not base_username[0].isalpha():
base_username = "user_" + base_username
# Make sure username is unique
username = base_username
counter = 1
while user_service.get_user_by_username(username):
username = f"{base_username}_{counter}"
counter += 1
# Validate referral code (user ID)
referred_by = None
if referral_code:
try:
referrer_id = int(referral_code)
referrer = user_service.get_user_by_id(referrer_id)
if referrer and referrer.get("status") == "active":
referred_by = referrer_id
except (ValueError, TypeError):
pass # Invalid referral code, ignore
# Create user without password (can set later)
user_id = user_service.create_user(
username=username,
password=None, # No password for code-login users
email=email,
nickname=username,
role="user",
status="active",
email_verified=True,
referred_by=referred_by,
)
if not user_id:
return jsonify({"code": 0, "msg": "Failed to create account", "data": None}), 500
# Grant registration bonus credits
register_bonus = int(os.getenv("CREDITS_REGISTER_BONUS", "0"))
if register_bonus > 0:
billing_service.add_credits(
user_id=user_id, amount=register_bonus, action="register_bonus", remark="Registration bonus"
)
# Grant referral bonus to referrer
if referred_by:
referral_bonus = int(os.getenv("CREDITS_REFERRAL_BONUS", "0"))
if referral_bonus > 0:
billing_service.add_credits(
user_id=referred_by,
amount=referral_bonus,
action="referral_bonus",
remark=f"Referral bonus for inviting user {username}",
reference_id=str(user_id),
)
user = user_service.get_user_by_id(user_id)
is_new_user = True
# Log registration
security.log_security_event(
"register_via_code", user_id, ip_address, user_agent, {"email": email, "referred_by": referred_by}
)
# Check user status
if user.get("status") == "disabled":
security.log_security_event(
"login_blocked", user.get("id"), ip_address, user_agent, {"reason": "account_disabled"}
)
return jsonify({"code": 0, "msg": "Account is disabled", "data": None}), 403
# Increment token_version (invalidates old sessions for single-client login)
try:
new_token_version = user_service.increment_token_version(user["id"])
except Exception as e:
logger.warning(f"Failed to increment token_version: {e}")
new_token_version = 1
# Generate token with new token_version
token = generate_token(
user_id=user["id"],
username=user["username"],
role=user.get("role", "user"),
token_version=new_token_version,
)
if not token:
return jsonify({"code": 500, "msg": "Token generation error", "data": None}), 500
# Update last login time
try:
from app.utils.db import get_db_connection
with get_db_connection() as db:
cur = db.cursor()
cur.execute("UPDATE qd_users SET last_login_at = NOW() WHERE id = ?", (user["id"],))
db.commit()
affected = cur.rowcount
cur.close()
if affected == 0:
logger.error(f"Failed to update last_login_at: no rows affected for user_id={user['id']}")
else:
logger.info(f"Updated last_login_at for user_id={user['id']}")
except Exception as e:
logger.error(f"Failed to update last_login_at for user_id={user.get('id')}: {e}")
# Log login
security.log_security_event("login_via_code", user["id"], ip_address, user_agent)
return jsonify(
{
"code": 1,
"msg": "Login successful" + (" (new account created)" if is_new_user else ""),
"data": {
"token": token,
"is_new_user": is_new_user,
"userinfo": {
"id": user["id"],
"username": user["username"],
"nickname": user.get("nickname", user["username"]),
"email": user.get("email"),
"avatar": user.get("avatar", "/avatar2.jpg"),
"timezone": str(user.get("timezone") or "").strip(),
"role": {
"id": user.get("role", "user"),
"permissions": _get_permissions(user.get("role", "user")),
},
},
},
}
)
except Exception as e:
logger.error(f"login_with_code error: {e}")
return jsonify({"code": 0, "msg": "Login failed", "data": None}), 500
# =============================================================================
# Registration Endpoints
# =============================================================================
@auth_bp.route("/send-code", methods=["POST"])
def send_verification_code():
"""
Send verification code to email.
Request body:
email: str
type: str (register, reset_password, change_password, change_email)
turnstile_token: str (optional)
"""
ip_address = _get_client_ip()
try:
from app.services.email_service import get_email_service
from app.services.security_service import get_security_service
security = get_security_service()
email_service = get_email_service()
data = request.get_json()
if not data:
return jsonify({"code": 0, "msg": "No data provided", "data": None}), 400
email = (data.get("email") or "").strip().lower()
code_type = data.get("type", "register")
turnstile_token = data.get("turnstile_token")
# Validate email
if not email or not email_service.is_valid_email(email):
return jsonify({"code": 0, "msg": "Invalid email address", "data": None}), 400
# For change_password type with logged-in user, skip Turnstile verification
# because user already authenticated
skip_turnstile = False
if code_type == "change_password":
# Try to get user_id from token (this route doesn't require login)
from app.utils.auth import verify_token
auth_header = request.headers.get("Authorization")
if auth_header:
parts = auth_header.split()
if len(parts) == 2 and parts[0].lower() == "bearer":
payload = verify_token(parts[1])
if payload and payload.get("user_id"):
skip_turnstile = True
# Verify Turnstile (skip for authenticated change_password requests)
if not skip_turnstile:
turnstile_ok, turnstile_msg = security.verify_turnstile(turnstile_token, ip_address)
if not turnstile_ok:
return jsonify({"code": 0, "msg": turnstile_msg, "data": None}), 400
# Check rate limit
can_send, rate_msg = security.can_send_verification_code(email, ip_address)
if not can_send:
return jsonify({"code": 0, "msg": rate_msg, "data": None}), 429
# For registration, check if email already exists
if code_type == "register":
from app.services.user_service import get_user_service
existing = get_user_service().get_user_by_email(email)
if existing:
return jsonify({"code": 0, "msg": "Email already registered", "data": None}), 400
# For login type - always allow (will auto-create if not exists)
# No special check needed
# For reset_password, check if email exists
if code_type == "reset_password":
from app.services.user_service import get_user_service
existing = get_user_service().get_user_by_email(email)
if not existing:
# Don't reveal if email exists or not (security best practice)
# But still return success to prevent email enumeration
return jsonify(
{"code": 1, "msg": "If the email exists, a verification code has been sent", "data": None}
)
# Send verification code
success, msg = email_service.send_verification_code(email, code_type, ip_address)
if success:
security.log_security_event(
"verification_code_sent", None, ip_address, _get_user_agent(), {"email": email, "type": code_type}
)
return jsonify({"code": 1, "msg": "Verification code sent", "data": None})
else:
return jsonify({"code": 0, "msg": msg, "data": None}), 500
except Exception as e:
logger.error(f"send_verification_code error: {e}")
return jsonify({"code": 0, "msg": "Failed to send verification code", "data": None}), 500
@auth_bp.route("/register", methods=["POST"])
def register():
"""
Register new user with email verification.
Request body:
email: str
code: str (verification code)
username: str
password: str
turnstile_token: str (optional)
referral_code: str (optional, referrer's user ID)
"""
ip_address = _get_client_ip()
user_agent = _get_user_agent()
try:
# Check if registration is enabled
if os.getenv("ENABLE_REGISTRATION", "true").lower() != "true":
return jsonify({"code": 0, "msg": "Registration is disabled", "data": None}), 403
from app.services.billing_service import get_billing_service
from app.services.email_service import get_email_service
from app.services.security_service import get_security_service
from app.services.user_service import get_user_service
security = get_security_service()
email_service = get_email_service()
user_service = get_user_service()
billing_service = get_billing_service()
data = request.get_json()
if not data:
return jsonify({"code": 0, "msg": "No data provided", "data": None}), 400
email = (data.get("email") or "").strip().lower()
code = data.get("code", "").strip()
username = (data.get("username") or "").strip()
password = data.get("password", "")
turnstile_token = data.get("turnstile_token")
referral_code = data.get("referral_code", "").strip()
# Validate inputs
if not email or not email_service.is_valid_email(email):
return jsonify({"code": 0, "msg": "Invalid email address", "data": None}), 400
if not code:
return jsonify({"code": 0, "msg": "Verification code is required", "data": None}), 400
if not username or len(username) < 3 or len(username) > 30:
return jsonify({"code": 0, "msg": "Username must be 3-30 characters", "data": None}), 400
# Validate username format (alphanumeric and underscore only)
import re
if not re.match(r"^[a-zA-Z][a-zA-Z0-9_]*$", username):
return jsonify(
{
"code": 0,
"msg": "Username must start with letter and contain only letters, numbers, and underscores",
"data": None,
}
), 400
# Validate password strength
pwd_valid, pwd_msg = security.validate_password_strength(password)
if not pwd_valid:
return jsonify({"code": 0, "msg": pwd_msg, "data": None}), 400
# Verify Turnstile
turnstile_ok, turnstile_msg = security.verify_turnstile(turnstile_token, ip_address)
if not turnstile_ok:
return jsonify({"code": 0, "msg": turnstile_msg, "data": None}), 400
# Verify email code
code_valid, code_msg = email_service.verify_code(email, code, "register")
if not code_valid:
return jsonify({"code": 0, "msg": code_msg, "data": None}), 400
# Check if username already exists
existing_user = user_service.get_user_by_username(username)
if existing_user:
return jsonify({"code": 0, "msg": "Username already taken", "data": None}), 400
# Check if email already exists
existing_email = user_service.get_user_by_email(email)
if existing_email:
return jsonify({"code": 0, "msg": "Email already registered", "data": None}), 400
# Validate referral code (user ID)
referred_by = None
if referral_code:
try:
referrer_id = int(referral_code)
referrer = user_service.get_user_by_id(referrer_id)
if referrer and referrer.get("status") == "active":
referred_by = referrer_id
except (ValueError, TypeError):
pass # Invalid referral code, ignore
# Create user
user_id = user_service.create_user(
username=username,
password=password,
email=email,
nickname=username,
role="user",
status="active",
email_verified=True,
referred_by=referred_by,
)
if not user_id:
return jsonify({"code": 0, "msg": "Failed to create account", "data": None}), 500
# Grant registration bonus credits
register_bonus = int(os.getenv("CREDITS_REGISTER_BONUS", "0"))
if register_bonus > 0:
billing_service.add_credits(
user_id=user_id, amount=register_bonus, action="register_bonus", remark="Registration bonus"
)
# Grant referral bonus to referrer
if referred_by:
referral_bonus = int(os.getenv("CREDITS_REFERRAL_BONUS", "0"))
if referral_bonus > 0:
billing_service.add_credits(
user_id=referred_by,
amount=referral_bonus,
action="referral_bonus",
remark=f"Referral bonus for inviting user {username}",
reference_id=str(user_id),
)
# Log registration
security.log_security_event(
"register", user_id, ip_address, user_agent, {"email": email, "referred_by": referred_by}
)
# Auto login after registration (get token_version for new user)
try:
new_token_version = user_service.get_token_version(user_id)
except Exception as e:
logger.warning(f"Failed to get token_version: {e}")
new_token_version = 1
token = generate_token(user_id=user_id, username=username, role="user", token_version=new_token_version)
return jsonify(
{
"code": 1,
"msg": "Registration successful",
"data": {
"token": token,
"userinfo": {
"id": user_id,
"username": username,
"nickname": username,
"email": email,
"avatar": "/avatar2.jpg",
"timezone": "",
"role": {"id": "user", "permissions": _get_permissions("user")},
},
},
}
)
except Exception as e:
logger.error(f"register error: {e}")
return jsonify({"code": 0, "msg": "Registration failed", "data": None}), 500
@auth_bp.route("/reset-password", methods=["POST"])
def reset_password():
"""
Reset password with email verification.
Request body:
email: str
code: str (verification code)
new_password: str
turnstile_token: str (optional)
"""
ip_address = _get_client_ip()
user_agent = _get_user_agent()
try:
from app.services.email_service import get_email_service
from app.services.security_service import get_security_service
from app.services.user_service import get_user_service
security = get_security_service()
email_service = get_email_service()
user_service = get_user_service()
data = request.get_json()
if not data:
return jsonify({"code": 0, "msg": "No data provided", "data": None}), 400
email = (data.get("email") or "").strip().lower()
code = data.get("code", "").strip()
new_password = data.get("new_password", "")
turnstile_token = data.get("turnstile_token")
# Validate inputs
if not email or not code or not new_password:
return jsonify({"code": 0, "msg": "Missing required fields", "data": None}), 400
# Validate password strength
pwd_valid, pwd_msg = security.validate_password_strength(new_password)
if not pwd_valid:
return jsonify({"code": 0, "msg": pwd_msg, "data": None}), 400
# Verify Turnstile
turnstile_ok, turnstile_msg = security.verify_turnstile(turnstile_token, ip_address)
if not turnstile_ok:
return jsonify({"code": 0, "msg": turnstile_msg, "data": None}), 400
# Verify email code
code_valid, code_msg = email_service.verify_code(email, code, "reset_password")
if not code_valid:
return jsonify({"code": 0, "msg": code_msg, "data": None}), 400
# Get user by email
user = user_service.get_user_by_email(email)
if not user:
return jsonify({"code": 0, "msg": "User not found", "data": None}), 404
# Update password
success = user_service.update_password(user["id"], new_password)
if not success:
return jsonify({"code": 0, "msg": "Failed to reset password", "data": None}), 500
# Clear any existing login blocks for this account
security.clear_login_attempts(user["username"], "account")
# Log password reset
security.log_security_event("password_reset", user["id"], ip_address, user_agent)
return jsonify({"code": 1, "msg": "Password reset successful", "data": None})
except Exception as e:
logger.error(f"reset_password error: {e}")
return jsonify({"code": 0, "msg": "Password reset failed", "data": None}), 500
@auth_bp.route("/change-password", methods=["POST"])
@login_required
def change_password():
"""
Change password with email verification (for logged-in users).
Request body:
code: str (verification code sent to user's email)
new_password: str
"""
ip_address = _get_client_ip()
user_agent = _get_user_agent()
user_id = g.user_id
try:
from app.services.email_service import get_email_service
from app.services.security_service import get_security_service
from app.services.user_service import get_user_service
security = get_security_service()
email_service = get_email_service()
user_service = get_user_service()
data = request.get_json()
if not data:
return jsonify({"code": 0, "msg": "No data provided", "data": None}), 400
code = data.get("code", "").strip()
new_password = data.get("new_password", "")
if not code or not new_password:
return jsonify({"code": 0, "msg": "Missing required fields", "data": None}), 400
# Validate password strength
pwd_valid, pwd_msg = security.validate_password_strength(new_password)
if not pwd_valid:
return jsonify({"code": 0, "msg": pwd_msg, "data": None}), 400
# Get user
user = user_service.get_user_by_id(user_id)
if not user or not user.get("email"):
return jsonify({"code": 0, "msg": "User email not found", "data": None}), 400
# Verify email code
code_valid, code_msg = email_service.verify_code(user["email"], code, "change_password")
if not code_valid:
return jsonify({"code": 0, "msg": code_msg, "data": None}), 400
# Update password
success = user_service.update_password(user_id, new_password)
if not success:
return jsonify({"code": 0, "msg": "Failed to change password", "data": None}), 500
# Log password change
security.log_security_event("password_changed", user_id, ip_address, user_agent)
return jsonify({"code": 1, "msg": "Password changed successfully", "data": None})
except Exception as e:
logger.error(f"change_password error: {e}")
return jsonify({"code": 0, "msg": "Password change failed", "data": None}), 500
# =============================================================================
# OAuth Endpoints
# =============================================================================
@auth_bp.route("/oauth/google", methods=["GET"])
def oauth_google():
"""Redirect to Google OAuth authorization page"""
try:
from app.services.oauth_service import get_oauth_service
oauth = get_oauth_service()
if not oauth.google_enabled:
return jsonify({"code": 0, "msg": "Google OAuth is not configured", "data": None}), 400
auth_url, state = oauth.get_google_auth_url()
return redirect(auth_url)
except Exception as e:
logger.error(f"oauth_google error: {e}")
return jsonify({"code": 0, "msg": str(e), "data": None}), 500
@auth_bp.route("/oauth/google/callback", methods=["GET"])
def oauth_google_callback():
"""Handle Google OAuth callback"""
ip_address = _get_client_ip()
user_agent = _get_user_agent()
try:
from app.services.oauth_service import get_oauth_service
from app.services.security_service import get_security_service
oauth = get_oauth_service()
security = get_security_service()
code = request.args.get("code")
state = request.args.get("state")
error = request.args.get("error")
frontend_url = oauth.frontend_url
if error:
return redirect(_build_frontend_login_redirect(frontend_url, oauth_error=error))
if not code or not state:
return redirect(_build_frontend_login_redirect(frontend_url, oauth_error="missing_params"))
# Handle callback
success, result = oauth.handle_google_callback(code, state)
if not success:
error_msg = result.get("error", "unknown_error")
return redirect(_build_frontend_login_redirect(frontend_url, oauth_error=error_msg))
# Get or create user
user_success, user_result = oauth.get_or_create_user_from_oauth(result)
if not user_success:
error_msg = user_result.get("error", "user_creation_failed")
return redirect(_build_frontend_login_redirect(frontend_url, oauth_error=error_msg))
# Increment token_version (invalidates old sessions for single-client login)
from app.services.user_service import get_user_service
user_service = get_user_service()
try:
new_token_version = user_service.increment_token_version(user_result["id"])
except Exception as e:
logger.warning(f"Failed to increment token_version: {e}")
new_token_version = 1
# Generate token with new token_version
token = generate_token(
user_id=user_result["id"],
username=user_result["username"],
role=user_result.get("role", "user"),
token_version=new_token_version,
)
# Log OAuth login
security.log_security_event("oauth_login", user_result["id"], ip_address, user_agent, {"provider": "google"})
# Redirect to frontend with token
return redirect(_build_frontend_login_redirect(frontend_url, oauth_token=token))
except Exception as e:
logger.error(f"oauth_google_callback error: {e}")
from app.services.oauth_service import get_oauth_service
frontend_url = get_oauth_service().frontend_url
return redirect(_build_frontend_login_redirect(frontend_url, oauth_error="server_error"))
@auth_bp.route("/oauth/github", methods=["GET"])
def oauth_github():
"""Redirect to GitHub OAuth authorization page"""
try:
from app.services.oauth_service import get_oauth_service
oauth = get_oauth_service()
if not oauth.github_enabled:
return jsonify({"code": 0, "msg": "GitHub OAuth is not configured", "data": None}), 400
auth_url, state = oauth.get_github_auth_url()
return redirect(auth_url)
except Exception as e:
logger.error(f"oauth_github error: {e}")
return jsonify({"code": 0, "msg": str(e), "data": None}), 500
@auth_bp.route("/oauth/github/callback", methods=["GET"])
def oauth_github_callback():
"""Handle GitHub OAuth callback"""
ip_address = _get_client_ip()
user_agent = _get_user_agent()
try:
from app.services.oauth_service import get_oauth_service
from app.services.security_service import get_security_service
oauth = get_oauth_service()
security = get_security_service()
code = request.args.get("code")
state = request.args.get("state")
error = request.args.get("error")
frontend_url = oauth.frontend_url
if error:
return redirect(_build_frontend_login_redirect(frontend_url, oauth_error=error))
if not code or not state:
return redirect(_build_frontend_login_redirect(frontend_url, oauth_error="missing_params"))
# Handle callback
success, result = oauth.handle_github_callback(code, state)
if not success:
error_msg = result.get("error", "unknown_error")
return redirect(_build_frontend_login_redirect(frontend_url, oauth_error=error_msg))
# Get or create user
user_success, user_result = oauth.get_or_create_user_from_oauth(result)
if not user_success:
error_msg = user_result.get("error", "user_creation_failed")
return redirect(_build_frontend_login_redirect(frontend_url, oauth_error=error_msg))
# Increment token_version (invalidates old sessions for single-client login)
from app.services.user_service import get_user_service
user_service = get_user_service()
try:
new_token_version = user_service.increment_token_version(user_result["id"])
except Exception as e:
logger.warning(f"Failed to increment token_version: {e}")
new_token_version = 1
# Generate token with new token_version
token = generate_token(
user_id=user_result["id"],
username=user_result["username"],
role=user_result.get("role", "user"),
token_version=new_token_version,
)
# Log OAuth login
security.log_security_event("oauth_login", user_result["id"], ip_address, user_agent, {"provider": "github"})
# Redirect to frontend with token
return redirect(_build_frontend_login_redirect(frontend_url, oauth_token=token))
except Exception as e:
logger.error(f"oauth_github_callback error: {e}")
from app.services.oauth_service import get_oauth_service
frontend_url = get_oauth_service().frontend_url
return redirect(_build_frontend_login_redirect(frontend_url, oauth_error="server_error"))
# =============================================================================
# Other Endpoints
# =============================================================================
@auth_bp.route("/logout", methods=["POST"])
def logout():
"""Logout (client removes token; server is stateless)."""
return jsonify({"code": 1, "msg": "Logout successful", "data": None})
@auth_bp.route("/info", methods=["GET"])
@login_required
def get_user_info():
"""Get current user info."""
try:
user_id = getattr(g, "user_id", 1)
username = getattr(g, "user", Config.ADMIN_USER)
role = getattr(g, "user_role", "admin")
# Try to get full user info from database
user_data = None
if not _is_single_user_mode():
try:
from app.services.user_service import get_user_service
user_data = get_user_service().get_user_by_id(user_id)
except Exception as e:
logger.warning(f"Failed to get user from database: {e}")
if user_data:
return jsonify(
{
"code": 1,
"msg": "Success",
"data": {
"id": user_data.get("id"),
"username": user_data.get("username"),
"nickname": user_data.get("nickname", "User"),
"email": user_data.get("email"),
"avatar": user_data.get("avatar", "/avatar2.jpg"),
"timezone": str(user_data.get("timezone") or "").strip(),
"role": {
"id": user_data.get("role", "user"),
"permissions": _get_permissions(user_data.get("role", "user")),
},
},
}
)
# Fallback for legacy mode
return jsonify(
{
"code": 1,
"msg": "Success",
"data": {
"id": user_id,
"username": username,
"nickname": "Admin",
"avatar": "/avatar2.jpg",
"timezone": "",
"role": {"id": role, "permissions": _get_permissions(role)},
},
}
)
except Exception as e:
logger.error(f"get_user_info error: {e}")
return jsonify({"code": 500, "msg": str(e), "data": None}), 500
def _get_permissions(role: str) -> list:
"""Get permissions list for a role"""
try:
from app.services.user_service import get_user_service
return get_user_service().get_user_permissions(role)
except Exception:
# Default permissions for admin
if role == "admin":
return [
"dashboard",
"view",
"indicator",
"backtest",
"strategy",
"portfolio",
"settings",
"user_manage",
"credentials",
]
return ["dashboard", "view", "indicator", "backtest", "strategy", "portfolio"]