from __future__ import annotations from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, status from fastapi.responses import Response from pydantic import BaseModel from sqlalchemy.engine import Connection from core.servers.schemas import ( CreateServerRequest, StopServerRequest, UpdateServerRequest, ) from core.servers.service import ServerService from database import get_db from dependencies import get_current_user, require_admin router = APIRouter(prefix="/servers", tags=["servers"]) def _ok(data): return {"success": True, "data": data, "error": None} class SendCommandRequest(BaseModel): command: str # ── Server CRUD ────────────────────────────────────────────────────────────── @router.get("") def list_servers( game_type: str | None = None, db: Annotated[Connection, Depends(get_db)] = None, _user: Annotated[dict, Depends(get_current_user)] = None, ): return _ok(ServerService(db).list_servers(game_type)) @router.post("", status_code=201) def create_server( body: CreateServerRequest, db: Annotated[Connection, Depends(get_db)] = None, _admin: Annotated[dict, Depends(require_admin)] = None, ): return _ok(ServerService(db).create_server( name=body.name, game_type=body.game_type, exe_path=body.exe_path, game_port=body.game_port, rcon_port=body.rcon_port, description=body.description, auto_restart=body.auto_restart, max_restarts=body.max_restarts, )) @router.get("/{server_id}") def get_server( server_id: int, db: Annotated[Connection, Depends(get_db)] = None, _user: Annotated[dict, Depends(get_current_user)] = None, ): return _ok(ServerService(db).get_server(server_id)) @router.put("/{server_id}") def update_server( server_id: int, body: UpdateServerRequest, db: Annotated[Connection, Depends(get_db)] = None, _admin: Annotated[dict, Depends(require_admin)] = None, ): return _ok(ServerService(db).update_server(server_id, **body.model_dump(exclude_none=True))) @router.delete("/{server_id}", status_code=204) def delete_server( server_id: int, db: Annotated[Connection, Depends(get_db)] = None, _admin: Annotated[dict, Depends(require_admin)] = None, ): ServerService(db).delete_server(server_id) return Response(status_code=204) # ── Lifecycle ──────────────────────────────────────────────────────────────── @router.post("/{server_id}/start") def start_server( server_id: int, db: Annotated[Connection, Depends(get_db)] = None, _admin: Annotated[dict, Depends(require_admin)] = None, ): return _ok(ServerService(db).start(server_id)) @router.post("/{server_id}/stop") def stop_server( server_id: int, body: StopServerRequest = None, db: Annotated[Connection, Depends(get_db)] = None, _admin: Annotated[dict, Depends(require_admin)] = None, ): force = body.force if body else False return _ok(ServerService(db).stop(server_id, force=force)) @router.post("/{server_id}/restart") def restart_server( server_id: int, db: Annotated[Connection, Depends(get_db)] = None, _admin: Annotated[dict, Depends(require_admin)] = None, ): return _ok(ServerService(db).restart(server_id)) @router.post("/{server_id}/kill") def kill_server( server_id: int, db: Annotated[Connection, Depends(get_db)] = None, _admin: Annotated[dict, Depends(require_admin)] = None, ): return _ok(ServerService(db).kill(server_id)) # ── Config ─────────────────────────────────────────────────────────────────── @router.get("/{server_id}/config") def get_config( server_id: int, db: Annotated[Connection, Depends(get_db)] = None, _user: Annotated[dict, Depends(get_current_user)] = None, ): return _ok(ServerService(db).get_config(server_id)) @router.get("/{server_id}/config/preview") def get_config_preview( server_id: int, db: Annotated[Connection, Depends(get_db)] = None, _admin: Annotated[dict, Depends(require_admin)] = None, ): return _ok(ServerService(db).get_config_preview(server_id)) @router.get("/{server_id}/config/{section}") def get_config_section( server_id: int, section: str, db: Annotated[Connection, Depends(get_db)] = None, _user: Annotated[dict, Depends(get_current_user)] = None, ): return _ok(ServerService(db).get_config_section(server_id, section)) @router.put("/{server_id}/config/{section}") def update_config_section( server_id: int, section: str, body: dict, # Dynamic — adapter-specific fields db: Annotated[Connection, Depends(get_db)] = None, _admin: Annotated[dict, Depends(require_admin)] = None, ): expected_version = body.pop("config_version", None) return _ok(ServerService(db).update_config_section( server_id, section, body, expected_version )) # ── RCon ────────────────────────────────────────────────────────────────────── @router.post("/{server_id}/rcon/command") def send_rcon_command( server_id: int, body: SendCommandRequest, db: Annotated[Connection, Depends(get_db)] = None, _admin: Annotated[dict, Depends(require_admin)] = None, ): """Send an RCon command to a running server.""" from adapters.registry import GameAdapterRegistry from adapters.exceptions import RemoteAdminError from core.dal.config_repository import ConfigRepository from core.dal.server_repository import ServerRepository server = ServerRepository(db).get_by_id(server_id) if server is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail={"code": "NOT_FOUND", "message": f"Server {server_id} not found"}, ) adapter = GameAdapterRegistry.get(server["game_type"]) if not adapter.has_capability("remote_admin"): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail={"code": "NOT_SUPPORTED", "message": f"Game type {server['game_type']} does not support RCon"}, ) # Get RCon password from config remote_admin_factory = adapter.get_remote_admin() config_gen = adapter.get_config_generator() sensitive = config_gen.get_sensitive_fields("rcon") if "rcon" in config_gen.get_sections() else [] config_repo = ConfigRepository(db) rcon_section = config_repo.get_section(server_id, "rcon", sensitive) if not rcon_section or not rcon_section.get("password"): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail={"code": "NO_RCON_PASSWORD", "message": "RCon password not configured for this server"}, ) password = rcon_section["password"] rcon_port = server.get("rcon_port") or (server["game_port"] + 3) client = remote_admin_factory.create_client( host="127.0.0.1", port=rcon_port, password=password, ) try: client.connect() result = client.send_command(body.command) client.disconnect() except RemoteAdminError as exc: raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail={"code": "RCON_ERROR", "message": f"RCon command failed: {exc}"}, ) except Exception as exc: raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail={"code": "RCON_ERROR", "message": f"RCon connection failed: {exc}"}, ) return _ok({"response": result})