55b7cbe904
Settings API: - 所有端点添加JWT认证(get_current_admin) - 不可改项(secret_key/api_key/encryption_key/database_url)禁止PUT修改 - 敏感字段GET响应自动脱敏(前8位+...) - Schedules/Presets/Retry CRUD添加JWT+审计日志 Servers API: - POST/PUT/DELETE添加JWT认证 - 服务器增删改写审计日志 Assets API: - Platform/Node写操作添加JWT认证 - Platform创建添加审计日志
263 lines
8.5 KiB
Python
263 lines
8.5 KiB
Python
"""Nexus — Platform & Node API Routes (Asset organization layer)
|
|
All write operations require JWT authentication.
|
|
"""
|
|
|
|
from typing import Optional
|
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
|
|
from server.api.dependencies import get_db
|
|
from server.api.auth_jwt import get_current_admin
|
|
from server.api.schemas import PlatformCreate, PlatformUpdate, NodeCreate, NodeUpdate
|
|
from server.infrastructure.database.platform_node_repo import PlatformRepositoryImpl, NodeRepositoryImpl
|
|
from server.infrastructure.database.audit_log_repo import AuditLogRepositoryImpl
|
|
from server.domain.models import Platform, Node, Admin, AuditLog
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
router = APIRouter(prefix="/api/assets", tags=["assets"])
|
|
|
|
|
|
# ── Platforms ──
|
|
|
|
@router.get("/platforms", response_model=list)
|
|
async def list_platforms(db: AsyncSession = Depends(get_db)):
|
|
"""List all platform templates"""
|
|
repo = PlatformRepositoryImpl(db)
|
|
platforms = await repo.get_all()
|
|
return [_platform_to_dict(p) for p in platforms]
|
|
|
|
|
|
@router.get("/platforms/{id}", response_model=dict)
|
|
async def get_platform(id: int, db: AsyncSession = Depends(get_db)):
|
|
"""Get a single platform by ID"""
|
|
repo = PlatformRepositoryImpl(db)
|
|
platform = await repo.get_by_id(id)
|
|
if not platform:
|
|
raise HTTPException(status_code=404, detail="Platform not found")
|
|
return _platform_to_dict(platform)
|
|
|
|
|
|
@router.post("/platforms", response_model=dict, status_code=201)
|
|
async def create_platform(
|
|
payload: PlatformCreate,
|
|
admin: Admin = Depends(get_current_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Create a new platform template"""
|
|
repo = PlatformRepositoryImpl(db)
|
|
platform = Platform(**payload.model_dump(exclude_none=True))
|
|
created = await repo.create(platform)
|
|
|
|
audit_repo = AuditLogRepositoryImpl(db)
|
|
await audit_repo.create(AuditLog(
|
|
admin_username=admin.username, action="create_platform",
|
|
target_type="platform", target_id=created.id,
|
|
detail=f"name={created.name}", ip_address="",
|
|
))
|
|
|
|
return _platform_to_dict(created)
|
|
|
|
|
|
@router.put("/platforms/{id}", response_model=dict)
|
|
async def update_platform(
|
|
id: int,
|
|
payload: PlatformUpdate,
|
|
admin: Admin = Depends(get_current_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Update 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")
|
|
for key, value in payload.model_dump(exclude_unset=True).items():
|
|
if hasattr(platform, key) and key != "id":
|
|
setattr(platform, key, value)
|
|
updated = await repo.update(platform)
|
|
return _platform_to_dict(updated)
|
|
|
|
|
|
@router.delete("/platforms/{id}", status_code=204)
|
|
async def delete_platform(
|
|
id: int,
|
|
admin: Admin = Depends(get_current_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Delete a platform template"""
|
|
repo = PlatformRepositoryImpl(db)
|
|
result = await repo.delete(id)
|
|
if not result:
|
|
raise HTTPException(status_code=404, detail="Platform not found")
|
|
|
|
|
|
# ── Nodes (Tree) ──
|
|
|
|
@router.get("/nodes", response_model=list)
|
|
async def list_nodes(
|
|
parent_id: Optional[int] = None,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""List nodes — all, or children of a specific parent"""
|
|
repo = NodeRepositoryImpl(db)
|
|
if parent_id is not None:
|
|
nodes = await repo.get_children(parent_id)
|
|
else:
|
|
nodes = await repo.get_all()
|
|
return [_node_to_dict(n) for n in nodes]
|
|
|
|
|
|
@router.get("/nodes/tree", response_model=list)
|
|
async def get_node_tree(db: AsyncSession = Depends(get_db)):
|
|
"""Get full node tree structure"""
|
|
repo = NodeRepositoryImpl(db)
|
|
all_nodes = await repo.get_all()
|
|
return _build_tree(all_nodes)
|
|
|
|
|
|
@router.post("/nodes", response_model=dict, status_code=201)
|
|
async def create_node(
|
|
payload: NodeCreate,
|
|
admin: Admin = Depends(get_current_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Create a new node"""
|
|
repo = NodeRepositoryImpl(db)
|
|
node = Node(**payload.model_dump(exclude_none=True))
|
|
created = await repo.create(node)
|
|
return _node_to_dict(created)
|
|
|
|
|
|
@router.put("/nodes/{id}", response_model=dict)
|
|
async def update_node(
|
|
id: int,
|
|
payload: NodeUpdate,
|
|
admin: Admin = Depends(get_current_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Update a node"""
|
|
repo = NodeRepositoryImpl(db)
|
|
node = await repo.get_by_id(id)
|
|
if not node:
|
|
raise HTTPException(status_code=404, detail="Node not found")
|
|
for key, value in payload.model_dump(exclude_unset=True).items():
|
|
if hasattr(node, key) and key != "id":
|
|
setattr(node, key, value)
|
|
updated = await repo.update(node)
|
|
return _node_to_dict(updated)
|
|
|
|
|
|
@router.delete("/nodes/{id}", status_code=204)
|
|
async def delete_node(
|
|
id: int,
|
|
admin: Admin = Depends(get_current_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Delete a node"""
|
|
repo = NodeRepositoryImpl(db)
|
|
result = await repo.delete(id)
|
|
if not result:
|
|
raise HTTPException(status_code=404, detail="Node not found")
|
|
|
|
|
|
# ── SSH Sessions & Command Logs ──
|
|
|
|
@router.get("/ssh-sessions", response_model=list)
|
|
async def list_ssh_sessions(
|
|
server_id: Optional[int] = None,
|
|
limit: int = 50,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""List SSH sessions"""
|
|
from server.infrastructure.database.ssh_session_repo import SshSessionRepositoryImpl
|
|
repo = SshSessionRepositoryImpl(db)
|
|
if server_id:
|
|
sessions = await repo.get_by_server(server_id, limit)
|
|
else:
|
|
from sqlalchemy import select
|
|
from server.domain.models import SshSession
|
|
result = await db.execute(select(SshSession).order_by(SshSession.started_at.desc()).limit(limit))
|
|
sessions = list(result.scalars().all())
|
|
return [_ssh_session_to_dict(s) for s in sessions]
|
|
|
|
|
|
@router.get("/command-logs", response_model=list)
|
|
async def list_command_logs(
|
|
server_id: Optional[int] = None,
|
|
session_id: Optional[str] = None,
|
|
limit: int = 200,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""List command logs"""
|
|
from server.infrastructure.database.ssh_session_repo import CommandLogRepositoryImpl
|
|
repo = CommandLogRepositoryImpl(db)
|
|
if session_id:
|
|
logs = await repo.get_by_session(session_id, limit)
|
|
elif server_id:
|
|
logs = await repo.get_by_server(server_id, limit)
|
|
else:
|
|
from sqlalchemy import select
|
|
from server.domain.models import CommandLog
|
|
result = await db.execute(select(CommandLog).order_by(CommandLog.created_at.desc()).limit(limit))
|
|
logs = list(result.scalars().all())
|
|
return [_command_log_to_dict(l) for l in logs]
|
|
|
|
|
|
# ── Helper functions ──
|
|
|
|
def _platform_to_dict(platform: Platform) -> dict:
|
|
return {
|
|
"id": platform.id,
|
|
"name": platform.name,
|
|
"category": platform.category,
|
|
"type": platform.type,
|
|
"default_protocols": platform.default_protocols,
|
|
"charset": platform.charset,
|
|
"created_at": str(platform.created_at) if platform.created_at else None,
|
|
}
|
|
|
|
|
|
def _node_to_dict(node: Node) -> dict:
|
|
return {
|
|
"id": node.id,
|
|
"name": node.name,
|
|
"parent_id": node.parent_id,
|
|
"sort_order": node.sort_order,
|
|
"created_at": str(node.created_at) if node.created_at else None,
|
|
}
|
|
|
|
|
|
def _build_tree(nodes: list) -> list:
|
|
"""Build tree structure from flat node list"""
|
|
node_map = {n.id: {**_node_to_dict(n), "children": []} for n in nodes}
|
|
tree = []
|
|
for n in nodes:
|
|
node_dict = node_map[n.id]
|
|
if n.parent_id and n.parent_id in node_map:
|
|
node_map[n.parent_id]["children"].append(node_dict)
|
|
else:
|
|
tree.append(node_dict)
|
|
return tree
|
|
|
|
|
|
def _ssh_session_to_dict(session) -> dict:
|
|
return {
|
|
"id": session.id,
|
|
"server_id": session.server_id,
|
|
"admin_id": session.admin_id,
|
|
"remote_addr": session.remote_addr,
|
|
"status": session.status,
|
|
"started_at": str(session.started_at) if session.started_at else None,
|
|
"closed_at": str(session.closed_at) if session.closed_at else None,
|
|
}
|
|
|
|
|
|
def _command_log_to_dict(log) -> dict:
|
|
return {
|
|
"id": log.id,
|
|
"session_id": log.session_id,
|
|
"server_id": log.server_id,
|
|
"admin_id": log.admin_id,
|
|
"command": log.command,
|
|
"remote_addr": log.remote_addr,
|
|
"created_at": str(log.created_at) if log.created_at else None,
|
|
} |