P1: 全局JWT保护 + 审计日志补全 — scripts/assets/servers

- scripts.py: 所有9个端点添加JWT认证 + CUD操作审计日志
- assets.py: 所有GET端点添加JWT + 写操作审计日志补全
- servers.py: GET端点JWT保护(上轮未提交)
- WSL全量验证通过: ALL CHECKS PASSED
This commit is contained in:
Your Name
2026-05-22 08:55:34 +08:00
parent 785ffe2a7e
commit 305f8d5689
3 changed files with 128 additions and 6 deletions
+61 -4
View File
@@ -1,5 +1,5 @@
"""Nexus — Platform & Node API Routes (Asset organization layer)
All write operations require JWT authentication.
All operations require JWT authentication.
"""
from typing import Optional
@@ -20,7 +20,10 @@ router = APIRouter(prefix="/api/assets", tags=["assets"])
# ── Platforms ──
@router.get("/platforms", response_model=list)
async def list_platforms(db: AsyncSession = Depends(get_db)):
async def list_platforms(
admin: Admin = Depends(get_current_admin),
db: AsyncSession = Depends(get_db),
):
"""List all platform templates"""
repo = PlatformRepositoryImpl(db)
platforms = await repo.get_all()
@@ -28,7 +31,11 @@ async def list_platforms(db: AsyncSession = Depends(get_db)):
@router.get("/platforms/{id}", response_model=dict)
async def get_platform(id: int, db: AsyncSession = Depends(get_db)):
async def get_platform(
id: int,
admin: Admin = Depends(get_current_admin),
db: AsyncSession = Depends(get_db),
):
"""Get a single platform by ID"""
repo = PlatformRepositoryImpl(db)
platform = await repo.get_by_id(id)
@@ -74,6 +81,14 @@ async def update_platform(
if hasattr(platform, key) and key != "id":
setattr(platform, key, value)
updated = await repo.update(platform)
audit_repo = AuditLogRepositoryImpl(db)
await audit_repo.create(AuditLog(
admin_username=admin.username, action="update_platform",
target_type="platform", target_id=id,
detail=f"name={updated.name}", ip_address="",
))
return _platform_to_dict(updated)
@@ -85,16 +100,27 @@ async def delete_platform(
):
"""Delete a platform template"""
repo = PlatformRepositoryImpl(db)
platform = await repo.get_by_id(id)
if not platform:
raise HTTPException(status_code=404, detail="Platform not found")
result = await repo.delete(id)
if not result:
raise HTTPException(status_code=404, detail="Platform not found")
audit_repo = AuditLogRepositoryImpl(db)
await audit_repo.create(AuditLog(
admin_username=admin.username, action="delete_platform",
target_type="platform", target_id=id,
detail=f"name={platform.name}", ip_address="",
))
# ── Nodes (Tree) ──
@router.get("/nodes", response_model=list)
async def list_nodes(
parent_id: Optional[int] = None,
admin: Admin = Depends(get_current_admin),
db: AsyncSession = Depends(get_db),
):
"""List nodes — all, or children of a specific parent"""
@@ -107,7 +133,10 @@ async def list_nodes(
@router.get("/nodes/tree", response_model=list)
async def get_node_tree(db: AsyncSession = Depends(get_db)):
async def get_node_tree(
admin: Admin = Depends(get_current_admin),
db: AsyncSession = Depends(get_db),
):
"""Get full node tree structure"""
repo = NodeRepositoryImpl(db)
all_nodes = await repo.get_all()
@@ -124,6 +153,14 @@ async def create_node(
repo = NodeRepositoryImpl(db)
node = Node(**payload.model_dump(exclude_none=True))
created = await repo.create(node)
audit_repo = AuditLogRepositoryImpl(db)
await audit_repo.create(AuditLog(
admin_username=admin.username, action="create_node",
target_type="node", target_id=created.id,
detail=f"name={created.name}", ip_address="",
))
return _node_to_dict(created)
@@ -143,6 +180,14 @@ async def update_node(
if hasattr(node, key) and key != "id":
setattr(node, key, value)
updated = await repo.update(node)
audit_repo = AuditLogRepositoryImpl(db)
await audit_repo.create(AuditLog(
admin_username=admin.username, action="update_node",
target_type="node", target_id=id,
detail=f"name={updated.name}", ip_address="",
))
return _node_to_dict(updated)
@@ -154,10 +199,20 @@ async def delete_node(
):
"""Delete a node"""
repo = NodeRepositoryImpl(db)
node = await repo.get_by_id(id)
if not node:
raise HTTPException(status_code=404, detail="Node not found")
result = await repo.delete(id)
if not result:
raise HTTPException(status_code=404, detail="Node not found")
audit_repo = AuditLogRepositoryImpl(db)
await audit_repo.create(AuditLog(
admin_username=admin.username, action="delete_node",
target_type="node", target_id=id,
detail=f"name={node.name}", ip_address="",
))
# ── SSH Sessions & Command Logs ──
@@ -165,6 +220,7 @@ async def delete_node(
async def list_ssh_sessions(
server_id: Optional[int] = None,
limit: int = 50,
admin: Admin = Depends(get_current_admin),
db: AsyncSession = Depends(get_db),
):
"""List SSH sessions"""
@@ -185,6 +241,7 @@ async def list_command_logs(
server_id: Optional[int] = None,
session_id: Optional[str] = None,
limit: int = 200,
admin: Admin = Depends(get_current_admin),
db: AsyncSession = Depends(get_db),
):
"""List command logs"""
+61 -2
View File
@@ -1,15 +1,19 @@
"""Nexus — Scripts API Routes (Script library CRUD + command execution)
Presentation layer — receives HTTP requests, delegates to ScriptService.
All operations require JWT authentication.
"""
from typing import Optional, List
from fastapi import APIRouter, Depends, HTTPException, Query
from server.api.dependencies import get_script_service
from server.api.dependencies import get_script_service, get_db
from server.api.auth_jwt import get_current_admin
from server.api.schemas import ScriptCreate, ScriptUpdate, ScriptExecute, DbCredentialCreate
from server.application.services.script_service import ScriptService
from server.domain.models import Script, DbCredential, Admin
from server.domain.models import Script, DbCredential, Admin, AuditLog
from server.infrastructure.database.audit_log_repo import AuditLogRepositoryImpl
from sqlalchemy.ext.asyncio import AsyncSession
router = APIRouter(prefix="/api/scripts", tags=["scripts"])
@@ -19,6 +23,7 @@ router = APIRouter(prefix="/api/scripts", tags=["scripts"])
@router.get("/", response_model=list)
async def list_scripts(
category: Optional[str] = Query(None, description="Filter by category"),
admin: Admin = Depends(get_current_admin),
service: ScriptService = Depends(get_script_service),
):
"""List all scripts, optionally filtered by category"""
@@ -29,6 +34,7 @@ async def list_scripts(
@router.get("/{id}", response_model=dict)
async def get_script(
id: int,
admin: Admin = Depends(get_current_admin),
service: ScriptService = Depends(get_script_service),
):
"""Get a single script by ID"""
@@ -41,11 +47,21 @@ async def get_script(
@router.post("/", response_model=dict, status_code=201)
async def create_script(
payload: ScriptCreate,
admin: Admin = Depends(get_current_admin),
service: ScriptService = Depends(get_script_service),
db: AsyncSession = Depends(get_db),
):
"""Create a new script"""
script = Script(**payload.model_dump(exclude_none=True))
created = await service.create_script(script)
audit_repo = AuditLogRepositoryImpl(db)
await audit_repo.create(AuditLog(
admin_username=admin.username, action="create_script",
target_type="script", target_id=created.id,
detail=f"name={created.name}", ip_address="",
))
return _script_to_dict(created)
@@ -53,7 +69,9 @@ async def create_script(
async def update_script(
id: int,
payload: ScriptUpdate,
admin: Admin = Depends(get_current_admin),
service: ScriptService = Depends(get_script_service),
db: AsyncSession = Depends(get_db),
):
"""Update an existing script"""
script = await service.get_script(id)
@@ -63,19 +81,39 @@ async def update_script(
if hasattr(script, key) and key != "id":
setattr(script, key, value)
updated = await service.update_script(script)
audit_repo = AuditLogRepositoryImpl(db)
await audit_repo.create(AuditLog(
admin_username=admin.username, action="update_script",
target_type="script", target_id=id,
detail=f"name={updated.name}", ip_address="",
))
return _script_to_dict(updated)
@router.delete("/{id}", status_code=204)
async def delete_script(
id: int,
admin: Admin = Depends(get_current_admin),
service: ScriptService = Depends(get_script_service),
db: AsyncSession = Depends(get_db),
):
"""Delete a script"""
script = await service.get_script(id)
if not script:
raise HTTPException(status_code=404, detail="Script not found")
result = await service.delete_script(id)
if not result:
raise HTTPException(status_code=404, detail="Script not found")
audit_repo = AuditLogRepositoryImpl(db)
await audit_repo.create(AuditLog(
admin_username=admin.username, action="delete_script",
target_type="script", target_id=id,
detail=f"name={script.name}", ip_address="",
))
# ── Command Execution ──
@@ -99,6 +137,7 @@ async def execute_command(
@router.get("/executions/{id}", response_model=dict)
async def get_execution(
id: int,
admin: Admin = Depends(get_current_admin),
service: ScriptService = Depends(get_script_service),
):
"""Get execution result by ID"""
@@ -112,6 +151,7 @@ async def get_execution(
@router.get("/credentials", response_model=list)
async def list_credentials(
admin: Admin = Depends(get_current_admin),
service: ScriptService = Depends(get_script_service),
):
"""List all database credentials"""
@@ -122,26 +162,45 @@ async def list_credentials(
@router.post("/credentials", response_model=dict, status_code=201)
async def create_credential(
payload: DbCredentialCreate,
admin: Admin = Depends(get_current_admin),
service: ScriptService = Depends(get_script_service),
db: AsyncSession = Depends(get_db),
):
"""Create a database credential (password will be encrypted)"""
data = payload.model_dump(exclude_none=True)
data["encrypted_password"] = data.pop("password", "")
credential = DbCredential(**data)
created = await service.create_credential(credential)
audit_repo = AuditLogRepositoryImpl(db)
await audit_repo.create(AuditLog(
admin_username=admin.username, action="create_credential",
target_type="credential", target_id=created.id,
detail=f"name={created.name}", ip_address="",
))
return _credential_to_dict(created)
@router.delete("/credentials/{id}", status_code=204)
async def delete_credential(
id: int,
admin: Admin = Depends(get_current_admin),
service: ScriptService = Depends(get_script_service),
db: AsyncSession = Depends(get_db),
):
"""Delete a database credential"""
result = await service.delete_credential(id)
if not result:
raise HTTPException(status_code=404, detail="Credential not found")
audit_repo = AuditLogRepositoryImpl(db)
await audit_repo.create(AuditLog(
admin_username=admin.username, action="delete_credential",
target_type="credential", target_id=id,
ip_address="",
))
# ── Helper functions ──
+6
View File
@@ -37,6 +37,7 @@ async def list_servers(
category: Optional[str] = Query(None, description="Filter by category"),
page: int = Query(1, ge=1, description="Page number"),
per_page: int = Query(50, ge=1, le=200, description="Items per page"),
admin: Admin = Depends(get_current_admin),
service: ServerService = Depends(get_server_service),
):
"""List servers with live status from Redis (paginated)
@@ -87,6 +88,7 @@ async def list_servers(
@router.get("/stats", response_model=dict)
async def server_stats(
admin: Admin = Depends(get_current_admin),
service: ServerService = Depends(get_server_service),
):
"""Aggregated server stats for dashboard — efficient single query"""
@@ -128,6 +130,7 @@ async def server_stats(
@router.get("/{id}", response_model=dict)
async def get_server(
id: int,
admin: Admin = Depends(get_current_admin),
service: ServerService = Depends(get_server_service),
):
"""Get a single server by ID with live status from Redis"""
@@ -242,6 +245,7 @@ async def delete_server(
@router.post("/check", response_model=dict)
async def check_servers(
payload: ServerCheck,
admin: Admin = Depends(get_current_admin),
service: ServerService = Depends(get_server_service),
):
"""Check health status of specified servers (Agent-based, no SSH)"""
@@ -273,6 +277,7 @@ async def push_to_servers(
@router.get("/logs", response_model=list)
async def get_all_sync_logs(
limit: int = Query(100, ge=1, le=500),
admin: Admin = Depends(get_current_admin),
db: AsyncSession = Depends(get_db),
):
"""Get recent sync logs across all servers (for dashboard charts)"""
@@ -287,6 +292,7 @@ async def get_all_sync_logs(
async def get_server_logs(
id: int,
limit: int = Query(50, ge=1, le=200),
admin: Admin = Depends(get_current_admin),
sync_service: SyncService = Depends(get_sync_service),
):
"""Get sync logs for a specific server"""