Initial commit: VPN Telegram bot (sanitized defaults)
This commit is contained in:
+574
@@ -0,0 +1,574 @@
|
||||
#!/usr/bin/env bash
|
||||
# VPN Service Bot — интерактивный установщик (Docker + Postgres + Caddy SSL)
|
||||
# Запуск на Ubuntu 22.04 / 24.04:
|
||||
# cd /path/to/project && sudo bash install.sh
|
||||
#
|
||||
# Если видишь ошибку вроде «set: pipefail» / «invalid option name» —
|
||||
# файл сохранён с Windows-переносами (CRLF). Исправь так:
|
||||
# sed -i 's/\r$//' install.sh && sudo bash install.sh
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() { echo -e "${GREEN}✓${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}!${NC} $*"; }
|
||||
err() { echo -e "${RED}✗${NC} $*" >&2; }
|
||||
info() { echo -e "${CYAN}→${NC} $*"; }
|
||||
title() {
|
||||
echo
|
||||
echo -e "${BOLD}════════════════════════════════════════${NC}"
|
||||
echo -e "${BOLD} $*${NC}"
|
||||
echo -e "${BOLD}════════════════════════════════════════${NC}"
|
||||
}
|
||||
|
||||
need_root() {
|
||||
if [[ "${EUID}" -ne 0 ]]; then
|
||||
err "Запусти от root: sudo bash install.sh"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
ask() {
|
||||
# ask "Prompt" "default" → sets REPLY
|
||||
local prompt="$1"
|
||||
local default="${2:-}"
|
||||
if [[ -n "$default" ]]; then
|
||||
read -r -p "$(echo -e "${CYAN}?${NC} ${prompt} [${default}]: ")" REPLY
|
||||
REPLY="${REPLY:-$default}"
|
||||
else
|
||||
read -r -p "$(echo -e "${CYAN}?${NC} ${prompt}: ")" REPLY
|
||||
fi
|
||||
}
|
||||
|
||||
ask_secret() {
|
||||
local prompt="$1"
|
||||
local default="${2:-}"
|
||||
if [[ -n "$default" ]]; then
|
||||
read -r -s -p "$(echo -e "${CYAN}?${NC} ${prompt} [скрыто / Enter = сгенерировать]: ")" REPLY
|
||||
echo
|
||||
REPLY="${REPLY:-$default}"
|
||||
else
|
||||
read -r -s -p "$(echo -e "${CYAN}?${NC} ${prompt}: ")" REPLY
|
||||
echo
|
||||
fi
|
||||
}
|
||||
|
||||
ask_yn() {
|
||||
local prompt="$1"
|
||||
local default="${2:-y}"
|
||||
local hint="y/n"
|
||||
[[ "$default" == "y" ]] && hint="Y/n"
|
||||
[[ "$default" == "n" ]] && hint="y/N"
|
||||
while true; do
|
||||
read -r -p "$(echo -e "${CYAN}?${NC} ${prompt} [${hint}]: ")" REPLY
|
||||
REPLY="${REPLY:-$default}"
|
||||
case "${REPLY,,}" in
|
||||
y|yes|д|да) REPLY=y; return 0 ;;
|
||||
n|no|н|нет) REPLY=n; return 0 ;;
|
||||
esac
|
||||
warn "Ответь y или n"
|
||||
done
|
||||
}
|
||||
|
||||
rand_pass() {
|
||||
if command -v openssl >/dev/null 2>&1; then
|
||||
openssl rand -base64 24 | tr -d '/+=' | head -c 24
|
||||
else
|
||||
head -c 48 /dev/urandom | base64 | tr -d '/+=' | head -c 24
|
||||
fi
|
||||
}
|
||||
|
||||
env_escape() {
|
||||
# Значение для .env: кавычки если есть спецсимволы
|
||||
local v="$1"
|
||||
if [[ -z "$v" ]]; then
|
||||
echo ""
|
||||
return
|
||||
fi
|
||||
if [[ "$v" =~ [[:space:]\#\"\'\\\$\`] ]]; then
|
||||
v="${v//\\/\\\\}"
|
||||
v="${v//\"/\\\"}"
|
||||
echo "\"$v\""
|
||||
else
|
||||
echo "$v"
|
||||
fi
|
||||
}
|
||||
|
||||
check_os() {
|
||||
if [[ ! -f /etc/os-release ]]; then
|
||||
warn "Не Linux? Скрипт рассчитан на Ubuntu 22.04/24.04"
|
||||
return
|
||||
fi
|
||||
# shellcheck source=/dev/null
|
||||
. /etc/os-release
|
||||
info "ОС: ${PRETTY_NAME:-unknown}"
|
||||
if [[ "${ID:-}" != "ubuntu" && "${ID:-}" != "debian" ]]; then
|
||||
warn "Рекомендуется Ubuntu/Debian. Продолжаем на свой страх."
|
||||
fi
|
||||
}
|
||||
|
||||
install_docker() {
|
||||
title "Docker"
|
||||
if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
|
||||
log "Docker уже установлен: $(docker --version)"
|
||||
log "Compose: $(docker compose version)"
|
||||
return
|
||||
fi
|
||||
|
||||
ask_yn "Docker не найден. Установить Docker Engine + Compose?" "y"
|
||||
if [[ "$REPLY" != "y" ]]; then
|
||||
err "Без Docker установка невозможна"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
info "Установка Docker…"
|
||||
apt-get update -y
|
||||
apt-get install -y ca-certificates curl gnupg
|
||||
install -m 0755 -d /etc/apt/keyrings
|
||||
if [[ ! -f /etc/apt/keyrings/docker.asc ]]; then
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
|
||||
chmod a+r /etc/apt/keyrings/docker.asc
|
||||
fi
|
||||
|
||||
local codename
|
||||
# shellcheck source=/dev/null
|
||||
. /etc/os-release
|
||||
codename="${VERSION_CODENAME:-jammy}"
|
||||
if [[ "${ID:-}" == "debian" ]]; then
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian ${codename} stable" \
|
||||
> /etc/apt/sources.list.d/docker.list
|
||||
else
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu ${codename} stable" \
|
||||
> /etc/apt/sources.list.d/docker.list
|
||||
fi
|
||||
|
||||
apt-get update -y
|
||||
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
systemctl enable --now docker
|
||||
log "Docker установлен"
|
||||
}
|
||||
|
||||
check_ports() {
|
||||
title "Порты 80 / 443 (нужны для Caddy SSL)"
|
||||
local busy=0
|
||||
for port in 80 443; do
|
||||
if ss -tlnp 2>/dev/null | grep -qE ":${port}\\b"; then
|
||||
warn "Порт ${port} занят:"
|
||||
ss -tlnp 2>/dev/null | grep -E ":${port}\\b" || true
|
||||
busy=1
|
||||
else
|
||||
log "Порт ${port} свободен"
|
||||
fi
|
||||
done
|
||||
if [[ "$busy" -eq 1 ]]; then
|
||||
ask_yn "Порты заняты. Остановить конфликтующие сервисы (nginx/apache2/caddy)?" "n"
|
||||
if [[ "$REPLY" == "y" ]]; then
|
||||
systemctl stop nginx 2>/dev/null || true
|
||||
systemctl disable nginx 2>/dev/null || true
|
||||
systemctl stop apache2 2>/dev/null || true
|
||||
systemctl disable apache2 2>/dev/null || true
|
||||
systemctl stop caddy 2>/dev/null || true
|
||||
# чужой docker caddy
|
||||
docker ps --format '{{.Names}}' 2>/dev/null | while read -r name; do
|
||||
if docker port "$name" 2>/dev/null | grep -qE ':80|:443'; then
|
||||
warn "Останавливаю контейнер $name (держит 80/443)"
|
||||
docker stop "$name" || true
|
||||
fi
|
||||
done
|
||||
else
|
||||
warn "Caddy может не получить SSL, пока 80/443 заняты"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
configure_firewall() {
|
||||
title "Firewall"
|
||||
if ! command -v ufw >/dev/null 2>&1; then
|
||||
ask_yn "UFW не установлен. Поставить и открыть 22/80/443?" "y"
|
||||
if [[ "$REPLY" == "y" ]]; then
|
||||
apt-get install -y ufw
|
||||
else
|
||||
return
|
||||
fi
|
||||
fi
|
||||
ask_yn "Настроить UFW: разрешить 22, 80, 443?" "y"
|
||||
if [[ "$REPLY" != "y" ]]; then
|
||||
return
|
||||
fi
|
||||
ufw allow OpenSSH >/dev/null 2>&1 || ufw allow 22/tcp >/dev/null 2>&1 || true
|
||||
ufw allow 80/tcp >/dev/null 2>&1 || true
|
||||
ufw allow 443/tcp >/dev/null 2>&1 || true
|
||||
ufw allow 443/udp >/dev/null 2>&1 || true
|
||||
echo "y" | ufw enable >/dev/null 2>&1 || true
|
||||
log "UFW: 22 / 80 / 443 открыты"
|
||||
}
|
||||
|
||||
collect_config() {
|
||||
title "Настройки проекта"
|
||||
|
||||
ask "Домен Mini App (A-запись → IP этого сервера)" "bot.example.com"
|
||||
WEBAPP_DOMAIN="${REPLY,,}"
|
||||
WEBAPP_DOMAIN="${WEBAPP_DOMAIN#https://}"
|
||||
WEBAPP_DOMAIN="${WEBAPP_DOMAIN#http://}"
|
||||
WEBAPP_DOMAIN="${WEBAPP_DOMAIN%%/*}"
|
||||
WEBAPP_URL="https://${WEBAPP_DOMAIN}"
|
||||
|
||||
ask "Email для Let's Encrypt (Caddy, необязательно)" ""
|
||||
ACME_EMAIL="$REPLY"
|
||||
|
||||
echo
|
||||
info "Remnawave панель"
|
||||
ask "REMNAWAVE_BASE_URL" "https://panel.example.com"
|
||||
REMNAWAVE_BASE_URL="${REPLY%/}"
|
||||
|
||||
ask_yn "Использовать API Token панели? (иначе логин/пароль)" "y"
|
||||
if [[ "$REPLY" == "y" ]]; then
|
||||
ask_secret "REMNAWAVE_TOKEN"
|
||||
REMNAWAVE_TOKEN="$REPLY"
|
||||
REMNAWAVE_USERNAME=""
|
||||
REMNAWAVE_PASSWORD=""
|
||||
while [[ -z "$REMNAWAVE_TOKEN" ]]; do
|
||||
warn "Токен обязателен"
|
||||
ask_secret "REMNAWAVE_TOKEN"
|
||||
REMNAWAVE_TOKEN="$REPLY"
|
||||
done
|
||||
else
|
||||
REMNAWAVE_TOKEN=""
|
||||
ask "REMNAWAVE_USERNAME" "admin"
|
||||
REMNAWAVE_USERNAME="$REPLY"
|
||||
ask_secret "REMNAWAVE_PASSWORD"
|
||||
REMNAWAVE_PASSWORD="$REPLY"
|
||||
fi
|
||||
|
||||
echo
|
||||
info "Telegram бот (@BotFather)"
|
||||
ask_secret "BOT_TOKEN"
|
||||
BOT_TOKEN="$REPLY"
|
||||
while [[ -z "$BOT_TOKEN" ]]; do
|
||||
warn "BOT_TOKEN обязателен"
|
||||
ask_secret "BOT_TOKEN"
|
||||
BOT_TOKEN="$REPLY"
|
||||
done
|
||||
|
||||
ask "ADMIN_IDS (Telegram ID через запятую)" ""
|
||||
ADMIN_IDS="$REPLY"
|
||||
|
||||
echo
|
||||
info "PostgreSQL (контейнер в Docker Compose)"
|
||||
ask "POSTGRES_USER" "vpnbot"
|
||||
POSTGRES_USER="$REPLY"
|
||||
GEN_PG="$(rand_pass)"
|
||||
ask_secret "POSTGRES_PASSWORD (Enter = сгенерировать)" "$GEN_PG"
|
||||
POSTGRES_PASSWORD="$REPLY"
|
||||
ask "POSTGRES_DB" "vpnbot"
|
||||
POSTGRES_DB="$REPLY"
|
||||
|
||||
echo
|
||||
info "Веб-админка https://${WEBAPP_DOMAIN}/admin"
|
||||
ask "WEB_ADMIN_LOGIN" "admin"
|
||||
WEB_ADMIN_LOGIN="$REPLY"
|
||||
GEN_ADM="$(rand_pass)"
|
||||
ask_secret "WEB_ADMIN_PASSWORD (Enter = сгенерировать)" "$GEN_ADM"
|
||||
WEB_ADMIN_PASSWORD="$REPLY"
|
||||
|
||||
echo
|
||||
ask_yn "Включить Telegram Stars для пополнения?" "y"
|
||||
if [[ "$REPLY" == "y" ]]; then
|
||||
STARS_ENABLED="true"
|
||||
ask "Курс ₽ за 1⭐" "1.5"
|
||||
STARS_RUB_PER_STAR="$REPLY"
|
||||
else
|
||||
STARS_ENABLED="false"
|
||||
STARS_RUB_PER_STAR="1.5"
|
||||
fi
|
||||
|
||||
ask "Ссылка покупки кода пополнения (TOPUP_CODE_PAY_URL, можно пусто)" ""
|
||||
TOPUP_CODE_PAY_URL="$REPLY"
|
||||
|
||||
echo
|
||||
ask_yn "Настроить SMTP сейчас? (можно позже в /admin → SMTP)" "n"
|
||||
if [[ "$REPLY" == "y" ]]; then
|
||||
ask "SMTP_HOST" ""
|
||||
SMTP_HOST="$REPLY"
|
||||
ask "SMTP_PORT" "587"
|
||||
SMTP_PORT="$REPLY"
|
||||
ask "SMTP_USER" ""
|
||||
SMTP_USER="$REPLY"
|
||||
ask_secret "SMTP_PASSWORD"
|
||||
SMTP_PASSWORD="$REPLY"
|
||||
ask "SMTP_FROM" ""
|
||||
SMTP_FROM="$REPLY"
|
||||
ask_yn "SMTP STARTTLS?" "y"
|
||||
[[ "$REPLY" == "y" ]] && SMTP_TLS="true" || SMTP_TLS="false"
|
||||
ask_yn "SMTP SSL (порт 465)?" "n"
|
||||
[[ "$REPLY" == "y" ]] && SMTP_SSL="true" || SMTP_SSL="false"
|
||||
else
|
||||
SMTP_HOST=""
|
||||
SMTP_PORT="587"
|
||||
SMTP_USER=""
|
||||
SMTP_PASSWORD=""
|
||||
SMTP_FROM=""
|
||||
SMTP_TLS="true"
|
||||
SMTP_SSL="false"
|
||||
fi
|
||||
|
||||
echo
|
||||
warn "Платёжные ключи (Crypto/Lava/Heleket/…) можно заполнить позже в веб-админке."
|
||||
ask_yn "Продолжить установку?" "y"
|
||||
if [[ "$REPLY" != "y" ]]; then
|
||||
err "Отменено"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
write_env() {
|
||||
title "Запись .env"
|
||||
local env_file="${SCRIPT_DIR}/.env"
|
||||
if [[ -f "$env_file" ]]; then
|
||||
local bak="${env_file}.bak.$(date +%Y%m%d-%H%M%S)"
|
||||
cp -a "$env_file" "$bak"
|
||||
warn "Старый .env сохранён: $bak"
|
||||
fi
|
||||
|
||||
cat > "$env_file" <<EOF
|
||||
# Сгенерировано install.sh $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
|
||||
REMNAWAVE_BASE_URL=$(env_escape "$REMNAWAVE_BASE_URL")
|
||||
REMNAWAVE_TOKEN=$(env_escape "$REMNAWAVE_TOKEN")
|
||||
REMNAWAVE_USERNAME=$(env_escape "$REMNAWAVE_USERNAME")
|
||||
REMNAWAVE_PASSWORD=$(env_escape "$REMNAWAVE_PASSWORD")
|
||||
|
||||
BOT_TOKEN=$(env_escape "$BOT_TOKEN")
|
||||
ADMIN_IDS=$(env_escape "$ADMIN_IDS")
|
||||
|
||||
STARS_ENABLED=${STARS_ENABLED}
|
||||
STARS_RUB_PER_STAR=${STARS_RUB_PER_STAR}
|
||||
|
||||
POSTGRES_USER=$(env_escape "$POSTGRES_USER")
|
||||
POSTGRES_PASSWORD=$(env_escape "$POSTGRES_PASSWORD")
|
||||
POSTGRES_DB=$(env_escape "$POSTGRES_DB")
|
||||
|
||||
WEBAPP_URL=$(env_escape "$WEBAPP_URL")
|
||||
WEBAPP_DOMAIN=$(env_escape "$WEBAPP_DOMAIN")
|
||||
WEBAPP_HOST=0.0.0.0
|
||||
WEBAPP_PORT=8080
|
||||
|
||||
WEB_ADMIN_LOGIN=$(env_escape "$WEB_ADMIN_LOGIN")
|
||||
WEB_ADMIN_PASSWORD=$(env_escape "$WEB_ADMIN_PASSWORD")
|
||||
|
||||
TOPUP_CODE_PAY_URL=$(env_escape "$TOPUP_CODE_PAY_URL")
|
||||
|
||||
CRYPTO_BOT_TOKEN=
|
||||
CRYPTO_PAY_TESTNET=false
|
||||
HELEKET_MERCHANT_ID=
|
||||
HELEKET_API_KEY=
|
||||
LAVA_SHOP_ID=
|
||||
LAVA_SECRET_KEY=
|
||||
LAVA_ADDITIONAL_KEY=
|
||||
ANYPAY_API_ID=
|
||||
ANYPAY_API_KEY=
|
||||
ANYPAY_PROJECT_ID=
|
||||
DIGISELLER_SELLER_ID=
|
||||
DIGISELLER_API_KEY=
|
||||
|
||||
SMTP_HOST=$(env_escape "$SMTP_HOST")
|
||||
SMTP_PORT=${SMTP_PORT}
|
||||
SMTP_USER=$(env_escape "$SMTP_USER")
|
||||
SMTP_PASSWORD=$(env_escape "$SMTP_PASSWORD")
|
||||
SMTP_FROM=$(env_escape "$SMTP_FROM")
|
||||
SMTP_TLS=${SMTP_TLS}
|
||||
SMTP_SSL=${SMTP_SSL}
|
||||
EOF
|
||||
|
||||
if [[ -n "${ACME_EMAIL:-}" ]]; then
|
||||
echo "ACME_EMAIL=$(env_escape "$ACME_EMAIL")" >> "$env_file"
|
||||
fi
|
||||
|
||||
chmod 600 "$env_file"
|
||||
log ".env записан: $env_file"
|
||||
}
|
||||
|
||||
ensure_files() {
|
||||
title "Проверка файлов проекта"
|
||||
local need=(docker-compose.yml Dockerfile requirements.txt bot.py tariffs.json caddy/Caddyfile)
|
||||
local missing=0
|
||||
for f in "${need[@]}"; do
|
||||
if [[ ! -e "${SCRIPT_DIR}/${f}" ]]; then
|
||||
err "Нет файла: $f"
|
||||
missing=1
|
||||
fi
|
||||
done
|
||||
if [[ "$missing" -eq 1 ]]; then
|
||||
err "Запускай install.sh из корня репозитория VPN Service Bot"
|
||||
exit 1
|
||||
fi
|
||||
log "Файлы проекта на месте"
|
||||
}
|
||||
|
||||
update_caddyfile_email() {
|
||||
if [[ -z "${ACME_EMAIL:-}" ]]; then
|
||||
return
|
||||
fi
|
||||
if grep -qE '^\s*email\s+' "${SCRIPT_DIR}/caddy/Caddyfile" 2>/dev/null; then
|
||||
log "Caddyfile уже содержит email — не трогаем"
|
||||
return
|
||||
fi
|
||||
local caddy_bak="${SCRIPT_DIR}/caddy/Caddyfile.bak.$(date +%Y%m%d-%H%M%S)"
|
||||
cp -a "${SCRIPT_DIR}/caddy/Caddyfile" "$caddy_bak"
|
||||
cat > "${SCRIPT_DIR}/caddy/Caddyfile" <<EOF
|
||||
{
|
||||
email ${ACME_EMAIL}
|
||||
}
|
||||
|
||||
{\$WEBAPP_DOMAIN:bot.example.com} {
|
||||
encode gzip
|
||||
reverse_proxy bot:8080
|
||||
|
||||
header {
|
||||
X-Content-Type-Options nosniff
|
||||
Referrer-Policy no-referrer-when-downgrade
|
||||
}
|
||||
|
||||
log {
|
||||
output stdout
|
||||
}
|
||||
}
|
||||
EOF
|
||||
log "Caddyfile: email для ACME = ${ACME_EMAIL} (бэкап: $(basename "$caddy_bak"))"
|
||||
}
|
||||
|
||||
dns_hint() {
|
||||
title "DNS"
|
||||
local ip
|
||||
ip="$(curl -4 -fsS --max-time 5 https://ifconfig.me 2>/dev/null || curl -4 -fsS --max-time 5 https://api.ipify.org 2>/dev/null || echo "?")"
|
||||
info "Публичный IP сервера: ${ip}"
|
||||
info "Создай A-запись: ${WEBAPP_DOMAIN} → ${ip}"
|
||||
ask_yn "DNS уже указывает на этот сервер?" "y"
|
||||
if [[ "$REPLY" != "y" ]]; then
|
||||
warn "Без DNS Let's Encrypt не выдаст сертификат. Можно продолжить — Caddy повторит позже."
|
||||
fi
|
||||
}
|
||||
|
||||
start_stack() {
|
||||
title "Сборка и запуск (Docker Compose)"
|
||||
cd "$SCRIPT_DIR"
|
||||
docker compose pull db caddy || true
|
||||
docker compose up -d --build
|
||||
log "Контейнеры запущены"
|
||||
echo
|
||||
docker compose ps
|
||||
echo
|
||||
info "Ждём health…"
|
||||
sleep 5
|
||||
local ok=0
|
||||
for _ in $(seq 1 30); do
|
||||
if docker compose exec -T bot curl -fsS http://127.0.0.1:8080/api/health >/dev/null 2>&1; then
|
||||
ok=1
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
if [[ "$ok" -eq 1 ]]; then
|
||||
log "API health OK (внутри контейнера)"
|
||||
else
|
||||
warn "Health пока не ответил — смотри: docker compose logs -f bot"
|
||||
fi
|
||||
}
|
||||
|
||||
print_done() {
|
||||
title "Готово"
|
||||
echo -e "${GREEN}Установка завершена.${NC}"
|
||||
echo
|
||||
echo " Кабинет: ${WEBAPP_URL}"
|
||||
echo " Админка: ${WEBAPP_URL}/admin"
|
||||
echo " Логин админа: ${WEB_ADMIN_LOGIN}"
|
||||
echo " Пароль админа: (тот, что задал / сгенерирован)"
|
||||
echo
|
||||
echo "Дальше:"
|
||||
echo " 1) @BotFather → /setdomain → ${WEBAPP_DOMAIN}"
|
||||
echo " 2) @BotFather → Menu Button → Web App → ${WEBAPP_URL}"
|
||||
echo " 3) Платежи: ${WEBAPP_URL}/admin → Платежи"
|
||||
echo " 4) SMTP: ${WEBAPP_URL}/admin → SMTP"
|
||||
echo " 5) Рост: ${WEBAPP_URL}/admin → Рост (промо, trial, бэкапы)"
|
||||
echo
|
||||
echo "Бэкапы Postgres (cron, опционально):"
|
||||
echo " 0 3 * * * cd ${SCRIPT_DIR} && docker compose exec -T -e PGPASSWORD=\$POSTGRES_PASSWORD db pg_dump -U \$POSTGRES_USER -d \$POSTGRES_DB | gzip > backups/vpnbot-\$(date +\\%F).sql.gz"
|
||||
echo
|
||||
echo "Команды:"
|
||||
echo " cd ${SCRIPT_DIR}"
|
||||
echo " docker compose ps"
|
||||
echo " docker compose logs -f bot"
|
||||
echo " docker compose logs -f caddy"
|
||||
echo " docker compose up -d --build"
|
||||
echo
|
||||
}
|
||||
|
||||
main() {
|
||||
clear 2>/dev/null || true
|
||||
echo -e "${BOLD}"
|
||||
echo " VPN Service Bot — установщик"
|
||||
echo " Docker · PostgreSQL · Caddy (Let's Encrypt SSL)"
|
||||
echo -e "${NC}"
|
||||
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
cat <<'HELP'
|
||||
Использование:
|
||||
sudo bash install.sh Интерактивная установка
|
||||
sudo bash install.sh --reinstall Пересобрать и перезапустить без опроса
|
||||
sudo bash install.sh --env-only Только пересоздать .env (опрос), без Docker up
|
||||
|
||||
Стек: db (Postgres 17) + bot + caddy (80/443, авто-SSL).
|
||||
HELP
|
||||
exit 0
|
||||
fi
|
||||
|
||||
need_root
|
||||
check_os
|
||||
ensure_files
|
||||
|
||||
if [[ "${1:-}" == "--reinstall" ]]; then
|
||||
if [[ ! -f "${SCRIPT_DIR}/.env" ]]; then
|
||||
err "Нет .env — сначала полный install.sh"
|
||||
exit 1
|
||||
fi
|
||||
# shellcheck disable=SC1091
|
||||
set -a
|
||||
# shellcheck source=/dev/null
|
||||
source "${SCRIPT_DIR}/.env"
|
||||
set +a
|
||||
WEBAPP_DOMAIN="${WEBAPP_DOMAIN:-}"
|
||||
WEBAPP_URL="${WEBAPP_URL:-https://${WEBAPP_DOMAIN}}"
|
||||
WEB_ADMIN_LOGIN="${WEB_ADMIN_LOGIN:-admin}"
|
||||
install_docker
|
||||
start_stack
|
||||
print_done
|
||||
exit 0
|
||||
fi
|
||||
|
||||
install_docker
|
||||
check_ports
|
||||
configure_firewall
|
||||
collect_config
|
||||
write_env
|
||||
update_caddyfile_email
|
||||
|
||||
if [[ "${1:-}" == "--env-only" ]]; then
|
||||
log "Только .env — контейнеры не трогали"
|
||||
echo "Запуск: docker compose up -d --build"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
dns_hint
|
||||
start_stack
|
||||
print_done
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user