feat(auth): add opaque bearer sessions
This commit is contained in:
@@ -5,12 +5,12 @@ from typing import Annotated
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_db
|
||||
from app.api.deps import get_current_user, get_db
|
||||
from app.schemas.agent_run import AgentRunRead, AgentRunStatsRead
|
||||
from app.schemas.common import ErrorResponse
|
||||
from app.services.agent_runs import AgentRunService
|
||||
|
||||
router = APIRouter(prefix="/agent-runs")
|
||||
router = APIRouter(prefix="/agent-runs", dependencies=[Depends(get_current_user)])
|
||||
DbSession = Annotated[Session, Depends(get_db)]
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Annotated
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_db, require_admin_user
|
||||
from app.api.deps import get_db, require_platform_admin_user
|
||||
from app.schemas.agent_trace import (
|
||||
AgentConversationTraceRead,
|
||||
AgentTraceDetailRead,
|
||||
@@ -26,7 +26,7 @@ DbSession = Annotated[Session, Depends(get_db)]
|
||||
)
|
||||
def list_agent_traces(
|
||||
db: DbSession,
|
||||
_: Annotated[object, Depends(require_admin_user)],
|
||||
_: Annotated[object, Depends(require_platform_admin_user)],
|
||||
agent: Annotated[str | None, Query(description="Agent 名称过滤。")] = None,
|
||||
status_value: Annotated[
|
||||
str | None,
|
||||
@@ -56,7 +56,7 @@ def list_agent_traces(
|
||||
def get_conversation_trace(
|
||||
conversation_id: str,
|
||||
db: DbSession,
|
||||
_: Annotated[object, Depends(require_admin_user)],
|
||||
_: Annotated[object, Depends(require_platform_admin_user)],
|
||||
) -> AgentConversationTraceRead:
|
||||
return AgentTraceService(db).get_conversation_trace(conversation_id)
|
||||
|
||||
@@ -76,7 +76,7 @@ def get_conversation_trace(
|
||||
def get_agent_trace(
|
||||
run_id: str,
|
||||
db: DbSession,
|
||||
_: Annotated[object, Depends(require_admin_user)],
|
||||
_: Annotated[object, Depends(require_platform_admin_user)],
|
||||
) -> AgentTraceDetailRead:
|
||||
trace = AgentTraceService(db).get_trace(run_id)
|
||||
if trace is None:
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing import Annotated
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_db
|
||||
from app.api.deps import get_current_user, get_db
|
||||
from app.schemas.digital_employee_dashboard import DigitalEmployeeDashboardRead
|
||||
from app.schemas.finance_dashboard import FinanceDashboardRead
|
||||
from app.schemas.system_dashboard import SystemDashboardRead
|
||||
@@ -14,7 +14,7 @@ from app.services.digital_employee_dashboard import DigitalEmployeeDashboardServ
|
||||
from app.services.finance_dashboard_snapshot import FinanceDashboardSnapshotService
|
||||
from app.services.system_dashboard import SystemDashboardService
|
||||
|
||||
router = APIRouter(prefix="/analytics")
|
||||
router = APIRouter(prefix="/analytics", dependencies=[Depends(get_current_user)])
|
||||
DbSession = Annotated[Session, Depends(get_db)]
|
||||
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@ from typing import Annotated
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_db
|
||||
from app.api.deps import get_db, require_platform_admin_user
|
||||
from app.schemas.audit_log import AuditLogRead
|
||||
from app.services.audit import AuditLogService
|
||||
|
||||
router = APIRouter(prefix="/audit-logs")
|
||||
router = APIRouter(prefix="/audit-logs", dependencies=[Depends(require_platform_admin_user)])
|
||||
DbSession = Annotated[Session, Depends(get_db)]
|
||||
|
||||
|
||||
|
||||
@@ -10,11 +10,14 @@ from app.schemas.auth import (
|
||||
AuthUserRead,
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
LogoutRequest,
|
||||
LogoutResponse,
|
||||
SessionFinishRequest,
|
||||
SessionFinishResponse,
|
||||
)
|
||||
from app.schemas.common import ErrorResponse
|
||||
from app.services.auth import AuthService
|
||||
from app.services.auth_sessions import AuthSessionService
|
||||
from app.services.user_session_metrics import UserSessionMetricService
|
||||
|
||||
router = APIRouter(prefix="/auth")
|
||||
@@ -44,7 +47,7 @@ def login(payload: LoginRequest, db: DbSession) -> LoginResponse:
|
||||
"/me",
|
||||
response_model=AuthUserRead,
|
||||
summary="读取当前登录用户",
|
||||
description="根据当前会话请求头刷新前端登录态中的员工姓名、部门、岗位和职级。",
|
||||
description="根据 Bearer 会话刷新前端登录态中的员工姓名、部门、岗位和职级。",
|
||||
)
|
||||
def get_current_auth_user(
|
||||
current_user: Annotated[CurrentUserContext, Depends(get_current_user)],
|
||||
@@ -68,12 +71,49 @@ def get_current_auth_user(
|
||||
managerName=current_user.manager_name,
|
||||
costCenter=current_user.cost_center,
|
||||
roleCodes=current_user.role_codes or ["manager"],
|
||||
email=current_user.username if "@" in current_user.username else f"{current_user.username}@local",
|
||||
email=(
|
||||
current_user.username
|
||||
if "@" in current_user.username
|
||||
else f"{current_user.username}@local"
|
||||
),
|
||||
avatar=name[:1].upper(),
|
||||
isAdmin=True,
|
||||
)
|
||||
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="当前登录用户不存在或已停用")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="当前登录用户不存在或已停用",
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/logout",
|
||||
response_model=LogoutResponse,
|
||||
summary="退出当前登录会话",
|
||||
description="撤销当前 Bearer Token;撤销后该凭证不可再次使用。",
|
||||
)
|
||||
def logout(
|
||||
payload: LogoutRequest,
|
||||
current_user: Annotated[CurrentUserContext, Depends(get_current_user)],
|
||||
db: DbSession,
|
||||
) -> LogoutResponse:
|
||||
try:
|
||||
if payload.sessionId:
|
||||
UserSessionMetricService(db).finish_session(
|
||||
session_id=payload.sessionId,
|
||||
expected_username=current_user.username,
|
||||
reason=payload.reason,
|
||||
last_activity_at=payload.lastActivityAt,
|
||||
activity_event_count=payload.activityEventCount,
|
||||
event={"page_path": payload.pagePath},
|
||||
commit=False,
|
||||
)
|
||||
AuthSessionService(db).revoke(current_user.auth_session_id, commit=False)
|
||||
db.commit()
|
||||
except Exception:
|
||||
db.rollback()
|
||||
raise
|
||||
return LogoutResponse()
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -84,10 +124,12 @@ def get_current_auth_user(
|
||||
def finish_session(
|
||||
session_id: str,
|
||||
payload: SessionFinishRequest,
|
||||
current_user: Annotated[CurrentUserContext, Depends(get_current_user)],
|
||||
db: DbSession,
|
||||
) -> SessionFinishResponse:
|
||||
session = UserSessionMetricService(db).finish_session(
|
||||
session_id=session_id,
|
||||
expected_username=current_user.username,
|
||||
reason=payload.reason,
|
||||
last_activity_at=payload.lastActivityAt,
|
||||
activity_event_count=payload.activityEventCount,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, status
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
|
||||
from app.api.deps import CurrentUserContext, get_optional_current_user
|
||||
from app.core.bootstrap import build_bootstrap_state, persist_bootstrap_config
|
||||
from app.core.config import get_settings
|
||||
from app.schemas.bootstrap import BootstrapSetupPayload, BootstrapStateRead
|
||||
@@ -16,7 +19,11 @@ router = APIRouter(prefix="/bootstrap")
|
||||
description="返回当前系统是否已完成初始化,以及公司、数据库和缓存配置快照。",
|
||||
)
|
||||
def get_bootstrap_state() -> BootstrapStateRead:
|
||||
return build_bootstrap_state(get_settings())
|
||||
settings = get_settings()
|
||||
return build_bootstrap_state(
|
||||
settings,
|
||||
redact_infrastructure=settings.setup_completed,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -26,5 +33,17 @@ def get_bootstrap_state() -> BootstrapStateRead:
|
||||
summary="写入初始化配置",
|
||||
description="保存系统初始化配置,并刷新运行时数据库连接。",
|
||||
)
|
||||
def initialize_bootstrap(payload: BootstrapSetupPayload) -> BootstrapStateRead:
|
||||
return persist_bootstrap_config(payload, get_settings())
|
||||
def initialize_bootstrap(
|
||||
payload: BootstrapSetupPayload,
|
||||
current_user: Annotated[
|
||||
CurrentUserContext | None,
|
||||
Depends(get_optional_current_user),
|
||||
],
|
||||
) -> BootstrapStateRead:
|
||||
settings = get_settings()
|
||||
if settings.setup_completed and (current_user is None or not current_user.is_admin):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="系统已完成初始化,只有平台管理员可以重新写入初始化配置。",
|
||||
)
|
||||
return persist_bootstrap_config(payload, settings)
|
||||
|
||||
@@ -6,7 +6,7 @@ from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile,
|
||||
from fastapi.responses import Response
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_db
|
||||
from app.api.deps import get_db, require_admin_user
|
||||
from app.api.pagination import PageNumber, PageSize, page_payload, wants_page
|
||||
from app.schemas.common import ErrorResponse, PaginatedResponse
|
||||
from app.schemas.employee import (
|
||||
@@ -19,7 +19,7 @@ from app.schemas.employee import (
|
||||
from app.services.employee import EmployeeService
|
||||
from app.services.employee_pagination import EmployeePaginationService
|
||||
|
||||
router = APIRouter()
|
||||
router = APIRouter(dependencies=[Depends(require_admin_user)])
|
||||
DbSession = Annotated[Session, Depends(get_db)]
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Annotated
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_db
|
||||
from app.api.deps import get_current_user, get_db
|
||||
from app.schemas.common import ErrorResponse
|
||||
from app.schemas.risk_observation import (
|
||||
RiskObservationDashboardRead,
|
||||
@@ -16,7 +16,7 @@ from app.schemas.risk_observation import (
|
||||
)
|
||||
from app.services.risk_observations import RiskObservationService
|
||||
|
||||
router = APIRouter(prefix="/risk-observations")
|
||||
router = APIRouter(prefix="/risk-observations", dependencies=[Depends(get_current_user)])
|
||||
DbSession = Annotated[Session, Depends(get_db)]
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,12 @@ from typing import Annotated
|
||||
from fastapi import APIRouter, Depends, Header, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import CurrentUserContext, get_db, require_admin_user
|
||||
from app.api.deps import (
|
||||
CurrentUserContext,
|
||||
get_current_user,
|
||||
get_db,
|
||||
require_platform_admin_user,
|
||||
)
|
||||
from app.core.config import get_settings as get_runtime_settings
|
||||
from app.schemas.common import ErrorResponse
|
||||
from app.schemas.settings import (
|
||||
@@ -52,7 +57,10 @@ def require_hermes_agent_token(
|
||||
summary="读取系统设置",
|
||||
description="返回公司、管理员、模型、日志、邮件和 ONLYOFFICE 的设置快照。",
|
||||
)
|
||||
def get_settings(db: DbSession) -> SettingsRead:
|
||||
def get_settings(
|
||||
db: DbSession,
|
||||
_: Annotated[CurrentUserContext, Depends(get_current_user)],
|
||||
) -> SettingsRead:
|
||||
return SettingsService(db).get_settings_snapshot()
|
||||
|
||||
|
||||
@@ -68,7 +76,11 @@ def get_settings(db: DbSession) -> SettingsRead:
|
||||
}
|
||||
},
|
||||
)
|
||||
def update_settings(payload: SettingsWrite, db: DbSession) -> SettingsRead:
|
||||
def update_settings(
|
||||
payload: SettingsWrite,
|
||||
db: DbSession,
|
||||
_: Annotated[CurrentUserContext, Depends(require_platform_admin_user)],
|
||||
) -> SettingsRead:
|
||||
try:
|
||||
return SettingsService(db).save_settings_snapshot(payload)
|
||||
except ValueError as exc:
|
||||
@@ -84,6 +96,7 @@ def update_settings(payload: SettingsWrite, db: DbSession) -> SettingsRead:
|
||||
def test_model_connectivity(
|
||||
payload: ModelConnectivityTestRequest,
|
||||
db: DbSession,
|
||||
_: Annotated[CurrentUserContext, Depends(require_platform_admin_user)],
|
||||
) -> ModelConnectivityTestRead:
|
||||
resolved_payload = payload
|
||||
|
||||
@@ -99,7 +112,10 @@ def test_model_connectivity(
|
||||
"/cache/clear",
|
||||
response_model=SettingsCacheClearRead,
|
||||
summary="清理系统缓存",
|
||||
description="清理 OCR、模型失败冷却、知识库索引和运行时配置等进程内缓存,不删除业务文件或数据库记录。",
|
||||
description=(
|
||||
"清理 OCR、模型失败冷却、知识库索引和运行时配置等进程内缓存,"
|
||||
"不删除业务文件或数据库记录。"
|
||||
),
|
||||
responses={
|
||||
status.HTTP_403_FORBIDDEN: {
|
||||
"model": ErrorResponse,
|
||||
@@ -108,7 +124,7 @@ def test_model_connectivity(
|
||||
},
|
||||
)
|
||||
def clear_system_cache(
|
||||
_: Annotated[CurrentUserContext, Depends(require_admin_user)],
|
||||
_: Annotated[CurrentUserContext, Depends(require_platform_admin_user)],
|
||||
) -> SettingsCacheClearRead:
|
||||
return SystemCacheService().clear_all()
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
|
||||
from app.api.deps import CurrentUserContext, require_admin_user
|
||||
from app.api.deps import CurrentUserContext, require_platform_admin_user
|
||||
from app.schemas.common import ErrorResponse
|
||||
from app.schemas.system_log import SystemLogEntryRead, SystemLogFileRead, SystemLogTailRead
|
||||
from app.services.system_logs import SystemLogService
|
||||
@@ -25,7 +25,7 @@ router = APIRouter(prefix="/system-logs")
|
||||
},
|
||||
)
|
||||
def list_system_log_entries(
|
||||
_: Annotated[CurrentUserContext, Depends(require_admin_user)],
|
||||
_: Annotated[CurrentUserContext, Depends(require_platform_admin_user)],
|
||||
limit: Annotated[int, Query(ge=20, le=1000, description="返回的日志记录数。")] = 300,
|
||||
) -> list[SystemLogEntryRead]:
|
||||
return SystemLogService().list_entries(entry_limit=limit)
|
||||
@@ -49,12 +49,15 @@ def list_system_log_entries(
|
||||
)
|
||||
def get_system_log_entry(
|
||||
entry_id: str,
|
||||
_: Annotated[CurrentUserContext, Depends(require_admin_user)],
|
||||
_: Annotated[CurrentUserContext, Depends(require_platform_admin_user)],
|
||||
) -> SystemLogEntryRead:
|
||||
try:
|
||||
return SystemLogService().get_entry(entry_id)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="日志记录不存在。") from exc
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="日志记录不存在。",
|
||||
) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -70,7 +73,7 @@ def get_system_log_entry(
|
||||
},
|
||||
)
|
||||
def list_system_log_files(
|
||||
_: Annotated[CurrentUserContext, Depends(require_admin_user)],
|
||||
_: Annotated[CurrentUserContext, Depends(require_platform_admin_user)],
|
||||
) -> list[SystemLogFileRead]:
|
||||
return SystemLogService().list_files()
|
||||
|
||||
@@ -93,10 +96,13 @@ def list_system_log_files(
|
||||
)
|
||||
def get_system_log_tail(
|
||||
file_name: str,
|
||||
_: Annotated[CurrentUserContext, Depends(require_admin_user)],
|
||||
_: Annotated[CurrentUserContext, Depends(require_platform_admin_user)],
|
||||
lines: Annotated[int, Query(ge=20, le=1000, description="返回的日志行数。")] = 300,
|
||||
) -> SystemLogTailRead:
|
||||
try:
|
||||
return SystemLogService().read_tail(file_name, line_limit=lines)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="日志文件不存在。") from exc
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="日志文件不存在。",
|
||||
) from exc
|
||||
|
||||
Reference in New Issue
Block a user