feat: improve authentication and user management

- Fix token missing issue after email code registration/login
- Add last login time update for code-based login
- Support email login in addition to username login
- Fix password login for code-registered users (allow setting password)
- Fix referral code parameter passing from URL hash
- Add email column to user management table
- Improve profile page layout (align card heights, reorganize layout)
- Add i18n support for Register Bonus, Referral Bonus, Code Lock Minutes, Code Max Attempts
- Fix billing service add_credits to support reference_id parameter
- Update password handling documentation
This commit is contained in:
TIANHE
2026-01-14 20:42:10 +08:00
parent fa721c034c
commit 445e7ac290
33 changed files with 7715 additions and 234 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ def register_routes(app: Flask):
from app.routes.strategy_code import strategy_code_bp
app.register_blueprint(health_bp)
app.register_blueprint(auth_bp, url_prefix='/api/user')
app.register_blueprint(auth_bp, url_prefix='/api/auth') # Auth routes
app.register_blueprint(user_bp, url_prefix='/api/users') # User management
app.register_blueprint(kline_bp, url_prefix='/api/indicator')
app.register_blueprint(analysis_bp, url_prefix='/api/analysis')
+19
View File
@@ -140,6 +140,25 @@ def multi_analysis():
logger.info(f"Analyze request: {market}:{symbol}, use_multi_agent={use_multi_agent}, model={model}")
# Step 0: Check billing (计费检查)
from app.services.billing_service import get_billing_service
billing_success, billing_msg = get_billing_service().check_and_consume(
user_id=user_id,
feature='ai_analysis',
reference_id=f'{market}:{symbol}'
)
if not billing_success:
if 'insufficient_credits' in billing_msg:
parts = billing_msg.split(':')
current = parts[1] if len(parts) > 1 else '0'
required = parts[2] if len(parts) > 2 else '?'
return jsonify({
'code': 0,
'msg': f'Insufficient credits. Current: {current}, Required: {required}',
'data': {'error_type': 'insufficient_credits', 'current': current, 'required': required}
}), 402
return jsonify({'code': 0, 'msg': billing_msg, 'data': None}), 400
# Step 1: Create a "pending" task record first (so user can see progress in history)
task_id = _store_task(user_id, market, symbol, model or '', language, 'pending', result={}, error_message='')
+838 -6
View File
@@ -1,11 +1,11 @@
"""
Authentication API Routes
Handles login, logout, and user info retrieval.
Handles login, logout, registration, password reset, and OAuth authentication.
Supports both multi-user (database) and single-user (legacy) modes.
"""
import os
from flask import Blueprint, request, jsonify, g
from flask import Blueprint, request, jsonify, g, redirect
from app.config.settings import Config
from app.utils.auth import generate_token, login_required, authenticate_legacy
from app.utils.logger import get_logger
@@ -20,6 +20,50 @@ def _is_single_user_mode() -> bool:
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():
"""
@@ -28,30 +72,62 @@ def login():
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 or password', 'data': None}), 400
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
is_demo = os.getenv('IS_DEMO_MODE', 'false').lower() == 'true'
user = None
# Try multi-user authentication first
# 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}")
@@ -60,9 +136,23 @@ def login():
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
# Generate token
# 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: Generate token
token = generate_token(
user_id=user.get('id') or user.get('user_id', 1),
username=user.get('username', username),
@@ -72,6 +162,13 @@ def login():
if not token:
return jsonify({'code': 500, 'msg': 'Token generation error', 'data': None}), 500
# Step 5: 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),
@@ -99,6 +196,741 @@ def login():
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.security_service import get_security_service
from app.services.email_service import get_email_service
from app.services.user_service import get_user_service
from app.services.billing_service import get_billing_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
# Generate token
token = generate_token(
user_id=user['id'],
username=user['username'],
role=user.get('role', 'user')
)
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()
cur.close()
except Exception as e:
logger.warning(f"Failed to update last_login_at: {e}")
# Log login
security.log_security_event('login_via_code', user['id'], ip_address, user_agent)
is_demo = os.getenv('IS_DEMO_MODE', 'false').lower() == 'true'
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']) + (' (Demo)' if is_demo else ''),
'email': user.get('email'),
'avatar': user.get('avatar', '/avatar2.jpg'),
'is_demo': is_demo,
'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.security_service import get_security_service
from app.services.email_service import get_email_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
# 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
# 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.security_service import get_security_service
from app.services.email_service import get_email_service
from app.services.user_service import get_user_service
from app.services.billing_service import get_billing_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
token = generate_token(user_id=user_id, username=username, role='user')
is_demo = os.getenv('IS_DEMO_MODE', 'false').lower() == 'true'
return jsonify({
'code': 1,
'msg': 'Registration successful',
'data': {
'token': token,
'userinfo': {
'id': user_id,
'username': username,
'nickname': username,
'email': email,
'avatar': '/avatar2.jpg',
'is_demo': is_demo,
'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.security_service import get_security_service
from app.services.email_service import get_email_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.security_service import get_security_service
from app.services.email_service import get_email_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(f"{frontend_url}/user/login?oauth_error={error}")
if not code or not state:
return redirect(f"{frontend_url}/user/login?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(f"{frontend_url}/user/login?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(f"{frontend_url}/user/login?oauth_error={error_msg}")
# Generate token
token = generate_token(
user_id=user_result['id'],
username=user_result['username'],
role=user_result.get('role', 'user')
)
# 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(f"{frontend_url}/user/login?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(f"{frontend_url}/user/login?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(f"{frontend_url}/user/login?oauth_error={error}")
if not code or not state:
return redirect(f"{frontend_url}/user/login?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(f"{frontend_url}/user/login?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(f"{frontend_url}/user/login?oauth_error={error_msg}")
# Generate token
token = generate_token(
user_id=user_result['id'],
username=user_result['username'],
role=user_result.get('role', 'user')
)
# 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(f"{frontend_url}/user/login?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(f"{frontend_url}/user/login?oauth_error=server_error")
# =============================================================================
# Other Endpoints
# =============================================================================
@auth_bp.route('/logout', methods=['POST'])
def logout():
"""Logout (client removes token; server is stateless)."""
+42 -7
View File
@@ -680,25 +680,60 @@ def delete_monitor(monitor_id):
@portfolio_bp.route('/monitors/<int:monitor_id>/run', methods=['POST'])
@login_required
def run_monitor_now(monitor_id):
"""Manually trigger a monitor to run immediately."""
"""Manually trigger a monitor to run immediately.
Supports two modes:
- async=true (default): Returns immediately, runs in background, notifies via notification system
- async=false: Waits for completion and returns result (may timeout for large portfolios)
"""
try:
from app.services.portfolio_monitor import run_single_monitor
# Get language from request body or Accept-Language header
user_id = g.user_id
# Get parameters from request body
data = request.get_json(force=True, silent=True) or {}
language = data.get('language')
async_mode = data.get('async', True) # Default to async mode
# Fallback to Accept-Language header
# Fallback to Accept-Language header for language
if not language:
accept_lang = request.headers.get('Accept-Language', '')
if 'zh' in accept_lang.lower():
language = 'en-US'
language = 'zh-CN'
else:
language = 'en-US'
result = run_single_monitor(monitor_id, override_language=language)
return jsonify({'code': 1, 'msg': 'success', 'data': result})
if async_mode:
# Async mode: Start background thread and return immediately
import threading
def run_in_background(mid, lang, uid):
try:
run_single_monitor(mid, override_language=lang, user_id=uid)
except Exception as e:
logger.error(f"Background monitor run failed: {e}")
thread = threading.Thread(
target=run_in_background,
args=(monitor_id, language, user_id),
daemon=True
)
thread.start()
return jsonify({
'code': 1,
'msg': 'success',
'data': {
'status': 'running',
'message': 'Monitor is running in background. Results will be sent via notification.'
}
})
else:
# Sync mode: Wait for completion (may timeout)
result = run_single_monitor(monitor_id, override_language=language, user_id=user_id)
return jsonify({'code': 1, 'msg': 'success', 'data': result})
except Exception as e:
logger.error(f"run_monitor_now failed: {str(e)}")
logger.error(traceback.format_exc())
+243 -2
View File
@@ -76,6 +76,13 @@ CONFIG_SCHEMA = {
'default': '123456',
'description': 'Administrator login password. MUST change in production'
},
{
'key': 'ADMIN_EMAIL',
'label': 'Admin Email',
'type': 'text',
'default': 'admin@example.com',
'description': 'Administrator email for password reset and notifications'
},
]
},
@@ -657,11 +664,245 @@ CONFIG_SCHEMA = {
]
},
# ==================== 13. 应用配置 ====================
# ==================== 13. 注册与安全 ====================
'security': {
'title': 'Registration & Security',
'icon': 'safety',
'order': 13,
'items': [
{
'key': 'ENABLE_REGISTRATION',
'label': 'Enable Registration',
'type': 'boolean',
'default': 'True',
'description': 'Allow new users to register accounts'
},
{
'key': 'TURNSTILE_SITE_KEY',
'label': 'Turnstile Site Key',
'type': 'text',
'required': False,
'link': 'https://dash.cloudflare.com/?to=/:account/turnstile',
'link_text': 'settings.link.getTurnstileKey',
'description': 'Cloudflare Turnstile site key for CAPTCHA verification'
},
{
'key': 'TURNSTILE_SECRET_KEY',
'label': 'Turnstile Secret Key',
'type': 'password',
'required': False,
'description': 'Cloudflare Turnstile secret key'
},
{
'key': 'FRONTEND_URL',
'label': 'Frontend URL',
'type': 'text',
'default': 'http://localhost:8080',
'description': 'Frontend URL for OAuth redirects'
},
{
'key': 'GOOGLE_CLIENT_ID',
'label': 'Google Client ID',
'type': 'text',
'required': False,
'link': 'https://console.cloud.google.com/apis/credentials',
'link_text': 'settings.link.getGoogleCredentials',
'description': 'Google OAuth Client ID'
},
{
'key': 'GOOGLE_CLIENT_SECRET',
'label': 'Google Client Secret',
'type': 'password',
'required': False,
'description': 'Google OAuth Client Secret'
},
{
'key': 'GOOGLE_REDIRECT_URI',
'label': 'Google Redirect URI',
'type': 'text',
'default': 'http://localhost:5000/api/auth/oauth/google/callback',
'description': 'Google OAuth callback URL'
},
{
'key': 'GITHUB_CLIENT_ID',
'label': 'GitHub Client ID',
'type': 'text',
'required': False,
'link': 'https://github.com/settings/developers',
'link_text': 'settings.link.getGithubCredentials',
'description': 'GitHub OAuth Client ID'
},
{
'key': 'GITHUB_CLIENT_SECRET',
'label': 'GitHub Client Secret',
'type': 'password',
'required': False,
'description': 'GitHub OAuth Client Secret'
},
{
'key': 'GITHUB_REDIRECT_URI',
'label': 'GitHub Redirect URI',
'type': 'text',
'default': 'http://localhost:5000/api/auth/oauth/github/callback',
'description': 'GitHub OAuth callback URL'
},
{
'key': 'SECURITY_IP_MAX_ATTEMPTS',
'label': 'IP Max Failed Attempts',
'type': 'number',
'default': '10',
'description': 'Block IP after this many failed login attempts'
},
{
'key': 'SECURITY_IP_WINDOW_MINUTES',
'label': 'IP Window (minutes)',
'type': 'number',
'default': '5',
'description': 'Time window for counting IP failed attempts'
},
{
'key': 'SECURITY_IP_BLOCK_MINUTES',
'label': 'IP Block Duration (minutes)',
'type': 'number',
'default': '15',
'description': 'How long to block IP after exceeding limit'
},
{
'key': 'SECURITY_ACCOUNT_MAX_ATTEMPTS',
'label': 'Account Max Failed Attempts',
'type': 'number',
'default': '5',
'description': 'Lock account after this many failed login attempts'
},
{
'key': 'SECURITY_ACCOUNT_WINDOW_MINUTES',
'label': 'Account Window (minutes)',
'type': 'number',
'default': '60',
'description': 'Time window for counting account failed attempts'
},
{
'key': 'SECURITY_ACCOUNT_BLOCK_MINUTES',
'label': 'Account Block Duration (minutes)',
'type': 'number',
'default': '30',
'description': 'How long to lock account after exceeding limit'
},
{
'key': 'VERIFICATION_CODE_EXPIRE_MINUTES',
'label': 'Verification Code Expiry (minutes)',
'type': 'number',
'default': '10',
'description': 'Email verification code validity period'
},
{
'key': 'VERIFICATION_CODE_RATE_LIMIT',
'label': 'Code Rate Limit (seconds)',
'type': 'number',
'default': '60',
'description': 'Minimum time between verification code requests per email'
},
{
'key': 'VERIFICATION_CODE_IP_HOURLY_LIMIT',
'label': 'Code Hourly Limit per IP',
'type': 'number',
'default': '10',
'description': 'Maximum verification codes per IP per hour'
},
{
'key': 'VERIFICATION_CODE_MAX_ATTEMPTS',
'label': 'Code Max Attempts',
'type': 'number',
'default': '5',
'description': 'Maximum attempts to verify a code before lockout'
},
{
'key': 'VERIFICATION_CODE_LOCK_MINUTES',
'label': 'Code Lock Minutes',
'type': 'number',
'default': '30',
'description': 'Lockout duration after exceeding max attempts'
},
]
},
# ==================== 14. 计费配置 ====================
'billing': {
'title': 'Billing & Credits',
'icon': 'dollar',
'order': 14,
'items': [
{
'key': 'BILLING_ENABLED',
'label': 'Enable Billing',
'type': 'boolean',
'default': 'False',
'description': 'Enable billing system. When enabled, users need credits to use certain features'
},
{
'key': 'BILLING_VIP_BYPASS',
'label': 'VIP Free',
'type': 'boolean',
'default': 'True',
'description': 'VIP users can use all paid features for free during VIP period'
},
{
'key': 'BILLING_COST_AI_ANALYSIS',
'label': 'AI Analysis Cost',
'type': 'number',
'default': '10',
'description': 'Credits consumed per AI analysis request'
},
{
'key': 'BILLING_COST_STRATEGY_RUN',
'label': 'Strategy Run Cost',
'type': 'number',
'default': '5',
'description': 'Credits consumed when starting a strategy'
},
{
'key': 'BILLING_COST_BACKTEST',
'label': 'Backtest Cost',
'type': 'number',
'default': '3',
'description': 'Credits consumed per backtest run'
},
{
'key': 'BILLING_COST_PORTFOLIO_MONITOR',
'label': 'Portfolio Monitor Cost',
'type': 'number',
'default': '8',
'description': 'Credits consumed per portfolio AI monitoring run'
},
{
'key': 'RECHARGE_TELEGRAM_URL',
'label': 'Recharge Telegram URL',
'type': 'text',
'default': 'https://t.me/your_support_bot',
'description': 'Telegram customer service URL for recharge inquiries'
},
{
'key': 'CREDITS_REGISTER_BONUS',
'label': 'Register Bonus',
'type': 'number',
'default': '100',
'description': 'Credits awarded to new users on registration'
},
{
'key': 'CREDITS_REFERRAL_BONUS',
'label': 'Referral Bonus',
'type': 'number',
'default': '50',
'description': 'Credits awarded to referrer when someone signs up with their code'
},
]
},
# ==================== 15. 应用配置 ====================
'app': {
'title': 'Application',
'icon': 'appstore',
'order': 13,
'order': 15,
'items': [
{
'key': 'CORS_ORIGINS',
+298 -10
View File
@@ -24,13 +24,15 @@ def list_users():
Query params:
page: int (default 1)
page_size: int (default 20, max 100)
search: str (optional, search by username/email/nickname)
"""
try:
page = request.args.get('page', 1, type=int)
page_size = request.args.get('page_size', 20, type=int)
search = request.args.get('search', '', type=str)
page_size = min(100, max(1, page_size))
result = get_user_service().list_users(page=page, page_size=page_size)
result = get_user_service().list_users(page=page, page_size=page_size, search=search)
return jsonify({
'code': 1,
@@ -213,13 +215,143 @@ def get_roles():
})
# ==================== Billing Management (Admin) ====================
@user_bp.route('/set-credits', methods=['POST'])
@login_required
@admin_required
def set_user_credits():
"""
Set user credits (admin only).
Request body:
user_id: int (required)
credits: int (required)
remark: str (optional)
"""
try:
from app.services.billing_service import get_billing_service
data = request.get_json() or {}
user_id = data.get('user_id')
credits = data.get('credits')
remark = data.get('remark', '')
if not user_id:
return jsonify({'code': 0, 'msg': 'Missing user_id', 'data': None}), 400
if credits is None or credits < 0:
return jsonify({'code': 0, 'msg': 'Credits must be a non-negative number', 'data': None}), 400
operator_id = getattr(g, 'user_id', None)
success, result = get_billing_service().set_credits(user_id, int(credits), remark, operator_id)
if success:
return jsonify({'code': 1, 'msg': 'Credits updated successfully', 'data': {'credits': result}})
else:
return jsonify({'code': 0, 'msg': result, 'data': None}), 400
except Exception as e:
logger.error(f"set_user_credits failed: {e}")
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500
@user_bp.route('/set-vip', methods=['POST'])
@login_required
@admin_required
def set_user_vip():
"""
Set user VIP status (admin only).
Request body:
user_id: int (required)
vip_days: int (optional, 0 to cancel VIP, positive number to grant VIP for days)
vip_expires_at: str (optional, ISO format datetime, overrides vip_days if provided)
remark: str (optional)
"""
try:
from datetime import datetime, timedelta, timezone
from app.services.billing_service import get_billing_service
data = request.get_json() or {}
user_id = data.get('user_id')
vip_days = data.get('vip_days')
vip_expires_at_str = data.get('vip_expires_at')
remark = data.get('remark', '')
if not user_id:
return jsonify({'code': 0, 'msg': 'Missing user_id', 'data': None}), 400
# Calculate expires_at
expires_at = None
if vip_expires_at_str:
try:
expires_at = datetime.fromisoformat(vip_expires_at_str.replace('Z', '+00:00'))
except ValueError:
return jsonify({'code': 0, 'msg': 'Invalid vip_expires_at format', 'data': None}), 400
elif vip_days is not None:
if vip_days > 0:
expires_at = datetime.now(timezone.utc) + timedelta(days=vip_days)
else:
expires_at = None # Cancel VIP
else:
return jsonify({'code': 0, 'msg': 'Provide vip_days or vip_expires_at', 'data': None}), 400
operator_id = getattr(g, 'user_id', None)
success, result = get_billing_service().set_vip(user_id, expires_at, remark, operator_id)
if success:
return jsonify({
'code': 1,
'msg': 'VIP status updated successfully',
'data': {'vip_expires_at': expires_at.isoformat() if expires_at else None}
})
else:
return jsonify({'code': 0, 'msg': result, 'data': None}), 400
except Exception as e:
logger.error(f"set_user_vip failed: {e}")
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500
@user_bp.route('/credits-log', methods=['GET'])
@login_required
@admin_required
def get_user_credits_log():
"""
Get user credits log (admin only).
Query params:
user_id: int (required)
page: int (default 1)
page_size: int (default 20)
"""
try:
from app.services.billing_service import get_billing_service
user_id = request.args.get('user_id', type=int)
if not user_id:
return jsonify({'code': 0, 'msg': 'Missing user_id', 'data': None}), 400
page = request.args.get('page', 1, type=int)
page_size = request.args.get('page_size', 20, type=int)
page_size = min(100, max(1, page_size))
result = get_billing_service().get_credits_log(user_id, page, page_size)
return jsonify({'code': 1, 'msg': 'success', 'data': result})
except Exception as e:
logger.error(f"get_user_credits_log failed: {e}")
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500
# Self-service endpoints (accessible by any logged-in user)
@user_bp.route('/profile', methods=['GET'])
@login_required
def get_profile():
"""Get current user's profile"""
"""Get current user's profile with billing info"""
try:
from app.services.billing_service import get_billing_service
user_id = getattr(g, 'user_id', None)
if not user_id:
return jsonify({'code': 0, 'msg': 'Not authenticated', 'data': None}), 401
@@ -231,6 +363,10 @@ def get_profile():
# Add permissions
user['permissions'] = get_user_service().get_user_permissions(user.get('role', 'user'))
# Add billing info
billing_info = get_billing_service().get_user_billing_info(user_id)
user['billing'] = billing_info
return jsonify({
'code': 1,
'msg': 'success',
@@ -249,8 +385,10 @@ def update_profile():
Request body:
nickname: str (optional)
email: str (optional)
avatar: str (optional)
Note: Email cannot be changed after registration (for security).
Only admin can change user email via User Management.
"""
try:
user_id = getattr(g, 'user_id', None)
@@ -260,8 +398,9 @@ def update_profile():
data = request.get_json() or {}
# Only allow updating certain fields for self-service
# Email is NOT allowed to be changed (security: bound to account)
allowed = {}
for field in ['nickname', 'email', 'avatar']:
for field in ['nickname', 'avatar']:
if field in data:
allowed[field] = data[field]
@@ -279,6 +418,117 @@ def update_profile():
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500
@user_bp.route('/my-credits-log', methods=['GET'])
@login_required
def get_my_credits_log():
"""
Get current user's credits log.
Query params:
page: int (default 1)
page_size: int (default 20)
"""
try:
from app.services.billing_service import get_billing_service
user_id = getattr(g, 'user_id', None)
if not user_id:
return jsonify({'code': 0, 'msg': 'Not authenticated', 'data': None}), 401
page = request.args.get('page', 1, type=int)
page_size = request.args.get('page_size', 20, type=int)
page_size = min(100, max(1, page_size))
result = get_billing_service().get_credits_log(user_id, page, page_size)
return jsonify({'code': 1, 'msg': 'success', 'data': result})
except Exception as e:
logger.error(f"get_my_credits_log failed: {e}")
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500
@user_bp.route('/my-referrals', methods=['GET'])
@login_required
def get_my_referrals():
"""
Get list of users referred by current user.
Query params:
page: int (default 1)
page_size: int (default 20)
Returns:
list: Users referred by current user (id, username, nickname, avatar, created_at)
total: Total count of referrals
referral_code: Current user's referral code (user ID)
referral_bonus: Credits earned per referral
register_bonus: Credits new users get on registration
"""
try:
import os
from app.utils.db import get_db_connection
user_id = getattr(g, 'user_id', None)
if not user_id:
return jsonify({'code': 0, 'msg': 'Not authenticated', 'data': None}), 401
page = request.args.get('page', 1, type=int)
page_size = request.args.get('page_size', 20, type=int)
page_size = min(100, max(1, page_size))
offset = (page - 1) * page_size
with get_db_connection() as db:
cur = db.cursor()
# Get total count
cur.execute(
"SELECT COUNT(*) as cnt FROM qd_users WHERE referred_by = ?",
(user_id,)
)
total = cur.fetchone()['cnt']
# Get referral list
cur.execute(
"""
SELECT id, username, nickname, avatar, created_at
FROM qd_users
WHERE referred_by = ?
ORDER BY created_at DESC
LIMIT ? OFFSET ?
""",
(user_id, page_size, offset)
)
rows = cur.fetchall()
cur.close()
referrals = []
for row in rows:
referrals.append({
'id': row['id'],
'username': row['username'],
'nickname': row['nickname'],
'avatar': row['avatar'],
'created_at': row['created_at'].isoformat() if row['created_at'] else None
})
return jsonify({
'code': 1,
'msg': 'success',
'data': {
'list': referrals,
'total': total,
'page': page,
'page_size': page_size,
'referral_code': str(user_id),
'referral_bonus': int(os.getenv('CREDITS_REFERRAL_BONUS', '0')),
'register_bonus': int(os.getenv('CREDITS_REGISTER_BONUS', '0'))
}
})
except Exception as e:
logger.error(f"get_my_referrals failed: {e}")
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500
@user_bp.route('/change-password', methods=['POST'])
@login_required
def change_password():
@@ -298,18 +548,56 @@ def change_password():
old_password = data.get('old_password', '')
new_password = data.get('new_password', '')
if not old_password or not new_password:
return jsonify({'code': 0, 'msg': 'Both old and new password required', 'data': None}), 400
if not new_password:
return jsonify({'code': 0, 'msg': 'New password required', 'data': None}), 400
if len(new_password) < 6:
return jsonify({'code': 0, 'msg': 'New password must be at least 6 characters', 'data': None}), 400
success = get_user_service().change_password(user_id, old_password, new_password)
# Check if user has a password set
user_service = get_user_service()
user = user_service.get_user_by_id(user_id)
if not user:
return jsonify({'code': 0, 'msg': 'User not found', 'data': None}), 404
if success:
return jsonify({'code': 1, 'msg': 'Password changed successfully', 'data': None})
# Get password_hash to check if user has no password
from app.utils.db import get_db_connection
with get_db_connection() as db:
cur = db.cursor()
cur.execute("SELECT password_hash FROM qd_users WHERE id = ?", (user_id,))
row = cur.fetchone()
cur.close()
password_hash = row.get('password_hash', '') if row else ''
has_password = password_hash and password_hash.strip() != ''
# If user has no password, allow setting password without old password
if not has_password:
if not old_password:
# No old password required for users without password
success = user_service.reset_password(user_id, new_password)
if success:
return jsonify({'code': 1, 'msg': 'Password set successfully', 'data': None})
else:
return jsonify({'code': 0, 'msg': 'Failed to set password', 'data': None}), 500
else:
# If old_password is provided but user has no password, ignore it
success = user_service.reset_password(user_id, new_password)
if success:
return jsonify({'code': 1, 'msg': 'Password set successfully', 'data': None})
else:
return jsonify({'code': 0, 'msg': 'Failed to set password', 'data': None}), 500
else:
return jsonify({'code': 0, 'msg': 'Old password incorrect', 'data': None}), 400
# User has existing password, require old password verification
if not old_password:
return jsonify({'code': 0, 'msg': 'Old password required', 'data': None}), 400
success = user_service.change_password(user_id, old_password, new_password)
if success:
return jsonify({'code': 1, 'msg': 'Password changed successfully', 'data': None})
else:
return jsonify({'code': 0, 'msg': 'Old password incorrect', 'data': None}), 400
except ValueError as e:
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 400
except Exception as e: