""" Authentication Utilities JWT token generation, verification, and middleware decorators. Supports multi-user authentication with role-based access control. """ import jwt import datetime import os from functools import wraps from flask import request, jsonify, g from app.config.settings import Config from app.utils.logger import get_logger logger = get_logger(__name__) def generate_token(user_id: int, username: str, role: str = 'user', token_version: int = 1) -> str: """ Generate JWT token with user information. Args: user_id: User ID username: Username role: User role (admin/manager/user/viewer) token_version: Token version for single-client enforcement Returns: JWT token string """ try: payload = { 'exp': datetime.datetime.utcnow() + datetime.timedelta(days=7), 'iat': datetime.datetime.utcnow(), 'sub': username, 'user_id': user_id, 'role': role, 'token_version': token_version, # 用于单一客户端登录控制 } return jwt.encode( payload, Config.SECRET_KEY, algorithm='HS256' ) except Exception as e: logger.error(f"Token generation failed: {e}") return None def verify_token(token: str) -> dict: """ Verify JWT token and return payload. Args: token: JWT token string Returns: Token payload dict or None if invalid """ try: payload = jwt.decode(token, Config.SECRET_KEY, algorithms=['HS256']) # 验证 token_version(单一客户端登录控制) user_id = payload.get('user_id') token_version = payload.get('token_version') if user_id and token_version is not None: # 检查数据库中的 token_version 是否匹配 if not _verify_token_version(user_id, token_version): logger.debug(f"Token version mismatch for user {user_id}: expected current, got {token_version}") return None return payload except jwt.ExpiredSignatureError: logger.debug("Token expired") return None except jwt.InvalidTokenError as e: logger.debug(f"Invalid token: {e}") return None def _verify_token_version(user_id: int, token_version: int) -> bool: """ 验证 token 版本是否与数据库中存储的版本匹配。 用于实现单一客户端登录(踢出重复登录)。 Args: user_id: 用户ID token_version: Token中的版本号 Returns: True if version matches, False otherwise """ try: from app.utils.db import get_db_connection with get_db_connection() as db: cur = db.cursor() cur.execute( "SELECT token_version FROM qd_users WHERE id = ?", (user_id,) ) row = cur.fetchone() cur.close() if not row: return False db_token_version = row.get('token_version') or 1 return int(token_version) == int(db_token_version) except Exception as e: logger.error(f"_verify_token_version failed: {e}") # 如果验证失败,为了安全起见,返回 False return False def get_current_user_id() -> int: """Get current user ID from flask.g context""" return getattr(g, 'user_id', None) def get_current_user_role() -> str: """Get current user role from flask.g context""" return getattr(g, 'user_role', 'user') def login_required(f): """ Decorator that enforces Bearer token auth. Sets g.user, g.user_id, g.user_role on successful auth. """ @wraps(f) def decorated(*args, **kwargs): token = None # Read token from Authorization: Bearer auth_header = request.headers.get('Authorization') if auth_header: parts = auth_header.split() if len(parts) == 2 and parts[0].lower() == 'bearer': token = parts[1] if not token: return jsonify({'code': 401, 'msg': 'Token missing', 'data': None}), 401 payload = verify_token(token) if not payload: return jsonify({'code': 401, 'msg': 'Token invalid or expired', 'data': None}), 401 # Store user info in flask.g g.user = payload.get('sub') g.user_id = payload.get('user_id') g.user_role = payload.get('role', 'user') return f(*args, **kwargs) return decorated def admin_required(f): """ Decorator that requires admin role. Must be used after @login_required. """ @wraps(f) def decorated(*args, **kwargs): role = getattr(g, 'user_role', None) if role != 'admin': return jsonify({'code': 403, 'msg': 'Admin access required', 'data': None}), 403 return f(*args, **kwargs) return decorated def manager_required(f): """ Decorator that requires manager or admin role. Must be used after @login_required. """ @wraps(f) def decorated(*args, **kwargs): role = getattr(g, 'user_role', None) if role not in ('admin', 'manager'): return jsonify({'code': 403, 'msg': 'Manager access required', 'data': None}), 403 return f(*args, **kwargs) return decorated def permission_required(permission: str): """ Decorator factory that checks for a specific permission. Must be used after @login_required. Usage: @login_required @permission_required('strategy') def my_endpoint(): ... """ def decorator(f): @wraps(f) def decorated(*args, **kwargs): role = getattr(g, 'user_role', 'user') # Import here to avoid circular import from app.services.user_service import get_user_service permissions = get_user_service().get_user_permissions(role) if permission not in permissions: return jsonify({ 'code': 403, 'msg': f'Permission denied: {permission}', 'data': None }), 403 return f(*args, **kwargs) return decorated return decorator # Legacy compatibility: single-user mode fallback 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 authenticate_legacy(username: str, password: str) -> dict: """ Legacy single-user authentication (for backward compatibility). Uses ADMIN_USER and ADMIN_PASSWORD from environment. """ if username == Config.ADMIN_USER and password == Config.ADMIN_PASSWORD: return { 'user_id': 1, 'username': username, 'role': 'admin', 'nickname': 'Admin', } return None