from flask import Blueprint, request, jsonify, Response, send_from_directory from functools import wraps from datetime import datetime, timezone from typing import Dict, Any, List, Tuple, Callable import os import time from models.state import ( user_sessions, user_ips, room_users, room_passwords, authorized_room_users, messages, banned_ips, banned_usernames, kicked_users, video_state, room_owners, nuked_rooms, ) admin_bp = Blueprint("admin", __name__) SERVER_START_TIME = time.time() def check_auth(username: str | None, password: str | None) -> bool: return username == os.getenv("ADMIN_USERNAME") and password == os.getenv( "ADMIN_PASSWORD" ) def authenticate() -> Response: return Response( "Access Denied", 401, {"WWW-Authenticate": 'Basic realm="Login Required"'} ) def requires_auth(f: Callable) -> Callable: @wraps(f) def decorated(*args: Any, **kwargs: Any) -> Any: 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() -> Response: """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() -> Response: total_messages = sum(len(msgs) for msgs in messages.values()) active_room_names = {r for r, u in room_users.items() if len(u) > 0} return jsonify({ "uptime": int(time.time() - SERVER_START_TIME), "active_users": len(user_sessions), "active_rooms": len(active_room_names), "total_messages": total_messages, "banned_count": len(banned_ips) + len(banned_usernames) }) @admin_bp.route("/admin/users", methods=["GET"]) @requires_auth def admin_users() -> Response: 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() -> Response: all_rooms = set(room_users.keys()) | set(room_passwords.keys()) return jsonify( [ { "name": r, "users": list(room_users.get(r, {}).keys()), "type": "watchparty" if r in video_state and video_state[r].get("url") else "chat", "owner": room_owners.get(r, "System"), "msg_count": len(messages.get(r, [])), "is_public": False, } for r in all_rooms ] ) @admin_bp.route("/admin/banip", methods=["POST"]) @requires_auth def admin_ban_ip() -> Tuple[Response, int] | Response: data: Dict[str, Any] = request.get_json() or {} 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) user_sessions.pop(username, None) for room in room_users: room_users[room].pop(username, None) for room in authorized_room_users: authorized_room_users[room].discard(username) return jsonify({"banned": ip_to_ban}) @admin_bp.route("/admin/banusername", methods=["POST"]) @requires_auth def admin_ban_username() -> Tuple[Response, int] | Response: data: Dict[str, Any] = request.get_json() or {} username = data.get("username") if not username: return jsonify({"error": "no username"}), 400 banned_usernames.add(username) user_sessions.pop(username, None) for room in room_users: room_users[room].pop(username, None) for room in authorized_room_users: authorized_room_users[room].discard(username) return jsonify({"banned": username}) @admin_bp.route("/admin/banrawip", methods=["POST"]) @requires_auth def admin_ban_raw_ip() -> Tuple[Response, int] | Response: data: Dict[str, Any] = request.get_json() or {} ip = data.get("ip") if not ip: return jsonify({"error": "no ip"}), 400 banned_ips.add(ip) return jsonify({"banned": ip}) @admin_bp.route("/admin/banned", methods=["GET"]) @requires_auth def admin_banned_list() -> Response: 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() -> Tuple[Response, int] | Response: data: Dict[str, Any] = request.get_json() or {} username = data.get("username") if not username: return jsonify({"error": "no username"}), 400 user_sessions.pop(username, None) user_ips.pop(username, None) for room in room_users: room_users[room].pop(username, None) for room in authorized_room_users: authorized_room_users[room].discard(username) return jsonify({"released": username}) @admin_bp.route("/admin/broadcast", methods=["POST"]) @requires_auth def admin_broadcast() -> Tuple[Response, int] | Response: """Sends a system notice to all active rooms.""" data: Dict[str, Any] = request.get_json() or {} text = data.get("text") if not text: return jsonify({"error": "no text"}), 400 timestamp = datetime.now(timezone.utc).isoformat() message = { "username": "SYSTEM", "ciphertext": "", "iv": "", "system_notice": text, "timestamp": timestamp, } count = 0 for room in list(room_users.keys()): messages.setdefault(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() -> Response: """Instantly deletes a room and clears its state.""" data: Dict[str, Any] = request.get_json() or {} room = data.get("room") nuked_rooms[room] = time.time() + 10 messages.pop(room, None) room_users.pop(room, None) room_passwords.pop(room, None) authorized_room_users.pop(room, None) room_owners.pop(room, None) video_state.pop(room, None) return jsonify({"status": "nuked", "room": room}) @admin_bp.route("/admin/unban", methods=["POST"]) @requires_auth def admin_unban() -> Tuple[Response, int] | Response: data: Dict[str, Any] = request.get_json() or {} ban_type = data.get("type") value = data.get("value") if not ban_type or not value: return jsonify({"error": "Missing type or value"}), 400 if ban_type == "ip": banned_ips.discard(value) elif ban_type == "username": banned_usernames.discard(value) else: return jsonify({"error": "Invalid ban type"}), 400 return jsonify({"unbanned": value}) @admin_bp.route("/admin/room/setowner", methods=["POST"]) @requires_auth def admin_set_room_owner() -> Tuple[Response, int] | Response: data: Dict[str, Any] = request.get_json() or {} room = data.get("room") owner = data.get("owner") if not room: return jsonify({"error": "Missing room name"}), 400 if owner: room_owners[room] = owner else: room_owners.pop(room, None) return jsonify({"status": "updated", "room": room, "owner": owner}) @admin_bp.route("/admin/kick", methods=["POST"]) @requires_auth def admin_kick_user() -> Tuple[Response, int] | Response: data: Dict[str, Any] = request.get_json() or {} room = data.get("room") target = data.get("target") if not room or not target: return jsonify({"error": "Missing room or target"}), 400 kicked_users.setdefault(room, set()).add(target) if room in authorized_room_users: authorized_room_users[room].discard(target) if room in room_users: room_users[room].pop(target, None) return jsonify({"kicked": target, "room": room}) @admin_bp.route("/admin/room/clearmessages", methods=["POST"]) @requires_auth def admin_clear_room_messages() -> Tuple[Response, int] | Response: data: Dict[str, Any] = request.get_json() or {} room = data.get("room") if not room: return jsonify({"error": "Missing room name"}), 400 messages[room] = [] return jsonify({"status": "cleared", "room": room}) @admin_bp.route("/admin/room/messages", methods=["GET"]) @requires_auth def admin_room_messages() -> Tuple[Response, int] | Response: room = request.args.get("room") if not room: return jsonify({"error": "Missing room parameter"}), 400 return jsonify(messages.get(room, []))