2026-01-14 05:29:55 +08:00
|
|
|
"""
|
|
|
|
|
Authentication Utilities
|
2025-12-29 03:06:49 +08:00
|
|
|
|
2026-01-14 05:29:55 +08:00
|
|
|
JWT token generation, verification, and middleware decorators.
|
|
|
|
|
Supports multi-user authentication with role-based access control.
|
|
|
|
|
"""
|
2025-12-29 03:06:49 +08:00
|
|
|
import jwt
|
|
|
|
|
import datetime
|
2026-01-14 05:29:55 +08:00
|
|
|
import os
|
2025-12-29 03:06:49 +08:00
|
|
|
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__)
|
|
|
|
|
|
2026-01-14 05:29:55 +08:00
|
|
|
|
|
|
|
|
def generate_token(user_id: int, username: str, role: str = 'user') -> str:
|
|
|
|
|
"""
|
|
|
|
|
Generate JWT token with user information.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
user_id: User ID
|
|
|
|
|
username: Username
|
|
|
|
|
role: User role (admin/manager/user/viewer)
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
JWT token string
|
|
|
|
|
"""
|
2025-12-29 03:06:49 +08:00
|
|
|
try:
|
|
|
|
|
payload = {
|
|
|
|
|
'exp': datetime.datetime.utcnow() + datetime.timedelta(days=7),
|
|
|
|
|
'iat': datetime.datetime.utcnow(),
|
2026-01-14 05:29:55 +08:00
|
|
|
'sub': username,
|
|
|
|
|
'user_id': user_id,
|
|
|
|
|
'role': role,
|
2025-12-29 03:06:49 +08:00
|
|
|
}
|
|
|
|
|
return jwt.encode(
|
|
|
|
|
payload,
|
|
|
|
|
Config.SECRET_KEY,
|
|
|
|
|
algorithm='HS256'
|
|
|
|
|
)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Token generation failed: {e}")
|
|
|
|
|
return None
|
|
|
|
|
|
2026-01-14 05:29:55 +08:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
"""
|
2025-12-29 03:06:49 +08:00
|
|
|
try:
|
|
|
|
|
payload = jwt.decode(token, Config.SECRET_KEY, algorithms=['HS256'])
|
2026-01-14 05:29:55 +08:00
|
|
|
return payload
|
2025-12-29 03:06:49 +08:00
|
|
|
except jwt.ExpiredSignatureError:
|
2026-01-14 05:29:55 +08:00
|
|
|
logger.debug("Token expired")
|
2025-12-29 03:06:49 +08:00
|
|
|
return None
|
2026-01-14 05:29:55 +08:00
|
|
|
except jwt.InvalidTokenError as e:
|
|
|
|
|
logger.debug(f"Invalid token: {e}")
|
2025-12-29 03:06:49 +08:00
|
|
|
return None
|
|
|
|
|
|
2026-01-14 05:29:55 +08:00
|
|
|
|
|
|
|
|
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')
|
|
|
|
|
|
|
|
|
|
|
2025-12-29 03:06:49 +08:00
|
|
|
def login_required(f):
|
2026-01-14 05:29:55 +08:00
|
|
|
"""
|
|
|
|
|
Decorator that enforces Bearer token auth.
|
|
|
|
|
|
|
|
|
|
Sets g.user, g.user_id, g.user_role on successful auth.
|
|
|
|
|
"""
|
2025-12-29 03:06:49 +08:00
|
|
|
@wraps(f)
|
|
|
|
|
def decorated(*args, **kwargs):
|
|
|
|
|
token = None
|
|
|
|
|
|
|
|
|
|
# Read token from Authorization: Bearer <token>
|
|
|
|
|
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
|
|
|
|
|
|
2026-01-14 05:29:55 +08:00
|
|
|
payload = verify_token(token)
|
|
|
|
|
if not payload:
|
2025-12-29 03:06:49 +08:00
|
|
|
return jsonify({'code': 401, 'msg': 'Token invalid or expired', 'data': None}), 401
|
2026-01-14 05:29:55 +08:00
|
|
|
|
|
|
|
|
# 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')
|
|
|
|
|
|
2025-12-29 03:06:49 +08:00
|
|
|
return f(*args, **kwargs)
|
|
|
|
|
|
|
|
|
|
return decorated
|
|
|
|
|
|
2026-01-14 05:29:55 +08:00
|
|
|
|
|
|
|
|
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
|