mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 19:07:04 +01:00
Stands up Endurain behind the existing Caddy edge and imports RunnerUp activities into it. Endurain is a VIEWER: screen-locker's workout gate still reads the phone over adb and is untouched here, because a WebDAV drop folder is spoofable by anything that can write to ~/cloud and must not become a screen-unlock authority. Primary path is phone -> WebDAV (existing dufs) -> ~/cloud/RunnerUp/ -> importer -> Endurain, with an adb pull as fallback when a run has not been uploaded yet. A systemd user timer runs the importer every 15 min. Findings that shaped the implementation, each verified against the running system rather than assumed: - Endurain moved to Codeberg; the GitHub repo is a read-only mirror stale at v0.17.7 against v0.19.0. The image is pulled from Codeberg. - postgres:18 moved PGDATA to /var/lib/postgresql/18/docker. Upstream's compose example still mounts the pre-18 path, which fails as an unexplained "container is unhealthy"; the real cause is only visible in docker logs. - Endurain seeds a hardcoded admin/admin via an Alembic migration with no env override, and endurain.kuhy.duckdns.org already resolved publicly. Publishing is therefore a separate script that refuses to install the Caddy site while those credentials still work. - Endurain performs NO duplicate detection, so the importer owns dedupe on two axes: a content hash, and an activity key that collapses the .gpx/.tcx pair RunnerUp writes for a single run (different bytes, same run). - Ambiguous uploads (5xx/timeout) are never re-queued, since the activity may already have been committed; only definite 4xx rejections are staged for retry. The importer exits non-zero so the timer surfaces the problem. - RunnerUp has no background sync at all, so every upload is a manual tap. - Its WebDAV synchronizer needs HTTPS with a real cert (Android blocks cleartext at targetSdk 36; self-signed is broken upstream) and a URL with no trailing slash, and cannot MKCOL against dufs - so the inbox must pre-exist. The phone gets a dufs credential scoped to /RunnerUp, verified unable to read Keepass or gdrive_backups. Verified: 52 tests at 100% branch coverage; the timer imports 4 real runs and re-runs create zero duplicates; https://endurain.kuhy.duckdns.org serves 200 with a valid cert and the other edge sites are unaffected; screen-locker's suite still passes 1090 tests at 100% coverage. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GbYEnfGnf1988cdFdGebot
194 lines
6.5 KiB
Bash
Executable File
194 lines
6.5 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# ============================================================================
|
|
# Provision the Endurain self-hosted fitness tracker.
|
|
#
|
|
# Idempotent: re-running keeps the existing .env (and therefore the existing
|
|
# database credentials — regenerating them would orphan the Postgres volume).
|
|
#
|
|
# ORDERING IS LOAD-BEARING. Endurain seeds a hardcoded admin/admin account via
|
|
# an Alembic migration with no env override, and endurain.kuhy.duckdns.org
|
|
# already resolves publicly. So this script brings the stack up on loopback
|
|
# ONLY and refuses to install the Caddy site; publishing is a separate,
|
|
# deliberate step taken after the admin password has been changed.
|
|
# ============================================================================
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_NAME="$(basename "$0")"
|
|
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
readonly SCRIPT_NAME REPO_DIR
|
|
readonly ENV_FILE="${REPO_DIR}/.env"
|
|
readonly LOCAL_PATH="${ENDURAIN_LOCAL_PATH:-/var/opt/endurain}"
|
|
readonly APP_URL="http://127.0.0.1:8085"
|
|
readonly PUBLIC_HOST="endurain.kuhy.duckdns.org"
|
|
|
|
log() { printf '%s\n' "$*"; }
|
|
log_ok() { printf ' ok: %s\n' "$*"; }
|
|
die() {
|
|
printf 'Error: %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
usage() {
|
|
echo "Usage: ${SCRIPT_NAME} [--recreate-env]"
|
|
echo " --recreate-env Regenerate secrets. DESTRUCTIVE: orphans the existing DB."
|
|
exit 0
|
|
}
|
|
|
|
RECREATE_ENV=0
|
|
|
|
validate_requirements() {
|
|
command -v docker >/dev/null || die "docker not installed"
|
|
docker compose version >/dev/null 2>&1 || die "docker compose plugin missing"
|
|
command -v openssl >/dev/null || die "openssl not installed"
|
|
# Fail early rather than after the volumes are created.
|
|
docker info >/dev/null 2>&1 || die "cannot talk to the docker daemon"
|
|
}
|
|
|
|
# Secrets are generated once and never printed to a file outside .env.
|
|
write_env() {
|
|
if [[ -f "${ENV_FILE}" && "${RECREATE_ENV}" -eq 0 ]]; then
|
|
log_ok ".env already exists — keeping existing credentials"
|
|
return
|
|
fi
|
|
local db_password secret_key fernet_key
|
|
db_password="$(openssl rand -hex 32)"
|
|
secret_key="$(openssl rand -hex 32)"
|
|
fernet_key="$(openssl rand -base64 32)"
|
|
|
|
umask 077
|
|
cat >"${ENV_FILE}" <<EOF
|
|
# Generated by ${SCRIPT_NAME} — gitignored, never commit.
|
|
DB_PASSWORD=${db_password}
|
|
POSTGRES_PASSWORD=${db_password}
|
|
SECRET_KEY=${secret_key}
|
|
FERNET_KEY=${fernet_key}
|
|
|
|
TZ=Europe/Warsaw
|
|
ENDURAIN_HOST=https://${PUBLIC_HOST}
|
|
ENVIRONMENT=production
|
|
BEHIND_PROXY=true
|
|
# Upstream defaults to "*"; only the local Caddy edge fronts this service.
|
|
TRUSTED_PROXIES=127.0.0.1
|
|
|
|
RATE_LIMIT_STORAGE_URI=redis://redis:6379/0
|
|
AUTH_SECURITY_STORAGE_URI=redis://redis:6379/0
|
|
POSTGRES_DB=endurain
|
|
POSTGRES_USER=endurain
|
|
# PGDATA intentionally NOT set: postgres:18 defaults to
|
|
# /var/lib/postgresql/18/docker, which is where the volume is mounted.
|
|
LOCAL_PATH=${LOCAL_PATH}
|
|
EOF
|
|
chmod 600 "${ENV_FILE}"
|
|
log_ok "Wrote ${ENV_FILE} (secrets generated, mode 600)"
|
|
}
|
|
|
|
# Each image runs as a different non-root uid, so the bind mounts must be
|
|
# chown'd per-service or the container fails to create its data directory:
|
|
# endurain -> 1000 (v0.19.0 removed the UID/GID env vars)
|
|
# postgres -> 999 (image drops privileges internally after entrypoint)
|
|
# redis -> 999
|
|
# Getting this wrong surfaces as "dependency failed to start: container
|
|
# endurain-postgres is unhealthy", with the real cause (mkdir: Permission
|
|
# denied) visible only in `docker logs endurain-postgres`.
|
|
readonly POSTGRES_UID=999
|
|
readonly REDIS_UID=999
|
|
readonly ENDURAIN_UID=1000
|
|
|
|
prepare_data_dirs() {
|
|
local d
|
|
for d in "${LOCAL_PATH}/backend/data" "${LOCAL_PATH}/backend/logs" \
|
|
"${LOCAL_PATH}/postgres" "${LOCAL_PATH}/redis"; do
|
|
sudo mkdir -p "${d}"
|
|
done
|
|
sudo chown -R "${ENDURAIN_UID}:${ENDURAIN_UID}" "${LOCAL_PATH}/backend"
|
|
sudo chown -R "${POSTGRES_UID}:${POSTGRES_UID}" "${LOCAL_PATH}/postgres"
|
|
sudo chown -R "${REDIS_UID}:${REDIS_UID}" "${LOCAL_PATH}/redis"
|
|
log_ok "Data directories ready under ${LOCAL_PATH}"
|
|
}
|
|
|
|
start_stack() {
|
|
docker compose -f "${REPO_DIR}/docker-compose.yml" up -d
|
|
log_ok "Stack started"
|
|
}
|
|
|
|
wait_for_health() {
|
|
for _ in $(seq 1 60); do
|
|
if curl -fsS --max-time 5 "${APP_URL}/api/v1/about" >/dev/null 2>&1; then
|
|
log_ok "Endurain healthy at ${APP_URL}"
|
|
return 0
|
|
fi
|
|
sleep 2
|
|
done
|
|
die "Endurain did not become healthy within 120s. Check: docker logs endurain-app"
|
|
}
|
|
|
|
# Fail closed: if the seeded admin/admin still works, the service must not be
|
|
# published. This is a gate, not a warning.
|
|
#
|
|
# The endpoint is /api/v1/auth/login with X-Client-Type: mobile. Two traps here,
|
|
# both of which produce a FALSE PASS (gate says "safe" while admin/admin works):
|
|
# - /api/v1/token does not exist; it falls through to the SPA index.
|
|
# - Without X-Client-Type: mobile the API answers 403 "CSRF token required"
|
|
# before ever checking the credentials.
|
|
# So only an explicit 200 means "still vulnerable", and any unexpected status is
|
|
# treated as vulnerable rather than assumed safe.
|
|
check_admin_password_changed() {
|
|
local code
|
|
code="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 10 \
|
|
-X POST "${APP_URL}/api/v1/auth/login" \
|
|
-H 'Content-Type: application/x-www-form-urlencoded' \
|
|
-H 'X-Client-Type: mobile' \
|
|
--data 'username=admin&password=admin' 2>/dev/null || echo 000)"
|
|
case "${code}" in
|
|
401 | 403) return 0 ;; # credentials rejected — password was changed
|
|
200) return 1 ;; # seeded admin/admin still works
|
|
*)
|
|
log " (unexpected status ${code} from the login probe)"
|
|
return 1
|
|
;; # fail closed on anything unexpected
|
|
esac
|
|
}
|
|
|
|
main() {
|
|
validate_requirements
|
|
log "=== Endurain setup ==="
|
|
write_env
|
|
prepare_data_dirs
|
|
start_stack
|
|
wait_for_health
|
|
|
|
log ""
|
|
if check_admin_password_changed; then
|
|
log_ok "Seeded admin/admin no longer accepted"
|
|
log ""
|
|
log "Next: install the Caddy site to publish it, then configure the phone."
|
|
log " ${REPO_DIR}/scripts/publish_endurain.sh"
|
|
else
|
|
log "!! ACTION REQUIRED — the service is NOT safe to publish yet."
|
|
log " Endurain still accepts the seeded admin/admin login."
|
|
log " 1. Open ${APP_URL} and log in as admin/admin"
|
|
log " 2. Change the admin password immediately (store it in Keepass)"
|
|
log " 3. Settings -> Security -> API Keys: create a key scoped"
|
|
log " 'activities:upload' and save it for the importer"
|
|
log " 4. Re-run this script to confirm, then publish."
|
|
log ""
|
|
log " The Caddy site is deliberately NOT installed while this is true:"
|
|
log " https://${PUBLIC_HOST} already resolves to this host."
|
|
fi
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--recreate-env)
|
|
RECREATE_ENV=1
|
|
shift
|
|
;;
|
|
-h | --help) usage ;;
|
|
*) die "Unknown option: $1" ;;
|
|
esac
|
|
done
|
|
|
|
main "$@"
|