From 384d7e48389de2d70d69d9b30012a07445af7570 Mon Sep 17 00:00:00 2001 From: RISE Date: Mon, 27 Apr 2026 01:42:47 +0800 Subject: [PATCH] aa --- app_web.py | 193 +++++++++++++++++++++++++++++++++++++------ templates/index.html | 123 +++++++++++++++++++++++++-- 2 files changed, 281 insertions(+), 35 deletions(-) diff --git a/app_web.py b/app_web.py index 547f416..04a4ad0 100644 --- a/app_web.py +++ b/app_web.py @@ -7,6 +7,7 @@ import io import json import logging import os +import secrets import sqlite3 import time import traceback @@ -16,8 +17,9 @@ from pathlib import Path from typing import Any, Dict, List, Optional, Tuple, Union from fastapi import FastAPI, Form, Query, Request -from fastapi.responses import HTMLResponse, RedirectResponse +from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse from fastapi.templating import Jinja2Templates +from starlette.middleware.sessions import SessionMiddleware BASE_DIR = Path(__file__).resolve().parent ENV_FILE = BASE_DIR / ".env" @@ -30,7 +32,43 @@ os.environ.setdefault("TELEGRAM_WEB_UI", "1") logger = logging.getLogger("uvicorn.error") +WEB_CONSOLE_AUTH_KEY = "web_console_authenticated" + + +def _web_console_password() -> str: + return os.getenv("WEB_CONSOLE_PASSWORD", "Aa123456") + + +def _web_session_secret() -> str: + s = (os.getenv("WEB_CONSOLE_SESSION_SECRET") or "").strip() + if s: + return s + return "telegram-scraper-web-console-dev-secret-change-me" + + +def is_console_authed(request: Request) -> bool: + return bool(request.session.get(WEB_CONSOLE_AUTH_KEY)) + + +def redirect_if_console_unauthed(request: Request) -> Optional[RedirectResponse]: + if is_console_authed(request): + return None + return RedirectResponse(url="/?needauth=1", status_code=303) + + +def json_if_console_unauthed(request: Request) -> Optional[JSONResponse]: + if is_console_authed(request): + return None + return JSONResponse({"ok": False, "error": "需要控制台密码验证"}, status_code=401) + + app = FastAPI(title="Telegram Scraper Web Console") +app.add_middleware( + SessionMiddleware, + secret_key=_web_session_secret(), + max_age=14 * 24 * 3600, + same_site="lax", +) templates = Jinja2Templates(directory=str(TEMPLATES_DIR)) # Starlette 较新版本:TemplateResponse(request, name, context);旧版:(name, context) @@ -608,6 +646,41 @@ def compute_storage_stats(base: Path, days: int, keyword_list: Tuple[str, ...]) } +@app.get("/auth/console/status") +async def auth_console_status(request: Request): + return {"ok": is_console_authed(request)} + + +@app.post("/auth/console/login") +async def auth_console_login(request: Request): + ct = (request.headers.get("content-type") or "").lower() + password = "" + if "application/json" in ct: + try: + body = await request.json() + password = str(body.get("password", "")) + except Exception: + password = "" + else: + form = await request.form() + password = str(form.get("password", "")) + expected = _web_console_password() + try: + ok = secrets.compare_digest(password.encode("utf-8"), expected.encode("utf-8")) + except Exception: + ok = False + if ok: + request.session[WEB_CONSOLE_AUTH_KEY] = True + return {"ok": True} + return JSONResponse({"ok": False, "error": "密码错误"}, status_code=401) + + +@app.post("/auth/console/logout") +async def auth_console_logout(request: Request): + request.session.pop(WEB_CONSOLE_AUTH_KEY, None) + return RedirectResponse(url="/", status_code=303) + + @app.get("/api/stats/overview") async def api_stats_overview( days: int = Query(30, ge=1, le=366), @@ -636,10 +709,13 @@ async def index(request: Request): monitored_ids = {str(row["channel_id"]) for row in monitored} account_channels: List[Dict] = [] err = "" - try: - account_channels = await service.list_account_channels() - except Exception as e: - err = str(e) + authed = is_console_authed(request) + if authed: + try: + account_channels = await service.list_account_channels() + except Exception as e: + err = str(e) + logs_text = "\n".join(service.logs) if authed else "" return template_response( request, "index.html", @@ -649,13 +725,15 @@ async def index(request: Request): "monitored": monitored, "monitored_ids": monitored_ids, "account_channels": account_channels, + "console_authed": authed, + "need_auth_banner": request.query_params.get("needauth") == "1", "connected": service.is_connected(), "job_running": service.is_job_running(), "job_name": service.job_name, "job_uptime": service.current_job_uptime(), "continuous_running": service.is_continuous_running(), "continuous_uptime": service.continuous_uptime(), - "logs": "\n".join(service.logs), + "logs": logs_text, "error": err, }, ) @@ -684,6 +762,9 @@ async def index(request: Request): @app.post("/config") async def save_config(request: Request): + redir = redirect_if_console_unauthed(request) + if redir: + return redir form = await request.form() updates: Dict[str, str] = {} for k, _label, _field_type in CONFIG_KEYS: @@ -696,7 +777,10 @@ async def save_config(request: Request): @app.post("/start") -async def start_scraper(): +async def start_scraper(request: Request): + redir = redirect_if_console_unauthed(request) + if redir: + return redir try: await service.ensure_ready() except Exception as e: @@ -705,13 +789,19 @@ async def start_scraper(): @app.post("/stop") -async def stop_scraper(): +async def stop_scraper(request: Request): + redir = redirect_if_console_unauthed(request) + if redir: + return redir await service.disconnect() return RedirectResponse(url="/", status_code=303) @app.post("/channels/add") -async def add_channel(channel_spec: str = Form(...)): +async def add_channel(request: Request, channel_spec: str = Form(...)): + redir = redirect_if_console_unauthed(request) + if redir: + return redir try: msg = await service.add_channel(channel_spec) service._append(msg) @@ -722,6 +812,9 @@ async def add_channel(channel_spec: str = Form(...)): @app.post("/channels/add-selected") async def add_channels_selected(request: Request): + redir = redirect_if_console_unauthed(request) + if redir: + return redir form = await request.form() raw_ids = form.getlist("channel_id") ids = [str(x).strip() for x in raw_ids if str(x).strip()] @@ -738,7 +831,10 @@ async def add_channels_selected(request: Request): @app.post("/channels/remove") -async def remove_channel(channel_spec: str = Form(...)): +async def remove_channel(request: Request, channel_spec: str = Form(...)): + redir = redirect_if_console_unauthed(request) + if redir: + return redir try: msg = await service.remove_channel(channel_spec) service._append(msg) @@ -748,7 +844,10 @@ async def remove_channel(channel_spec: str = Form(...)): @app.post("/jobs/scrape") -async def start_scrape(selection: str = Form("all")): +async def start_scrape(request: Request, selection: str = Form("all")): + redir = redirect_if_console_unauthed(request) + if redir: + return redir try: msg = await service.start_scrape_job(selection) service._append(msg) @@ -758,7 +857,10 @@ async def start_scrape(selection: str = Form("all")): @app.post("/jobs/export") -async def start_export(selection: str = Form("all")): +async def start_export(request: Request, selection: str = Form("all")): + redir = redirect_if_console_unauthed(request) + if redir: + return redir try: msg = await service.start_export_job(selection) service._append(msg) @@ -768,7 +870,10 @@ async def start_export(selection: str = Form("all")): @app.post("/jobs/rescrape") -async def start_rescrape(selection: str = Form("all")): +async def start_rescrape(request: Request, selection: str = Form("all")): + redir = redirect_if_console_unauthed(request) + if redir: + return redir try: msg = await service.start_rescrape_job(selection) service._append(msg) @@ -778,7 +883,10 @@ async def start_rescrape(selection: str = Form("all")): @app.post("/jobs/continuous/start") -async def start_continuous(): +async def start_continuous(request: Request): + redir = redirect_if_console_unauthed(request) + if redir: + return redir try: msg = await service.start_continuous() service._append(msg) @@ -788,7 +896,10 @@ async def start_continuous(): @app.post("/jobs/continuous/stop") -async def stop_continuous(): +async def stop_continuous(request: Request): + redir = redirect_if_console_unauthed(request) + if redir: + return redir try: msg = await service.stop_continuous() service._append(msg) @@ -798,7 +909,10 @@ async def stop_continuous(): @app.get("/status") -async def status(): +async def status(request: Request): + denied = json_if_console_unauthed(request) + if denied: + return denied return { "ready": service.scraper is not None, "connected": service.is_connected(), @@ -818,7 +932,10 @@ async def api_monitored_channels(): @app.get("/api/channels/account") -async def api_account_channels(): +async def api_account_channels(request: Request): + denied = json_if_console_unauthed(request) + if denied: + return denied try: items = await service.list_account_channels() return {"items": items} @@ -827,62 +944,84 @@ async def api_account_channels(): @app.post("/api/channels/add") -async def api_add_channel(channel_spec: str = Form(...)): +async def api_add_channel(request: Request, channel_spec: str = Form(...)): + denied = json_if_console_unauthed(request) + if denied: + return denied msg = await service.add_channel(channel_spec) service._append(msg) return {"ok": True, "message": msg} @app.post("/api/channels/remove") -async def api_remove_channel(channel_spec: str = Form(...)): +async def api_remove_channel(request: Request, channel_spec: str = Form(...)): + denied = json_if_console_unauthed(request) + if denied: + return denied msg = await service.remove_channel(channel_spec) service._append(msg) return {"ok": True, "message": msg} @app.post("/api/jobs/scrape") -async def api_job_scrape(selection: str = Form("all")): +async def api_job_scrape(request: Request, selection: str = Form("all")): + denied = json_if_console_unauthed(request) + if denied: + return denied msg = await service.start_scrape_job(selection) service._append(msg) return {"ok": True, "message": msg} @app.post("/api/jobs/export") -async def api_job_export(selection: str = Form("all")): +async def api_job_export(request: Request, selection: str = Form("all")): + denied = json_if_console_unauthed(request) + if denied: + return denied msg = await service.start_export_job(selection) service._append(msg) return {"ok": True, "message": msg} @app.post("/api/jobs/rescrape") -async def api_job_rescrape(selection: str = Form("all")): +async def api_job_rescrape(request: Request, selection: str = Form("all")): + denied = json_if_console_unauthed(request) + if denied: + return denied msg = await service.start_rescrape_job(selection) service._append(msg) return {"ok": True, "message": msg} @app.get("/api/jobs/status") -async def api_job_status(): - return { +async def api_job_status(request: Request): + payload = { "connected": service.is_connected(), "job_running": service.is_job_running(), "job_name": service.job_name, "job_uptime": service.current_job_uptime(), "continuous_running": service.is_continuous_running(), "continuous_uptime": service.continuous_uptime(), - "logs": list(service.logs), + "logs": list(service.logs) if is_console_authed(request) else [], } + return payload @app.post("/api/jobs/continuous/start") -async def api_start_continuous(): +async def api_start_continuous(request: Request): + denied = json_if_console_unauthed(request) + if denied: + return denied msg = await service.start_continuous() service._append(msg) return {"ok": True, "message": msg} @app.post("/api/jobs/continuous/stop") -async def api_stop_continuous(): +async def api_stop_continuous(request: Request): + denied = json_if_console_unauthed(request) + if denied: + return denied msg = await service.stop_continuous() service._append(msg) return {"ok": True, "message": msg} diff --git a/templates/index.html b/templates/index.html index 6a691f1..276f6aa 100644 --- a/templates/index.html +++ b/templates/index.html @@ -722,6 +722,9 @@ + {% if need_auth_banner %} +
+ {% endif %} {% if error %}
{% endif %} @@ -760,6 +763,11 @@ + {% if console_authed %} +
+ +
+ {% endif %} @@ -821,6 +829,26 @@ + + + + + @@ -1003,7 +1031,7 @@ @@ -1014,14 +1042,93 @@ return Number(n).toLocaleString("zh-CN"); } + function openDialogById(id) { + var el = document.getElementById(id); + if (el && typeof el.showModal === "function") el.showModal(); + } + + async function consoleAuthStatus() { + try { + var r = await fetch("/auth/console/status", { credentials: "same-origin" }); + if (!r.ok) return false; + var j = await r.json(); + return j.ok === true; + } catch (_e) { + return false; + } + } + + var pendingOpenDlgId = null; + document.querySelectorAll("[data-open]").forEach(function (btn) { - btn.addEventListener("click", function () { - var id = "dlg-" + btn.getAttribute("data-open"); - var el = document.getElementById(id); - if (el && typeof el.showModal === "function") el.showModal(); + btn.addEventListener("click", async function () { + var key = btn.getAttribute("data-open"); + var dlgId = "dlg-" + key; + if (!await consoleAuthStatus()) { + pendingOpenDlgId = dlgId; + openDialogById("dlg-console-auth"); + return; + } + openDialogById(dlgId); }); }); + (function () { + var authForm = document.getElementById("form-console-auth"); + var authErr = document.getElementById("console-auth-err"); + var authPwd = document.getElementById("console-auth-password"); + if (!authForm) return; + authForm.addEventListener("submit", async function (ev) { + ev.preventDefault(); + if (authErr) { + authErr.style.display = "none"; + authErr.textContent = ""; + } + var pw = authPwd ? authPwd.value : ""; + try { + var res = await fetch("/auth/console/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "same-origin", + body: JSON.stringify({ password: pw }), + }); + var data = await res.json().catch(function () { return ({}); }); + if (res.ok && data.ok) { + var authDlg = document.getElementById("dlg-console-auth"); + if (authDlg) authDlg.close(); + var tail = ""; + if (pendingOpenDlgId) { + tail = "?opendlg=" + encodeURIComponent(pendingOpenDlgId.replace(/^dlg-/, "")); + pendingOpenDlgId = null; + } + if (authPwd) authPwd.value = ""; + window.location.href = "/" + tail; + } else { + if (authErr) { + authErr.textContent = (data && data.error) ? data.error : "验证失败"; + authErr.style.display = "block"; + } + } + } catch (_e) { + if (authErr) { + authErr.textContent = "网络错误"; + authErr.style.display = "block"; + } + } + }); + })(); + + (function () { + var p = new URLSearchParams(window.location.search); + var o = p.get("opendlg"); + if (!o) return; + var el = document.getElementById("dlg-" + o); + if (el && typeof el.showModal === "function") el.showModal(); + p.delete("opendlg"); + var clean = window.location.pathname + (p.toString() ? "?" + p.toString() : ""); + history.replaceState(null, "", clean); + })(); + document.querySelectorAll("[data-close]").forEach(function (btn) { btn.addEventListener("click", function () { var d = btn.closest("dialog"); @@ -1200,7 +1307,7 @@ async function refreshStatus() { try { - var statusResp = await fetch("/api/jobs/status"); + var statusResp = await fetch("/api/jobs/status", { credentials: "same-origin" }); if (statusResp.ok) { var status = await statusResp.json(); var runningEl = document.getElementById("job-running"); @@ -1248,7 +1355,7 @@ } } - var channelsResp = await fetch("/api/channels/monitored"); + var channelsResp = await fetch("/api/channels/monitored", { credentials: "same-origin" }); if (channelsResp.ok) { var payload = await channelsResp.json(); var wrap = document.getElementById("monitored-list");