aa
This commit is contained in:
185
app_web.py
185
app_web.py
@@ -7,6 +7,7 @@ import io
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import secrets
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
@@ -16,8 +17,9 @@ from pathlib import Path
|
|||||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||||
|
|
||||||
from fastapi import FastAPI, Form, Query, Request
|
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 fastapi.templating import Jinja2Templates
|
||||||
|
from starlette.middleware.sessions import SessionMiddleware
|
||||||
|
|
||||||
BASE_DIR = Path(__file__).resolve().parent
|
BASE_DIR = Path(__file__).resolve().parent
|
||||||
ENV_FILE = BASE_DIR / ".env"
|
ENV_FILE = BASE_DIR / ".env"
|
||||||
@@ -30,7 +32,43 @@ os.environ.setdefault("TELEGRAM_WEB_UI", "1")
|
|||||||
|
|
||||||
logger = logging.getLogger("uvicorn.error")
|
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 = 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))
|
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
|
||||||
|
|
||||||
# Starlette 较新版本:TemplateResponse(request, name, context);旧版:(name, context)
|
# 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")
|
@app.get("/api/stats/overview")
|
||||||
async def api_stats_overview(
|
async def api_stats_overview(
|
||||||
days: int = Query(30, ge=1, le=366),
|
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}
|
monitored_ids = {str(row["channel_id"]) for row in monitored}
|
||||||
account_channels: List[Dict] = []
|
account_channels: List[Dict] = []
|
||||||
err = ""
|
err = ""
|
||||||
|
authed = is_console_authed(request)
|
||||||
|
if authed:
|
||||||
try:
|
try:
|
||||||
account_channels = await service.list_account_channels()
|
account_channels = await service.list_account_channels()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
err = str(e)
|
err = str(e)
|
||||||
|
logs_text = "\n".join(service.logs) if authed else ""
|
||||||
return template_response(
|
return template_response(
|
||||||
request,
|
request,
|
||||||
"index.html",
|
"index.html",
|
||||||
@@ -649,13 +725,15 @@ async def index(request: Request):
|
|||||||
"monitored": monitored,
|
"monitored": monitored,
|
||||||
"monitored_ids": monitored_ids,
|
"monitored_ids": monitored_ids,
|
||||||
"account_channels": account_channels,
|
"account_channels": account_channels,
|
||||||
|
"console_authed": authed,
|
||||||
|
"need_auth_banner": request.query_params.get("needauth") == "1",
|
||||||
"connected": service.is_connected(),
|
"connected": service.is_connected(),
|
||||||
"job_running": service.is_job_running(),
|
"job_running": service.is_job_running(),
|
||||||
"job_name": service.job_name,
|
"job_name": service.job_name,
|
||||||
"job_uptime": service.current_job_uptime(),
|
"job_uptime": service.current_job_uptime(),
|
||||||
"continuous_running": service.is_continuous_running(),
|
"continuous_running": service.is_continuous_running(),
|
||||||
"continuous_uptime": service.continuous_uptime(),
|
"continuous_uptime": service.continuous_uptime(),
|
||||||
"logs": "\n".join(service.logs),
|
"logs": logs_text,
|
||||||
"error": err,
|
"error": err,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -684,6 +762,9 @@ async def index(request: Request):
|
|||||||
|
|
||||||
@app.post("/config")
|
@app.post("/config")
|
||||||
async def save_config(request: Request):
|
async def save_config(request: Request):
|
||||||
|
redir = redirect_if_console_unauthed(request)
|
||||||
|
if redir:
|
||||||
|
return redir
|
||||||
form = await request.form()
|
form = await request.form()
|
||||||
updates: Dict[str, str] = {}
|
updates: Dict[str, str] = {}
|
||||||
for k, _label, _field_type in CONFIG_KEYS:
|
for k, _label, _field_type in CONFIG_KEYS:
|
||||||
@@ -696,7 +777,10 @@ async def save_config(request: Request):
|
|||||||
|
|
||||||
|
|
||||||
@app.post("/start")
|
@app.post("/start")
|
||||||
async def start_scraper():
|
async def start_scraper(request: Request):
|
||||||
|
redir = redirect_if_console_unauthed(request)
|
||||||
|
if redir:
|
||||||
|
return redir
|
||||||
try:
|
try:
|
||||||
await service.ensure_ready()
|
await service.ensure_ready()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -705,13 +789,19 @@ async def start_scraper():
|
|||||||
|
|
||||||
|
|
||||||
@app.post("/stop")
|
@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()
|
await service.disconnect()
|
||||||
return RedirectResponse(url="/", status_code=303)
|
return RedirectResponse(url="/", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/channels/add")
|
@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:
|
try:
|
||||||
msg = await service.add_channel(channel_spec)
|
msg = await service.add_channel(channel_spec)
|
||||||
service._append(msg)
|
service._append(msg)
|
||||||
@@ -722,6 +812,9 @@ async def add_channel(channel_spec: str = Form(...)):
|
|||||||
|
|
||||||
@app.post("/channels/add-selected")
|
@app.post("/channels/add-selected")
|
||||||
async def add_channels_selected(request: Request):
|
async def add_channels_selected(request: Request):
|
||||||
|
redir = redirect_if_console_unauthed(request)
|
||||||
|
if redir:
|
||||||
|
return redir
|
||||||
form = await request.form()
|
form = await request.form()
|
||||||
raw_ids = form.getlist("channel_id")
|
raw_ids = form.getlist("channel_id")
|
||||||
ids = [str(x).strip() for x in raw_ids if str(x).strip()]
|
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")
|
@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:
|
try:
|
||||||
msg = await service.remove_channel(channel_spec)
|
msg = await service.remove_channel(channel_spec)
|
||||||
service._append(msg)
|
service._append(msg)
|
||||||
@@ -748,7 +844,10 @@ async def remove_channel(channel_spec: str = Form(...)):
|
|||||||
|
|
||||||
|
|
||||||
@app.post("/jobs/scrape")
|
@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:
|
try:
|
||||||
msg = await service.start_scrape_job(selection)
|
msg = await service.start_scrape_job(selection)
|
||||||
service._append(msg)
|
service._append(msg)
|
||||||
@@ -758,7 +857,10 @@ async def start_scrape(selection: str = Form("all")):
|
|||||||
|
|
||||||
|
|
||||||
@app.post("/jobs/export")
|
@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:
|
try:
|
||||||
msg = await service.start_export_job(selection)
|
msg = await service.start_export_job(selection)
|
||||||
service._append(msg)
|
service._append(msg)
|
||||||
@@ -768,7 +870,10 @@ async def start_export(selection: str = Form("all")):
|
|||||||
|
|
||||||
|
|
||||||
@app.post("/jobs/rescrape")
|
@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:
|
try:
|
||||||
msg = await service.start_rescrape_job(selection)
|
msg = await service.start_rescrape_job(selection)
|
||||||
service._append(msg)
|
service._append(msg)
|
||||||
@@ -778,7 +883,10 @@ async def start_rescrape(selection: str = Form("all")):
|
|||||||
|
|
||||||
|
|
||||||
@app.post("/jobs/continuous/start")
|
@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:
|
try:
|
||||||
msg = await service.start_continuous()
|
msg = await service.start_continuous()
|
||||||
service._append(msg)
|
service._append(msg)
|
||||||
@@ -788,7 +896,10 @@ async def start_continuous():
|
|||||||
|
|
||||||
|
|
||||||
@app.post("/jobs/continuous/stop")
|
@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:
|
try:
|
||||||
msg = await service.stop_continuous()
|
msg = await service.stop_continuous()
|
||||||
service._append(msg)
|
service._append(msg)
|
||||||
@@ -798,7 +909,10 @@ async def stop_continuous():
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/status")
|
@app.get("/status")
|
||||||
async def status():
|
async def status(request: Request):
|
||||||
|
denied = json_if_console_unauthed(request)
|
||||||
|
if denied:
|
||||||
|
return denied
|
||||||
return {
|
return {
|
||||||
"ready": service.scraper is not None,
|
"ready": service.scraper is not None,
|
||||||
"connected": service.is_connected(),
|
"connected": service.is_connected(),
|
||||||
@@ -818,7 +932,10 @@ async def api_monitored_channels():
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/api/channels/account")
|
@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:
|
try:
|
||||||
items = await service.list_account_channels()
|
items = await service.list_account_channels()
|
||||||
return {"items": items}
|
return {"items": items}
|
||||||
@@ -827,62 +944,84 @@ async def api_account_channels():
|
|||||||
|
|
||||||
|
|
||||||
@app.post("/api/channels/add")
|
@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)
|
msg = await service.add_channel(channel_spec)
|
||||||
service._append(msg)
|
service._append(msg)
|
||||||
return {"ok": True, "message": msg}
|
return {"ok": True, "message": msg}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/channels/remove")
|
@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)
|
msg = await service.remove_channel(channel_spec)
|
||||||
service._append(msg)
|
service._append(msg)
|
||||||
return {"ok": True, "message": msg}
|
return {"ok": True, "message": msg}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/jobs/scrape")
|
@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)
|
msg = await service.start_scrape_job(selection)
|
||||||
service._append(msg)
|
service._append(msg)
|
||||||
return {"ok": True, "message": msg}
|
return {"ok": True, "message": msg}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/jobs/export")
|
@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)
|
msg = await service.start_export_job(selection)
|
||||||
service._append(msg)
|
service._append(msg)
|
||||||
return {"ok": True, "message": msg}
|
return {"ok": True, "message": msg}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/jobs/rescrape")
|
@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)
|
msg = await service.start_rescrape_job(selection)
|
||||||
service._append(msg)
|
service._append(msg)
|
||||||
return {"ok": True, "message": msg}
|
return {"ok": True, "message": msg}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/jobs/status")
|
@app.get("/api/jobs/status")
|
||||||
async def api_job_status():
|
async def api_job_status(request: Request):
|
||||||
return {
|
payload = {
|
||||||
"connected": service.is_connected(),
|
"connected": service.is_connected(),
|
||||||
"job_running": service.is_job_running(),
|
"job_running": service.is_job_running(),
|
||||||
"job_name": service.job_name,
|
"job_name": service.job_name,
|
||||||
"job_uptime": service.current_job_uptime(),
|
"job_uptime": service.current_job_uptime(),
|
||||||
"continuous_running": service.is_continuous_running(),
|
"continuous_running": service.is_continuous_running(),
|
||||||
"continuous_uptime": service.continuous_uptime(),
|
"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")
|
@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()
|
msg = await service.start_continuous()
|
||||||
service._append(msg)
|
service._append(msg)
|
||||||
return {"ok": True, "message": msg}
|
return {"ok": True, "message": msg}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/jobs/continuous/stop")
|
@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()
|
msg = await service.stop_continuous()
|
||||||
service._append(msg)
|
service._append(msg)
|
||||||
return {"ok": True, "message": msg}
|
return {"ok": True, "message": msg}
|
||||||
|
|||||||
@@ -722,6 +722,9 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
{% if need_auth_banner %}
|
||||||
|
<div class="shell"><p class="banner-warn" role="alert">该操作需要控制台密码:请点击右上角任意按钮,在弹出框中输入密码后再试。</p></div>
|
||||||
|
{% endif %}
|
||||||
{% if error %}
|
{% if error %}
|
||||||
<div class="shell"><p class="banner-warn" role="alert">列表加载提示:{{ error }}</p></div>
|
<div class="shell"><p class="banner-warn" role="alert">列表加载提示:{{ error }}</p></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -760,6 +763,11 @@
|
|||||||
<button type="button" class="btn-ghost" data-open="channels">频道管理</button>
|
<button type="button" class="btn-ghost" data-open="channels">频道管理</button>
|
||||||
<button type="button" class="btn-ghost" data-open="config">环境配置</button>
|
<button type="button" class="btn-ghost" data-open="config">环境配置</button>
|
||||||
<button type="button" class="btn-ghost" data-open="logs">运行日志</button>
|
<button type="button" class="btn-ghost" data-open="logs">运行日志</button>
|
||||||
|
{% if console_authed %}
|
||||||
|
<form method="post" action="/auth/console/logout" style="margin:0;">
|
||||||
|
<button type="submit" class="btn-ghost">退出验证</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -821,6 +829,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<dialog class="modal" id="dlg-console-auth" aria-labelledby="dlg-console-auth-title">
|
||||||
|
<div class="modal-head">
|
||||||
|
<h2 id="dlg-console-auth-title">控制台密码验证</h2>
|
||||||
|
<button type="button" class="modal-close" data-close>关闭</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p class="hint">须验证通过后才能打开「连接 / 常规操作 / 频道管理 / 环境配置 / 运行日志」等面板。请在服务器环境变量中设置 <code>WEB_CONSOLE_PASSWORD</code>(未设置时使用程序内置默认值)。务必设置随机长字符串 <code>WEB_CONSOLE_SESSION_SECRET</code> 用于会话签名,勿泄露。</p>
|
||||||
|
<form id="form-console-auth" class="config-form">
|
||||||
|
<div class="field-row">
|
||||||
|
<label for="console-auth-password">密码</label>
|
||||||
|
<input type="password" id="console-auth-password" name="password" autocomplete="current-password" />
|
||||||
|
</div>
|
||||||
|
<div class="config-actions">
|
||||||
|
<button type="submit" class="btn-primary">验证并继续</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<p id="console-auth-err" class="banner-warn" style="display:none;margin-top:12px;" role="alert"></p>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
<dialog class="modal" id="dlg-conn" aria-labelledby="dlg-conn-title">
|
<dialog class="modal" id="dlg-conn" aria-labelledby="dlg-conn-title">
|
||||||
<div class="modal-head">
|
<div class="modal-head">
|
||||||
<h2 id="dlg-conn-title">连接与持续抓取</h2>
|
<h2 id="dlg-conn-title">连接与持续抓取</h2>
|
||||||
@@ -945,7 +973,7 @@
|
|||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="list" role="list">
|
<div class="list" role="list">
|
||||||
<div class="line" role="listitem">请先连接 Telegram 后刷新本页加载列表</div>
|
<div class="line" role="listitem">{% if not console_authed %}请先通过右上角验证(输入密码)后刷新本页以加载账号频道列表。{% else %}请先连接 Telegram 后刷新本页加载列表{% endif %}</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -1003,7 +1031,7 @@
|
|||||||
<button type="button" class="modal-close" data-close>关闭</button>
|
<button type="button" class="modal-close" data-close>关闭</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<pre id="logs-box" tabindex="0" aria-label="运行日志">{{ logs }}</pre>
|
<pre id="logs-box" tabindex="0" aria-label="运行日志">{% if logs %}{{ logs }}{% else %}(验证通过前不展示日志内容;验证后刷新或等待自动刷新。){% endif %}</pre>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
@@ -1014,14 +1042,93 @@
|
|||||||
return Number(n).toLocaleString("zh-CN");
|
return Number(n).toLocaleString("zh-CN");
|
||||||
}
|
}
|
||||||
|
|
||||||
document.querySelectorAll("[data-open]").forEach(function (btn) {
|
function openDialogById(id) {
|
||||||
btn.addEventListener("click", function () {
|
|
||||||
var id = "dlg-" + btn.getAttribute("data-open");
|
|
||||||
var el = document.getElementById(id);
|
var el = document.getElementById(id);
|
||||||
if (el && typeof el.showModal === "function") el.showModal();
|
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", 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) {
|
document.querySelectorAll("[data-close]").forEach(function (btn) {
|
||||||
btn.addEventListener("click", function () {
|
btn.addEventListener("click", function () {
|
||||||
var d = btn.closest("dialog");
|
var d = btn.closest("dialog");
|
||||||
@@ -1200,7 +1307,7 @@
|
|||||||
|
|
||||||
async function refreshStatus() {
|
async function refreshStatus() {
|
||||||
try {
|
try {
|
||||||
var statusResp = await fetch("/api/jobs/status");
|
var statusResp = await fetch("/api/jobs/status", { credentials: "same-origin" });
|
||||||
if (statusResp.ok) {
|
if (statusResp.ok) {
|
||||||
var status = await statusResp.json();
|
var status = await statusResp.json();
|
||||||
var runningEl = document.getElementById("job-running");
|
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) {
|
if (channelsResp.ok) {
|
||||||
var payload = await channelsResp.json();
|
var payload = await channelsResp.json();
|
||||||
var wrap = document.getElementById("monitored-list");
|
var wrap = document.getElementById("monitored-list");
|
||||||
|
|||||||
Reference in New Issue
Block a user