diff --git a/deploy/README.md b/deploy/README.md index f61f2f7..844ae4d 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -38,6 +38,7 @@ This creates: - `/home/bestmidi/chgrid/server/.venv` - `/home/bestmidi/chgrid/server/config.toml` (if missing) - `/home/bestmidi/chgrid/server/.env` with `CHGRID_AUTH_SECRET` (if missing) +- On first run only, if no admin exists, it prompts to create one immediately. Edit `/home/bestmidi/chgrid/server/config.toml`: - `server.bind_ip = "127.0.0.1"` @@ -48,6 +49,14 @@ Edit `/home/bestmidi/chgrid/server/config.toml`: - `storage.state_file = "runtime/items.json"` - `auth.db_file = "runtime/chatgrid.db"` +If you skip first-run admin creation, run later: + +```bash +cd /home/bestmidi/chgrid/server +source .env +.venv/bin/python main.py --config config.toml --bootstrap-admin +``` + ## 4) Build and publish client ```bash diff --git a/deploy/scripts/install_server.sh b/deploy/scripts/install_server.sh index f166f2b..bc926e4 100755 --- a/deploy/scripts/install_server.sh +++ b/deploy/scripts/install_server.sh @@ -61,5 +61,63 @@ PY echo "created $SERVER_DIR/.env with CHGRID_AUTH_SECRET" fi +# Load generated/shared auth secret for bootstrap checks. +if [[ -f .env ]]; then + set -a + # shellcheck disable=SC1091 + source .env + set +a +fi + +if [[ -n "${CHGRID_AUTH_SECRET:-}" ]]; then + HAS_ADMIN="$( + .venv/bin/python - <<'PY' +from pathlib import Path +import os +from app.auth_service import AuthService +from app.config import load_config + +cfg = load_config(Path("config.toml")) +secret = os.getenv("CHGRID_AUTH_SECRET", "").strip() +if not secret: + print("unknown") + raise SystemExit(0) +db_file = cfg.auth.db_file.strip() or "runtime/chatgrid.db" +db_path = Path(db_file) +if not db_path.is_absolute(): + db_path = Path.cwd() / db_path +svc = AuthService( + db_path=db_path, + token_hash_secret=secret, + password_min_length=cfg.auth.password_min_length, + password_max_length=cfg.auth.password_max_length, + username_min_length=cfg.auth.username_min_length, + username_max_length=cfg.auth.username_max_length, +) +try: + print("yes" if svc.has_admin() else "no") +finally: + svc.close() +PY + )" + + if [[ "$HAS_ADMIN" == "no" ]]; then + if [[ -t 0 ]]; then + read -r -p "No admin account found. Create one now? [y/N] " CREATE_ADMIN_NOW + case "${CREATE_ADMIN_NOW:-}" in + y|Y|yes|YES) + .venv/bin/python main.py --config config.toml --bootstrap-admin + ;; + *) + echo "skipped admin bootstrap (you can run: .venv/bin/python main.py --config config.toml --bootstrap-admin)" + ;; + esac + else + echo "no admin account found (non-interactive run)." + echo "run once to bootstrap: .venv/bin/python main.py --config config.toml --bootstrap-admin" + fi + fi +fi + echo "server install complete" echo "next: edit $SERVER_DIR/config.toml (TLS, bind_ip, port)" diff --git a/server/app/auth_service.py b/server/app/auth_service.py index 5a507cb..37e6859 100644 --- a/server/app/auth_service.py +++ b/server/app/auth_service.py @@ -83,12 +83,17 @@ class AuthService: def bootstrap_admin(self, username: str, password: str, email: str | None = None) -> AuthUser: """Create the first admin account, or fail if one already exists.""" - existing = self._conn.execute("SELECT 1 FROM users WHERE role = 'admin' LIMIT 1").fetchone() - if existing is not None: + if self.has_admin(): raise AuthError("An admin account already exists.") created = self.register(username, password, email=email, role="admin") return created.user + def has_admin(self) -> bool: + """Return True when at least one admin account exists.""" + + existing = self._conn.execute("SELECT 1 FROM users WHERE role = 'admin' LIMIT 1").fetchone() + return existing is not None + def register( self, username: str,