Files
Nexus/server/api/assets.py
T

320 lines
10 KiB
Python
Raw Normal View History

"""Nexus — Platform & Node API Routes (Asset organization layer)
All 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(
admin: Admin = Depends(get_current_admin),
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,
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)
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)
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)
@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)
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"""
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(
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()
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)
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)
@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)
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)
@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)
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 ──
@router.get("/ssh-sessions", response_model=list)
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"""
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,
admin: Admin = Depends(get_current_admin),
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,
}