Skip to content

Commit d4e1178

Browse files
fix(admin): 修复配置目录启动崩溃、登录失效与测试页鉴权
Close #159 #151 #152
1 parent a84f87a commit d4e1178

5 files changed

Lines changed: 91 additions & 30 deletions

File tree

‎src/api/admin.py‎

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import importlib
44
import json
55
from datetime import datetime, timezone
6-
from fastapi import APIRouter, Depends, HTTPException, Header, Request
6+
from fastapi import APIRouter, Depends, HTTPException, Header, Request, Response
77
from fastapi.responses import JSONResponse
88
from pydantic import BaseModel
99
from typing import Optional, List, Dict, Any
@@ -46,6 +46,7 @@
4646

4747
# Store active admin session tokens (in production, use Redis or database)
4848
active_admin_tokens = set()
49+
ADMIN_SESSION_COOKIE_NAME = "admin_session"
4950
SUPPORTED_API_CAPTCHA_METHODS = {"yescaptcha", "capmonster", "ezcaptcha", "capsolver"}
5051

5152

@@ -688,24 +689,40 @@ class TokenRefreshConfigRequest(BaseModel):
688689

689690
# ========== Auth Middleware ==========
690691

691-
async def verify_admin_token(authorization: str = Header(None)):
692+
async def verify_admin_token(request: Request, authorization: str = Header(None)):
692693
"""Verify admin session token (NOT API key)"""
693-
if not authorization or not authorization.startswith("Bearer "):
694-
raise HTTPException(status_code=401, detail="Missing authorization")
694+
header_token = ""
695+
if authorization and authorization.startswith("Bearer "):
696+
header_token = authorization[7:].strip()
695697

696-
token = authorization[7:]
698+
cookie_token = get_admin_token_from_cookie(request) or ""
697699

698-
# Check if token is in active session tokens
699-
if token not in active_admin_tokens:
700+
if header_token and header_token in active_admin_tokens:
701+
return header_token
702+
703+
if cookie_token and cookie_token in active_admin_tokens:
704+
return cookie_token
705+
706+
if header_token or cookie_token:
700707
raise HTTPException(status_code=401, detail="Invalid or expired admin token")
701708

702-
return token
709+
raise HTTPException(status_code=401, detail="Missing authorization")
710+
711+
712+
def get_admin_token_from_cookie(request: Request) -> Optional[str]:
713+
token = str(request.cookies.get(ADMIN_SESSION_COOKIE_NAME) or "").strip()
714+
return token or None
715+
716+
717+
def is_admin_session_token_valid(token: Optional[str]) -> bool:
718+
normalized = str(token or "").strip()
719+
return bool(normalized) and normalized in active_admin_tokens
703720

704721

705722
# ========== Auth Endpoints ==========
706723

707724
@router.post("/api/admin/login")
708-
async def admin_login(request: LoginRequest):
725+
async def admin_login(request: LoginRequest, response: Response):
709726
"""Admin login - returns session token (NOT API key)"""
710727
admin_config = await db.get_admin_config()
711728

@@ -718,6 +735,15 @@ async def admin_login(request: LoginRequest):
718735
# Store in active tokens
719736
active_admin_tokens.add(session_token)
720737

738+
response.set_cookie(
739+
key=ADMIN_SESSION_COOKIE_NAME,
740+
value=session_token,
741+
httponly=True,
742+
samesite="lax",
743+
secure=False,
744+
path="/",
745+
)
746+
721747
return {
722748
"success": True,
723749
"token": session_token, # Session token (NOT API key)
@@ -726,9 +752,10 @@ async def admin_login(request: LoginRequest):
726752

727753

728754
@router.post("/api/admin/logout")
729-
async def admin_logout(token: str = Depends(verify_admin_token)):
755+
async def admin_logout(response: Response, token: str = Depends(verify_admin_token)):
730756
"""Admin logout - invalidate session token"""
731757
active_admin_tokens.discard(token)
758+
response.delete_cookie(ADMIN_SESSION_COOKIE_NAME, path="/")
732759
return {"success": True, "message": "退出登录成功"}
733760

734761

@@ -1419,15 +1446,15 @@ async def get_system_info(token: str = Depends(verify_admin_token)):
14191446
# ========== Additional Routes for Frontend Compatibility ==========
14201447

14211448
@router.post("/api/login")
1422-
async def login(request: LoginRequest):
1449+
async def login(request: LoginRequest, response: Response):
14231450
"""Login endpoint (alias for /api/admin/login)"""
1424-
return await admin_login(request)
1451+
return await admin_login(request, response)
14251452

14261453

14271454
@router.post("/api/logout")
1428-
async def logout(token: str = Depends(verify_admin_token)):
1455+
async def logout(response: Response, token: str = Depends(verify_admin_token)):
14291456
"""Logout endpoint (alias for /api/admin/logout)"""
1430-
return await admin_logout(token)
1457+
return await admin_logout(response, token)
14311458

14321459

14331460
@router.get("/health")

‎src/core/config.py‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,23 @@ def _load_config(self) -> Dict[str, Any]:
3636
"""Load configuration from setting.toml, falling back to the example file."""
3737
config_dir = Path(__file__).parent.parent.parent / "config"
3838
config_path = config_dir / "setting.toml"
39-
if not config_path.exists():
40-
config_path = config_dir / "setting_example.toml"
39+
fallback_path = config_dir / "setting_example.toml"
40+
41+
if config_path.exists() and not config_path.is_file():
42+
print(
43+
f"[Config] 检测到 {config_path} 不是普通文件,"
44+
f"将回退到 {fallback_path.name}。请检查 Docker 挂载或本地配置路径。"
45+
)
46+
config_path = fallback_path
47+
elif not config_path.exists():
48+
config_path = fallback_path
49+
50+
if not config_path.is_file():
51+
raise FileNotFoundError(
52+
f"配置文件不存在或不可读取: {config_path}. "
53+
f"请确认 config 目录下存在可用的 setting.toml 或 setting_example.toml"
54+
)
55+
4156
with open(config_path, "rb") as f:
4257
return tomli.load(f)
4358

‎src/main.py‎

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
import sys
55
import warnings
66

7-
from fastapi import FastAPI
8-
from fastapi.responses import HTMLResponse, FileResponse, Response
7+
from fastapi import FastAPI, Request
8+
from fastapi.responses import HTMLResponse, FileResponse, Response, RedirectResponse
99
from fastapi.staticfiles import StaticFiles
1010
from fastapi.middleware.cors import CORSMiddleware
1111
from contextlib import asynccontextmanager
@@ -280,14 +280,23 @@ async def auto_unban_task():
280280

281281
# HTML routes for frontend
282282
static_path = Path(__file__).parent.parent / "static"
283+
_STATIC_PAGE_NO_CACHE_HEADERS = {
284+
"Cache-Control": "no-store, no-cache, must-revalidate",
285+
"Pragma": "no-cache",
286+
"Expires": "0",
287+
}
288+
289+
290+
def _static_page_response(file_path: Path) -> FileResponse:
291+
return FileResponse(str(file_path), headers=_STATIC_PAGE_NO_CACHE_HEADERS)
283292

284293

285294
@app.get("/", response_class=HTMLResponse)
286295
async def index():
287296
"""Redirect to login page"""
288297
login_file = static_path / "login.html"
289298
if login_file.exists():
290-
return FileResponse(str(login_file))
299+
return _static_page_response(login_file)
291300
return HTMLResponse(content="<h1>Flow2API</h1><p>Frontend not found</p>", status_code=404)
292301

293302

@@ -296,25 +305,31 @@ async def login_page():
296305
"""Login page"""
297306
login_file = static_path / "login.html"
298307
if login_file.exists():
299-
return FileResponse(str(login_file))
308+
return _static_page_response(login_file)
300309
return HTMLResponse(content="<h1>Login Page Not Found</h1>", status_code=404)
301310

302311

303312
@app.get("/manage", response_class=HTMLResponse)
304-
async def manage_page():
313+
async def manage_page(request: Request):
305314
"""Management console page"""
315+
guard_response = _ensure_admin_page_session(request)
316+
if guard_response is not None:
317+
return guard_response
306318
manage_file = static_path / "manage.html"
307319
if manage_file.exists():
308-
return FileResponse(str(manage_file))
320+
return _static_page_response(manage_file)
309321
return HTMLResponse(content="<h1>Management Page Not Found</h1>", status_code=404)
310322

311323

312324
@app.get("/test", response_class=HTMLResponse)
313-
async def test_page():
325+
async def test_page(request: Request):
314326
"""Model testing page"""
327+
guard_response = _ensure_admin_page_session(request)
328+
if guard_response is not None:
329+
return guard_response
315330
test_file = static_path / "test.html"
316331
if test_file.exists():
317-
return FileResponse(str(test_file))
332+
return _static_page_response(test_file)
318333
return HTMLResponse(content="<h1>Test Page Not Found</h1>", status_code=404)
319334

320335

@@ -323,3 +338,8 @@ async def metrics():
323338
"""Prometheus metrics endpoint for the main Flow2API service."""
324339
payload = await render_main_metrics(db, concurrency_manager=concurrency_manager)
325340
return Response(content=payload, media_type=CONTENT_TYPE_LATEST)
341+
def _ensure_admin_page_session(request: Request):
342+
token = admin.get_admin_token_from_cookie(request)
343+
if not admin.is_admin_session_token_valid(token):
344+
return RedirectResponse(url="/login", status_code=302)
345+
return None

‎static/login.html‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ <h1 class="text-4xl font-bold">Flow2API</h1>
4545

4646
<script>
4747
const form=document.getElementById('loginForm'),btn=document.getElementById('loginButton');
48-
form.addEventListener('submit',async(e)=>{e.preventDefault();btn.disabled=true;btn.textContent='登录中...';try{const fd=new FormData(form),r=await fetch('/api/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:fd.get('username'),password:fd.get('password')})});const d=await r.json();d.success?(localStorage.setItem('adminToken',d.token),location.href='/manage'):showToast(d.message||'登录失败','error')}catch(e){showToast('网络错误,请稍后重试','error')}finally{btn.disabled=false;btn.textContent='登录'}});
48+
form.addEventListener('submit',async(e)=>{e.preventDefault();btn.disabled=true;btn.textContent='登录中...';try{const fd=new FormData(form),r=await fetch('/api/login',{method:'POST',credentials:'include',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:fd.get('username'),password:fd.get('password')})});const d=await r.json();if(d.success){location.href='/manage'}else showToast(d.message||'登录失败','error')}catch(e){showToast('网络错误,请稍后重试','error')}finally{btn.disabled=false;btn.textContent='登录'}});
4949
function showToast(m,t='error'){const d=document.createElement('div'),bc={success:'bg-green-600',error:'bg-destructive',info:'bg-primary'};d.className=`fixed bottom-4 right-4 ${bc[t]||bc.error} text-white px-4 py-2.5 rounded-lg shadow-lg text-sm font-medium z-50 animate-slide-up`;d.textContent=m;document.body.appendChild(d);setTimeout(()=>{d.style.opacity='0';d.style.transition='opacity .3s';setTimeout(()=>d.parentNode&&document.body.removeChild(d),300)},2000)}
50-
window.addEventListener('DOMContentLoaded',()=>{const t=localStorage.getItem('adminToken');t&&fetch('/api/stats',{headers:{Authorization:`Bearer ${t}`}}).then(r=>{if(r.ok)location.href='/manage'})});
50+
window.addEventListener('DOMContentLoaded',()=>{fetch('/api/stats',{credentials:'include'}).then(r=>{if(r.ok)location.href='/manage'}).catch(()=>{})});
5151
</script>
5252
</body>
5353
</html>

0 commit comments

Comments
 (0)