305f8d5689
- scripts.py: 所有9个端点添加JWT认证 + CUD操作审计日志 - assets.py: 所有GET端点添加JWT + 写操作审计日志补全 - servers.py: GET端点JWT保护(上轮未提交) - WSL全量验证通过: ALL CHECKS PASSED
246 lines
8.2 KiB
Python
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,
|
|
}
|