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
This commit is contained in:
Tran G. (Revernomad) Khoa
2026-04-17 11:58:34 +07:00
parent 620429c9b8
commit 6511353b55
119 changed files with 13752 additions and 5000 deletions

86
backend/dependencies.py Normal file
View File

@@ -0,0 +1,86 @@
"""Reusable FastAPI dependencies."""
from __future__ import annotations
import logging
from typing import Annotated
from fastapi import Depends, Header, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jose import JWTError
from sqlalchemy.engine import Connection
from core.auth.utils import decode_access_token
from database import get_db
logger = logging.getLogger(__name__)
_security = HTTPBearer()
def get_current_user(
credentials: Annotated[HTTPAuthorizationCredentials, Depends(_security)],
db: Annotated[Connection, Depends(get_db)],
) -> dict:
"""Decode JWT and return user dict. Raises 401 on any failure."""
token = credentials.credentials
try:
payload = decode_access_token(token)
except JWTError as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail={"code": "UNAUTHORIZED", "message": "Invalid or expired token"},
)
# Optionally verify user still exists in DB
from core.dal.base_repository import BaseRepository
from sqlalchemy import text
row = db.execute(
text("SELECT id, username, role FROM users WHERE id = :id"),
{"id": int(payload["sub"])},
).fetchone()
if row is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail={"code": "UNAUTHORIZED", "message": "User not found"},
)
return dict(row._mapping)
def require_admin(
user: Annotated[dict, Depends(get_current_user)],
) -> dict:
"""Raise 403 if user is not admin."""
if user["role"] != "admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"code": "FORBIDDEN", "message": "Admin role required"},
)
return user
def get_server_or_404(server_id: int, db: Connection) -> dict:
"""Load server by ID or raise 404."""
from sqlalchemy import text
row = db.execute(
text("SELECT * FROM servers WHERE id = :id"), {"id": server_id}
).fetchone()
if row is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={"code": "NOT_FOUND", "message": f"Server {server_id} not found"},
)
return dict(row._mapping)
def get_adapter_for_server(server_id: int, db: Connection):
"""Load server and resolve its adapter. Raises 404 if server not found."""
server = get_server_or_404(server_id, db)
from adapters.registry import GameAdapterRegistry
try:
return GameAdapterRegistry.get(server["game_type"])
except KeyError:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={
"code": "GAME_TYPE_NOT_FOUND",
"message": f"No adapter for game type '{server['game_type']}'",
},
)