- POST /servers/<id>/console runs a command via rcon.execute_command and persists every outcome (success / empty / error) to command_history. - GET /servers/<id>/console/history returns paginated newest-first JSON for client-side up-arrow recall. - server_detail() now passes the last 50 history rows as console_history for server-side replay on page load. - 404 on ownership mismatch — no admin override. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
112 lines
3.4 KiB
Python
112 lines
3.4 KiB
Python
from flask import Blueprint, Response, jsonify, render_template, request
|
|
from sqlalchemy import select
|
|
|
|
from l4d2web.auth import current_user, require_login
|
|
from l4d2web.db import session_scope
|
|
from l4d2web.models import CommandHistory, Server
|
|
from l4d2web.services import rcon
|
|
from l4d2web.services.rcon import RconAuthError, RconError
|
|
|
|
|
|
bp = Blueprint("console", __name__)
|
|
|
|
_HISTORY_DEFAULT_LIMIT = 50
|
|
_HISTORY_MAX_LIMIT = 200
|
|
_HISTORY_MIN_LIMIT = 1
|
|
|
|
|
|
@bp.post("/servers/<int:server_id>/console")
|
|
@require_login
|
|
def run_console_command(server_id: int) -> Response:
|
|
user = current_user()
|
|
assert user is not None
|
|
|
|
with session_scope() as db:
|
|
server = db.scalar(
|
|
select(Server).where(Server.id == server_id, Server.user_id == user.id)
|
|
)
|
|
if server is None:
|
|
return Response(status=404)
|
|
|
|
command_raw = request.form.get("command", "")
|
|
command = command_raw.strip()
|
|
|
|
try:
|
|
reply = rcon.execute_command("127.0.0.1", server.port, server.rcon_password, command)
|
|
is_error = False
|
|
db.add(
|
|
CommandHistory(
|
|
user_id=user.id,
|
|
server_id=server_id,
|
|
command=command,
|
|
reply=reply,
|
|
is_error=False,
|
|
)
|
|
)
|
|
except (RconAuthError, RconError) as exc:
|
|
reply = str(exc)
|
|
is_error = True
|
|
db.add(
|
|
CommandHistory(
|
|
user_id=user.id,
|
|
server_id=server_id,
|
|
command=command,
|
|
reply=reply,
|
|
is_error=True,
|
|
)
|
|
)
|
|
except ValueError:
|
|
# Input validation failure — command never reached the wire; no history row.
|
|
return render_template(
|
|
"_console_line.html",
|
|
command=command,
|
|
reply="invalid command",
|
|
is_error=True,
|
|
)
|
|
|
|
return render_template(
|
|
"_console_line.html",
|
|
command=command,
|
|
reply=reply,
|
|
is_error=is_error,
|
|
)
|
|
|
|
|
|
@bp.get("/servers/<int:server_id>/console/history")
|
|
@require_login
|
|
def console_history(server_id: int) -> Response:
|
|
user = current_user()
|
|
assert user is not None
|
|
|
|
with session_scope() as db:
|
|
server = db.scalar(
|
|
select(Server).where(Server.id == server_id, Server.user_id == user.id)
|
|
)
|
|
if server is None:
|
|
return Response(status=404)
|
|
|
|
try:
|
|
raw_before = request.args.get("before")
|
|
before = int(raw_before) if raw_before is not None else None
|
|
except (TypeError, ValueError):
|
|
before = None
|
|
|
|
try:
|
|
raw_limit = request.args.get("limit")
|
|
limit = int(raw_limit) if raw_limit is not None else _HISTORY_DEFAULT_LIMIT
|
|
except (TypeError, ValueError):
|
|
limit = _HISTORY_DEFAULT_LIMIT
|
|
|
|
limit = max(_HISTORY_MIN_LIMIT, min(_HISTORY_MAX_LIMIT, limit))
|
|
|
|
query = select(CommandHistory).where(
|
|
CommandHistory.user_id == user.id,
|
|
CommandHistory.server_id == server_id,
|
|
)
|
|
if before is not None:
|
|
query = query.where(CommandHistory.id < before)
|
|
query = query.order_by(CommandHistory.id.desc()).limit(limit)
|
|
|
|
rows = db.scalars(query).all()
|
|
|
|
return jsonify([{"id": row.id, "command": row.command} for row in rows])
|