#!/usr/bin/env python3 """ Backend Server for Plants vs. Zombies (ai.qpid.pro) Zero external dependencies. Manages: - SQLite User Accounts & Game Progression Profiles (/api/auth/login, /api/auth/register, /api/auth/save, /api/auth/sync, /api/auth/sessions) - Campaign Levels API (/api/campaign/levels, /api/admin/campaign/*) - Community Bug Reports & Feedback (/api/bugs) - Public Game Configurator & Balance API (/api/config) - Admin Management API (/api/admin/*, /api/admin/logs, /api/admin/sessions, /api/admin/password/change) """ import http.server import json import os import sys import html import urllib.parse import secrets import re import threading import time from datetime import datetime, timedelta BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.join(BASE_DIR, "scripts")) import db PORT = 8001 MAX_BODY_SIZE = 5 * 1024 * 1024 # 5MB (for backups) REPORTS_DIR = os.path.join(BASE_DIR, "reports") BUGS_JSON = os.path.join(REPORTS_DIR, "bugs.json") BUGS_MD = os.path.join(BASE_DIR, "BUG_REPORTS.md") os.makedirs(REPORTS_DIR, exist_ok=True) db.init_db() # Run maintenance upon server initialization try: _init_maint = db.perform_system_maintenance() sys.stderr.write(f"[{datetime.now().strftime('%H:%M:%S')}] Initial system maintenance: {_init_maint}\n") except Exception as _e: sys.stderr.write(f"[{datetime.now().strftime('%H:%M:%S')}] Initial system maintenance warning: {_e}\n") # Daemon background thread for daily system maintenance (24-hour interval) _maintenance_thread = None def start_daily_maintenance_thread(interval_seconds=86400): global _maintenance_thread if _maintenance_thread is not None and _maintenance_thread.is_alive(): return _maintenance_thread def maintenance_loop(): while True: time.sleep(interval_seconds) try: summary = db.perform_system_maintenance() sys.stderr.write(f"[{datetime.now().strftime('%H:%M:%S')}] Daily system maintenance completed: {summary}\n") except Exception as e: sys.stderr.write(f"[{datetime.now().strftime('%H:%M:%S')}] Daily system maintenance error: {e}\n") _maintenance_thread = threading.Thread(target=maintenance_loop, daemon=True, name="DailyMaintenanceThread") _maintenance_thread.start() return _maintenance_thread start_daily_maintenance_thread() ADMIN_TOKENS = {} # token -> expires_at timestamp def parse_device_info(user_agent): """Extract friendly device and browser name from User-Agent string.""" return db.parse_device_info(user_agent) def create_admin_token(duration_days=7): token = "adm_" + secrets.token_hex(24) expires_at = (datetime.now() + timedelta(days=duration_days)).timestamp() ADMIN_TOKENS[token] = expires_at return token def is_valid_admin_token(token): if not token or not isinstance(token, str): return False token = token.strip() if token in ADMIN_TOKENS: if datetime.now().timestamp() < ADMIN_TOKENS[token]: return True else: del ADMIN_TOKENS[token] return False # Check if this token belongs to an admin user session in DB user = db.validate_session(token) if user and user.get("isAdmin"): return True return False # In-memory rate limiter for failed login attempts FAILED_LOGIN_ATTEMPTS = {} # ip -> list of timestamps def is_login_rate_limited(ip, max_attempts=5, window_seconds=60): now = datetime.now().timestamp() attempts = [t for t in FAILED_LOGIN_ATTEMPTS.get(ip, []) if now - t < window_seconds] FAILED_LOGIN_ATTEMPTS[ip] = attempts return len(attempts) >= max_attempts def record_failed_login(ip): now = datetime.now().timestamp() attempts = [t for t in FAILED_LOGIN_ATTEMPTS.get(ip, []) if now - t < 60] attempts.append(now) FAILED_LOGIN_ATTEMPTS[ip] = attempts def clear_failed_logins(ip): FAILED_LOGIN_ATTEMPTS.pop(ip, None) DEFAULT_BUGS = [ { "id": 1, "author": "Player_Kopri", "category": "bug", "text": "Зомби после поедания растения не шел дальше, а оставался стоять на месте.", "status": "исправлено", "timestamp": "2026-09-16 09:30", "adminReply": "Исправлено в v1.6: сброшен флаг isEating и таймер поедания." }, { "id": 2, "author": "DaveFan", "category": "bug", "text": "Режим 'Я, Зомби' не работал: сразу после старта выводилось сообщение 'победа'.", "status": "исправлено", "timestamp": "2026-09-16 09:45", "adminReply": "Исправлено в v1.6: устранено состояние гонки при инициализации газона." }, { "id": 3, "author": "ZombossHater", "category": "visual", "text": "Элементы главного меню и кнопки вываливаются за границы экрана на небольших дисплеях.", "status": "исправлено", "timestamp": "2026-09-16 10:50", "adminReply": "Исправлено в v2.2: адаптивная панель навигации, компактные карточки и вертикальный скролл." } ] def load_bugs(): if not os.path.exists(BUGS_JSON): save_bugs(DEFAULT_BUGS) return DEFAULT_BUGS try: with open(BUGS_JSON, "r", encoding="utf-8") as f: return json.load(f) except Exception: return DEFAULT_BUGS def save_bugs(bugs): with open(BUGS_JSON, "w", encoding="utf-8") as f: json.dump(bugs, f, ensure_ascii=False, indent=2) sync_markdown(bugs) def sync_markdown(bugs): lines = [ "# Журнал баг-репортов и обратной связи от игроков", "", "> Данный файл обновляется автоматически при поступлении новых баг-репортов из игры.", "> При каждом обновлении и новой итерации разработки ассистент анализирует этот список.", "", f"**Всего репортов:** {len(bugs)} | **Последнее обновление:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", "", "| ID | Дата | Автор | Категория | Статус | Описание | Ответ / Фикс |", "|:---:|:---:|:---|:---:|:---:|:---|:---|" ] for b in reversed(bugs): bid = b.get("id", 0) ts = b.get("timestamp", "") author = str(b.get("author", "Аноним")).replace("|", "/") cat = b.get("category", "bug") cat_icon = "🔴 Баг" if cat == "bug" else ("💡 Идея" if cat == "feature" else ("🎨 UI" if cat == "visual" else "⚖️ Баланс")) st = b.get("status", "новый") st_badge = "✅ Исправлено" if st == "исправлено" else ("🟡 В работе" if st == "в работе" else ("⚪ Отклонено" if st == "отклонено" else "🔴 Новый")) text = str(b.get("text", "")).replace("|", "/").replace("\n", " ") reply = str(b.get("adminReply") or "—").replace("|", "/").replace("\n", " ") lines.append(f"| {bid} | {ts} | {author} | {cat_icon} | {st_badge} | {text} | {reply} |") lines.append("") with open(BUGS_MD, "w", encoding="utf-8") as f: f.write("\n".join(lines)) class FeedbackHandler(http.server.BaseHTTPRequestHandler): def get_client_ip(self): xff = self.headers.get("X-Forwarded-For") if xff: return xff.split(",")[0].strip() x_real = self.headers.get("X-Real-IP") if x_real: return x_real.strip() return self.client_address[0] def _set_cors(self): origin = self.headers.get("Origin", "") if not origin: self.send_header("Access-Control-Allow-Origin", "*") else: try: parsed = urllib.parse.urlparse(origin) host = parsed.hostname or "" if host in ["localhost", "127.0.0.1", "ai.qpid.pro", "pvz.qpid.pro", "qpid.pro", "ai", "racks"] or host.endswith(".qpid.pro") or host.endswith(".ai") or host.endswith(".racks"): self.send_header("Access-Control-Allow-Origin", origin) else: self.send_header("Access-Control-Allow-Origin", "*") except Exception: self.send_header("Access-Control-Allow-Origin", "*") self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, DELETE") self.send_header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Admin-Token, X-Auth-Token, X-Session-Token") self.send_header("Access-Control-Allow-Credentials", "true") def _send_json(self, data, status=200): body = json.dumps(data, ensure_ascii=False).encode("utf-8") self.send_response(status) self._set_cors() self.send_header("Content-Type", "application/json; charset=utf-8") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def extract_token(self, payload=None): """Extracts Bearer token, custom header, cookie, query param, or JSON payload token.""" auth_header = self.headers.get("Authorization", "") if auth_header.startswith("Bearer "): return auth_header[7:].strip() for h in ["X-Admin-Token", "X-Auth-Token", "X-Session-Token"]: if h in self.headers and self.headers[h]: return self.headers.get(h, "").strip() # Check cookies cookie_header = self.headers.get("Cookie", "") if cookie_header: for part in cookie_header.split(";"): part = part.strip() if part.startswith("admin_token="): return part.split("=", 1)[1].strip() if part.startswith("token="): return part.split("=", 1)[1].strip() if part.startswith("session_token="): return part.split("=", 1)[1].strip() # Check query parameters if "?" in self.path: parsed_query = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query) if "token" in parsed_query and parsed_query["token"]: return parsed_query["token"][0].strip() if "adminToken" in parsed_query and parsed_query["adminToken"]: return parsed_query["adminToken"][0].strip() # Check payload if payload and isinstance(payload, dict): if "token" in payload and payload["token"]: return str(payload["token"]).strip() if "adminToken" in payload and payload["adminToken"]: return str(payload["adminToken"]).strip() if "sessionToken" in payload and payload["sessionToken"]: return str(payload["sessionToken"]).strip() return None def extract_admin_token(self, payload=None): return self.extract_token(payload) def require_admin(self, payload=None): token = self.extract_admin_token(payload) if not token or not is_valid_admin_token(token): self._send_json({"success": False, "message": "Доступ запрещен: требуется токен администратора"}, 401) return False return True def do_OPTIONS(self): self.send_response(204) self._set_cors() self.end_headers() def do_GET(self): parsed_url = urllib.parse.urlparse(self.path) path = parsed_url.path query_params = urllib.parse.parse_qs(parsed_url.query) # 1. Health check if path == "/health": self.send_response(200) self._set_cors() self.send_header("Content-Type", "text/plain") self.end_headers() self.wfile.write(b"OK") return # 2. Public Game Balance Config if path in ["/api/config", "/api/bugs/config"]: config = db.get_game_config() self._send_json(config, 200) return # 3. Campaign Levels Public API (GET /api/campaign/levels) if path in ["/api/campaign/levels", "/api/bugs/campaign/levels"]: level_num = query_params.get("level", [None])[0] if level_num is not None: lvl = db.get_campaign_levels(level_num) if not lvl: self._send_json({"success": False, "message": "Уровень не найден"}, 404) return self._send_json({"success": True, "level": lvl}, 200) return levels = db.get_campaign_levels() self._send_json({"success": True, "levels": levels, "count": len(levels)}, 200) return # 4. List Registered Users in Database if path in ["/api/users", "/api/bugs/users", "/api/auth/users"]: users = db.list_all_users() self._send_json({"success": True, "users": users}, 200) return # 5. Bug Reports Feed if path == "/api/bugs" or path == "/bugs" or path == "/api/bugs/list": bugs = load_bugs() self._send_json(bugs, 200) return # 6. User Active Sessions (GET /api/auth/sessions) if path in ["/api/auth/sessions", "/api/bugs/auth/sessions"]: token = self.extract_token() if not token: self._send_json({"success": False, "message": "Токен авторизации не передан"}, 401) return user = db.validate_session(token) if not user: self._send_json({"success": False, "message": "Недействительный или истекший токен сессии"}, 401) return client_ip = self.get_client_ip() db.update_session_activity(token, client_ip) user_sessions = db.get_user_sessions(user["id"]) for s in user_sessions: s["isCurrent"] = (s["token"] == token) s["is_current"] = (s["token"] == token) self._send_json({"success": True, "sessions": user_sessions, "total": len(user_sessions)}, 200) return # 7. Public / User Profile Fetch (/api/auth/profile, /api/auth/me, /api/auth/sync) if path in ["/api/auth/profile", "/api/auth/me", "/api/auth/sync", "/api/bugs/auth/profile", "/api/bugs/auth/me"]: token = self.extract_token() if not token: self._send_json({"success": False, "message": "Токен авторизации не передан"}, 401) return user = db.validate_session(token) if not user: self._send_json({"success": False, "message": "Недействительный или истекший токен сессии"}, 401) return client_ip = self.get_client_ip() db.update_session_activity(token, client_ip) profile_data = db.get_user_profile(user["username"]) # Client has synchronized with the server: clear admin_updated_at flag try: conn_sync = db.get_connection() c_sync = conn_sync.cursor() c_sync.execute("UPDATE profiles SET admin_updated_at = NULL WHERE user_id = ?", (user["id"],)) conn_sync.commit() conn_sync.close() except Exception: pass self._send_json({ "success": True, "token": token, "user_id": user["id"], "username": user["username"], "isAdmin": user.get("isAdmin", False), "data": profile_data }, 200) return # 8. Global Community Leaderboard API (GET) if path in ["/api/leaderboard", "/api/leaderboards", "/api/bugs/leaderboard"]: mode_param = query_params.get("mode", [None])[0] records = db.get_leaderboard_scores(mode_param) self._send_json({"success": True, "records": records}, 200) return # ==================================================== # ADMIN API (GET) # ==================================================== # Admin: Dashboard Stats if path == "/api/admin/stats": if not self.require_admin(): return stats = db.admin_get_stats() self._send_json({"success": True, "stats": stats}, 200) return # Admin: Game Config & Presets if path == "/api/admin/config": if not self.require_admin(): return config = db.get_game_config() self._send_json({"success": True, "config": config}, 200) return # Admin: Campaign Levels Editor List if path in ["/api/admin/campaign/levels", "/api/admin/campaign"]: if not self.require_admin(): return levels = db.get_campaign_levels() self._send_json({"success": True, "levels": levels, "count": len(levels)}, 200) return # Admin: Login Audit Logs if path == "/api/admin/logs": if not self.require_admin(): return limit = int(query_params.get("limit", [100])[0]) username = query_params.get("username", [None])[0] ip = query_params.get("ip", [None])[0] logs = db.get_login_logs(limit=limit, username=username, ip=ip) self._send_json({"success": True, "logs": logs, "count": len(logs)}, 200) return # Admin: All Player Active Sessions if path in ["/api/admin/sessions", "/api/admin/sessions/list"]: if not self.require_admin(): return sessions = db.get_all_active_sessions() self._send_json({"success": True, "sessions": sessions, "count": len(sessions)}, 200) return # Admin: List all users with detail if path == "/api/admin/users": if not self.require_admin(): return users = db.admin_list_users() self._send_json({"success": True, "users": users}, 200) return # Admin: Get single user detail (/api/admin/user/) m_user = re.match(r"^/api/admin/user/(\d+)$", path) if m_user: if not self.require_admin(): return user_id = int(m_user.group(1)) detail = db.admin_get_user_detail(user_id) if not detail: self._send_json({"success": False, "message": "Пользователь не найден"}, 404) return self._send_json({"success": True, "user": detail}, 200) return # Admin: Bug Reports List if path == "/api/admin/bugs": if not self.require_admin(): return bugs = load_bugs() self._send_json({"success": True, "bugs": bugs}, 200) return # Admin: Database Backup Export if path == "/api/admin/backup/export": if not self.require_admin(): return export_data = db.admin_export_db() self._send_json({"success": True, "data": export_data}, 200) return # Admin: System Maintenance & Cleanup (GET) if path in ["/api/admin/system/cleanup", "/api/admin/cleanup"]: if not self.require_admin(): return summary = db.perform_system_maintenance() self._send_json(summary, 200) return self._send_json({"error": "Not Found", "path": path}, 404) def do_POST(self): parsed_url = urllib.parse.urlparse(self.path) path = parsed_url.path length = int(self.headers.get("Content-Length", 0)) # Check payload size limit if length > MAX_BODY_SIZE: self._send_json({"success": False, "message": "Payload Too Large (Max 5MB)"}, 413) self.close_connection = True return body = self.rfile.read(length).decode("utf-8") if length > 0 else "{}" try: payload = json.loads(body) if body else {} except Exception as e: self._send_json({"error": f"Invalid JSON: {e}"}, 400) return client_ip = self.get_client_ip() user_agent = self.headers.get("User-Agent", "") device_name = parse_device_info(user_agent) # ==================================================== # ADMIN AUTHENTICATION & MANAGEMENT # ==================================================== if path == "/api/admin/auth": password = str(payload.get("password", "")).strip() if not password: self._send_json({"success": False, "message": "Введите пароль администратора"}, 400) return if db.verify_admin_password(password): token = create_admin_token() self._send_json({ "success": True, "token": token, "admin": True, "message": "Успешная авторизация администратора" }, 200) return self._send_json({"success": False, "message": "Неверный пароль администратора"}, 401) return # Admin: Change Master Password if path == "/api/admin/password/change": admin_token = self.extract_admin_token(payload) is_authed = admin_token and is_valid_admin_token(admin_token) current_password = str(payload.get("currentPassword", payload.get("current_password", ""))).strip() new_password = str(payload.get("newPassword", payload.get("new_password", payload.get("password", "")))).strip() if not new_password: self._send_json({"success": False, "message": "Новый пароль не может быть пустым"}, 400) return if not is_authed: if not current_password or not db.verify_admin_password(current_password): self._send_json({"success": False, "message": "Неверный текущий пароль администратора"}, 401) return res = db.set_admin_password(new_password) self._send_json(res, 200 if res.get("success") else 400) return # Admin: Terminate Any Session (POST /api/admin/session/terminate) if path in ["/api/admin/session/terminate", "/api/admin/sessions/terminate"]: if not self.require_admin(payload): return target_token = payload.get("targetToken") or payload.get("target_token") or payload.get("token") if not target_token: self._send_json({"success": False, "message": "Укажите токен сессии для завершения"}, 400) return db.terminate_session(str(target_token).strip()) self._send_json({"success": True, "message": "Сессия успешно завершена"}, 200) return # Admin: System Maintenance & Cleanup (POST /api/admin/system/cleanup) if path in ["/api/admin/system/cleanup", "/api/admin/cleanup"]: if not self.require_admin(payload): return audit_days = payload.get("audit_days", payload.get("auditDays", 30)) session_days = payload.get("session_days", payload.get("sessionDays", 30)) try: audit_days = int(audit_days) except (ValueError, TypeError): audit_days = 30 try: session_days = int(session_days) except (ValueError, TypeError): session_days = 30 summary = db.perform_system_maintenance(audit_days=audit_days, session_days=session_days) self._send_json(summary, 200) return # Admin: Save Campaign Level (POST /api/admin/campaign/level/save) if path in ["/api/admin/campaign/level/save", "/api/admin/campaign/save"]: if not self.require_admin(payload): return level_data = payload.get("level") or payload try: saved = db.save_campaign_level(level_data) self._send_json({"success": True, "message": "Уровень кампании сохранен", "level": saved}, 200) except Exception as e: self._send_json({"success": False, "message": f"Ошибка сохранения уровня: {e}"}, 400) return # Admin: Reset Campaign Level (POST /api/admin/campaign/level/reset) if path in ["/api/admin/campaign/level/reset", "/api/admin/campaign/reset"]: if not self.require_admin(payload): return target_lvl = payload.get("level") res = db.reset_campaign_levels(target_lvl) self._send_json(res, 200 if res.get("success") else 400) return # Admin: Save / Apply Game Balance Config if path == "/api/admin/config": if not self.require_admin(payload): return config_data = payload.get("config") or payload preset_name = payload.get("preset") or config_data.get("preset", "Custom") # If applying a standard preset from default catalogue if "applyPreset" in payload and payload["applyPreset"] in db.DEFAULT_GAME_CONFIG["presets"]: target_preset = payload["applyPreset"] preset_obj = db.DEFAULT_GAME_CONFIG["presets"][target_preset] current_cfg = db.get_game_config() current_cfg["preset"] = target_preset current_cfg["economy"].update(preset_obj.get("economy", {})) current_cfg["zombies"].update(preset_obj.get("zombies", {})) current_cfg["plants"].update(preset_obj.get("plants", {})) current_cfg["zenGarden"].update(preset_obj.get("zenGarden", {})) current_cfg["events"].update(preset_obj.get("events", {})) saved = db.save_game_config(current_cfg, target_preset) self._send_json({"success": True, "message": f"Пресет '{target_preset}' успешно применен", "config": saved}, 200) return try: saved = db.save_game_config(config_data, preset_name) self._send_json({"success": True, "message": "Конфигурация баланса сохранена", "config": saved}, 200) except Exception as e: self._send_json({"success": False, "message": f"Ошибка сохранения: {e}"}, 400) return # Admin: Create User if path == "/api/admin/user/create": if not self.require_admin(payload): return username = str(payload.get("username", "")).strip() password = str(payload.get("password", "")).strip() initial_data = payload.get("initialData", payload.get("profile", {})) is_admin = 1 if payload.get("isAdmin") else 0 if not username or not password: self._send_json({"success": False, "message": "Укажите имя пользователя и пароль"}, 400) return res = db.admin_create_user(username, password, initial_data, is_admin) self._send_json(res, 200 if res.get("success") else 400) return # Admin: Edit User Profile (/api/admin/user//edit) m_edit = re.match(r"^/api/admin/user/(\d+)/edit$", path) if m_edit: if not self.require_admin(payload): return user_id = int(m_edit.group(1)) updates = payload.get("updates", payload) res = db.admin_update_user_profile(user_id, updates) self._send_json(res, 200 if res.get("success") else 400) return # Admin: Ban / Unban User (/api/admin/user//ban) m_ban = re.match(r"^/api/admin/user/(\d+)/ban$", path) if m_ban: if not self.require_admin(payload): return user_id = int(m_ban.group(1)) is_banned = payload.get("is_banned", payload.get("isBanned", True)) reason = payload.get("reason", payload.get("ban_reason", "")) res = db.admin_set_user_ban(user_id, is_banned, reason) self._send_json(res, 200 if res.get("success") else 400) return # Admin: Delete User (/api/admin/user//delete) m_del = re.match(r"^/api/admin/user/(\d+)/delete$", path) if m_del: if not self.require_admin(payload): return user_id = int(m_del.group(1)) res = db.admin_delete_user(user_id) self._send_json(res, 200 if res.get("success") else 400) return # Admin: Reset Password (/api/admin/user//password) m_pwd = re.match(r"^/api/admin/user/(\d+)/password$", path) if m_pwd: if not self.require_admin(payload): return user_id = int(m_pwd.group(1)) new_password = str(payload.get("password", payload.get("newPassword", ""))).strip() res = db.admin_reset_user_password(user_id, new_password) self._send_json(res, 200 if res.get("success") else 400) return # Admin: Moderate Bug Report (/api/admin/bugs//status) m_bug = re.match(r"^/api/admin/bugs/(\d+)/status$", path) if m_bug: if not self.require_admin(payload): return bug_id = int(m_bug.group(1)) bugs = load_bugs() if payload.get("delete"): bugs = [b for b in bugs if b.get("id") != bug_id] save_bugs(bugs) self._send_json({"success": True, "message": f"Баг #{bug_id} удален"}, 200) return found = False for b in bugs: if b.get("id") == bug_id: found = True if "status" in payload: b["status"] = str(payload["status"]).strip() if "adminReply" in payload: b["adminReply"] = str(payload["adminReply"]).strip() break if not found: self._send_json({"success": False, "message": "Баг-репорт не найден"}, 404) return save_bugs(bugs) self._send_json({"success": True, "message": "Статус баг-репорта обновлен", "bugId": bug_id}, 200) return # Admin: Database Backup Import if path == "/api/admin/backup/import": if not self.require_admin(payload): return import_data = payload.get("data", payload) res = db.admin_import_db(import_data) if res.get("success") and "bugs" in import_data: save_bugs(import_data["bugs"]) self._send_json(res, 200 if res.get("success") else 400) return # ==================================================== # USER AUTHENTICATION, SESSIONS & PROGRESS SAVE # ==================================================== # 1a. User Login with Rate Limiting & Audit Logging if path in ["/api/auth/login", "/api/bugs/auth/login"]: if is_login_rate_limited(client_ip): db.record_login_log(None, payload.get("username", ""), client_ip, user_agent, "rate_limited", device_name) self._send_json({ "success": False, "message": "Слишком много неудачных попыток входа. Пожалуйста, подождите 1 минуту." }, 429) return username = str(payload.get("username", "")).strip() password_hash = str(payload.get("passwordHash", "")).strip() if not username or not password_hash: record_failed_login(client_ip) db.record_login_log(None, username, client_ip, user_agent, "empty_credentials", device_name) self._send_json({"success": False, "message": "Введите логин и пароль"}, 400) return if not re.match(r"^[a-zA-Zа-яА-ЯёЁ0-9_-]{3,20}$", username): record_failed_login(client_ip) db.record_login_log(None, username, client_ip, user_agent, "invalid_username_format", device_name) self._send_json({"success": False, "message": "Логин может содержать только буквы, цифры, '-' и '_' (3-20 симв.)"}, 400) return user = db.get_user_by_username(username) if not user: record_failed_login(client_ip) db.record_login_log(None, username, client_ip, user_agent, "user_not_found", device_name) self._send_json({"success": False, "message": "Пользователь с таким логином не найден. Зарегистрируйтесь."}, 404) return res = db.authenticate_user(username, password_hash, ip_address=client_ip, user_agent=user_agent, device_name=device_name) if not res or not res.get("success"): record_failed_login(client_ip) self._send_json(res or {"success": False, "message": "Неверный логин или пароль"}, 401) return clear_failed_logins(client_ip) self._send_json(res, 200) return # 1b. User Registration if path in ["/api/auth/register", "/api/bugs/auth/register"]: username = str(payload.get("username", "")).strip() password_hash = str(payload.get("passwordHash", "")).strip() initial_data = payload.get("initialData") if not username or not password_hash: self._send_json({"success": False, "message": "Введите логин и пароль для регистрации"}, 400) return if not re.match(r"^[a-zA-Zа-яА-ЯёЁ0-9_-]{3,20}$", username): self._send_json({"success": False, "message": "Логин может содержать только буквы, цифры, '-' и '_' (3-20 симв.)"}, 400) return if len(password_hash) < 1: self._send_json({"success": False, "message": "Пароль не может быть пустым"}, 400) return if username.lower() in ["гость", "guest", "admin", "null", "undefined", "system", "anonymous"]: self._send_json({"success": False, "message": "Это имя пользователя зарезервировано"}, 400) return user = db.get_user_by_username(username) if user: self._send_json({"success": False, "message": "Пользователь с таким логином уже существует"}, 409) return res = db.register_user(username, password_hash, initial_data, ip_address=client_ip, user_agent=user_agent, device_name=device_name) self._send_json(res, 200 if res.get("success") else 400) return # 1c. User Logout if path in ["/api/auth/logout", "/api/bugs/auth/logout"]: token = self.extract_token(payload) if token: db.delete_session(token) self._send_json({"success": True, "message": "Сессия успешно завершена"}, 200) return # 1d. User Active Sessions List (POST /api/auth/sessions) if path in ["/api/auth/sessions", "/api/bugs/auth/sessions"]: token = self.extract_token(payload) if not token: self._send_json({"success": False, "message": "Токен авторизации не передан"}, 401) return user = db.validate_session(token) if not user: self._send_json({"success": False, "message": "Недействительный или истекший токен сессии"}, 401) return db.update_session_activity(token, client_ip) user_sessions = db.get_user_sessions(user["id"]) for s in user_sessions: s["isCurrent"] = (s["token"] == token) s["is_current"] = (s["token"] == token) self._send_json({"success": True, "sessions": user_sessions, "total": len(user_sessions)}, 200) return # 1e. User Session Termination (POST /api/auth/sessions/terminate) if path in ["/api/auth/sessions/terminate", "/api/bugs/auth/sessions/terminate"]: token = self.extract_token(payload) if not token: self._send_json({"success": False, "message": "Токен авторизации не передан"}, 401) return user = db.validate_session(token) if not user: self._send_json({"success": False, "message": "Недействительный или истекший токен сессии"}, 401) return db.update_session_activity(token, client_ip) # Terminate all other sessions if requested if payload.get("all_others") or payload.get("allOthers"): db.terminate_all_user_sessions(user["id"], except_token=token) self._send_json({"success": True, "message": "Все остальные сессии успешно завершены"}, 200) return target_token = payload.get("targetToken") or payload.get("target_token") if not target_token: # If no specific target provided, terminate requesting token (logout) target_token = token db.terminate_session(str(target_token).strip(), user_id=user["id"]) self._send_json({"success": True, "message": "Сессия успешно завершена"}, 200) return # 1f. User Profile Fetch via POST if path in ["/api/auth/profile", "/api/auth/me", "/api/bugs/auth/profile", "/api/bugs/auth/me"]: token = self.extract_token(payload) if not token: self._send_json({"success": False, "message": "Токен авторизации не передан"}, 401) return session_user = db.validate_session(token) if not session_user: self._send_json({"success": False, "message": "Недействительный или истекший токен сессии"}, 401) return db.update_session_activity(token, client_ip) profile_data = db.get_user_profile(session_user["username"]) # Client has synchronized with the server: clear admin_updated_at flag try: conn_sync = db.get_connection() c_sync = conn_sync.cursor() c_sync.execute("UPDATE profiles SET admin_updated_at = NULL WHERE user_id = ?", (session_user["id"],)) conn_sync.commit() conn_sync.close() except Exception: pass self._send_json({ "success": True, "token": token, "user_id": session_user["id"], "username": session_user["username"], "isAdmin": session_user.get("isAdmin", False), "data": profile_data }, 200) return # 2. User Progress Profile Save & Sync (Token Authenticated & Server Authoritative) if path in ["/api/auth/save", "/api/bugs/auth/save", "/api/auth/sync"]: token = self.extract_token(payload) if not token: self._send_json({"success": False, "message": "Требуется авторизация (отсутствует токен сессии)"}, 401) return session_user = db.validate_session(token) if not session_user: self._send_json({"success": False, "message": "Недействительный или истекший токен сессии"}, 401) return username = str(payload.get("username", "")).strip() or session_user["username"] if session_user["username"].lower() != username.lower(): self._send_json({"success": False, "message": "Доступ запрещен (несоответствие пользователя)"}, 403) return user_data = payload.get("data") if user_data is None: # No data sent -> return fresh authoritative profile from database db.update_session_activity(token, client_ip) profile_data = db.get_user_profile(username) self._send_json({ "success": True, "token": token, "user_id": session_user["id"], "username": username, "isAdmin": session_user.get("isAdmin", False), "data": profile_data }, 200) return res = db.save_user_profile(username, user_data, token=token, ip_address=client_ip) self._send_json(res, 200 if res.get("success") else 400) return # 2b. Global Leaderboard Score Submission (POST) if path in ["/api/leaderboard", "/api/leaderboards", "/api/bugs/leaderboard"]: token = self.extract_token(payload) user_id = None player_name = str(payload.get("name", payload.get("player_name", payload.get("username", "")))).strip() if token: session_user = db.validate_session(token) if session_user: user_id = session_user["id"] db.update_session_activity(token, client_ip) if not player_name or player_name.lower() in ["гость", "guest", "player"]: player_name = session_user["username"] if not player_name: player_name = "Гость" mode = str(payload.get("mode", "survival")).strip() score = payload.get("score", 0) detail = str(payload.get("detail", "")).strip() res = db.submit_leaderboard_score(user_id, player_name, mode, score, detail) self._send_json(res, 200 if res.get("success") else 400) return # 3. Bug Report Submission (Sanitized) if path.startswith("/api/bugs") or path.startswith("/bugs"): raw_text = str(payload.get("text", "")).strip() if not raw_text: self._send_json({"error": "Empty report text"}, 400) return raw_author = str(payload.get("author", "Гость")).strip() or "Гость" category = str(payload.get("category", "bug")).strip() if category not in ["bug", "feature", "visual", "balance"]: category = "bug" clean_author = html.escape(raw_author)[:30] clean_text = html.escape(raw_text)[:1000] bugs = load_bugs() new_id = max([b.get("id", 0) for b in bugs] + [0]) + 1 new_bug = { "id": new_id, "author": clean_author, "category": category, "text": clean_text, "status": "новый", "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M"), "adminReply": None } bugs.append(new_bug) save_bugs(bugs) self._send_json({"success": True, "report": new_bug, "total": len(bugs)}, 201) return self._send_json({"error": "Not Found", "path": path}, 404) def log_message(self, format, *args): sys.stderr.write(f"[{datetime.now().strftime('%H:%M:%S')}] {self.address_string()} - {format%args}\n") if __name__ == "__main__": load_bugs() server = http.server.HTTPServer(("0.0.0.0", PORT), FeedbackHandler) print(f"PvZ Backend Server running on port {PORT} with Admin Panel & SQLite database...") try: server.serve_forever() except KeyboardInterrupt: print("\nShutting down server.") server.server_close()