diff --git a/backend/routes/auth.py b/backend/routes/auth.py index a1710a7..edd5fb5 100644 --- a/backend/routes/auth.py +++ b/backend/routes/auth.py @@ -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} diff --git a/backend/routes/nodes.py b/backend/routes/nodes.py index bdf7f12..123926a 100644 --- a/backend/routes/nodes.py +++ b/backend/routes/nodes.py @@ -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} diff --git a/backend/routes/streams.py b/backend/routes/streams.py index bc32b82..ec3cb1d 100644 --- a/backend/routes/streams.py +++ b/backend/routes/streams.py @@ -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: diff --git a/frontend/src/admin/pages/Settings.tsx b/frontend/src/admin/pages/Settings.tsx index 940504b..1d5ea8a 100644 --- a/frontend/src/admin/pages/Settings.tsx +++ b/frontend/src/admin/pages/Settings.tsx @@ -34,7 +34,7 @@ export default function SettingsPage() { async function saveStrategy(val: string) { setSaving(true) try { - await api.put(`/nodes/strategy?strategy=${val}&multicast_enabled=${multicastEnabled}`) + await api.put('/nodes/strategy', { strategy: val, multicast_enabled: multicastEnabled }) setStrategy(val) message.success('策略已更新') } catch { message.error('更新失败') } @@ -44,7 +44,7 @@ export default function SettingsPage() { async function toggleMulticast(val: boolean) { setMulticastEnabled(val) try { - await api.put(`/nodes/strategy?strategy=${strategy}&multicast_enabled=${val}`) + await api.put('/nodes/strategy', { strategy, multicast_enabled: val }) message.success('组播设置已更新') } catch { message.error('更新失败') } } diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts index a9d975a..c387e05 100644 --- a/frontend/src/api/auth.ts +++ b/frontend/src/api/auth.ts @@ -1,7 +1,7 @@ import api, { setToken, getToken } from './client' export async function login(password: string): Promise { - const { data } = await api.post(`/auth/login?password=${encodeURIComponent(password)}`) + const { data } = await api.post('/auth/login', { password }) setToken(data.token) return data.token } @@ -10,7 +10,7 @@ export async function verifyToken(): Promise { const token = getToken() if (!token) return false try { - await api.post(`/auth/verify?token=${encodeURIComponent(token)}`) + await api.post('/auth/verify', { token }) return true } catch { return false diff --git a/frontend/src/viewer/pages/HomePage.tsx b/frontend/src/viewer/pages/HomePage.tsx index 8fff81c..a897320 100644 --- a/frontend/src/viewer/pages/HomePage.tsx +++ b/frontend/src/viewer/pages/HomePage.tsx @@ -1,12 +1,11 @@ import { useState, useEffect } from 'react' import { useNavigate } from 'react-router-dom' import { - Layout, Row, Col, Card, Typography, Tag, Button, Skeleton, Empty, Space, + Row, Col, Card, Typography, Tag, Button, Skeleton, Empty, Space, } from 'antd' import { LoginOutlined, FireOutlined, ClockCircleOutlined } from '@ant-design/icons' import { fetchStreams, type StreamInfo } from '../../api/streams' -const { Header, Content } = Layout const { Title, Text } = Typography const statusMeta: Record = { @@ -34,8 +33,8 @@ export default function HomePage() { }) return ( - -
+
管理后台 -
- + +
全部直播 {loading ? ( - {[1,2,3,4,5,6].map((i) => ( + {[1, 2, 3, 4, 5, 6].map((i) => ( @@ -120,7 +119,7 @@ export default function HomePage() { })} )} - - +
+ ) } diff --git a/frontend/src/viewer/pages/WatchPage.tsx b/frontend/src/viewer/pages/WatchPage.tsx index 2455918..0486d29 100644 --- a/frontend/src/viewer/pages/WatchPage.tsx +++ b/frontend/src/viewer/pages/WatchPage.tsx @@ -1,16 +1,16 @@ import { useState, useEffect, useRef } from 'react' -import { useParams } from 'react-router-dom' +import { useParams, useNavigate } from 'react-router-dom' import { - Layout, Card, Select, Button, Typography, Space, Tag, Spin, Skeleton, Empty, message, + Select, Button, Typography, Space, Tag, Spin, Skeleton, Empty, message, } from 'antd' import { ArrowLeftOutlined, WifiOutlined, SignalFilled } from '@ant-design/icons' import Hls from 'hls.js' import { getWatchUrls, type WatchInfo, type LineInfo } from '../../api/streams' -const { Header, Content } = Layout const { Title, Text } = Typography export default function WatchPage() { + const navigate = useNavigate() const { id } = useParams<{ id: string }>() const videoRef = useRef(null) const [info, setInfo] = useState(null) @@ -41,7 +41,7 @@ export default function WatchPage() { setActiveQuality(data.qualities[0].label) } } - } catch { /* 直播尚未就绪 */ } + } catch { } setLoading(false) } @@ -98,7 +98,13 @@ export default function WatchPage() { if (!info) { return ( -
+
) @@ -107,19 +113,25 @@ export default function WatchPage() { const statusLabel = info.status === 'live' ? '直播中' : info.status === 'pending' ? '待开播' : info.status === 'ended' ? '已结束' : info.status return ( - -
+
@@ -157,21 +169,28 @@ export default function WatchPage() { placeholder="选择线路" /> -
- +
-
+
+
) }