diff --git a/blueprints/admin.py b/blueprints/admin.py index 45fe8fc..487f971 100644 --- a/blueprints/admin.py +++ b/blueprints/admin.py @@ -230,3 +230,87 @@ def admin_nuke_room(): db_remove_public_room(room) return jsonify({"status": "nuked", "room": room}) + + +@admin_bp.route("/admin/unban", methods=["POST"]) +@requires_auth +def admin_unban(): + data = request.get_json() + 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) + from models.database import db_remove_ban + db_remove_ban("ip", value) + elif ban_type == "username": + banned_usernames.discard(value) + from models.database import db_remove_ban + db_remove_ban("username", 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(): + data = request.get_json() + room = data.get("room") + owner = data.get("owner") # If None, clears the owner + + if not room: + return jsonify({"error": "Missing room name"}), 400 + + if owner: + room_owners[room] = owner + from models.database import db_set_room_owner + db_set_room_owner(room, owner) + else: + room_owners.pop(room, None) + from models.database import db_delete_room_owner + db_delete_room_owner(room) + + return jsonify({"status": "updated", "room": room, "owner": owner}) + + +@admin_bp.route("/admin/kick", methods=["POST"]) +@requires_auth +def admin_kick_user(): + data = request.get_json() + 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) + room_users.get(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(): + data = request.get_json() + 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(): + room = request.args.get("room") + if not room: + return jsonify({"error": "Missing room parameter"}), 400 + return jsonify(messages.get(room, [])) + diff --git a/config.py b/config.py index ce855ff..0830b6d 100644 --- a/config.py +++ b/config.py @@ -1,6 +1,19 @@ import os import secrets +# Manually load .env variables if present +_env_path = os.path.join(os.path.dirname(__file__), ".env") +if os.path.exists(_env_path): + try: + with open(_env_path, "r") as f: + for line in f: + line = line.strip() + if line and not line.startswith("#") and "=" in line: + key, val = line.split("=", 1) + os.environ[key.strip()] = val.strip() + except Exception: + pass + class Config: ADMIN_USERNAME = os.getenv("ADMIN_USERNAME") diff --git a/static/assets/css/admin.css b/static/assets/css/admin.css index 972dd12..05226d6 100644 --- a/static/assets/css/admin.css +++ b/static/assets/css/admin.css @@ -1,76 +1,92 @@ -/* === Admin Panel === */ -#serverStats { - display: flex; - flex-direction: row; - justify-content: center; - gap: 15px; - width: 100%; - max-width: 1000px; - margin-bottom: 30px; - background-color: #2a2a2a; - padding: 20px; - border-radius: 8px; - border: 1px solid #444; - flex-wrap: wrap; -} - -#serverStats > div { - background-color: #3d3d3d; - padding: 15px 20px; - border-radius: 6px; - min-width: 120px; - text-align: center; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); - flex: 1; -} - -#roomList, -#userList, -#bannedList { - width: 100%; - max-width: 1000px; - display: flex; - flex-direction: column; - gap: 10px; - margin-left: 0; /* Override collapsible default */ -} - -#roomList > div, -#userList > div, -#bannedList > div { - background-color: #2a2a2a; - border: 1px solid #444; - padding: 12px 15px; - border-radius: 6px; - display: flex; - align-items: center; - justify-content: space-between; - flex-wrap: wrap; - gap: 10px; -} - -/* Make buttons in lists smaller/neater */ -#roomList button, -#userList button { - min-height: 32px; - height: auto; - font-size: 12px; - padding: 4px 12px; -} - -#broadcastInput { - background-color: #ffffff; - color: #444; -} - -#bulkInput { - width: 100%; - max-width: 600px; - background-color: #ffffff; - color: #333; - border: 1px solid #ccc; - border-radius: 4px; - padding: 10px; - font-family: monospace; - margin-bottom: 10px; -} \ No newline at end of file +/* === Admin Panel === */ +#serverStats { + display: flex; + flex-direction: row; + justify-content: center; + gap: 15px; + width: 100%; + max-width: 1000px; + margin-bottom: 30px; + background-color: #2a2a2a; + padding: 20px; + border-radius: 8px; + border: 1px solid #444; + flex-wrap: wrap; +} + +#serverStats > div { + background-color: #3d3d3d; + padding: 15px 20px; + border-radius: 6px; + min-width: 120px; + text-align: center; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + flex: 1; +} + +#roomList, +#userList, +#bannedList { + width: 100%; + max-width: 1000px; + display: flex; + flex-direction: column; + gap: 10px; + margin-left: 0; /* Override collapsible default */ +} + +#roomList > div, +#userList > div, +#bannedList > div { + background-color: #2a2a2a; + border: 1px solid #444; + padding: 12px 15px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 10px; +} + +/* Make buttons in lists smaller/neater */ +#roomList button, +#userList button { + min-height: 32px; + height: auto; + font-size: 12px; + padding: 4px 12px; +} + +#broadcastInput { + background-color: #ffffff; + color: #444; +} + +#bulkInput { + width: 100%; + max-width: 600px; + background-color: #ffffff; + color: #333; + border: 1px solid #ccc; + border-radius: 4px; + padding: 10px; + font-family: monospace; + margin-bottom: 10px; +} + +/* Transcript Overlay Modal */ +#transcriptOverlay button { + background-color: #d9534f; + color: white; + border: none; + padding: 6px 12px; + border-radius: 4px; + cursor: pointer; + height: auto; + min-height: auto; + font-size: 14px; +} +#transcriptOverlay button:hover { + background-color: #c9302c; +} \ No newline at end of file diff --git a/static/assets/js/admin.js b/static/assets/js/admin.js index 701ec0d..95ec274 100644 --- a/static/assets/js/admin.js +++ b/static/assets/js/admin.js @@ -57,35 +57,133 @@ async function loadRooms() { const div = document.getElementById('roomList'); const countDisplay = document.getElementById('activeRoomCount'); - const activeRooms = rooms.filter(r => r.users.length > 0); // Filter active only + div.innerHTML = ''; - div.innerHTML = ''; // Clear previous content - - activeRooms.forEach(r => { + rooms.forEach(r => { const container = document.createElement('div'); + container.style.border = '1px solid #444'; + container.style.padding = '12px 15px'; + container.style.marginBottom = '10px'; + container.style.borderRadius = '6px'; + container.style.background = '#222'; + container.style.display = 'block'; + + const titleRow = document.createElement('div'); + titleRow.style.display = 'flex'; + titleRow.style.justifyContent = 'space-between'; + titleRow.style.alignItems = 'center'; + titleRow.style.marginBottom = '8px'; + + const titleGroup = document.createElement('div'); const link = document.createElement('a'); link.href = r.type === 'watchparty' ? `/watchparty_chat.html?room=${encodeURIComponent(r.name)}` : `/chat.html?room=${encodeURIComponent(r.name)}`; link.target = "_blank"; link.textContent = r.name; + link.style.fontWeight = 'bold'; + link.style.fontSize = '1.1em'; - const details = document.createElement('span'); - details.style.fontSize = '0.9em'; - details.style.color = '#666'; - details.style.marginLeft = '10px'; - details.textContent = `Owner: ${r.owner} | Msgs: ${r.msg_count} | ${r.is_public ? 'Public' : 'Unlisted'} | Users: ${r.users.join(', ')}`; + const typeSpan = document.createElement('span'); + typeSpan.textContent = ` (${r.type} | ${r.is_public ? 'Public' : 'Unlisted'})`; + typeSpan.style.color = '#888'; + typeSpan.style.fontSize = '0.95em'; + typeSpan.style.marginLeft = '5px'; + + titleGroup.append(link, typeSpan); + titleRow.appendChild(titleGroup); + + const actionsDiv = document.createElement('div'); + + const viewBtn = document.createElement('button'); + viewBtn.textContent = 'View Transcript'; + viewBtn.style.marginLeft = '5px'; + viewBtn.onclick = () => viewTranscript(r.name); + + const clearBtn = document.createElement('button'); + clearBtn.textContent = 'Clear History'; + clearBtn.style.marginLeft = '5px'; + clearBtn.onclick = () => clearHistory(r.name); const nukeBtn = document.createElement('button'); nukeBtn.textContent = 'Nuke'; - nukeBtn.style.marginLeft = '10px'; - nukeBtn.style.backgroundColor = '#d9534f'; // Red warning color + nukeBtn.style.marginLeft = '5px'; + nukeBtn.style.backgroundColor = '#d9534f'; + nukeBtn.style.color = 'white'; nukeBtn.onclick = () => nukeRoom(r.name); - container.append(link, details, nukeBtn); + actionsDiv.append(viewBtn, clearBtn, nukeBtn); + titleRow.appendChild(actionsDiv); + container.appendChild(titleRow); + + const metaRow = document.createElement('div'); + metaRow.style.fontSize = '0.9em'; + metaRow.style.color = '#aaa'; + metaRow.style.marginBottom = '8px'; + metaRow.style.display = 'flex'; + metaRow.style.alignItems = 'center'; + metaRow.style.gap = '10px'; + + const ownerLabel = document.createElement('span'); + ownerLabel.textContent = `Owner: ${r.owner}`; + + const changeOwnerBtn = document.createElement('button'); + changeOwnerBtn.textContent = 'Assign Owner'; + changeOwnerBtn.style.padding = '2px 6px'; + changeOwnerBtn.style.fontSize = '11px'; + changeOwnerBtn.onclick = () => assignOwner(r.name); + + const reclaimOwnerBtn = document.createElement('button'); + reclaimOwnerBtn.textContent = 'Reclaim Owner (Reset)'; + reclaimOwnerBtn.style.padding = '2px 6px'; + reclaimOwnerBtn.style.fontSize = '11px'; + reclaimOwnerBtn.onclick = () => reclaimOwner(r.name); + + metaRow.append(ownerLabel, changeOwnerBtn, reclaimOwnerBtn); + container.appendChild(metaRow); + + const usersRow = document.createElement('div'); + usersRow.style.fontSize = '0.9em'; + usersRow.style.color = '#ccc'; + + if (r.users.length === 0) { + usersRow.textContent = 'Users: (None)'; + } else { + usersRow.textContent = 'Users: '; + r.users.forEach(user => { + const userSpan = document.createElement('span'); + userSpan.style.display = 'inline-flex'; + userSpan.style.alignItems = 'center'; + userSpan.style.marginRight = '8px'; + userSpan.style.marginBottom = '4px'; + userSpan.style.padding = '2px 6px'; + userSpan.style.border = '1px solid #555'; + userSpan.style.borderRadius = '3px'; + userSpan.style.background = '#111'; + + const nameSpan = document.createElement('span'); + nameSpan.textContent = user; + + const kickBtn = document.createElement('button'); + kickBtn.textContent = 'Kick'; + kickBtn.style.marginLeft = '5px'; + kickBtn.style.padding = '1px 4px'; + kickBtn.style.fontSize = '10px'; + kickBtn.style.background = 'none'; + kickBtn.style.border = 'none'; + kickBtn.style.color = '#ff5722'; + kickBtn.style.cursor = 'pointer'; + kickBtn.style.minHeight = 'auto'; + kickBtn.onclick = () => adminKickUser(r.name, user); + + userSpan.append(nameSpan, kickBtn); + usersRow.appendChild(userSpan); + }); + } + container.appendChild(usersRow); div.appendChild(container); }); - countDisplay.textContent = `(${activeRooms.length})`; + countDisplay.textContent = `(${rooms.length})`; } async function banUser(username) { @@ -123,7 +221,29 @@ async function loadBanned() { div.innerHTML = ''; banned.forEach(item => { const el = document.createElement('div'); - el.textContent = `${item.type}: ${item.value}`; + el.style.display = 'flex'; + el.style.alignItems = 'center'; + el.style.justifyContent = 'space-between'; + + const label = document.createElement('span'); + label.textContent = `${item.type}: ${item.value}`; + + const unbanBtn = document.createElement('button'); + unbanBtn.textContent = 'Unban'; + unbanBtn.style.marginLeft = '10px'; + unbanBtn.onclick = async () => { + if (confirm(`Remove ban for ${item.value}?`)) { + await fetch('/admin/unban', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ type: item.type, value: item.value }) + }); + loadBanned(); + loadStats(); + } + }; + + el.append(label, unbanBtn); div.appendChild(el); }); } @@ -256,4 +376,88 @@ document.addEventListener("DOMContentLoaded", () => { }); } }); -}); \ No newline at end of file +}); + +async function viewTranscript(room) { + const res = await fetch(`/admin/room/messages?room=${encodeURIComponent(room)}`); + const msgs = await res.json(); + + let overlay = document.getElementById('transcriptOverlay'); + if (!overlay) { + overlay = document.createElement('div'); + overlay.id = 'transcriptOverlay'; + overlay.style.position = 'fixed'; + overlay.style.top = '10%'; + overlay.style.left = '10%'; + overlay.style.width = '80%'; + overlay.style.height = '80%'; + overlay.style.background = '#1a1f2c'; + overlay.style.color = 'white'; + overlay.style.border = '2px solid #555'; + overlay.style.borderRadius = '8px'; + overlay.style.padding = '20px'; + overlay.style.zIndex = '10000'; + overlay.style.overflowY = 'auto'; + document.body.appendChild(overlay); + } + overlay.style.display = 'block'; + overlay.innerHTML = ` +