feat(admin): 添加用户管理相关文件
添加用户管理视图、API和状态管理文件
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
from .api_client import APIClient
|
||||
from .auth_manager import AuthManager
|
||||
|
||||
__all__ = ["APIClient", "AuthManager"]
|
||||
@@ -0,0 +1,306 @@
|
||||
import time
|
||||
from typing import Optional, Dict, Any, Union
|
||||
from datetime import datetime
|
||||
import requests
|
||||
from apitest.models.test_models import HTTPMethod, PerformanceMetrics
|
||||
from apitest.models.exceptions import RequestException
|
||||
|
||||
|
||||
class APIClient:
|
||||
"""API客户端"""
|
||||
|
||||
def __init__(self, base_url: str, timeout: int = 5000, max_retries: int = 3, logger=None):
|
||||
"""初始化API客户端
|
||||
|
||||
Args:
|
||||
base_url: 基础URL
|
||||
timeout: 超时时间(毫秒)
|
||||
max_retries: 最大重试次数
|
||||
logger: 日志记录器
|
||||
"""
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.timeout = timeout / 1000
|
||||
self.max_retries = max_retries
|
||||
self.logger = logger
|
||||
self._session = requests.Session()
|
||||
self._default_headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
}
|
||||
|
||||
def set_default_headers(self, headers: Dict[str, str]):
|
||||
"""设置默认请求头
|
||||
|
||||
Args:
|
||||
headers: 请求头字典
|
||||
"""
|
||||
self._default_headers.update(headers)
|
||||
|
||||
def set_auth_token(self, token: str):
|
||||
"""设置认证token
|
||||
|
||||
Args:
|
||||
token: 认证token
|
||||
"""
|
||||
self._default_headers["Authorization"] = f"Bearer {token}"
|
||||
|
||||
def _build_url(self, endpoint: str) -> str:
|
||||
"""构建完整URL
|
||||
|
||||
Args:
|
||||
endpoint: API端点
|
||||
|
||||
Returns:
|
||||
完整URL
|
||||
"""
|
||||
endpoint = endpoint.lstrip("/")
|
||||
return f"{self.base_url}/{endpoint}"
|
||||
|
||||
def _merge_headers(self, headers: Optional[Dict[str, str]]) -> Dict[str, str]:
|
||||
"""合并请求头
|
||||
|
||||
Args:
|
||||
headers: 请求头字典
|
||||
|
||||
Returns:
|
||||
合并后的请求头
|
||||
"""
|
||||
merged = self._default_headers.copy()
|
||||
if headers:
|
||||
merged.update(headers)
|
||||
return merged
|
||||
|
||||
def _calculate_metrics(
|
||||
self,
|
||||
start_time: float,
|
||||
request_data: Union[Dict, str, None],
|
||||
response_data: Any
|
||||
) -> PerformanceMetrics:
|
||||
"""计算性能指标
|
||||
|
||||
Args:
|
||||
start_time: 请求开始时间
|
||||
request_data: 请求数据
|
||||
response_data: 响应数据
|
||||
|
||||
Returns:
|
||||
性能指标
|
||||
"""
|
||||
end_time = time.time()
|
||||
response_time = int((end_time - start_time) * 1000)
|
||||
|
||||
request_size = 0
|
||||
if request_data:
|
||||
if isinstance(request_data, dict):
|
||||
request_size = len(str(request_data))
|
||||
elif isinstance(request_data, str):
|
||||
request_size = len(request_data)
|
||||
|
||||
response_size = 0
|
||||
if response_data:
|
||||
response_size = len(str(response_data))
|
||||
|
||||
return PerformanceMetrics(
|
||||
response_time=response_time,
|
||||
request_size=request_size,
|
||||
response_size=response_size,
|
||||
timestamp=datetime.now()
|
||||
)
|
||||
|
||||
def _execute_request(
|
||||
self,
|
||||
method: HTTPMethod,
|
||||
url: str,
|
||||
headers: Dict[str, str],
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
body: Optional[Dict[str, Any]] = None
|
||||
) -> requests.Response:
|
||||
"""执行HTTP请求
|
||||
|
||||
Args:
|
||||
method: HTTP方法
|
||||
url: 请求URL
|
||||
headers: 请求头
|
||||
params: URL参数
|
||||
body: 请求体
|
||||
|
||||
Returns:
|
||||
响应对象
|
||||
|
||||
Raises:
|
||||
RequestException: 请求失败时抛出
|
||||
"""
|
||||
try:
|
||||
if method == HTTPMethod.GET:
|
||||
return self._session.get(url, headers=headers, params=params, timeout=self.timeout)
|
||||
elif method == HTTPMethod.POST:
|
||||
return self._session.post(url, headers=headers, params=params, json=body, timeout=self.timeout)
|
||||
elif method == HTTPMethod.PUT:
|
||||
return self._session.put(url, headers=headers, params=params, json=body, timeout=self.timeout)
|
||||
elif method == HTTPMethod.DELETE:
|
||||
return self._session.delete(url, headers=headers, params=params, timeout=self.timeout)
|
||||
elif method == HTTPMethod.PATCH:
|
||||
return self._session.patch(url, headers=headers, params=params, json=body, timeout=self.timeout)
|
||||
elif method == HTTPMethod.HEAD:
|
||||
return self._session.head(url, headers=headers, params=params, timeout=self.timeout)
|
||||
elif method == HTTPMethod.OPTIONS:
|
||||
return self._session.options(url, headers=headers, params=params, timeout=self.timeout)
|
||||
else:
|
||||
raise RequestException(f"不支持的HTTP方法: {method}")
|
||||
|
||||
except requests.Timeout:
|
||||
raise RequestException(f"请求超时: {url}")
|
||||
except requests.ConnectionError:
|
||||
raise RequestException(f"连接失败: {url}")
|
||||
except requests.RequestException as e:
|
||||
raise RequestException(f"请求异常: {e}")
|
||||
|
||||
def request(
|
||||
self,
|
||||
method: HTTPMethod,
|
||||
endpoint: str,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
body: Optional[Dict[str, Any]] = None,
|
||||
retry_count: int = 0
|
||||
) -> Dict[str, Any]:
|
||||
"""发送HTTP请求
|
||||
|
||||
Args:
|
||||
method: HTTP方法
|
||||
endpoint: API端点
|
||||
headers: 请求头
|
||||
params: URL参数
|
||||
body: 请求体
|
||||
retry_count: 当前重试次数
|
||||
|
||||
Returns:
|
||||
包含响应数据和性能指标的字典
|
||||
|
||||
Raises:
|
||||
RequestException: 请求失败且重试次数用尽时抛出
|
||||
"""
|
||||
url = self._build_url(endpoint)
|
||||
merged_headers = self._merge_headers(headers)
|
||||
|
||||
if self.logger:
|
||||
self.logger.debug(f"发送{method.value}请求: {url}")
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
response = self._execute_request(method, url, merged_headers, params, body)
|
||||
|
||||
try:
|
||||
response_body = response.json()
|
||||
except ValueError:
|
||||
response_body = response.text
|
||||
|
||||
performance = self._calculate_metrics(start_time, body, response_body)
|
||||
|
||||
if self.logger:
|
||||
self.logger.debug(
|
||||
f"响应: HTTP {response.status_code}, "
|
||||
f"耗时: {performance.response_time}ms, "
|
||||
f"大小: {performance.response_size}字节"
|
||||
)
|
||||
|
||||
return {
|
||||
"status_code": response.status_code,
|
||||
"response_body": response_body,
|
||||
"response_headers": dict(response.headers),
|
||||
"performance": performance
|
||||
}
|
||||
|
||||
except RequestException as e:
|
||||
if retry_count < self.max_retries:
|
||||
if self.logger:
|
||||
self.logger.warning(f"请求失败,正在重试 ({retry_count + 1}/{self.max_retries}): {e}")
|
||||
time.sleep(1 * (retry_count + 1))
|
||||
return self.request(method, endpoint, headers, params, body, retry_count + 1)
|
||||
else:
|
||||
if self.logger:
|
||||
self.logger.error(f"请求失败,重试次数用尽: {e}")
|
||||
raise
|
||||
|
||||
def get(
|
||||
self,
|
||||
endpoint: str,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
params: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""发送GET请求
|
||||
|
||||
Args:
|
||||
endpoint: API端点
|
||||
headers: 请求头
|
||||
params: URL参数
|
||||
|
||||
Returns:
|
||||
响应数据
|
||||
"""
|
||||
return self.request(HTTPMethod.GET, endpoint, headers, params)
|
||||
|
||||
def post(
|
||||
self,
|
||||
endpoint: str,
|
||||
body: Optional[Dict[str, Any]] = None,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
params: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""发送POST请求
|
||||
|
||||
Args:
|
||||
endpoint: API端点
|
||||
body: 请求体
|
||||
headers: 请求头
|
||||
params: URL参数
|
||||
|
||||
Returns:
|
||||
响应数据
|
||||
"""
|
||||
return self.request(HTTPMethod.POST, endpoint, headers, params, body)
|
||||
|
||||
def put(
|
||||
self,
|
||||
endpoint: str,
|
||||
body: Optional[Dict[str, Any]] = None,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
params: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""发送PUT请求
|
||||
|
||||
Args:
|
||||
endpoint: API端点
|
||||
body: 请求体
|
||||
headers: 请求头
|
||||
params: URL参数
|
||||
|
||||
Returns:
|
||||
响应数据
|
||||
"""
|
||||
return self.request(HTTPMethod.PUT, endpoint, headers, params, body)
|
||||
|
||||
def delete(
|
||||
self,
|
||||
endpoint: str,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
params: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""发送DELETE请求
|
||||
|
||||
Args:
|
||||
endpoint: API端点
|
||||
headers: 请求头
|
||||
params: URL参数
|
||||
|
||||
Returns:
|
||||
响应数据
|
||||
"""
|
||||
return self.request(HTTPMethod.DELETE, endpoint, headers, params)
|
||||
|
||||
def close(self):
|
||||
"""关闭会话"""
|
||||
self._session.close()
|
||||
if self.logger:
|
||||
self.logger.debug("API客户端会话已关闭")
|
||||
@@ -0,0 +1,201 @@
|
||||
import json
|
||||
import time
|
||||
from typing import Optional, Dict, Any
|
||||
from datetime import datetime, timedelta
|
||||
from apitest.models.exceptions import AuthException
|
||||
|
||||
|
||||
class AuthManager:
|
||||
"""认证管理器"""
|
||||
|
||||
def __init__(self, base_url: str, credentials: Dict[str, str], logger):
|
||||
"""初始化认证管理器
|
||||
|
||||
Args:
|
||||
base_url: 基础URL
|
||||
credentials: 认证凭据
|
||||
logger: 日志记录器
|
||||
"""
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.credentials = credentials
|
||||
self.logger = logger
|
||||
self._token: Optional[str] = None
|
||||
self._token_expiry: Optional[datetime] = None
|
||||
self._refresh_token: Optional[str] = None
|
||||
self._login_endpoint: str = "/sys/auth/login"
|
||||
|
||||
def set_login_endpoint(self, endpoint: str):
|
||||
"""设置登录端点
|
||||
|
||||
Args:
|
||||
endpoint: 登录端点
|
||||
"""
|
||||
self._login_endpoint = endpoint
|
||||
|
||||
def login(self, login_endpoint: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""执行登录操作
|
||||
|
||||
Args:
|
||||
login_endpoint: 登录端点,默认使用配置的端点
|
||||
|
||||
Returns:
|
||||
登录响应数据
|
||||
|
||||
Raises:
|
||||
AuthException: 登录失败时抛出
|
||||
"""
|
||||
endpoint = login_endpoint or self._login_endpoint
|
||||
url = f"{self.base_url}{endpoint}"
|
||||
|
||||
self.logger.info(f"尝试登录: {url}")
|
||||
|
||||
try:
|
||||
import requests
|
||||
|
||||
response = requests.post(
|
||||
url,
|
||||
json={
|
||||
"username": self.credentials.get("username", ""),
|
||||
"password": self.credentials.get("password", "")
|
||||
},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
|
||||
if "data" in data and "token" in data["data"]:
|
||||
self._token = data["data"]["token"]
|
||||
self._refresh_token = data["data"].get("refreshToken")
|
||||
|
||||
expiry_seconds = data["data"].get("expiresIn", 3600)
|
||||
self._token_expiry = datetime.now() + timedelta(seconds=expiry_seconds)
|
||||
|
||||
self.logger.info("登录成功")
|
||||
return data
|
||||
else:
|
||||
raise AuthException("登录响应中未找到token")
|
||||
else:
|
||||
raise AuthException(f"登录失败: HTTP {response.status_code}")
|
||||
|
||||
except requests.RequestException as e:
|
||||
raise AuthException(f"登录请求失败: {e}")
|
||||
|
||||
def get_token(self) -> Optional[str]:
|
||||
"""获取当前token
|
||||
|
||||
Returns:
|
||||
当前token,如果未登录则返回None
|
||||
"""
|
||||
return self._token
|
||||
|
||||
def set_token(self, token: str, expiry_seconds: int = 3600):
|
||||
"""设置token
|
||||
|
||||
Args:
|
||||
token: 认证令牌
|
||||
expiry_seconds: 过期时间(秒),默认3600秒
|
||||
"""
|
||||
self._token = token
|
||||
self._token_expiry = datetime.now() + timedelta(seconds=expiry_seconds)
|
||||
self.logger.info("Token已设置")
|
||||
|
||||
def is_token_valid(self) -> bool:
|
||||
"""检查token是否有效
|
||||
|
||||
Returns:
|
||||
token是否有效
|
||||
"""
|
||||
if not self._token or not self._token_expiry:
|
||||
return False
|
||||
|
||||
return datetime.now() < self._token_expiry
|
||||
|
||||
def refresh_token(self) -> bool:
|
||||
"""刷新token
|
||||
|
||||
Returns:
|
||||
刷新是否成功
|
||||
"""
|
||||
if not self._refresh_token:
|
||||
self.logger.warning("没有可用的refresh token")
|
||||
return False
|
||||
|
||||
try:
|
||||
import requests
|
||||
|
||||
url = f"{self.base_url}/sys/auth/refresh"
|
||||
response = requests.post(
|
||||
url,
|
||||
json={"refreshToken": self._refresh_token},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
|
||||
if "data" in data and "token" in data["data"]:
|
||||
self._token = data["data"]["token"]
|
||||
self._refresh_token = data["data"].get("refreshToken")
|
||||
|
||||
expiry_seconds = data["data"].get("expiresIn", 3600)
|
||||
self._token_expiry = datetime.now() + timedelta(seconds=expiry_seconds)
|
||||
|
||||
self.logger.info("Token刷新成功")
|
||||
return True
|
||||
|
||||
self.logger.warning("Token刷新失败,尝试重新登录")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Token刷新异常: {e}")
|
||||
return False
|
||||
|
||||
def ensure_authenticated(self) -> str:
|
||||
"""确保已认证,如果token过期则自动刷新或重新登录
|
||||
|
||||
Returns:
|
||||
有效的token
|
||||
|
||||
Raises:
|
||||
AuthException: 认证失败时抛出
|
||||
"""
|
||||
if not self._token:
|
||||
self.login()
|
||||
elif not self.is_token_valid():
|
||||
if not self.refresh_token():
|
||||
self.login()
|
||||
|
||||
if not self._token:
|
||||
raise AuthException("无法获取有效的认证token")
|
||||
|
||||
return self._token
|
||||
|
||||
def logout(self):
|
||||
"""登出"""
|
||||
self._token = None
|
||||
self._refresh_token = None
|
||||
self._token_expiry = None
|
||||
self.logger.info("已登出")
|
||||
|
||||
def get_auth_headers(self) -> Dict[str, str]:
|
||||
"""获取认证请求头
|
||||
|
||||
Returns:
|
||||
包含认证信息的请求头字典
|
||||
"""
|
||||
token = self.ensure_authenticated()
|
||||
return {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
def set_credentials(self, username: str, password: str):
|
||||
"""设置认证凭据
|
||||
|
||||
Args:
|
||||
username: 用户名
|
||||
password: 密码
|
||||
"""
|
||||
self.credentials = {"username": username, "password": password}
|
||||
self.logger.info("认证凭据已更新")
|
||||
Reference in New Issue
Block a user