Files
Nexus/server/api/rdp_hosts.py
T
Nexus Agent f127f07ab2 feat(rdp): 3389 页独立管理 RDP 主机
rdp_hosts 表与专用 API,在 3389远程页添加/连接,不再依赖子机列表。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 20:58:46 +08:00

243 lines
7.4 KiB
Python

"""Standalone RDP host CRUD + connect params (3389 page, not Server assets)."""
from __future__ import annotations
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel, Field
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from server.api.auth_jwt import get_current_admin
from server.api.dependencies import get_db
from server.config import settings
from server.domain.models import Admin, AuditLog, RdpHost
from server.infrastructure.database.audit_log_repo import AuditLogRepositoryImpl
from server.infrastructure.database.crypto import decrypt_value, encrypt_value
from server.infrastructure.database.rdp_host_repo import RdpHostRepositoryImpl
from server.utils.rdp_config import DEFAULT_RDP_PORT, normalize_rdp_port
router = APIRouter(prefix="/api/rdp/hosts", tags=["rdp"])
class RdpHostCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
host: str = Field(..., min_length=1, max_length=255)
port: int = Field(DEFAULT_RDP_PORT, ge=1, le=65535)
username: str = Field(..., min_length=1, max_length=100)
password: str = Field(..., min_length=1)
description: Optional[str] = Field(None, max_length=2000)
class RdpHostUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=100)
host: Optional[str] = Field(None, min_length=1, max_length=255)
port: Optional[int] = Field(None, ge=1, le=65535)
username: Optional[str] = Field(None, min_length=1, max_length=100)
password: Optional[str] = None
description: Optional[str] = Field(None, max_length=2000)
def _password_set(enc: str | None) -> bool:
return bool(str(enc or "").strip())
def _host_to_dict(row: RdpHost) -> dict:
return {
"id": row.id,
"name": row.name,
"host": row.host,
"port": row.port,
"username": row.username,
"password_set": _password_set(row.password),
"description": row.description or "",
"created_at": row.created_at.isoformat() if row.created_at else None,
"updated_at": row.updated_at.isoformat() if row.updated_at else None,
}
def _decrypt_password(row: RdpHost) -> str:
enc = str(row.password or "").strip()
if not enc:
return ""
try:
return decrypt_value(enc)
except Exception:
return ""
async def _audit(
db: AsyncSession,
*,
admin: Admin,
action: str,
host_id: int,
detail: str,
request: Request,
) -> None:
audit_repo = AuditLogRepositoryImpl(db)
await audit_repo.create(
AuditLog(
admin_username=admin.username,
action=action,
target_type="rdp_host",
target_id=host_id,
detail=detail,
ip_address=request.client.host if request.client else "",
)
)
@router.get("/", response_model=dict)
async def list_rdp_hosts(
admin: Admin = Depends(get_current_admin),
db: AsyncSession = Depends(get_db),
):
repo = RdpHostRepositoryImpl(db)
rows = await repo.list_all()
return {"items": [_host_to_dict(r) for r in rows], "total": len(rows)}
@router.post("/", response_model=dict, status_code=201)
async def create_rdp_host(
payload: RdpHostCreate,
request: Request,
admin: Admin = Depends(get_current_admin),
db: AsyncSession = Depends(get_db),
):
repo = RdpHostRepositoryImpl(db)
name = payload.name.strip()
host = payload.host.strip()
username = payload.username.strip()
password = payload.password.strip()
if not password:
raise HTTPException(status_code=400, detail="请填写 RDP 密码")
row = RdpHost(
name=name,
host=host,
port=normalize_rdp_port(payload.port),
username=username,
password=encrypt_value(password),
description=(payload.description or "").strip() or None,
)
try:
created = await repo.create(row)
except IntegrityError:
await db.rollback()
raise HTTPException(status_code=409, detail=f"名称「{name}」已存在")
await _audit(
db,
admin=admin,
action="create_rdp_host",
host_id=created.id,
detail=f"名称={name} 地址={host}:{row.port}",
request=request,
)
return _host_to_dict(created)
@router.put("/{host_id}", response_model=dict)
async def update_rdp_host(
host_id: int,
payload: RdpHostUpdate,
request: Request,
admin: Admin = Depends(get_current_admin),
db: AsyncSession = Depends(get_db),
):
repo = RdpHostRepositoryImpl(db)
row = await repo.get_by_id(host_id)
if not row:
raise HTTPException(status_code=404, detail="RDP 主机不存在")
data = payload.model_dump(exclude_unset=True)
if "name" in data and data["name"] is not None:
row.name = data["name"].strip()
if "host" in data and data["host"] is not None:
row.host = data["host"].strip()
if "port" in data and data["port"] is not None:
row.port = normalize_rdp_port(data["port"])
if "username" in data and data["username"] is not None:
row.username = data["username"].strip()
if "description" in data:
row.description = (data["description"] or "").strip() or None
if "password" in data and data["password"] is not None:
plain = str(data["password"]).strip()
if plain:
row.password = encrypt_value(plain)
try:
updated = await repo.update(row)
except IntegrityError:
await db.rollback()
raise HTTPException(status_code=409, detail=f"名称「{row.name}」已存在")
await _audit(
db,
admin=admin,
action="update_rdp_host",
host_id=host_id,
detail=f"名称={updated.name} 地址={updated.host}:{updated.port}",
request=request,
)
return _host_to_dict(updated)
@router.delete("/{host_id}", status_code=204)
async def delete_rdp_host(
host_id: int,
request: Request,
admin: Admin = Depends(get_current_admin),
db: AsyncSession = Depends(get_db),
):
repo = RdpHostRepositoryImpl(db)
row = await repo.get_by_id(host_id)
if not row:
raise HTTPException(status_code=404, detail="RDP 主机不存在")
name = row.name
if not await repo.delete(host_id):
raise HTTPException(status_code=404, detail="RDP 主机不存在")
await _audit(
db,
admin=admin,
action="delete_rdp_host",
host_id=host_id,
detail=f"名称={name}",
request=request,
)
@router.get("/{host_id}/connect", response_model=dict)
async def get_rdp_host_connect_params(
host_id: int,
admin: Admin = Depends(get_current_admin),
db: AsyncSession = Depends(get_db),
):
if not settings.GUACD_HOST:
raise HTTPException(status_code=503, detail="服务端未配置 guacd,无法使用浏览器 RDP")
repo = RdpHostRepositoryImpl(db)
row = await repo.get_by_id(host_id)
if not row:
raise HTTPException(status_code=404, detail="RDP 主机不存在")
host = (row.host or "").strip()
if not host:
raise HTTPException(status_code=400, detail="缺少地址(IP/域名)")
password = _decrypt_password(row)
if not password:
raise HTTPException(status_code=400, detail="RDP 密码无效或未设置")
return {
"host_id": row.id,
"name": row.name,
"hostname": host,
"port": row.port,
"username": row.username,
"password": password,
}