全局整改: 布局重构、鉴权加固、API修复
- 首页/播放页: 去掉 antd Layout 改用 div 避免 flex 布局问题
- 播放页: 返回按钮改为 navigate('/') 替代 window.close()
- 后端登录/验证: 改用 POST JSON body 替代 query param
- 后端鉴权: require_auth 依赖注入保护所有管理接口
- nodes路由: 修复 /strategy 被 /{node_id} 拦截的顺序问题
- 前端 Settings: 策略更新改为 JSON body
This commit is contained in:
1 parent
f3b2a9cb46
commit
b81a7bf293
7 files changed
+110
-64
No files matched your search
+24
-7
@@ -1,6 +1,6 @@
|
||||
import hashlib
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi import APIRouter, HTTPException, Depends, Header
|
||||
from pydantic import BaseModel
|
||||
from config import settings
|
||||
|
||||
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
||||
@@ -8,6 +8,14 @@ router = APIRouter(prefix="/api/auth", tags=["auth"])
|
||||
_tokens: set[str] = set()
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
password: str
|
||||
|
||||
|
||||
class VerifyRequest(BaseModel):
|
||||
token: str
|
||||
|
||||
|
||||
def hash_password(pw: str) -> str:
|
||||
return hashlib.sha256(f"{pw}:livestream-salt".encode()).hexdigest()
|
||||
|
||||
@@ -15,17 +23,26 @@ def hash_password(pw: str) -> str:
|
||||
ADMIN_HASH = hash_password(settings.admin_password)
|
||||
|
||||
|
||||
async def require_auth(authorization: str = Header(None)) -> bool:
|
||||
if not authorization:
|
||||
raise HTTPException(status_code=401, detail="Missing token")
|
||||
token = authorization.replace("Bearer ", "")
|
||||
if token not in _tokens:
|
||||
raise HTTPException(status_code=401, detail="Invalid token")
|
||||
return True
|
||||
|
||||
|
||||
@router.post("/login")
|
||||
async def login(password: str):
|
||||
if hash_password(password) != ADMIN_HASH:
|
||||
async def login(body: LoginRequest):
|
||||
if hash_password(body.password) != ADMIN_HASH:
|
||||
raise HTTPException(status_code=401, detail="Invalid password")
|
||||
token = hashlib.sha256(f"{password}:token".encode()).hexdigest()
|
||||
token = hashlib.sha256(f"{body.password}:token".encode()).hexdigest()
|
||||
_tokens.add(token)
|
||||
return {"token": token}
|
||||
|
||||
|
||||
@router.post("/verify")
|
||||
async def verify(token: str):
|
||||
if token not in _tokens:
|
||||
async def verify(body: VerifyRequest):
|
||||
if body.token not in _tokens:
|
||||
raise HTTPException(status_code=401, detail="Invalid token")
|
||||
return {"valid": True}
|
||||
+28
-23
@@ -1,6 +1,7 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from pydantic import BaseModel
|
||||
from config import settings, Settings
|
||||
from config import settings
|
||||
from routes.auth import require_auth
|
||||
from services.load_balancer import lb
|
||||
from services.multicast_detector import multicast_detector
|
||||
|
||||
@@ -12,8 +13,13 @@ class NodeUpdate(BaseModel):
|
||||
enabled: bool | None = None
|
||||
|
||||
|
||||
class StrategyUpdate(BaseModel):
|
||||
strategy: str
|
||||
multicast_enabled: bool | None = None
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_nodes():
|
||||
async def list_nodes(_: bool = Depends(require_auth)):
|
||||
nodes = []
|
||||
for n in settings.edge_nodes:
|
||||
nodes.append({
|
||||
@@ -28,8 +34,26 @@ async def list_nodes():
|
||||
return nodes
|
||||
|
||||
|
||||
@router.get("/strategy")
|
||||
async def get_strategy(_: bool = Depends(require_auth)):
|
||||
return {
|
||||
"strategy": settings.load_balance_strategy,
|
||||
"multicast_enabled": settings.multicast_enabled,
|
||||
"multicast_available": multicast_detector.is_available(),
|
||||
}
|
||||
|
||||
|
||||
@router.put("/strategy")
|
||||
async def update_strategy(body: StrategyUpdate, _: bool = Depends(require_auth)):
|
||||
if body.strategy in ("round-robin", "weighted", "least-conn"):
|
||||
settings.load_balance_strategy = body.strategy
|
||||
if body.multicast_enabled is not None:
|
||||
settings.multicast_enabled = body.multicast_enabled
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.put("/{node_id}")
|
||||
async def update_node(node_id: str, body: NodeUpdate):
|
||||
async def update_node(node_id: str, body: NodeUpdate, _: bool = Depends(require_auth)):
|
||||
found = False
|
||||
for n in settings.edge_nodes:
|
||||
if n["id"] == node_id:
|
||||
@@ -41,22 +65,3 @@ async def update_node(node_id: str, body: NodeUpdate):
|
||||
if not found:
|
||||
raise HTTPException(status_code=404, detail="Node not found")
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.get("/strategy")
|
||||
async def get_strategy():
|
||||
return {
|
||||
"strategy": settings.load_balance_strategy,
|
||||
"multicast_enabled": settings.multicast_enabled,
|
||||
"multicast_available": multicast_detector.is_available(),
|
||||
}
|
||||
|
||||
|
||||
@router.put("/strategy")
|
||||
async def update_strategy(strategy: str, multicast_enabled: bool | None = None):
|
||||
global settings
|
||||
if strategy in ("round-robin", "weighted", "least-conn"):
|
||||
settings.load_balance_strategy = strategy
|
||||
if multicast_enabled is not None:
|
||||
settings.multicast_enabled = multicast_enabled
|
||||
return {"ok": True}
|
||||
@@ -3,6 +3,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from database import get_db
|
||||
from routes.auth import require_auth
|
||||
from services.stream_service import StreamService
|
||||
from services.media_service import media_service
|
||||
from services.load_balancer import lb
|
||||
@@ -48,7 +49,8 @@ async def list_streams(db: AsyncSession = Depends(get_db)):
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def create_stream(body: StreamCreate, db: AsyncSession = Depends(get_db)):
|
||||
async def create_stream(body: StreamCreate, db: AsyncSession = Depends(get_db),
|
||||
_: bool = Depends(require_auth)):
|
||||
svc = StreamService(db)
|
||||
stream = await svc.create_stream(
|
||||
title=body.title,
|
||||
@@ -88,7 +90,8 @@ async def get_stream(stream_id: int, db: AsyncSession = Depends(get_db)):
|
||||
|
||||
|
||||
@router.put("/{stream_id}")
|
||||
async def update_stream(stream_id: int, body: StreamUpdate, db: AsyncSession = Depends(get_db)):
|
||||
async def update_stream(stream_id: int, body: StreamUpdate, db: AsyncSession = Depends(get_db),
|
||||
_: bool = Depends(require_auth)):
|
||||
svc = StreamService(db)
|
||||
existing = await svc.get_stream(stream_id)
|
||||
if not existing:
|
||||
@@ -100,7 +103,8 @@ async def update_stream(stream_id: int, body: StreamUpdate, db: AsyncSession = D
|
||||
|
||||
|
||||
@router.delete("/{stream_id}")
|
||||
async def delete_stream(stream_id: int, db: AsyncSession = Depends(get_db)):
|
||||
async def delete_stream(stream_id: int, db: AsyncSession = Depends(get_db),
|
||||
_: bool = Depends(require_auth)):
|
||||
svc = StreamService(db)
|
||||
existing = await svc.get_stream(stream_id)
|
||||
if not existing:
|
||||
@@ -127,7 +131,8 @@ async def get_watch_urls(stream_id: int, db: AsyncSession = Depends(get_db)):
|
||||
|
||||
|
||||
@router.post("/{stream_id}/go-live")
|
||||
async def stream_go_live(stream_id: int, db: AsyncSession = Depends(get_db)):
|
||||
async def stream_go_live(stream_id: int, db: AsyncSession = Depends(get_db),
|
||||
_: bool = Depends(require_auth)):
|
||||
svc = StreamService(db)
|
||||
stream = await svc.get_stream(stream_id)
|
||||
if not stream:
|
||||
@@ -137,7 +142,8 @@ async def stream_go_live(stream_id: int, db: AsyncSession = Depends(get_db)):
|
||||
|
||||
|
||||
@router.post("/{stream_id}/end")
|
||||
async def stream_end(stream_id: int, db: AsyncSession = Depends(get_db)):
|
||||
async def stream_end(stream_id: int, db: AsyncSession = Depends(get_db),
|
||||
_: bool = Depends(require_auth)):
|
||||
svc = StreamService(db)
|
||||
stream = await svc.get_stream(stream_id)
|
||||
if not stream:
|
||||
|
||||
Reference in new issue
Block a user