242 lines
7.4 KiB
Python
242 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,
|
|
}
|