232 lines
5.8 KiB
Python
232 lines
5.8 KiB
Python
from flask import Blueprint, request, jsonify, Response, send_from_directory
|
|
from functools import wraps
|
|
from datetime import datetime, timezone
|
|
import os
|
|
import time
|
|
|
|
from models.state import (
|
|
user_sessions,
|
|
user_ips,
|
|
room_users,
|
|
messages,
|
|
banned_ips,
|
|
banned_usernames,
|
|
kicked_users,
|
|
video_state,
|
|
room_owners,
|
|
nuked_rooms,
|
|
public_chat_rooms,
|
|
)
|
|
|
|
admin_bp = Blueprint("admin", __name__)
|
|
|
|
SERVER_START_TIME = time.time()
|
|
|
|
|
|
def check_auth(username, password):
|
|
return username == os.getenv("ADMIN_USERNAME") and password == os.getenv(
|
|
"ADMIN_PASSWORD"
|
|
)
|
|
|
|
|
|
def authenticate():
|
|
return Response(
|
|
"Access Denied", 401, {"WWW-Authenticate": 'Basic realm="Login Required"'}
|
|
)
|
|
|
|
|
|
def requires_auth(f):
|
|
@wraps(f)
|
|
def decorated(*args, **kwargs):
|
|
auth = request.authorization
|
|
if not auth or not check_auth(auth.username, auth.password):
|
|
return authenticate()
|
|
return f(*args, **kwargs)
|
|
|
|
return decorated
|
|
|
|
|
|
@admin_bp.route("/admin")
|
|
@requires_auth
|
|
def admin_panel():
|
|
"""Serves the static admin HTML file."""
|
|
return send_from_directory("static", "admin.html")
|
|
|
|
|
|
@admin_bp.route("/admin/stats", methods=["GET"])
|
|
@requires_auth
|
|
def admin_stats():
|
|
total_messages = sum(len(msgs) for msgs in messages.values())
|
|
return jsonify({
|
|
"uptime": int(time.time() - SERVER_START_TIME),
|
|
"active_users": len(user_sessions),
|
|
"active_rooms": len(room_users),
|
|
"total_messages": total_messages,
|
|
"banned_count": len(banned_ips) + len(banned_usernames)
|
|
})
|
|
|
|
|
|
@admin_bp.route("/admin/users", methods=["GET"])
|
|
@requires_auth
|
|
def admin_users():
|
|
return jsonify(
|
|
[
|
|
{
|
|
"username": u,
|
|
"ip": user_ips.get(u, "unknown"),
|
|
"last_active": s["last_active"],
|
|
}
|
|
for u, s in user_sessions.items()
|
|
]
|
|
)
|
|
|
|
|
|
@admin_bp.route("/admin/rooms", methods=["GET"])
|
|
@requires_auth
|
|
def admin_rooms():
|
|
return jsonify(
|
|
[
|
|
{
|
|
"name": r,
|
|
"users": list(room_users.get(r, {}).keys()),
|
|
"type": "watchparty" if r in video_state else "chat",
|
|
"owner": room_owners.get(r, "System"),
|
|
"msg_count": len(messages.get(r, [])),
|
|
"is_public": r in public_chat_rooms,
|
|
}
|
|
for r in list(messages.keys())
|
|
]
|
|
)
|
|
|
|
|
|
@admin_bp.route("/admin/banip", methods=["POST"])
|
|
@requires_auth
|
|
def admin_ban_ip():
|
|
data = request.get_json()
|
|
username = data.get("username")
|
|
ip_to_ban = user_ips.get(username)
|
|
|
|
if not ip_to_ban:
|
|
return jsonify({"error": "no ip"}), 400
|
|
|
|
banned_ips.add(ip_to_ban)
|
|
from models.database import db_add_ban
|
|
db_add_ban("ip", ip_to_ban)
|
|
|
|
user_sessions.pop(username, None)
|
|
|
|
for room in room_users:
|
|
room_users[room].pop(username, None)
|
|
|
|
return jsonify({"banned": ip_to_ban})
|
|
|
|
|
|
@admin_bp.route("/admin/banusername", methods=["POST"])
|
|
@requires_auth
|
|
def admin_ban_username():
|
|
data = request.get_json()
|
|
username = data.get("username")
|
|
|
|
if not username:
|
|
return jsonify({"error": "no username"}), 400
|
|
|
|
banned_usernames.add(username)
|
|
from models.database import db_add_ban
|
|
db_add_ban("username", username)
|
|
|
|
user_sessions.pop(username, None)
|
|
|
|
for room in room_users:
|
|
room_users[room].pop(username, None)
|
|
|
|
return jsonify({"banned": username})
|
|
|
|
|
|
@admin_bp.route("/admin/banrawip", methods=["POST"])
|
|
@requires_auth
|
|
def admin_ban_raw_ip():
|
|
data = request.get_json()
|
|
ip = data.get("ip")
|
|
|
|
if not ip:
|
|
return jsonify({"error": "no ip"}), 400
|
|
|
|
banned_ips.add(ip)
|
|
from models.database import db_add_ban
|
|
db_add_ban("ip", ip)
|
|
|
|
return jsonify({"banned": ip})
|
|
|
|
|
|
@admin_bp.route("/admin/banned", methods=["GET"])
|
|
@requires_auth
|
|
def admin_banned_list():
|
|
banned = [{"type": "ip", "value": ip} for ip in banned_ips]
|
|
banned += [{"type": "username", "value": uname} for uname in banned_usernames]
|
|
return jsonify(banned)
|
|
|
|
|
|
@admin_bp.route("/admin/releaseuser", methods=["POST"])
|
|
@requires_auth
|
|
def admin_release_user():
|
|
data = request.get_json()
|
|
username = data.get("username")
|
|
|
|
if not username:
|
|
return jsonify({"error": "no username"}), 400
|
|
|
|
user_sessions.pop(username, None)
|
|
user_ips.pop(username, None)
|
|
from models.database import db_delete_user_ip
|
|
db_delete_user_ip(username)
|
|
|
|
for room in room_users:
|
|
room_users[room].pop(username, None)
|
|
|
|
return jsonify({"released": username})
|
|
|
|
|
|
@admin_bp.route("/admin/broadcast", methods=["POST"])
|
|
@requires_auth
|
|
def admin_broadcast():
|
|
"""Sends a system message to all active rooms."""
|
|
data = request.get_json()
|
|
text = data.get("text")
|
|
|
|
if not text:
|
|
return jsonify({"error": "no text"}), 400
|
|
|
|
timestamp = datetime.now(timezone.utc).isoformat()
|
|
message = {"username": "SYSTEM", "text": text, "timestamp": timestamp}
|
|
|
|
count = 0
|
|
for room in messages:
|
|
messages[room].append(message)
|
|
count += 1
|
|
|
|
return jsonify({"status": "sent", "rooms_affected": count})
|
|
|
|
|
|
@admin_bp.route("/admin/nukeroom", methods=["POST"])
|
|
@requires_auth
|
|
def admin_nuke_room():
|
|
"""Instantly deletes a room and clears its state."""
|
|
data = request.get_json()
|
|
room = data.get("room")
|
|
|
|
# Lock the room for 10 seconds to force all clients to disconnect
|
|
nuked_rooms[room] = time.time() + 10
|
|
|
|
messages.pop(room, None)
|
|
room_users.pop(room, None)
|
|
room_owners.pop(room, None)
|
|
from models.database import db_delete_room_owner, db_remove_public_room
|
|
db_delete_room_owner(room)
|
|
|
|
video_state.pop(room, None)
|
|
|
|
if room in public_chat_rooms:
|
|
public_chat_rooms.remove(room)
|
|
db_remove_public_room(room)
|
|
|
|
return jsonify({"status": "nuked", "room": room})
|