Continuation of the previous commit (gdrive_backup.py removal landed separately by accident) — adds the new module itself, the admin routes/template using it, generalized Setting keys, updated docs/ provisioning notes, and tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
213 lines
7.9 KiB
Bash
213 lines
7.9 KiB
Bash
#!/bin/bash
|
||
# Provisions a NEW, separate deployment of this codebase for another client
|
||
# on a fresh VPS — clone, .env, webapp build, nginx vhost, TLS, systemd
|
||
# units. Automates docs/DEPLOYMENT.md steps 2-5; run this instead of doing
|
||
# that runbook by hand.
|
||
#
|
||
# Prerequisite (docs/DEPLOYMENT.md step 1, one-time OS setup, not run by
|
||
# this script): python3.11, nginx, certbot, nodejs/npm, rclone already
|
||
# installed on this server.
|
||
#
|
||
# Never touches an existing deployment: refuses to run if --install-dir
|
||
# already exists, and every irreversible/system-level step (nginx reload,
|
||
# certbot, systemctl enable) asks for confirmation first.
|
||
set -euo pipefail
|
||
|
||
usage() {
|
||
cat <<'EOF'
|
||
Usage: ./provision_client.sh --slug SLUG --domain DOMAIN --bot-token TOKEN --cafe-name "Name" [--install-dir DIR] [--repo-url URL]
|
||
|
||
--slug short identifier, lowercase letters/digits/hyphens only
|
||
(used for the install dir and systemd unit names)
|
||
--domain public domain this instance will be served from
|
||
--bot-token Telegram bot token from @BotFather
|
||
--cafe-name display name for this client (webapp title, PWA manifest)
|
||
--install-dir defaults to /opt/<slug>-bot
|
||
--repo-url defaults to this project's own git remote
|
||
EOF
|
||
}
|
||
|
||
SLUG=""
|
||
DOMAIN=""
|
||
BOT_TOKEN=""
|
||
CAFE_NAME=""
|
||
INSTALL_DIR=""
|
||
REPO_URL="https://git.zotac.keenetic.link/zotac/bober-bbq-bot.git"
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--slug) SLUG="$2"; shift 2 ;;
|
||
--domain) DOMAIN="$2"; shift 2 ;;
|
||
--bot-token) BOT_TOKEN="$2"; shift 2 ;;
|
||
--cafe-name) CAFE_NAME="$2"; shift 2 ;;
|
||
--install-dir) INSTALL_DIR="$2"; shift 2 ;;
|
||
--repo-url) REPO_URL="$2"; shift 2 ;;
|
||
-h|--help) usage; exit 0 ;;
|
||
*) echo "Невідомий аргумент: $1"; usage; exit 1 ;;
|
||
esac
|
||
done
|
||
|
||
if [[ -z "$SLUG" || -z "$DOMAIN" || -z "$BOT_TOKEN" || -z "$CAFE_NAME" ]]; then
|
||
echo "Не вистачає обов'язкового аргументу."
|
||
usage
|
||
exit 1
|
||
fi
|
||
if [[ ! "$SLUG" =~ ^[a-z0-9-]+$ ]]; then
|
||
echo "--slug має містити лише малі латинські літери, цифри й дефіс (напр. \"vegcafe\")."
|
||
exit 1
|
||
fi
|
||
|
||
INSTALL_DIR="${INSTALL_DIR:-/opt/${SLUG}-bot}"
|
||
SERVICE_WEB="${SLUG}-web"
|
||
SERVICE_BOT="${SLUG}-bot"
|
||
LOG_FILE_WEB="/var/log/${SERVICE_WEB}.log"
|
||
LOG_FILE_BOT="/var/log/${SERVICE_BOT}.log"
|
||
|
||
confirm() {
|
||
local reply
|
||
read -rp "$1 [y/N] " reply
|
||
[[ "$reply" =~ ^[Yy]$ ]]
|
||
}
|
||
|
||
echo "=== Провижнінг нового клієнта ==="
|
||
echo "Slug: $SLUG"
|
||
echo "Домен: $DOMAIN"
|
||
echo "Install dir: $INSTALL_DIR"
|
||
echo "Cafe name: $CAFE_NAME"
|
||
echo "Юніти: $SERVICE_WEB / $SERVICE_BOT"
|
||
echo ""
|
||
if ! confirm "Продовжити?"; then
|
||
echo "Скасовано."
|
||
exit 0
|
||
fi
|
||
|
||
if [[ -e "$INSTALL_DIR" ]]; then
|
||
echo "ПОМИЛКА: $INSTALL_DIR вже існує — оберіть інший --install-dir."
|
||
echo "(Це також запобігає випадковому запуску поверх існуючого деплою.)"
|
||
exit 1
|
||
fi
|
||
|
||
echo "==> 1/8 git clone"
|
||
git clone "$REPO_URL" "$INSTALL_DIR"
|
||
cd "$INSTALL_DIR"
|
||
|
||
echo "==> 2/8 venv + залежності"
|
||
python3.11 -m venv .venv
|
||
.venv/bin/pip install -q -r requirements.txt
|
||
|
||
echo "==> 3/8 .env"
|
||
SECRET_KEY=$(openssl rand -hex 32)
|
||
ADMIN_PASSWORD=$(openssl rand -hex 12)
|
||
cp .env.example .env
|
||
sed -i "s|^BOT_TOKEN=.*|BOT_TOKEN=${BOT_TOKEN}|" .env
|
||
sed -i "s|^WEBAPP_URL=.*|WEBAPP_URL=https://${DOMAIN}/webapp/|" .env
|
||
sed -i "s|^BACKEND_PUBLIC_URL=.*|BACKEND_PUBLIC_URL=https://${DOMAIN}|" .env
|
||
sed -i "s|^MONOBANK_WEBHOOK_URL=.*|MONOBANK_WEBHOOK_URL=https://${DOMAIN}/api/payments/monobank/webhook|" .env
|
||
sed -i "s|^SECRET_KEY=.*|SECRET_KEY=${SECRET_KEY}|" .env
|
||
sed -i "s|^FLASK_ADMIN_PASSWORD=.*|FLASK_ADMIN_PASSWORD=${ADMIN_PASSWORD}|" .env
|
||
{
|
||
echo ""
|
||
echo "# --- provision_client.sh: deployment identity ---"
|
||
echo "SERVICE_WEB=${SERVICE_WEB}"
|
||
echo "SERVICE_BOT=${SERVICE_BOT}"
|
||
echo "LOG_FILE_WEB=${LOG_FILE_WEB}"
|
||
echo "LOG_FILE_BOT=${LOG_FILE_BOT}"
|
||
} >> .env
|
||
echo "Адмін-логін: admin / пароль: ${ADMIN_PASSWORD} (запишіть — більше ніде не показується)"
|
||
|
||
echo "==> 4/8 webapp/.env.local"
|
||
echo "VITE_CAFE_NAME=${CAFE_NAME}" > webapp/.env.local
|
||
|
||
echo "==> 5/8 webapp build"
|
||
( cd webapp && npm install --silent && npm run build )
|
||
|
||
echo "==> 6/8 nginx vhost"
|
||
if confirm "Створити nginx vhost для ${DOMAIN} і перезавантажити nginx?"; then
|
||
NGINX_CONF="/etc/nginx/sites-available/${SLUG}"
|
||
sudo tee "$NGINX_CONF" > /dev/null <<EOF
|
||
server {
|
||
listen 80;
|
||
server_name ${DOMAIN};
|
||
|
||
location / {
|
||
proxy_pass http://127.0.0.1:8000;
|
||
proxy_set_header Host \$host;
|
||
proxy_set_header X-Real-IP \$remote_addr;
|
||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||
}
|
||
}
|
||
EOF
|
||
sudo ln -sf "$NGINX_CONF" "/etc/nginx/sites-enabled/${SLUG}"
|
||
sudo nginx -t
|
||
sudo systemctl reload nginx
|
||
else
|
||
echo "Пропущено — налаштуйте nginx вручну (docs/DEPLOYMENT.md, крок 4)."
|
||
fi
|
||
|
||
echo "==> 7/8 certbot (TLS)"
|
||
if confirm "Запустити certbot для ${DOMAIN}? (потрібен вже налаштований DNS-запис на цей сервер)"; then
|
||
sudo certbot --nginx -d "$DOMAIN"
|
||
else
|
||
echo "Пропущено — без TLS Telegram Mini App і вебхуки не запрацюють."
|
||
echo "Запустіть пізніше: sudo certbot --nginx -d ${DOMAIN}"
|
||
fi
|
||
|
||
echo "==> 8/8 systemd-юніти"
|
||
if confirm "Створити й запустити systemd-юніти (${SERVICE_WEB}, ${SERVICE_BOT})?"; then
|
||
sudo tee "/etc/systemd/system/${SERVICE_WEB}.service" > /dev/null <<EOF
|
||
[Unit]
|
||
Description=${CAFE_NAME} backend (API + admin)
|
||
After=network.target
|
||
|
||
[Service]
|
||
WorkingDirectory=${INSTALL_DIR}
|
||
ExecStart=${INSTALL_DIR}/.venv/bin/python run_web.py
|
||
Restart=on-failure
|
||
EnvironmentFile=${INSTALL_DIR}/.env
|
||
StandardOutput=append:${LOG_FILE_WEB}
|
||
StandardError=append:${LOG_FILE_WEB}
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
sudo tee "/etc/systemd/system/${SERVICE_BOT}.service" > /dev/null <<EOF
|
||
[Unit]
|
||
Description=${CAFE_NAME} Telegram bot
|
||
After=network.target ${SERVICE_WEB}.service
|
||
|
||
[Service]
|
||
WorkingDirectory=${INSTALL_DIR}
|
||
ExecStart=${INSTALL_DIR}/.venv/bin/python run_bot.py
|
||
Restart=on-failure
|
||
EnvironmentFile=${INSTALL_DIR}/.env
|
||
StandardOutput=append:${LOG_FILE_BOT}
|
||
StandardError=append:${LOG_FILE_BOT}
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
sudo systemctl daemon-reload
|
||
sudo systemctl enable --now "$SERVICE_WEB" "$SERVICE_BOT"
|
||
else
|
||
echo "Пропущено — створіть юніти вручну (docs/DEPLOYMENT.md, крок 5)."
|
||
fi
|
||
|
||
cat <<EOF
|
||
|
||
=== Готово. Залишились ручні кроки (свідомо не автоматизуються) ===
|
||
|
||
1. У @BotFather: /setmenubutton -> https://${DOMAIN}/webapp/
|
||
2. Додати бота в чати доставки/самовивозу, вписати chat_id в Адмінку -> Налаштування.
|
||
3. Якщо клієнт приймає картки — MONOBANK_TOKEN у .env і перевірити вебхук.
|
||
4. Замінити 4 іконки під webapp/public/icons/icon-{32,180,192,512}.png на бренд
|
||
клієнта, потім ще раз: cd webapp && npm run build
|
||
5. Адмінка (https://${DOMAIN}/admin, admin / пароль вище) -> Налаштування:
|
||
cafe_name, адреса, графік, кольори, лого.
|
||
6. Адмінка -> Меню: видалити демо-меню (Шашлик/BBQ), додати асортимент клієнта.
|
||
7. Зареєструвати https://${DOMAIN}/health в UptimeRobot (чи аналог) — вручну,
|
||
акаунт створює власник бізнесу, не цей скрипт.
|
||
8. Резервні копії: Адмінка -> Резервні копії -> налаштувати сховище (Google
|
||
Drive або S3-сумісне) і увімкнути (або окремий cron — docs/DEPLOYMENT.md,
|
||
крок 7).
|
||
|
||
EOF
|