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.
This commit is contained in:
dienakdz
2026-04-09 14:30:51 +07:00
parent 103055b3df
commit 87f2845483
157 changed files with 19026 additions and 17773 deletions
@@ -1,11 +1,14 @@
"""
Security Service - Handles Turnstile verification, rate limiting, and brute-force protection.
"""
import os
import json
import requests
import os
from datetime import datetime, timedelta
from typing import Tuple, Optional, Dict, Any
from typing import Any, Dict, Tuple
import requests
from app.utils.db import get_db_connection
from app.utils.logger import get_logger
@@ -25,94 +28,90 @@ def get_security_service():
class SecurityService:
"""Security service for authentication protection"""
def __init__(self):
self._load_config()
def _load_config(self):
"""Load security configuration from environment variables"""
# Turnstile config
self.turnstile_site_key = os.getenv('TURNSTILE_SITE_KEY', '')
self.turnstile_secret_key = os.getenv('TURNSTILE_SECRET_KEY', '')
self.turnstile_site_key = os.getenv("TURNSTILE_SITE_KEY", "")
self.turnstile_secret_key = os.getenv("TURNSTILE_SECRET_KEY", "")
self.turnstile_enabled = bool(self.turnstile_site_key and self.turnstile_secret_key)
# IP rate limit config
self.ip_max_attempts = int(os.getenv('SECURITY_IP_MAX_ATTEMPTS', '10'))
self.ip_window_minutes = int(os.getenv('SECURITY_IP_WINDOW_MINUTES', '5'))
self.ip_block_minutes = int(os.getenv('SECURITY_IP_BLOCK_MINUTES', '15'))
self.ip_max_attempts = int(os.getenv("SECURITY_IP_MAX_ATTEMPTS", "10"))
self.ip_window_minutes = int(os.getenv("SECURITY_IP_WINDOW_MINUTES", "5"))
self.ip_block_minutes = int(os.getenv("SECURITY_IP_BLOCK_MINUTES", "15"))
# Account rate limit config
self.account_max_attempts = int(os.getenv('SECURITY_ACCOUNT_MAX_ATTEMPTS', '5'))
self.account_window_minutes = int(os.getenv('SECURITY_ACCOUNT_WINDOW_MINUTES', '60'))
self.account_block_minutes = int(os.getenv('SECURITY_ACCOUNT_BLOCK_MINUTES', '30'))
self.account_max_attempts = int(os.getenv("SECURITY_ACCOUNT_MAX_ATTEMPTS", "5"))
self.account_window_minutes = int(os.getenv("SECURITY_ACCOUNT_WINDOW_MINUTES", "60"))
self.account_block_minutes = int(os.getenv("SECURITY_ACCOUNT_BLOCK_MINUTES", "30"))
# Verification code rate limit
self.code_rate_limit_seconds = int(os.getenv('VERIFICATION_CODE_RATE_LIMIT', '60'))
self.code_ip_hourly_limit = int(os.getenv('VERIFICATION_CODE_IP_HOURLY_LIMIT', '10'))
self.code_rate_limit_seconds = int(os.getenv("VERIFICATION_CODE_RATE_LIMIT", "60"))
self.code_ip_hourly_limit = int(os.getenv("VERIFICATION_CODE_IP_HOURLY_LIMIT", "10"))
def get_security_config(self) -> Dict[str, Any]:
"""Get public security config for frontend"""
return {
'turnstile_enabled': self.turnstile_enabled,
'turnstile_site_key': self.turnstile_site_key,
'registration_enabled': os.getenv('ENABLE_REGISTRATION', 'true').lower() == 'true',
'oauth_google_enabled': bool(os.getenv('GOOGLE_CLIENT_ID', '')),
'oauth_github_enabled': bool(os.getenv('GITHUB_CLIENT_ID', '')),
"turnstile_enabled": self.turnstile_enabled,
"turnstile_site_key": self.turnstile_site_key,
"registration_enabled": os.getenv("ENABLE_REGISTRATION", "true").lower() == "true",
"oauth_google_enabled": bool(os.getenv("GOOGLE_CLIENT_ID", "")),
"oauth_github_enabled": bool(os.getenv("GITHUB_CLIENT_ID", "")),
}
# =========================================================================
# Turnstile Verification
# =========================================================================
def verify_turnstile(self, token: str, ip_address: str = None) -> Tuple[bool, str]:
"""
Verify Cloudflare Turnstile token.
Returns:
(success, message)
"""
if not self.turnstile_enabled:
# If Turnstile is not configured, skip verification
return True, 'turnstile_disabled'
return True, "turnstile_disabled"
if not token:
return False, 'Missing Turnstile token'
return False, "Missing Turnstile token"
try:
response = requests.post(
'https://challenges.cloudflare.com/turnstile/v0/siteverify',
data={
'secret': self.turnstile_secret_key,
'response': token,
'remoteip': ip_address
},
timeout=10
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
data={"secret": self.turnstile_secret_key, "response": token, "remoteip": ip_address},
timeout=10,
)
result = response.json()
if result.get('success'):
return True, 'verified'
if result.get("success"):
return True, "verified"
else:
error_codes = result.get('error-codes', [])
error_codes = result.get("error-codes", [])
logger.warning(f"Turnstile verification failed: {error_codes}")
return False, 'Turnstile verification failed'
return False, "Turnstile verification failed"
except requests.RequestException as e:
logger.error(f"Turnstile API error: {e}")
# On API error, we might want to allow (fail-open) or deny (fail-closed)
# For security, we'll deny
return False, 'Turnstile service unavailable'
return False, "Turnstile service unavailable"
# =========================================================================
# Rate Limiting & Brute-Force Protection
# =========================================================================
def record_login_attempt(self, identifier: str, identifier_type: str,
success: bool, ip_address: str = None,
user_agent: str = None) -> bool:
def record_login_attempt(
self, identifier: str, identifier_type: str, success: bool, ip_address: str = None, user_agent: str = None
) -> bool:
"""
Record a login attempt for rate limiting.
Args:
identifier: IP address or username
identifier_type: 'ip' or 'account'
@@ -125,11 +124,11 @@ class SecurityService:
cur = db.cursor()
cur.execute(
"""
INSERT INTO qd_login_attempts
INSERT INTO qd_login_attempts
(identifier, identifier_type, success, ip_address, user_agent)
VALUES (?, ?, ?, ?, ?)
""",
(identifier, identifier_type, success, ip_address, user_agent)
(identifier, identifier_type, success, ip_address, user_agent),
)
db.commit()
cur.close()
@@ -137,16 +136,16 @@ class SecurityService:
except Exception as e:
logger.error(f"Failed to record login attempt: {e}")
return False
def is_blocked(self, identifier: str, identifier_type: str) -> Tuple[bool, int]:
"""
Check if an identifier (IP or account) is blocked due to too many failed attempts.
Returns:
(is_blocked, remaining_seconds)
"""
try:
if identifier_type == 'ip':
if identifier_type == "ip":
max_attempts = self.ip_max_attempts
window_minutes = self.ip_window_minutes
block_minutes = self.ip_block_minutes
@@ -154,30 +153,30 @@ class SecurityService:
max_attempts = self.account_max_attempts
window_minutes = self.account_window_minutes
block_minutes = self.account_block_minutes
with get_db_connection() as db:
cur = db.cursor()
# Count failed attempts in the time window
window_start = datetime.now() - timedelta(minutes=window_minutes)
cur.execute(
"""
SELECT COUNT(*) as count, MAX(attempt_time) as last_attempt
FROM qd_login_attempts
WHERE identifier = ? AND identifier_type = ?
WHERE identifier = ? AND identifier_type = ?
AND success = FALSE AND attempt_time > ?
""",
(identifier, identifier_type, window_start)
(identifier, identifier_type, window_start),
)
row = cur.fetchone()
cur.close()
if not row:
return False, 0
failed_count = row['count'] or 0
last_attempt = row['last_attempt']
failed_count = row["count"] or 0
last_attempt = row["last_attempt"]
if failed_count >= max_attempts:
# Check if still in block period
if last_attempt:
@@ -185,34 +184,37 @@ class SecurityService:
if datetime.now() < block_until:
remaining = int((block_until - datetime.now()).total_seconds())
return True, remaining
return False, 0
except Exception as e:
logger.error(f"Failed to check block status: {e}")
return False, 0
def check_login_allowed(self, username: str, ip_address: str) -> Tuple[bool, str]:
"""
Check if login is allowed for the given username and IP.
Returns:
(allowed, message)
"""
# Check IP block
ip_blocked, ip_remaining = self.is_blocked(ip_address, 'ip')
ip_blocked, ip_remaining = self.is_blocked(ip_address, "ip")
if ip_blocked:
minutes = ip_remaining // 60
return False, f'Too many failed attempts from this IP. Try again in {minutes + 1} minutes.'
return False, f"Too many failed attempts from this IP. Try again in {minutes + 1} minutes."
# Check account block
account_blocked, account_remaining = self.is_blocked(username, 'account')
account_blocked, account_remaining = self.is_blocked(username, "account")
if account_blocked:
minutes = account_remaining // 60
return False, f'Account temporarily locked due to too many failed attempts. Try again in {minutes + 1} minutes.'
return True, 'allowed'
return (
False,
f"Account temporarily locked due to too many failed attempts. Try again in {minutes + 1} minutes.",
)
return True, "allowed"
def clear_login_attempts(self, identifier: str, identifier_type: str) -> bool:
"""
Clear login attempts for an identifier (called after successful login).
@@ -225,7 +227,7 @@ class SecurityService:
DELETE FROM qd_login_attempts
WHERE identifier = ? AND identifier_type = ?
""",
(identifier, identifier_type)
(identifier, identifier_type),
)
db.commit()
cur.close()
@@ -233,17 +235,17 @@ class SecurityService:
except Exception as e:
logger.error(f"Failed to clear login attempts: {e}")
return False
# =========================================================================
# Security Audit Logging
# =========================================================================
def log_security_event(self, action: str, user_id: int = None,
ip_address: str = None, user_agent: str = None,
details: dict = None) -> bool:
def log_security_event(
self, action: str, user_id: int = None, ip_address: str = None, user_agent: str = None, details: dict = None
) -> bool:
"""
Log a security-related event.
Args:
action: Event type (login, logout, register, reset_password, etc.)
user_id: User ID if applicable
@@ -253,16 +255,16 @@ class SecurityService:
"""
try:
details_json = json.dumps(details) if details else None
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
"""
INSERT INTO qd_security_logs
INSERT INTO qd_security_logs
(user_id, action, ip_address, user_agent, details)
VALUES (?, ?, ?, ?, ?)
""",
(user_id, action, ip_address, user_agent, details_json)
(user_id, action, ip_address, user_agent, details_json),
)
db.commit()
cur.close()
@@ -270,22 +272,22 @@ class SecurityService:
except Exception as e:
logger.error(f"Failed to log security event: {e}")
return False
# =========================================================================
# Verification Code Rate Limiting
# =========================================================================
def can_send_verification_code(self, email: str, ip_address: str) -> Tuple[bool, str]:
"""
Check if we can send a verification code to this email from this IP.
Returns:
(allowed, message)
"""
try:
with get_db_connection() as db:
cur = db.cursor()
# Check email rate limit (one code per minute per email)
rate_limit_time = datetime.now() - timedelta(seconds=self.code_rate_limit_seconds)
cur.execute(
@@ -293,12 +295,12 @@ class SecurityService:
SELECT COUNT(*) as count FROM qd_verification_codes
WHERE email = ? AND created_at > ?
""",
(email, rate_limit_time)
(email, rate_limit_time),
)
row = cur.fetchone()
if row and row['count'] > 0:
return False, f'Please wait {self.code_rate_limit_seconds} seconds before requesting another code'
if row and row["count"] > 0:
return False, f"Please wait {self.code_rate_limit_seconds} seconds before requesting another code"
# Check IP hourly limit
hour_ago = datetime.now() - timedelta(hours=1)
cur.execute(
@@ -306,88 +308,82 @@ class SecurityService:
SELECT COUNT(*) as count FROM qd_verification_codes
WHERE ip_address = ? AND created_at > ?
""",
(ip_address, hour_ago)
(ip_address, hour_ago),
)
row = cur.fetchone()
if row and row['count'] >= self.code_ip_hourly_limit:
return False, 'Too many verification code requests from this IP. Try again later.'
if row and row["count"] >= self.code_ip_hourly_limit:
return False, "Too many verification code requests from this IP. Try again later."
cur.close()
return True, 'allowed'
return True, "allowed"
except Exception as e:
logger.error(f"Failed to check verification code rate limit: {e}")
return True, 'allowed' # Fail open on DB errors
return True, "allowed" # Fail open on DB errors
# =========================================================================
# Password Strength Validation
# =========================================================================
def validate_password_strength(self, password: str) -> Tuple[bool, str]:
"""
Validate password meets minimum security requirements.
Requirements:
- At least 8 characters
- Contains at least one uppercase letter
- Contains at least one lowercase letter
- Contains at least one digit
Returns:
(valid, message)
"""
if len(password) < 8:
return False, 'Password must be at least 8 characters long'
return False, "Password must be at least 8 characters long"
if not any(c.isupper() for c in password):
return False, 'Password must contain at least one uppercase letter'
return False, "Password must contain at least one uppercase letter"
if not any(c.islower() for c in password):
return False, 'Password must contain at least one lowercase letter'
return False, "Password must contain at least one lowercase letter"
if not any(c.isdigit() for c in password):
return False, 'Password must contain at least one digit'
return True, 'valid'
return False, "Password must contain at least one digit"
return True, "valid"
# =========================================================================
# Cleanup
# =========================================================================
def cleanup_old_records(self, days: int = 7) -> int:
"""
Clean up old login attempts and expired verification codes.
Returns:
Number of records deleted
"""
deleted = 0
cutoff = datetime.now() - timedelta(days=days)
try:
with get_db_connection() as db:
cur = db.cursor()
# Clean old login attempts
cur.execute(
"DELETE FROM qd_login_attempts WHERE attempt_time < ?",
(cutoff,)
)
cur.execute("DELETE FROM qd_login_attempts WHERE attempt_time < ?", (cutoff,))
deleted += cur.rowcount or 0
# Clean expired verification codes
cur.execute(
"DELETE FROM qd_verification_codes WHERE expires_at < ?",
(cutoff,)
)
cur.execute("DELETE FROM qd_verification_codes WHERE expires_at < ?", (cutoff,))
deleted += cur.rowcount or 0
db.commit()
cur.close()
logger.info(f"Security cleanup: deleted {deleted} old records")
return deleted
except Exception as e:
logger.error(f"Security cleanup failed: {e}")
return 0