release: nexus btpanel session fix and app-v2
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
"""Nexus — FastAPI Dependency Injection
|
||||
Wires Repository implementations → Service instances → API route handlers.
|
||||
All DI occurs here; routes never import infrastructure directly.
|
||||
|
||||
D7 Fix: Service factories now read session from request.state.db (set by
|
||||
DbSessionMiddleware) instead of creating unclosed AsyncSessionLocal() instances.
|
||||
This fixes the P0 session leak bug where 4 service factories never closed sessions.
|
||||
"""
|
||||
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from fastapi import Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from server.infrastructure.database.session import AsyncSessionLocal
|
||||
from server.infrastructure.database.server_repo import ServerRepositoryImpl
|
||||
from server.infrastructure.database.sync_log_repo import SyncLogRepositoryImpl
|
||||
from server.infrastructure.database.script_repo import ScriptRepositoryImpl, ScriptExecutionRepositoryImpl
|
||||
from server.infrastructure.database.db_credential_repo import DbCredentialRepositoryImpl
|
||||
from server.infrastructure.database.admin_repo import AdminRepositoryImpl, LoginAttemptRepositoryImpl
|
||||
from server.infrastructure.database.refresh_token_repo import RefreshTokenRepositoryImpl
|
||||
from server.infrastructure.database.setting_repo import SettingRepositoryImpl
|
||||
from server.infrastructure.database.audit_log_repo import AuditLogRepositoryImpl
|
||||
from server.infrastructure.database.push_schedule_repo import PushScheduleRepositoryImpl, PushRetryJobRepositoryImpl
|
||||
from server.infrastructure.database.password_preset_repo import PasswordPresetRepositoryImpl
|
||||
|
||||
from server.application.services.server_service import ServerService
|
||||
from server.application.services.script_service import ScriptService
|
||||
from server.application.services.auth_service import AuthService
|
||||
from server.application.services.sync_service import SyncService
|
||||
|
||||
|
||||
async def get_db(request: Request) -> AsyncGenerator[AsyncSession, None]:
|
||||
"""Yield DB session from middleware (request.state.db).
|
||||
|
||||
DbSessionMiddleware ensures every /api/ request has a session.
|
||||
This dependency simply yields it so routes can use Depends(get_db).
|
||||
No double-session: one session per request, shared everywhere.
|
||||
"""
|
||||
session = getattr(request.state, "db", None)
|
||||
if session is not None:
|
||||
yield session
|
||||
else:
|
||||
# Fallback for tests or edge cases without middleware
|
||||
session = AsyncSessionLocal()
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
await session.close()
|
||||
|
||||
|
||||
async def _get_request_session(request: Request) -> AsyncSession:
|
||||
"""Get DB session from middleware (request.state.db).
|
||||
|
||||
DbSessionMiddleware opens a session at the start of each API request
|
||||
and stores it in request.state.db. This ensures:
|
||||
1. Session is always properly closed after the request
|
||||
2. All repositories/services in the same request share the same session
|
||||
3. No more leaked sessions from service factories
|
||||
"""
|
||||
session = getattr(request.state, "db", None)
|
||||
if session is None:
|
||||
# Fallback: if middleware didn't set session (shouldn't happen for /api/ routes)
|
||||
raise RuntimeError("DB session not found in request.state — DbSessionMiddleware may be missing")
|
||||
return session
|
||||
|
||||
|
||||
# ── Repository Factories ──
|
||||
|
||||
def _server_repo(session: AsyncSession) -> ServerRepositoryImpl:
|
||||
return ServerRepositoryImpl(session)
|
||||
|
||||
def _sync_log_repo(session: AsyncSession) -> SyncLogRepositoryImpl:
|
||||
return SyncLogRepositoryImpl(session)
|
||||
|
||||
def _script_repo(session: AsyncSession) -> ScriptRepositoryImpl:
|
||||
return ScriptRepositoryImpl(session)
|
||||
|
||||
def _execution_repo(session: AsyncSession) -> ScriptExecutionRepositoryImpl:
|
||||
return ScriptExecutionRepositoryImpl(session)
|
||||
|
||||
def _credential_repo(session: AsyncSession) -> DbCredentialRepositoryImpl:
|
||||
return DbCredentialRepositoryImpl(session)
|
||||
|
||||
def _admin_repo(session: AsyncSession) -> AdminRepositoryImpl:
|
||||
return AdminRepositoryImpl(session)
|
||||
|
||||
def _refresh_token_repo(session: AsyncSession) -> RefreshTokenRepositoryImpl:
|
||||
return RefreshTokenRepositoryImpl(session)
|
||||
|
||||
|
||||
def _attempt_repo(session: AsyncSession) -> LoginAttemptRepositoryImpl:
|
||||
return LoginAttemptRepositoryImpl(session)
|
||||
|
||||
def _setting_repo(session: AsyncSession) -> SettingRepositoryImpl:
|
||||
return SettingRepositoryImpl(session)
|
||||
|
||||
def _audit_repo(session: AsyncSession) -> AuditLogRepositoryImpl:
|
||||
return AuditLogRepositoryImpl(session)
|
||||
|
||||
def _schedule_repo(session: AsyncSession) -> PushScheduleRepositoryImpl:
|
||||
return PushScheduleRepositoryImpl(session)
|
||||
|
||||
def _retry_repo(session: AsyncSession) -> PushRetryJobRepositoryImpl:
|
||||
return PushRetryJobRepositoryImpl(session)
|
||||
|
||||
def _preset_repo(session: AsyncSession) -> PasswordPresetRepositoryImpl:
|
||||
return PasswordPresetRepositoryImpl(session)
|
||||
|
||||
|
||||
# ── Service Factories (D7 fixed: use request.state.db) ──
|
||||
|
||||
async def get_server_service(request: Request) -> ServerService:
|
||||
session = await _get_request_session(request)
|
||||
return ServerService(
|
||||
server_repo=_server_repo(session),
|
||||
sync_log_repo=_sync_log_repo(session),
|
||||
)
|
||||
|
||||
|
||||
async def get_script_service(request: Request) -> ScriptService:
|
||||
session = await _get_request_session(request)
|
||||
return ScriptService(
|
||||
script_repo=_script_repo(session),
|
||||
execution_repo=_execution_repo(session),
|
||||
credential_repo=_credential_repo(session),
|
||||
server_repo=_server_repo(session),
|
||||
audit_repo=_audit_repo(session),
|
||||
)
|
||||
|
||||
|
||||
async def get_auth_service(request: Request) -> AuthService:
|
||||
session = await _get_request_session(request)
|
||||
return AuthService(
|
||||
admin_repo=_admin_repo(session),
|
||||
attempt_repo=_attempt_repo(session),
|
||||
audit_repo=_audit_repo(session),
|
||||
refresh_token_repo=_refresh_token_repo(session),
|
||||
)
|
||||
|
||||
|
||||
async def get_sync_service(request: Request) -> SyncService:
|
||||
session = await _get_request_session(request)
|
||||
return SyncService(
|
||||
server_repo=_server_repo(session),
|
||||
sync_log_repo=_sync_log_repo(session),
|
||||
audit_repo=_audit_repo(session),
|
||||
retry_repo=_retry_repo(session),
|
||||
)
|
||||
|
||||
|
||||
# ── Command Safety Detection (P2-18) ──
|
||||
|
||||
import re
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger("nexus.command_safety")
|
||||
|
||||
# Patterns that indicate highly dangerous commands
|
||||
_DANGEROUS_PATTERNS = [
|
||||
(r"\brm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+|-r[a-zA-Z]*\s+|.*--no-preserve-root\s+)/\s*$", "rm -rf / (全盘删除)"),
|
||||
(r"\bdd\s+.*of=/dev/[a-z]+\s", "dd 写入裸设备 (数据毁灭)"),
|
||||
(r":\(\)\{\s*:\|:&\s*\}", "Fork bomb (内存耗尽)"),
|
||||
(r"mkfs\.", "格式化文件系统"),
|
||||
(r">\s*/dev/sd[a-z]", "覆写磁盘设备"),
|
||||
(r"chmod\s+(-R\s+)?000\s+/", "根目录权限清零"),
|
||||
(r"chown\s+(-R\s+)?\S+\s+/", "根目录归属变更"),
|
||||
]
|
||||
|
||||
_DANGEROUS_COMPILED = [(re.compile(p, re.IGNORECASE), desc) for p, desc in _DANGEROUS_PATTERNS]
|
||||
|
||||
|
||||
def check_dangerous_command(command: str) -> list[str]:
|
||||
"""Check a command for dangerous patterns.
|
||||
|
||||
Returns a list of warning descriptions (empty if safe).
|
||||
This is a warning system, not a blocker — commands are still executed,
|
||||
but dangerous ones are logged at WARNING level for audit trail.
|
||||
"""
|
||||
warnings = []
|
||||
for pattern, desc in _DANGEROUS_COMPILED:
|
||||
if pattern.search(command):
|
||||
warnings.append(desc)
|
||||
if warnings:
|
||||
_logger.warning(f"Dangerous command detected: {command[:200]} -> {warnings}")
|
||||
return warnings
|
||||
Reference in New Issue
Block a user