Add authentication features for admin access

- Introduce a new `auth` module with login, logout, and user verification endpoints for a single admin user.
- Update backend routes to require admin authentication for sensitive operations, enhancing security.
- Modify frontend components to include an authentication provider and gate, ensuring only authorized users can access the application.
- Implement a login page for admin access, improving user experience and security management.
- Update API request handling to redirect unauthorized users to the login page, ensuring proper access control.
This commit is contained in:
Xin Wang
2026-07-09 23:27:50 +08:00
parent d857401ef3
commit 590cb33cbd
20 changed files with 534 additions and 29 deletions

View File

@@ -6,12 +6,17 @@ from db.models import Assistant, AssistantModelBinding, ModelResource
from db.session import get_session
from fastapi import APIRouter, Depends, HTTPException
from schemas import AssistantOut, AssistantUpsert
from services.auth import require_admin
from services.masking import mask, resolve_incoming_key
from services.node_specs import validate_graph
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
router = APIRouter(prefix="/api/assistants", tags=["assistants"])
router = APIRouter(
prefix="/api/assistants",
tags=["assistants"],
dependencies=[Depends(require_admin)],
)
CAPABILITIES = ("LLM", "ASR", "TTS", "Realtime", "Embedding", "Agent")

56
backend/routes/auth.py Normal file
View File

@@ -0,0 +1,56 @@
"""Single-admin login endpoints."""
from fastapi import APIRouter, Depends, HTTPException, Response, status
from pydantic import BaseModel
from services.auth import (
AdminUser,
authenticate_admin,
clear_auth_cookie,
create_admin_token,
require_admin,
set_auth_cookie,
)
router = APIRouter(prefix="/api/auth", tags=["auth"])
class LoginIn(BaseModel):
username: str
password: str
class AdminUserOut(BaseModel):
username: str
displayName: str
role: str
def _to_out(user: AdminUser) -> AdminUserOut:
return AdminUserOut(
username=user.username,
displayName=user.display_name,
role=user.role,
)
@router.post("/login", response_model=AdminUserOut)
async def login(body: LoginIn, response: Response):
user = authenticate_admin(body.username, body.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户名或密码错误",
)
set_auth_cookie(response, create_admin_token())
return _to_out(user)
@router.post("/logout")
async def logout(response: Response):
clear_auth_cookie(response)
return {"ok": True}
@router.get("/me", response_model=AdminUserOut)
async def me(user: AdminUser = Depends(require_admin)):
return _to_out(user)

View File

@@ -10,11 +10,16 @@ from db.models import KnowledgeBase, ModelResource
from db.session import get_session
from fastapi import APIRouter, Depends, HTTPException
from schemas import KnowledgeBaseOut, KnowledgeBaseUpsert
from services.auth import require_admin
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
router = APIRouter(prefix="/api/knowledge-bases", tags=["knowledge-bases"])
router = APIRouter(
prefix="/api/knowledge-bases",
tags=["knowledge-bases"],
dependencies=[Depends(require_admin)],
)
async def _validate_embedding_resource(

View File

@@ -16,13 +16,18 @@ from schemas import (
ModelResourceTestResult,
ModelResourceUpsert,
)
from services.auth import require_admin
from services.interface_catalog import validate_fields
from services.masking import mask_secrets, merge_secrets
from services.model_resource_tester import test_model_resource
from sqlalchemy import delete, select, update
from sqlalchemy.ext.asyncio import AsyncSession
router = APIRouter(prefix="/api", tags=["model-registry"])
router = APIRouter(
prefix="/api",
tags=["model-registry"],
dependencies=[Depends(require_admin)],
)
def _definition_dict(row: InterfaceDefinition) -> dict:

View File

@@ -3,10 +3,15 @@
规格是只读的、与代码同生命周期(改了要重启后端 + 前端刷新),所以无需鉴权与缓存层。
"""
from fastapi import APIRouter
from fastapi import APIRouter, Depends
from services.auth import require_admin
from services.node_specs import node_types_response
router = APIRouter(prefix="/api/node-types", tags=["workflow"])
router = APIRouter(
prefix="/api/node-types",
tags=["workflow"],
dependencies=[Depends(require_admin)],
)
@router.get("")

View File

@@ -11,9 +11,10 @@
import asyncio
from db.session import SessionLocal
from fastapi import APIRouter, WebSocket
from fastapi import APIRouter, Depends, WebSocket
from loguru import logger
from models import AssistantConfig, SignalingOffer
from services.auth import require_admin, require_admin_websocket
from services.config_resolver import resolve_runtime_config
from starlette.websockets import WebSocketDisconnect, WebSocketState
@@ -24,7 +25,7 @@ from services.webrtc_ice import aiortc_ice_servers, client_ice_servers
router = APIRouter(tags=["voice"])
@router.get("/api/webrtc/ice-servers")
@router.get("/api/webrtc/ice-servers", dependencies=[Depends(require_admin)])
async def ice_servers():
"""Browser fetches STUN/TURN config (with ephemeral TURN creds when configured)."""
return {"iceServers": client_ice_servers()}
@@ -32,6 +33,8 @@ async def ice_servers():
@router.websocket("/ws/voice")
async def voice_signaling(websocket: WebSocket):
if not await require_admin_websocket(websocket):
return
await websocket.accept()
peers: dict = {}
try:

View File

@@ -15,6 +15,7 @@ from db.session import SessionLocal
from fastapi import APIRouter, WebSocket
from loguru import logger
from models import AssistantConfig
from services.auth import require_admin_websocket
from services.config_resolver import resolve_runtime_config
from starlette.websockets import WebSocketDisconnect
@@ -38,6 +39,8 @@ async def voice_stream(websocket: WebSocket):
from services.pipecat.pipeline import run_pipeline
from services.pipecat.transports import build_ws_transport
if not await require_admin_websocket(websocket):
return
await websocket.accept()
try:
cfg = await _resolve_start_config(await websocket.receive_text())