Files
Nexus/server/api/scripts.py
T
Your Name 305f8d5689 P1: 全局JWT保护 + 审计日志补全 — scripts/assets/servers
- scripts.py: 所有9个端点添加JWT认证 + CUD操作审计日志
- assets.py: 所有GET端点添加JWT + 写操作审计日志补全
- servers.py: GET端点JWT保护(上轮未提交)
- WSL全量验证通过: ALL CHECKS PASSED
2026-05-22 08:55:34 +08:00

246 lines
8.2 KiB
Python

"""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, 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, AuditLog
from server.infrastructure.database.audit_log_repo import AuditLogRepositoryImpl
from sqlalchemy.ext.asyncio import AsyncSession
router = APIRouter(prefix="/api/scripts", tags=["scripts"])
# ── Script CRUD ──
@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"""
scripts = await service.list_scripts(category)
return [_script_to_dict(s) for s in 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"""
script = await service.get_script(id)
if not script:
raise HTTPException(status_code=404, detail="Script not found")
return _script_to_dict(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)
@router.put("/{id}", response_model=dict)
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)
if not script:
raise HTTPException(status_code=404, detail="Script not found")
for key, value in payload.model_dump(exclude_unset=True).items():
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 ──
@router.post("/exec", response_model=dict)
async def execute_command(
payload: ScriptExecute,
admin: Admin = Depends(get_current_admin),
service: ScriptService = Depends(get_script_service),
):
"""Execute a shell command on multiple servers via Agent /api/exec"""
execution = await service.execute_command(
command=payload.command,
server_ids=payload.server_ids,
script_id=payload.script_id,
timeout=payload.timeout,
operator=admin.username,
)
return _execution_to_dict(execution)
@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"""
execution = await service.get_execution(id)
if not execution:
raise HTTPException(status_code=404, detail="Execution not found")
return _execution_to_dict(execution)
# ── DB Credentials ──
@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"""
credentials = await service.list_credentials()
return [_credential_to_dict(c) for c in 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 ──
def _script_to_dict(script: Script) -> dict:
return {
"id": script.id,
"name": script.name,
"category": script.category,
"content": script.content,
"description": script.description,
"created_by": script.created_by,
"created_at": str(script.created_at) if script.created_at else None,
"updated_at": str(script.updated_at) if script.updated_at else None,
}
def _execution_to_dict(execution) -> dict:
return {
"id": execution.id,
"script_id": execution.script_id,
"command": execution.command,
"server_ids": execution.server_ids,
"status": execution.status,
"results": execution.results,
"operator": execution.operator,
"started_at": str(execution.started_at) if execution.started_at else None,
"completed_at": str(execution.completed_at) if execution.completed_at else None,
}
def _credential_to_dict(credential: DbCredential) -> dict:
"""Convert credential to dict (never expose password in API response)"""
return {
"id": credential.id,
"name": credential.name,
"db_type": credential.db_type,
"host": credential.host,
"port": credential.port,
"username": credential.username,
"database": credential.database,
"created_at": str(credential.created_at) if credential.created_at else None,
}