mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-09 19:22:26 +08:00
- 新增 Management Token(访问令牌)功能,支持创建、更新、删除和管理 - 前端添加访问令牌管理页面,支持普通用户和管理员 - 后端实现完整的令牌生命周期管理 API - 添加数据库迁移脚本创建 management_tokens 表 - Nginx 配置添加 gzip 压缩,优化响应传输 - Dialog 组件添加 persistent 属性,防止意外关闭 - 为管理后台 API 添加详细的中文文档注释 - 简化多处类型注解,统一代码风格
185 lines
6.1 KiB
Python
185 lines
6.1 KiB
Python
"""普通用户可访问的监控与审计端点。"""
|
||
|
||
from dataclasses import dataclass
|
||
from datetime import datetime, timedelta, timezone
|
||
from typing import Optional
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||
from sqlalchemy.orm import Session
|
||
|
||
from src.api.base.adapter import ApiAdapter, ApiMode
|
||
from src.api.base.pagination import PaginationMeta, build_pagination_payload, paginate_query
|
||
from src.api.base.pipeline import ApiRequestPipeline
|
||
from src.core.logger import logger
|
||
from src.database import get_db
|
||
from src.models.database import ApiKey, AuditLog
|
||
from src.plugins.manager import get_plugin_manager
|
||
|
||
router = APIRouter(prefix="/api/monitoring", tags=["Monitoring"])
|
||
pipeline = ApiRequestPipeline()
|
||
|
||
|
||
@router.get("/my-audit-logs")
|
||
async def get_my_audit_logs(
|
||
request: Request,
|
||
event_type: Optional[str] = Query(None, description="事件类型筛选"),
|
||
days: int = Query(30, description="查询天数"),
|
||
limit: int = Query(50, description="返回数量限制"),
|
||
offset: int = Query(0, ge=0, description="偏移量"),
|
||
db: Session = Depends(get_db),
|
||
):
|
||
"""
|
||
获取我的审计日志
|
||
|
||
获取当前用户的审计日志记录。需要登录。
|
||
|
||
**查询参数**:
|
||
- `event_type`: 可选,事件类型筛选
|
||
- `days`: 查询最近多少天的日志,默认 30 天
|
||
- `limit`: 返回数量限制,默认 50
|
||
- `offset`: 分页偏移量,默认 0
|
||
|
||
**返回字段**:
|
||
- `items`: 审计日志列表,每条日志包含:
|
||
- `id`: 日志 ID
|
||
- `event_type`: 事件类型
|
||
- `description`: 事件描述
|
||
- `ip_address`: IP 地址
|
||
- `status_code`: HTTP 状态码
|
||
- `created_at`: 创建时间
|
||
- `meta`: 分页元数据(total, limit, offset, count)
|
||
- `filters`: 筛选条件
|
||
"""
|
||
adapter = UserAuditLogsAdapter(event_type=event_type, days=days, limit=limit, offset=offset)
|
||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||
|
||
|
||
@router.get("/rate-limit-status")
|
||
async def get_rate_limit_status(request: Request, db: Session = Depends(get_db)):
|
||
"""
|
||
获取速率限制状态
|
||
|
||
获取当前用户所有活跃 API Key 的速率限制状态。需要登录。
|
||
|
||
**返回字段**:
|
||
- `user_id`: 用户 ID
|
||
- `api_keys`: API Key 限流状态列表,每个包含:
|
||
- `api_key_name`: API Key 名称
|
||
- `limit`: 速率限制上限
|
||
- `remaining`: 剩余可用次数
|
||
- `reset_time`: 限制重置时间
|
||
- `window`: 时间窗口
|
||
"""
|
||
adapter = UserRateLimitStatusAdapter()
|
||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||
|
||
|
||
class AuthenticatedApiAdapter(ApiAdapter):
|
||
"""需要用户登录的适配器基类。"""
|
||
|
||
mode = ApiMode.USER
|
||
|
||
def authorize(self, context): # type: ignore[override]
|
||
if not context.user:
|
||
raise HTTPException(status_code=401, detail="未登录")
|
||
|
||
|
||
@dataclass
|
||
class UserAuditLogsAdapter(AuthenticatedApiAdapter):
|
||
event_type: Optional[str]
|
||
days: int
|
||
limit: int
|
||
offset: int
|
||
|
||
async def handle(self, context): # type: ignore[override]
|
||
db = context.db
|
||
user = context.user
|
||
if not user:
|
||
raise HTTPException(status_code=401, detail="未登录")
|
||
|
||
query = db.query(AuditLog).filter(AuditLog.user_id == user.id)
|
||
if self.event_type:
|
||
query = query.filter(AuditLog.event_type == self.event_type)
|
||
|
||
cutoff_time = datetime.now(timezone.utc) - timedelta(days=self.days)
|
||
query = query.filter(AuditLog.created_at >= cutoff_time)
|
||
query = query.order_by(AuditLog.created_at.desc())
|
||
|
||
total, logs = paginate_query(query, self.limit, self.offset)
|
||
|
||
items = [
|
||
{
|
||
"id": log.id,
|
||
"event_type": log.event_type,
|
||
"description": log.description,
|
||
"ip_address": log.ip_address,
|
||
"status_code": log.status_code,
|
||
"created_at": log.created_at.isoformat() if log.created_at else None,
|
||
}
|
||
for log in logs
|
||
]
|
||
|
||
meta = PaginationMeta(
|
||
total=total,
|
||
limit=self.limit,
|
||
offset=self.offset,
|
||
count=len(items),
|
||
)
|
||
|
||
return build_pagination_payload(
|
||
items,
|
||
meta,
|
||
filters={
|
||
"event_type": self.event_type,
|
||
"days": self.days,
|
||
},
|
||
)
|
||
|
||
|
||
class UserRateLimitStatusAdapter(AuthenticatedApiAdapter):
|
||
async def handle(self, context): # type: ignore[override]
|
||
db = context.db
|
||
user = context.user
|
||
if not user:
|
||
raise HTTPException(status_code=401, detail="未登录")
|
||
|
||
rate_limiter = _get_rate_limit_plugin()
|
||
if not rate_limiter or not hasattr(rate_limiter, "get_rate_limit_headers"):
|
||
raise HTTPException(status_code=503, detail="速率限制插件未启用或不支持状态查询")
|
||
|
||
api_keys = (
|
||
db.query(ApiKey)
|
||
.filter(ApiKey.user_id == user.id, ApiKey.is_active.is_(True))
|
||
.order_by(ApiKey.created_at.desc())
|
||
.all()
|
||
)
|
||
|
||
rate_limit_info = []
|
||
for key in api_keys:
|
||
try:
|
||
headers = rate_limiter.get_rate_limit_headers(key)
|
||
except Exception as exc:
|
||
logger.warning(f"无法获取Key {key.id} 的限流信息: {exc}")
|
||
headers = {}
|
||
|
||
rate_limit_info.append(
|
||
{
|
||
"api_key_name": key.name or f"Key-{key.id}",
|
||
"limit": headers.get("X-RateLimit-Limit"),
|
||
"remaining": headers.get("X-RateLimit-Remaining"),
|
||
"reset_time": headers.get("X-RateLimit-Reset"),
|
||
"window": headers.get("X-RateLimit-Window"),
|
||
}
|
||
)
|
||
|
||
return {"user_id": user.id, "api_keys": rate_limit_info}
|
||
|
||
|
||
def _get_rate_limit_plugin():
|
||
try:
|
||
plugin_manager = get_plugin_manager()
|
||
return plugin_manager.get_plugin("rate_limit")
|
||
except Exception as exc:
|
||
logger.warning(f"获取速率限制插件失败: {exc}")
|
||
return None
|