feat(core): 初始化 Key-IP Sentinel 服务与部署骨架

- 搭建 FastAPI、Redis、PostgreSQL、Nginx 与 Docker Compose 基础结构
- 实现反向代理、首用绑定、拦截告警、归档任务和管理接口
- 提供 Vue3 管理后台初版,以及 uv/requirements 双依赖配置
This commit is contained in:
2026-03-04 00:18:33 +08:00
commit ab1bd90c65
50 changed files with 5645 additions and 0 deletions

77
app/api/settings.py Normal file
View File

@@ -0,0 +1,77 @@
from __future__ import annotations
import logging
from typing import Literal
from fastapi import APIRouter, Depends, HTTPException, Request, status
from pydantic import BaseModel, Field
from redis.asyncio import Redis
from app.config import RUNTIME_SETTINGS_REDIS_KEY, RuntimeSettings, Settings
from app.core.ip_utils import extract_client_ip
from app.dependencies import get_redis, get_runtime_settings, get_settings, require_admin
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/admin/api/settings", tags=["settings"], dependencies=[Depends(require_admin)])
class SettingsResponse(BaseModel):
alert_webhook_url: str | None = None
alert_threshold_count: int = Field(ge=1)
alert_threshold_seconds: int = Field(ge=1)
archive_days: int = Field(ge=1)
failsafe_mode: Literal["open", "closed"]
class SettingsUpdateRequest(SettingsResponse):
pass
def serialize_runtime_settings(runtime_settings: RuntimeSettings) -> dict[str, str]:
return {
"alert_webhook_url": runtime_settings.alert_webhook_url or "",
"alert_threshold_count": str(runtime_settings.alert_threshold_count),
"alert_threshold_seconds": str(runtime_settings.alert_threshold_seconds),
"archive_days": str(runtime_settings.archive_days),
"failsafe_mode": runtime_settings.failsafe_mode,
}
@router.get("", response_model=SettingsResponse)
async def get_runtime_config(
runtime_settings: RuntimeSettings = Depends(get_runtime_settings),
) -> SettingsResponse:
return SettingsResponse(**runtime_settings.model_dump())
@router.put("", response_model=SettingsResponse)
async def update_runtime_config(
payload: SettingsUpdateRequest,
request: Request,
settings: Settings = Depends(get_settings),
redis: Redis | None = Depends(get_redis),
):
if redis is None:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Settings persistence is unavailable because Redis is offline.",
)
updated = RuntimeSettings(**payload.model_dump())
try:
await redis.hset(RUNTIME_SETTINGS_REDIS_KEY, mapping=serialize_runtime_settings(updated))
except Exception as exc:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Failed to persist runtime settings.",
) from exc
async with request.app.state.runtime_settings_lock:
request.app.state.runtime_settings = updated
logger.info(
"Runtime settings updated.",
extra={"client_ip": extract_client_ip(request, settings)},
)
return SettingsResponse(**updated.model_dump())