Files
languard-servers-manager/backend/core/servers/bans_router.py
Tran G. (Revernomad) Khoa 6511353b55 feat: implement full backend + frontend server detail, settings, and create server pages
Backend:
- Complete FastAPI backend with 42+ REST endpoints (auth, servers, config,
  players, bans, missions, mods, games, system)
- Game adapter architecture with Arma 3 as first-class adapter
- WebSocket real-time events for status, metrics, logs, players
- Background thread system (process monitor, metrics, log tail, RCon poller)
- Fernet encryption for sensitive config fields at rest
- JWT auth with admin/viewer roles, bcrypt password hashing
- SQLite with WAL mode, parameterized queries, migration system
- APScheduler cleanup jobs for logs, metrics, events

Frontend:
- Server Detail page with 7 tabs (overview, config, players, bans,
  missions, mods, logs)
- Settings page with password change and admin user management
- Create Server wizard (4-step; known bug: silent validation failure)
- New hooks: useServerDetail, useAuth, useGames
- New components: ServerHeader, ConfigEditor, PlayerTable, BanTable,
  MissionList, ModList, LogViewer, PasswordChange, UserManager
- WebSocket onEvent callback for real-time log accumulation
- 120 unit tests passing (Vitest + React Testing Library)

Docs:
- Added .gitignore, CLAUDE.md, README.md
- Updated FRONTEND.md, ARCHITECTURE.md with current implementation state
- Added .env.example for backend configuration

Known issues:
- Create Server form: "Next" buttons don't validate before advancing,
  causing silent submit failure when fields are invalid
- Config sub-tabs need UX redesign for non-technical users
2026-04-17 11:58:34 +07:00

142 lines
4.5 KiB
Python

"""Ban management endpoints — create, list, and revoke bans."""
from __future__ import annotations
import logging
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, field_validator
from sqlalchemy.engine import Connection
from adapters.arma3.ban_manager import Arma3BanManager
from core.dal.ban_repository import BanRepository
from core.servers.service import ServerService
from database import get_db
from dependencies import get_current_user, require_admin
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/servers/{server_id}/bans", tags=["bans"])
def _ok(data):
return {"success": True, "data": data, "error": None}
class CreateBanRequest(BaseModel):
player_uid: str
ban_type: str = "GUID"
reason: str = ""
duration_minutes: int = 0 # 0 = permanent
@field_validator("ban_type")
@classmethod
def validate_ban_type(cls, v: str) -> str:
if v not in ("GUID", "IP"):
raise ValueError("ban_type must be 'GUID' or 'IP'")
return v
@field_validator("duration_minutes")
@classmethod
def validate_duration(cls, v: int) -> int:
if v < 0:
raise ValueError("duration_minutes cannot be negative")
return v
@router.get("")
def list_bans(
server_id: int,
db: Annotated[Connection, Depends(get_db)],
_user: Annotated[dict, Depends(get_current_user)],
) -> dict:
"""List all active bans for the server."""
ServerService(db).get_server(server_id) # raises 404 if not found
ban_repo = BanRepository(db)
bans = ban_repo.get_all(server_id=server_id)
return _ok(bans)
@router.post("", status_code=status.HTTP_201_CREATED)
def create_ban(
server_id: int,
body: CreateBanRequest,
db: Annotated[Connection, Depends(get_db)],
_admin: Annotated[dict, Depends(require_admin)],
) -> dict:
"""Create a new ban. Writes to DB and syncs to bans.txt."""
ServerService(db).get_server(server_id) # raises 404 if not found
ban_repo = BanRepository(db)
# Calculate expires_at if duration is set
expires_at = None
if body.duration_minutes > 0:
from datetime import datetime, timezone, timedelta
expires_at = (
datetime.now(timezone.utc) + timedelta(minutes=body.duration_minutes)
).isoformat()
ban_id = ban_repo.create(
server_id=server_id,
guid=body.player_uid if body.ban_type == "GUID" else None,
name=None,
reason=body.reason,
banned_by=_admin["username"],
expires_at=expires_at,
game_data={"ban_type": body.ban_type, "duration_minutes": body.duration_minutes},
)
db.commit()
ban = ban_repo.get_by_id(ban_id)
# Sync to bans.txt (non-blocking — log error but don't fail request)
_sync_ban_to_file(server_id, body.player_uid, body.ban_type, body.reason, body.duration_minutes)
return _ok(ban)
@router.delete("/{ban_id}")
def revoke_ban(
server_id: int,
ban_id: int,
db: Annotated[Connection, Depends(get_db)],
_admin: Annotated[dict, Depends(require_admin)],
) -> dict:
"""Revoke a ban (marks as inactive in DB, removes from bans.txt)."""
ServerService(db).get_server(server_id) # raises 404 if not found
ban_repo = BanRepository(db)
ban = ban_repo.get_by_id(ban_id)
if ban is None or ban["server_id"] != server_id:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={"code": "NOT_FOUND", "message": "Ban not found"},
)
ban_repo.deactivate(ban_id)
db.commit()
# Remove from bans.txt
_remove_ban_from_file(server_id, ban.get("guid") or "")
return _ok({"message": f"Ban {ban_id} revoked"})
# ── File sync helpers ──
def _sync_ban_to_file(
server_id: int, identifier: str, ban_type: str, reason: str, duration_minutes: int
) -> None:
"""Write ban to bans.txt. Log error but don't fail the request."""
try:
mgr = Arma3BanManager(server_id)
mgr.add_ban(identifier, ban_type, reason, duration_minutes)
except Exception as exc:
logger.error("Failed to sync ban to bans.txt for server %d: %s", server_id, exc)
def _remove_ban_from_file(server_id: int, identifier: str) -> None:
"""Remove ban from bans.txt. Log error but don't fail the request."""
try:
mgr = Arma3BanManager(server_id)
mgr.remove_ban(identifier)
except Exception as exc:
logger.error("Failed to remove ban from bans.txt for server %d: %s", server_id, exc)