Each allowed_domains entry can now carry its own backend {host, port}.
That lets one proxy on port 25565 serve multiple MC servers, picking
the upstream from the domain the client typed.
- proxy/main.py: ProxyState.backend_for(domain) → tuple|None,
honors per-domain backend first, falls back to top-level backend.
handle_client uses backend_for(); blocked / disabled domains
return None (and still get a Login Disconnect on join attempts).
- api/routes/{config,domains}.py: DomainBackend model + optional
backend field on create/patch. PATCH supports clear_backend=true
to drop a per-domain override and revert to default.
- frontend/Domains.jsx: full rewrite — new-domain form has host/port
inputs, table shows each row's effective backend, inline edit +
reset button per row.
- frontend/Settings.jsx: backend section relabeled "기본 백엔드 (fallback)"
- README updated with multi-server example config.
86 lines
2.7 KiB
Python
86 lines
2.7 KiB
Python
"""허용 도메인 CRUD."""
|
|
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, HTTPException, Response
|
|
from pydantic import BaseModel, Field
|
|
|
|
from config_io import load_config, save_config
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
class DomainBackend(BaseModel):
|
|
host: str
|
|
port: int = Field(ge=1, le=65535)
|
|
|
|
|
|
class DomainCreate(BaseModel):
|
|
domain: str
|
|
enabled: bool = True
|
|
note: str = ""
|
|
backend: DomainBackend | None = None # 비우면 기본 backend 사용
|
|
|
|
|
|
class DomainPatch(BaseModel):
|
|
enabled: bool | None = None
|
|
note: str | None = None
|
|
# 명시적으로 None 을 보내려면 클라이언트가 "backend": null 을 보내야 한다.
|
|
# 빠뜨리면 기존 값 유지.
|
|
backend: DomainBackend | None = None
|
|
clear_backend: bool = False # True 면 기존 backend 삭제 → 기본값으로 복귀
|
|
|
|
|
|
@router.get("/domains")
|
|
def list_domains() -> list[dict]:
|
|
return load_config().get("allowed_domains", [])
|
|
|
|
|
|
@router.post("/domains", status_code=201)
|
|
def add_domain(body: DomainCreate) -> dict:
|
|
cfg = load_config()
|
|
domains = cfg.setdefault("allowed_domains", [])
|
|
name = body.domain.strip().lower()
|
|
if not name:
|
|
raise HTTPException(status_code=400, detail="domain required")
|
|
if any(d["domain"].lower() == name for d in domains):
|
|
raise HTTPException(status_code=409, detail="domain already exists")
|
|
entry: dict = {"domain": name, "enabled": body.enabled, "note": body.note}
|
|
if body.backend is not None:
|
|
entry["backend"] = body.backend.model_dump()
|
|
domains.append(entry)
|
|
save_config(cfg)
|
|
return entry
|
|
|
|
|
|
@router.delete("/domains/{domain}", status_code=204)
|
|
def delete_domain(domain: str) -> Response:
|
|
cfg = load_config()
|
|
name = domain.strip().lower()
|
|
before = len(cfg.get("allowed_domains", []))
|
|
cfg["allowed_domains"] = [
|
|
d for d in cfg.get("allowed_domains", []) if d["domain"].lower() != name
|
|
]
|
|
if len(cfg["allowed_domains"]) == before:
|
|
raise HTTPException(status_code=404, detail="domain not found")
|
|
save_config(cfg)
|
|
return Response(status_code=204)
|
|
|
|
|
|
@router.patch("/domains/{domain}")
|
|
def patch_domain(domain: str, body: DomainPatch) -> dict:
|
|
cfg = load_config()
|
|
name = domain.strip().lower()
|
|
for d in cfg.get("allowed_domains", []):
|
|
if d["domain"].lower() == name:
|
|
if body.enabled is not None:
|
|
d["enabled"] = body.enabled
|
|
if body.note is not None:
|
|
d["note"] = body.note
|
|
if body.clear_backend:
|
|
d.pop("backend", None)
|
|
elif body.backend is not None:
|
|
d["backend"] = body.backend.model_dump()
|
|
save_config(cfg)
|
|
return d
|
|
raise HTTPException(status_code=404, detail="domain not found")
|