f69efb67ad
- Tauri desktop app (apps/desktop) - Python Django API service (services/api) - Deployment scripts and docs
158 lines
4.9 KiB
Bash
158 lines
4.9 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
APP_DIR="${REMOTE_APP_DIR:-/opt/abyssinfo-home-vpn}"
|
|
API_DIR="${APP_DIR}/api"
|
|
VENV_DIR="${APP_DIR}/.venv-api"
|
|
ZIP_FILE="${API_ZIP_FILE:-/tmp/abyssinfo-api.zip}"
|
|
API_DOMAIN="${API_DOMAIN:-vpn-api.nm.101-96-214-81.sslip.io}"
|
|
API_LEGACY_DOMAIN="${API_LEGACY_DOMAIN:-vpn-api.nm.101-96-214-81.nip.io}"
|
|
API_PORT="${API_PORT:-8000}"
|
|
NETMAKER_DOMAIN="${NETMAKER_BASE_DOMAIN:-nm.101-96-214-81.nip.io}"
|
|
|
|
echo "[AbyssInfo] Deploying Django API"
|
|
echo "[AbyssInfo] API domain: ${API_DOMAIN}"
|
|
|
|
if [[ "$(id -u)" != "0" ]]; then
|
|
echo "[AbyssInfo] Please run as root." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ ! -f "${ZIP_FILE}" ]]; then
|
|
echo "[AbyssInfo] Missing ${ZIP_FILE}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "[AbyssInfo] Installing Python runtime ..."
|
|
dnf install -y python3.12 python3.12-pip unzip
|
|
|
|
mkdir -p "${API_DIR}"
|
|
PREVIOUS_ENV="/tmp/abyssinfo-api.previous.env"
|
|
if [[ -f "${API_DIR}/.env" ]]; then
|
|
cp "${API_DIR}/.env" "${PREVIOUS_ENV}"
|
|
else
|
|
rm -f "${PREVIOUS_ENV}"
|
|
fi
|
|
|
|
find "${API_DIR}" -mindepth 1 \
|
|
! -name db.sqlite3 \
|
|
! -path "${API_DIR}/db.sqlite3" \
|
|
-exec rm -rf {} +
|
|
|
|
echo "[AbyssInfo] Extracting API package ..."
|
|
set +e
|
|
unzip -q "${ZIP_FILE}" -d "${API_DIR}"
|
|
UNZIP_EXIT=$?
|
|
set -e
|
|
if [[ "${UNZIP_EXIT}" -gt 1 ]]; then
|
|
echo "[AbyssInfo] Failed to unzip API package. Exit code ${UNZIP_EXIT}" >&2
|
|
exit "${UNZIP_EXIT}"
|
|
fi
|
|
|
|
if [[ ! -f "${API_DIR}/.env" ]]; then
|
|
echo "[AbyssInfo] Missing ${API_DIR}/.env" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "[AbyssInfo] Normalizing production env ..."
|
|
python3.12 - <<PY
|
|
from pathlib import Path
|
|
import secrets
|
|
|
|
env_path = Path("${API_DIR}/.env")
|
|
previous_env_path = Path("${PREVIOUS_ENV}")
|
|
values = {}
|
|
for candidate in [previous_env_path, env_path]:
|
|
if not candidate.exists():
|
|
continue
|
|
for line in candidate.read_text(encoding="utf-8").splitlines():
|
|
if not line or line.startswith("#") or "=" not in line:
|
|
continue
|
|
key, value = line.split("=", 1)
|
|
key = key.lstrip("\\ufeff")
|
|
if value or key not in values:
|
|
values[key] = value
|
|
|
|
if not values.get("DJANGO_SECRET_KEY"):
|
|
values["DJANGO_SECRET_KEY"] = secrets.token_urlsafe(48)
|
|
if not values.get("JWT_SECRET"):
|
|
values["JWT_SECRET"] = secrets.token_urlsafe(48)
|
|
|
|
values["DJANGO_DEBUG"] = "false"
|
|
values["DJANGO_ALLOWED_HOSTS"] = "127.0.0.1,localhost,${API_DOMAIN},${API_LEGACY_DOMAIN}"
|
|
values["CORS_ALLOWED_ORIGINS"] = "http://localhost:5173,http://127.0.0.1:5173,http://tauri.localhost,https://tauri.localhost,tauri://localhost,https://${API_DOMAIN}"
|
|
values["FEISHU_REDIRECT_URI"] = "https://${API_DOMAIN}/auth/feishu/callback"
|
|
|
|
ordered = [
|
|
"DJANGO_SECRET_KEY", "DJANGO_DEBUG", "DJANGO_ALLOWED_HOSTS", "CORS_ALLOWED_ORIGINS",
|
|
"FEISHU_APP_ID", "FEISHU_APP_SECRET", "FEISHU_REDIRECT_URI", "FEISHU_EVENT_VERIFICATION_TOKEN", "TAURI_DEEP_LINK_SCHEME",
|
|
"JWT_SECRET", "JWT_EXPIRES_SECONDS", "OAUTH_STATE_EXPIRES_SECONDS", "LOGIN_CODE_EXPIRES_SECONDS",
|
|
"NETMAKER_BASE_URL", "NETMAKER_API_TOKEN", "NETMAKER_NETWORK_ID",
|
|
"NETMAKER_ENROLLMENT_TAGS", "NETMAKER_ENROLLMENT_TTL_SECONDS",
|
|
]
|
|
|
|
env_path.write_text("\\n".join(f"{key}={values.get(key, '')}" for key in ordered) + "\\n", encoding="utf-8")
|
|
PY
|
|
|
|
echo "[AbyssInfo] Creating virtual environment ..."
|
|
if [[ ! -x "${VENV_DIR}/bin/python" ]]; then
|
|
python3.12 -m venv "${VENV_DIR}"
|
|
fi
|
|
"${VENV_DIR}/bin/python" -m pip install --upgrade pip
|
|
"${VENV_DIR}/bin/pip" install -r "${API_DIR}/requirements.txt"
|
|
|
|
echo "[AbyssInfo] Running Django checks and migrations ..."
|
|
cd "${API_DIR}"
|
|
"${VENV_DIR}/bin/python" manage.py check
|
|
"${VENV_DIR}/bin/python" manage.py migrate --noinput
|
|
"${VENV_DIR}/bin/python" manage.py collectstatic --noinput
|
|
|
|
echo "[AbyssInfo] Installing systemd service ..."
|
|
cat >/etc/systemd/system/abyssinfo-api.service <<SERVICE
|
|
[Unit]
|
|
Description=AbyssInfo Home VPN Django API
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
WorkingDirectory=${API_DIR}
|
|
Environment=PYTHONUNBUFFERED=1
|
|
ExecStart=${VENV_DIR}/bin/gunicorn config.wsgi:application --bind 0.0.0.0:${API_PORT} --workers 2 --timeout 60
|
|
Restart=always
|
|
RestartSec=3
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
SERVICE
|
|
|
|
systemctl daemon-reload
|
|
systemctl enable --now abyssinfo-api
|
|
systemctl restart abyssinfo-api
|
|
|
|
echo "[AbyssInfo] Updating Netmaker Caddy reverse proxy ..."
|
|
CADDYFILE="/root/netmaker/Caddyfile"
|
|
if [[ ! -f "${CADDYFILE}" ]]; then
|
|
echo "[AbyssInfo] Missing ${CADDYFILE}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if ! grep -q "https://${API_DOMAIN}" "${CADDYFILE}"; then
|
|
cat >>"${CADDYFILE}" <<CADDY
|
|
|
|
# AbyssInfo Django API
|
|
https://${API_DOMAIN} {
|
|
reverse_proxy http://host.docker.internal:${API_PORT}
|
|
}
|
|
CADDY
|
|
fi
|
|
|
|
(cd /root/netmaker && docker compose exec -T caddy caddy reload --config /etc/caddy/Caddyfile)
|
|
|
|
echo "[AbyssInfo] Verifying API health ..."
|
|
curl -fsS "http://127.0.0.1:${API_PORT}/health/" >/dev/null
|
|
curl -kfsS "https://${API_DOMAIN}/health/" >/dev/null
|
|
|
|
echo "[AbyssInfo] Django API deployed:"
|
|
echo " https://${API_DOMAIN}/health/"
|