version 1.2.3: SSH-туннель в веб-панели, авто-ID бота, фиксы
- templates/index.html: вкладка SSL — замена самоподписного сертификата на SSH-туннель, авто-ID при установке бота - app.py: API /api/panel/access (get/set ip/ssh), /api/bot/detect-id, правка парсинга трафика - bot.py: позиционный парсинг mita get users (8 колонок) - install.sh, mita-ctl.sh: автоопределение Telegram ID через getUpdates API, исправлен unbound variable BOLD, SCRIPT_DIR - install-panel.sh: копирование bot.py и install-bot.sh в /opt/mita-panel/ - README: обновлена дорожная карта
This commit is contained in:
@@ -78,8 +78,9 @@ curl -Ls https://raw.githubusercontent.com/grendervilll/mitactl/main/bootstrap.s
|
|||||||
- [x] Генерация клиентских конфигов (mieru JSON и sing-box JSON)
|
- [x] Генерация клиентских конфигов (mieru JSON и sing-box JSON)
|
||||||
- [x] Скачивание конфигов и отображение QR-кодов
|
- [x] Скачивание конфигов и отображение QR-кодов
|
||||||
- [x] Управление WARP на пользователя (toggle + правила: домены, IP, geosite, geoip, URL источники)
|
- [x] Управление WARP на пользователя (toggle + правила: домены, IP, geosite, geoip, URL источники)
|
||||||
- [x] SSL-сертификаты: самоподписной (10 лет) создаётся автоматически при установке, Let's Encrypt через UI
|
- [x] SSL-сертификаты: самоподписной (10 лет) авто при установке, LE через UI, кнопка перезапуска
|
||||||
- [x] Кнопка перезапуска панели на вкладке SSL
|
- [x] Переключение режима доступа: IP / SSH-туннель через вкладку SSL в веб-панели
|
||||||
|
- [x] Автоопределение Telegram ID через API бота при установке (веб-панель, CLI, install.sh)
|
||||||
- [x] Интеграция fail2ban: rate-limiting панели + fail2ban jail (пресеты)
|
- [x] Интеграция fail2ban: rate-limiting панели + fail2ban jail (пресеты)
|
||||||
- [x] Управление файрволом (UFW/iptables) через CLI и панель
|
- [x] Управление файрволом (UFW/iptables) через CLI и панель
|
||||||
- [x] Режим SSH-туннеля: панель только на 127.0.0.1
|
- [x] Режим SSH-туннеля: панель только на 127.0.0.1
|
||||||
|
|||||||
@@ -677,6 +677,99 @@ def api_panel_restart():
|
|||||||
return jsonify({"ok": False, "error": r.stderr.strip()}), 500
|
return jsonify({"ok": False, "error": r.stderr.strip()}), 500
|
||||||
return jsonify({"ok": True})
|
return jsonify({"ok": True})
|
||||||
|
|
||||||
|
@app.route(f"{BASE}/api/panel/access")
|
||||||
|
@login_required
|
||||||
|
def api_panel_access_get():
|
||||||
|
pc = load_panel_config()
|
||||||
|
panel_port = os.environ.get("PANEL_PORT", "8080")
|
||||||
|
secret = os.environ.get("SECRET_PATH", "")
|
||||||
|
mode = pc.get("access_mode", "ip")
|
||||||
|
ssh_port = pc.get("ssh_port", 22)
|
||||||
|
return jsonify({
|
||||||
|
"mode": mode,
|
||||||
|
"panel_port": panel_port,
|
||||||
|
"secret": secret,
|
||||||
|
"ssh_port": ssh_port,
|
||||||
|
"server_ip": get_server_ip(),
|
||||||
|
})
|
||||||
|
|
||||||
|
@app.route(f"{BASE}/api/panel/access", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def api_panel_access_set():
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
mode = data.get("mode", "").strip()
|
||||||
|
|
||||||
|
if mode not in ("ip", "ssh"):
|
||||||
|
return jsonify({"ok": False, "error": "mode должен быть ip или ssh"}), 400
|
||||||
|
|
||||||
|
pc = load_panel_config()
|
||||||
|
panel_port = os.environ.get("PANEL_PORT", "8080")
|
||||||
|
secret = os.environ.get("SECRET_PATH", "")
|
||||||
|
|
||||||
|
if mode == "ssh":
|
||||||
|
bind = "127.0.0.1"
|
||||||
|
# Закрыть порт в firewall
|
||||||
|
_fw_close_port(panel_port)
|
||||||
|
else:
|
||||||
|
bind = "0.0.0.0"
|
||||||
|
_fw_open_port(panel_port)
|
||||||
|
|
||||||
|
# Обновить start.sh с новым bind
|
||||||
|
start_sh = "/opt/mita-panel/start.sh"
|
||||||
|
try:
|
||||||
|
Path(start_sh).write_text(f"""#!/bin/bash
|
||||||
|
set -a; source /etc/mita/panel.env; set +a
|
||||||
|
SSL_ARGS=""
|
||||||
|
if [[ -n "$SSL_CERT" && -n "$SSL_KEY" && -f "$SSL_CERT" && -f "$SSL_KEY" ]]; then
|
||||||
|
SSL_ARGS="--certfile=$SSL_CERT --keyfile=$SSL_KEY"
|
||||||
|
fi
|
||||||
|
exec /opt/mita-panel/venv/bin/gunicorn \\
|
||||||
|
--bind {bind}:$PANEL_PORT \\
|
||||||
|
--workers 2 --timeout 120 \\
|
||||||
|
--access-logfile /var/log/mita-panel-access.log \\
|
||||||
|
--error-logfile /var/log/mita-panel.log \\
|
||||||
|
$SSL_ARGS app:app
|
||||||
|
""")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Сохранить режим в panel.json
|
||||||
|
pc["access_mode"] = mode
|
||||||
|
Path(PANEL_CONFIG).write_text(json.dumps(pc, indent=2))
|
||||||
|
|
||||||
|
subprocess.run(["systemctl","restart","mita-panel"], capture_output=True, timeout=10)
|
||||||
|
|
||||||
|
ssh_port = pc.get("ssh_port", 22)
|
||||||
|
return jsonify({
|
||||||
|
"ok": True,
|
||||||
|
"mode": mode,
|
||||||
|
"panel_port": panel_port,
|
||||||
|
"secret": secret,
|
||||||
|
"server_ip": get_server_ip(),
|
||||||
|
"ssh_port": ssh_port,
|
||||||
|
})
|
||||||
|
|
||||||
|
def _fw_close_port(port):
|
||||||
|
try:
|
||||||
|
if subprocess.run(["which","ufw"], capture_output=True).returncode == 0:
|
||||||
|
subprocess.run(["ufw","delete","allow",f"{port}/tcp"], capture_output=True)
|
||||||
|
elif subprocess.run(["which","iptables"], capture_output=True).returncode == 0:
|
||||||
|
subprocess.run(["iptables","-D","INPUT","-p","tcp","--dport",str(port),"-j","ACCEPT"], capture_output=True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _fw_open_port(port):
|
||||||
|
try:
|
||||||
|
if subprocess.run(["which","ufw"], capture_output=True).returncode == 0 \
|
||||||
|
and subprocess.run(["ufw","status"], capture_output=True, text=True).stdout.find("active") != -1:
|
||||||
|
subprocess.run(["ufw","allow",f"{port}/tcp","comment","mita-panel"], capture_output=True)
|
||||||
|
elif subprocess.run(["which","iptables"], capture_output=True).returncode == 0:
|
||||||
|
r = subprocess.run(["iptables","-C","INPUT","-p","tcp","--dport",str(port),"-j","ACCEPT"], capture_output=True)
|
||||||
|
if r.returncode != 0:
|
||||||
|
subprocess.run(["iptables","-I","INPUT","1","-p","tcp","--dport",str(port),"-j","ACCEPT"], capture_output=True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def _update_env(key, value):
|
def _update_env(key, value):
|
||||||
env_file = "/etc/mita/panel.env"
|
env_file = "/etc/mita/panel.env"
|
||||||
lines = []
|
lines = []
|
||||||
@@ -1132,6 +1225,43 @@ def api_bot_config():
|
|||||||
"admin_ids": cfg.get("admin_ids", []),
|
"admin_ids": cfg.get("admin_ids", []),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@app.route(f"{BASE}/api/bot/detect-id", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def api_bot_detect_id():
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
token = data.get("token","").strip()
|
||||||
|
if not token:
|
||||||
|
return jsonify({"ok": False, "error": "Токен обязателен"}), 400
|
||||||
|
|
||||||
|
import urllib.request
|
||||||
|
url = f"https://api.telegram.org/bot{token}/getUpdates"
|
||||||
|
# Очищаем pending updates перед опросом
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(url + "?offset=-1", headers={"User-Agent": "mita-panel"})
|
||||||
|
urllib.request.urlopen(req, timeout=5).read()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
admin_id = ""
|
||||||
|
for _ in range(20):
|
||||||
|
time.sleep(3)
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(url, headers={"User-Agent": "mita-panel"})
|
||||||
|
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||||
|
data_raw = json.loads(resp.read())
|
||||||
|
ids = [r["message"]["from"]["id"]
|
||||||
|
for r in data_raw.get("result", [])
|
||||||
|
if "message" in r and "from" in r["message"]]
|
||||||
|
if ids:
|
||||||
|
admin_id = str(ids[-1])
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if admin_id:
|
||||||
|
return jsonify({"ok": True, "admin_id": admin_id})
|
||||||
|
return jsonify({"ok": False, "error": "Не получено ни одного сообщения. Отправьте любое сообщение боту и попробуйте снова."}), 404
|
||||||
|
|
||||||
# ── API: скачать конфиг как файл ─────────────────────────────────────────────
|
# ── API: скачать конфиг как файл ─────────────────────────────────────────────
|
||||||
@app.route(f"{BASE}/api/users/config/download")
|
@app.route(f"{BASE}/api/users/config/download")
|
||||||
@login_required
|
@login_required
|
||||||
|
|||||||
@@ -84,6 +84,9 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
cp "$SCRIPT_DIR/app.py" "$PANEL_DIR/"
|
cp "$SCRIPT_DIR/app.py" "$PANEL_DIR/"
|
||||||
cp -r "$SCRIPT_DIR/templates" "$PANEL_DIR/"
|
cp -r "$SCRIPT_DIR/templates" "$PANEL_DIR/"
|
||||||
[[ -d "$SCRIPT_DIR/static" ]] && cp -r "$SCRIPT_DIR/static" "$PANEL_DIR/"
|
[[ -d "$SCRIPT_DIR/static" ]] && cp -r "$SCRIPT_DIR/static" "$PANEL_DIR/"
|
||||||
|
# Копируем установщик бота рядом — чтобы работала установка через веб-панель и mita-ctl
|
||||||
|
[[ -f "$SCRIPT_DIR/install-bot.sh" ]] && cp "$SCRIPT_DIR/install-bot.sh" "$PANEL_DIR/"
|
||||||
|
[[ -f "$SCRIPT_DIR/bot.py" ]] && cp "$SCRIPT_DIR/bot.py" "$PANEL_DIR/"
|
||||||
ok "Файлы скопированы в $PANEL_DIR"
|
ok "Файлы скопированы в $PANEL_DIR"
|
||||||
|
|
||||||
# ── генерация секретов ─────────────────────────────────────────────
|
# ── генерация секретов ─────────────────────────────────────────────
|
||||||
|
|||||||
+36
-12
@@ -10,7 +10,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
|
|
||||||
# ========================= ЦВЕТА ДЛЯ ВЫВОДА =========================
|
# ========================= ЦВЕТА ДЛЯ ВЫВОДА =========================
|
||||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
|
||||||
BLUE='\033[0;34m'; CYAN='\033[0;36m'; NC='\033[0m'
|
BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
|
||||||
|
|
||||||
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
|
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
|
||||||
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
|
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
|
||||||
@@ -782,10 +782,10 @@ else
|
|||||||
echo -e " В ответ вы получите ${BOLD}токен${NC} — длинную строку вида:"
|
echo -e " В ответ вы получите ${BOLD}токен${NC} — длинную строку вида:"
|
||||||
echo -e " ${YELLOW}1234567890:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw${NC}"
|
echo -e " ${YELLOW}1234567890:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${BOLD}2.${NC} Найдите ${GREEN}@userinfobot${NC} и нажмите /start"
|
echo -e " ${BOLD}2.${NC} Откройте своего бота в Telegram и отправьте любое сообщение"
|
||||||
echo -e " Он покажет ваш ${BOLD}Telegram ID${NC} — число (например 123456789)"
|
echo -e " (например «привет»). ID определится автоматически."
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${BOLD}3.${NC} Когда токен и ID готовы — нажмите Enter для продолжения"
|
echo -e " ${BOLD}3.${NC} Когда токен готов — нажмите Enter для продолжения"
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
echo ""
|
echo ""
|
||||||
@@ -797,16 +797,40 @@ else
|
|||||||
break
|
break
|
||||||
done
|
done
|
||||||
|
|
||||||
while true; do
|
echo ""
|
||||||
echo ""
|
info "Определение вашего Telegram ID..."
|
||||||
read -r -p " Впишите ваш Telegram ID (число): " BOT_ADMIN
|
echo -e " Отправьте ${YELLOW}любое сообщение${NC} своему боту в Telegram прямо сейчас."
|
||||||
if [[ -z "$BOT_ADMIN" || ! "$BOT_ADMIN" =~ ^[0-9]+$ ]]; then
|
echo -e " Ожидание сообщения (макс. 60 сек)..."
|
||||||
warn "Telegram ID должен быть числом (узнайте у @userinfobot)"
|
|
||||||
continue
|
BOT_ADMIN=""
|
||||||
fi
|
for i in $(seq 1 30); do
|
||||||
break
|
BOT_ADMIN=$(curl -s --max-time 5 "https://api.telegram.org/bot${BOT_TOKEN}/getUpdates" 2>/dev/null | \
|
||||||
|
python3 -c "
|
||||||
|
import sys,json
|
||||||
|
try:
|
||||||
|
d=json.load(sys.stdin)
|
||||||
|
ids=[r['message']['from']['id'] for r in d.get('result',[]) if 'message' in r and 'from' in r['message']]
|
||||||
|
print(ids[-1] if ids else '')
|
||||||
|
except: pass
|
||||||
|
" 2>/dev/null)
|
||||||
|
[[ -n "$BOT_ADMIN" ]] && break
|
||||||
|
sleep 2
|
||||||
done
|
done
|
||||||
|
|
||||||
|
if [[ -z "$BOT_ADMIN" || ! "$BOT_ADMIN" =~ ^[0-9]+$ ]]; then
|
||||||
|
warn "Не удалось определить ID автоматически."
|
||||||
|
while true; do
|
||||||
|
echo ""
|
||||||
|
read -r -p " Впишите ваш Telegram ID вручную (число): " BOT_ADMIN
|
||||||
|
if [[ -n "$BOT_ADMIN" && "$BOT_ADMIN" =~ ^[0-9]+$ ]]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
warn "Telegram ID должен быть числом"
|
||||||
|
done
|
||||||
|
else
|
||||||
|
ok "Telegram ID определён: ${GREEN}${BOT_ADMIN}${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
info "Запуск установки Telegram-бота..."
|
info "Запуск установки Telegram-бота..."
|
||||||
BOT_TOKEN_NONINTERACTIVE="$BOT_TOKEN" BOT_ADMIN_NONINTERACTIVE="$BOT_ADMIN" bash "$BOT_INSTALLER"
|
BOT_TOKEN_NONINTERACTIVE="$BOT_TOKEN" BOT_ADMIN_NONINTERACTIVE="$BOT_ADMIN" bash "$BOT_INSTALLER"
|
||||||
|
|||||||
+27
-2
@@ -1151,7 +1151,7 @@ _bot_install() {
|
|||||||
section "Установка Telegram-бота"
|
section "Установка Telegram-бота"
|
||||||
|
|
||||||
local installer=""
|
local installer=""
|
||||||
for candidate in /opt/mita-panel/install-bot.sh "$SCRIPT_DIR/install-bot.sh"; do
|
for candidate in /opt/mita-panel/install-bot.sh; do
|
||||||
[[ -f "$candidate" ]] && { installer="$candidate"; break; }
|
[[ -f "$candidate" ]] && { installer="$candidate"; break; }
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -1164,7 +1164,32 @@ _bot_install() {
|
|||||||
read -r -p "Токен бота (от @BotFather): " token
|
read -r -p "Токен бота (от @BotFather): " token
|
||||||
[[ -z "$token" ]] && { error "Токен обязателен"; return; }
|
[[ -z "$token" ]] && { error "Токен обязателен"; return; }
|
||||||
|
|
||||||
read -r -p "Ваш Telegram ID (число, @userinfobot): " admin_id
|
echo ""
|
||||||
|
info "Определение вашего Telegram ID..."
|
||||||
|
echo -e " Отправьте ${YELLOW}любое сообщение${NC} своему боту в Telegram прямо сейчас."
|
||||||
|
echo -e " Ожидание сообщения (макс. 60 сек)..."
|
||||||
|
|
||||||
|
admin_id=""
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
admin_id=$(curl -s --max-time 5 "https://api.telegram.org/bot${token}/getUpdates" 2>/dev/null | \
|
||||||
|
python3 -c "
|
||||||
|
import sys,json
|
||||||
|
try:
|
||||||
|
d=json.load(sys.stdin)
|
||||||
|
ids=[r['message']['from']['id'] for r in d.get('result',[]) if 'message' in r and 'from' in r['message']]
|
||||||
|
print(ids[-1] if ids else '')
|
||||||
|
except: pass
|
||||||
|
" 2>/dev/null)
|
||||||
|
[[ -n "$admin_id" ]] && break
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$admin_id" || ! "$admin_id" =~ ^[0-9]+$ ]]; then
|
||||||
|
warn "Не удалось определить ID автоматически."
|
||||||
|
read -r -p "Впишите ваш Telegram ID вручную (число): " admin_id
|
||||||
|
else
|
||||||
|
ok "Telegram ID определён: ${GREEN}${admin_id}${NC}"
|
||||||
|
fi
|
||||||
[[ -z "$admin_id" || ! "$admin_id" =~ ^[0-9]+$ ]] && { error "ID должен быть числом"; return; }
|
[[ -z "$admin_id" || ! "$admin_id" =~ ^[0-9]+$ ]] && { error "ID должен быть числом"; return; }
|
||||||
|
|
||||||
BOT_TOKEN_NONINTERACTIVE="$token" BOT_ADMIN_NONINTERACTIVE="$admin_id" bash "$installer"
|
BOT_TOKEN_NONINTERACTIVE="$token" BOT_ADMIN_NONINTERACTIVE="$admin_id" bash "$installer"
|
||||||
|
|||||||
+83
-26
@@ -52,9 +52,8 @@
|
|||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-label">Аптайм</div>
|
<div class="stat-label">Аптайм</div>
|
||||||
<div class="stat-value" style="font-size:1.1rem" id="s-uptime">—</div>
|
<div class="stat-value" style="font-size:1.1rem" id="s-uptime">—</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-title">Быстрые действия</div>
|
<div class="card-title">Быстрые действия</div>
|
||||||
@@ -144,17 +143,19 @@
|
|||||||
|
|
||||||
<div class="grid-2">
|
<div class="grid-2">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-title">Самоподписной сертификат</div>
|
<div class="card-title">SSH-туннель</div>
|
||||||
<p style="color:var(--muted);font-size:.875rem;margin-bottom:16px;line-height:1.6">
|
<p style="color:var(--muted);font-size:.875rem;margin-bottom:16px;line-height:1.6">
|
||||||
Генерируется мгновенно, действует <strong style="color:var(--text)">10 лет</strong>.
|
Панель будет слушать только <strong style="color:var(--text)">127.0.0.1</strong> — недоступна снаружи.
|
||||||
Браузер покажет предупреждение — его можно принять.
|
Подключение через зашифрованный SSH-туннель с вашего компьютера.
|
||||||
Подходит для личного использования.
|
|
||||||
</p>
|
</p>
|
||||||
<button class="btn btn-secondary" onclick="genSelfSigned()">
|
<div id="ssh-tunnel-info" style="font-size:.82rem;color:var(--muted);margin-bottom:14px;line-height:1.7">
|
||||||
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/></svg>
|
Загрузка...
|
||||||
Создать самоподписной
|
</div>
|
||||||
|
<button class="btn btn-warn" onclick="switchToSSHTunnel()">
|
||||||
|
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 11V7a4 4 0 118 0m-4 8v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2z"/></svg>
|
||||||
|
Переключить на SSH-туннель
|
||||||
</button>
|
</button>
|
||||||
<div id="selfsigned-result" style="margin-top:12px"></div>
|
<div id="ssh-result" style="margin-top:12px"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-title">Let's Encrypt (бесплатный)</div>
|
<div class="card-title">Let's Encrypt (бесплатный)</div>
|
||||||
@@ -264,17 +265,21 @@
|
|||||||
<div class="card" id="bot-install-card" style="display:none">
|
<div class="card" id="bot-install-card" style="display:none">
|
||||||
<div class="card-title">Установка бота</div>
|
<div class="card-title">Установка бота</div>
|
||||||
<p style="color:var(--muted);font-size:.875rem;margin-bottom:18px;line-height:1.6">
|
<p style="color:var(--muted);font-size:.875rem;margin-bottom:18px;line-height:1.6">
|
||||||
Создайте бота через <strong style="color:var(--accent)">@BotFather</strong> в Telegram,
|
Создайте бота через <strong style="color:var(--accent)">@BotFather</strong> в Telegram и скопируйте токен.
|
||||||
скопируйте токен и узнайте ваш Telegram ID через <strong style="color:var(--accent)">@userinfobot</strong>.
|
Затем нажмите «Определить ID» и отправьте любое сообщение своему боту.
|
||||||
</p>
|
</p>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Токен бота (от @BotFather)</label>
|
<label>Токен бота (от @BotFather)</label>
|
||||||
<input type="text" id="bot-token" placeholder="123456:ABC-DEF1234gh...">
|
<input type="text" id="bot-token" placeholder="123456:ABC-DEF1234gh...">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="max-width:200px">
|
<div class="form-group" style="max-width:240px">
|
||||||
<label>Ваш Telegram ID</label>
|
<label>Ваш Telegram ID</label>
|
||||||
<input type="text" id="bot-admin-id" placeholder="123456789">
|
<div style="display:flex;gap:8px">
|
||||||
|
<input type="text" id="bot-admin-id" placeholder="123456789" style="flex:1">
|
||||||
|
<button class="btn btn-ok btn-sm" onclick="detectBotAdminId()" style="white-space:nowrap">🔍 Определить ID</button>
|
||||||
|
</div>
|
||||||
|
<div id="bot-detect-status" style="font-size:.75rem;color:var(--muted);margin-top:4px"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" onclick="installBot()">
|
<button class="btn btn-primary" onclick="installBot()">
|
||||||
@@ -856,20 +861,43 @@ async function loadSSL(){
|
|||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
loadPanelAccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function genSelfSigned(){
|
async function loadPanelAccess() {
|
||||||
const btn=event.target;
|
const d = await api('/api/panel/access');
|
||||||
btn.disabled=true;btn.innerHTML='<span class="spinner"></span> Генерация...';
|
if (!d) return;
|
||||||
const el=document.getElementById('selfsigned-result');
|
const el = document.getElementById('ssh-tunnel-info');
|
||||||
const d=await api('/api/ssl/selfsigned','POST');
|
if (!el) return;
|
||||||
btn.disabled=false;btn.textContent='Создать самоподписной';
|
const icon = d.mode === 'ssh' ? '🔒' : '🌐';
|
||||||
if(!d)return;
|
const label = d.mode === 'ssh' ? 'SSH-туннель (только 127.0.0.1)' : 'Доступ по IP (0.0.0.0)';
|
||||||
if(d.ok){
|
const sshCmd = `ssh -L ${d.panel_port}:127.0.0.1:${d.panel_port} -p ${d.ssh_port} root@${d.server_ip} -N`;
|
||||||
el.innerHTML=`<div class="alert alert-ok">✓ Сертификат создан (10 лет)<br><small>${escH(d.expires||'')}</small><br><small style="opacity:.7">${escH(d.note||'')}</small></div>`;
|
el.innerHTML = `
|
||||||
loadSSL();
|
<div style="margin-bottom:8px">${icon} <strong>${label}</strong></div>
|
||||||
}else{
|
<div>Сервер: <code style="color:var(--accent)">${escH(d.server_ip)}</code></div>
|
||||||
el.innerHTML=`<div class="alert alert-err">Ошибка: ${escH(d.error||'')}</div>`;
|
<div>Порт: <code style="color:var(--accent)">${d.panel_port}</code></div>
|
||||||
|
<div style="margin-top:8px;font-size:.8rem;color:var(--ok)">Команда для туннеля:</div>
|
||||||
|
<div class="code-block" id="ssh-cmd-block">${escH(sshCmd)}</div>
|
||||||
|
<button class="btn btn-secondary btn-sm" style="margin-top:4px" onclick="copyText(document.getElementById('ssh-cmd-block').textContent)">Копировать</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function switchToSSHTunnel() {
|
||||||
|
const btn = document.querySelector('#page-ssl .btn-warn');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<span class="spinner"></span> Переключение...';
|
||||||
|
|
||||||
|
const el = document.getElementById('ssh-result');
|
||||||
|
const d = await api('/api/panel/access', 'POST', {mode: 'ssh'});
|
||||||
|
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = '<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 11V7a4 4 0 118 0m-4 8v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2z"/></svg> Переключить на SSH-туннель';
|
||||||
|
|
||||||
|
if (d?.ok) {
|
||||||
|
el.innerHTML = '<div class="alert alert-ok">✓ Панель переключена на SSH-туннель. Используйте команду ниже для подключения.</div>';
|
||||||
|
showToast('SSH-туннель активирован');
|
||||||
|
loadPanelAccess();
|
||||||
|
} else {
|
||||||
|
el.innerHTML = `<div class="alert alert-err">Ошибка: ${escH(d?.error||'')}</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -942,6 +970,35 @@ async function loadBotPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function detectBotAdminId() {
|
||||||
|
const token = document.getElementById('bot-token').value.trim();
|
||||||
|
if (!token) { showToast('Введите токен бота', true); return; }
|
||||||
|
|
||||||
|
const el = document.getElementById('bot-detect-status');
|
||||||
|
const btn = document.querySelector('#bot-install-card .btn-ok');
|
||||||
|
|
||||||
|
el.textContent = 'Отправьте любое сообщение боту в Telegram. Ожидание (60 сек)...';
|
||||||
|
el.style.color = 'var(--warn)';
|
||||||
|
if (btn) { btn.disabled = true; btn.textContent = '⏳ Ожидание...'; }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const d = await api('/api/bot/detect-id', 'POST', {token});
|
||||||
|
if (d?.ok) {
|
||||||
|
document.getElementById('bot-admin-id').value = d.admin_id;
|
||||||
|
el.textContent = '✓ ID определён: ' + d.admin_id;
|
||||||
|
el.style.color = 'var(--ok)';
|
||||||
|
showToast('Telegram ID определён');
|
||||||
|
} else {
|
||||||
|
el.textContent = '⚠ ' + (d?.error || 'Не удалось');
|
||||||
|
el.style.color = 'var(--danger)';
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
el.textContent = '⚠ Ошибка: ' + e.message;
|
||||||
|
el.style.color = 'var(--danger)';
|
||||||
|
}
|
||||||
|
if (btn) { btn.disabled = false; btn.textContent = '🔍 Определить ID'; }
|
||||||
|
}
|
||||||
|
|
||||||
async function installBot() {
|
async function installBot() {
|
||||||
const token = document.getElementById('bot-token').value.trim();
|
const token = document.getElementById('bot-token').value.trim();
|
||||||
const adminId = document.getElementById('bot-admin-id').value.trim();
|
const adminId = document.getElementById('bot-admin-id').value.trim();
|
||||||
|
|||||||
Reference in New Issue
Block a user