From e9136f801a4aa498cd0acec200b4ff4fba163681 Mon Sep 17 00:00:00 2001 From: Solanki Sumit <125974181+YAMRAJ13y@users.noreply.github.com> Date: Tue, 23 Jun 2026 23:38:05 +0530 Subject: [PATCH] fix(setup): load .env so a pre-seeded admin password is honored on native installs (#4787) setup.py read ODYSSEUS_ADMIN_USER / ODYSSEUS_ADMIN_PASSWORD via os.getenv() but never loaded .env, so on native Linux/macOS installs a password pre-seeded in .env (documented in docs/setup.md and .env.example) was silently ignored and a random one generated, breaking the first login. Docker was unaffected because compose passes the vars into the container env. Call load_dotenv(BASE_DIR/.env, encoding="utf-8-sig") at the top of main(), mirroring app.py (utf-8-sig tolerates a Notepad UTF-8 BOM). load_dotenv does not override already-exported OS vars, so the existing precedence is kept. python-dotenv is already a required dependency. Adds a regression test that pre-seeds credentials only in .env (not the shell) and asserts the stored bcrypt hash matches the pre-seeded password. Co-authored-by: Claude Opus 4.8 (1M context) --- setup.py | 9 +++++++ tests/test_setup_admin_user.py | 47 ++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/setup.py b/setup.py index a9c565282..5b4eadcb5 100644 --- a/setup.py +++ b/setup.py @@ -239,6 +239,15 @@ def check_arch(): def main(): print("\n=== Odysseus Setup ===\n") + # Load .env so pre-seeded ODYSSEUS_ADMIN_USER / ODYSSEUS_ADMIN_PASSWORD (and + # other deployment vars) are honored on native installs, not just when they + # are exported in the shell. Mirrors app.py: encoding="utf-8-sig" tolerates a + # UTF-8 BOM in a Notepad-saved .env. load_dotenv does not override already + # exported OS env vars, so the existing precedence is preserved. python-dotenv + # is a hard dependency (requirements.txt) and is verified by check_deps below. + from dotenv import load_dotenv + load_dotenv(os.path.join(BASE_DIR, ".env"), encoding="utf-8-sig") + # Fail fast with a clear message if the CPU architecture is wrong (Apple # Silicon under an x86/Rosetta Python) before importing anything native. check_arch() diff --git a/tests/test_setup_admin_user.py b/tests/test_setup_admin_user.py index 9ecfb416b..b0fde4d75 100644 --- a/tests/test_setup_admin_user.py +++ b/tests/test_setup_admin_user.py @@ -1,5 +1,6 @@ import importlib.util import json +import os from pathlib import Path @@ -23,3 +24,49 @@ def test_create_default_admin_normalizes_env_username(tmp_path, monkeypatch): data = json.loads(auth_path.read_text(encoding="utf-8")) assert "adminuser" in data["users"] assert "AdminUser" not in data["users"] + + +def test_main_loads_admin_password_from_env_file(tmp_path, monkeypatch): + """Regression: setup.py must honor an admin password pre-seeded in .env on + native installs, even when the var is not exported into the shell + (docs/setup.md documents this). Previously setup.py never called + load_dotenv(), so os.getenv() saw nothing and a random password was + generated instead.""" + import bcrypt + + setup_module = _load_setup_module() + + # Credentials live ONLY in a .env beside setup.py (written with a UTF-8 BOM, + # the Notepad-on-Windows case that utf-8-sig must tolerate) — not exported. + monkeypatch.delenv("ODYSSEUS_ADMIN_USER", raising=False) + monkeypatch.delenv("ODYSSEUS_ADMIN_PASSWORD", raising=False) + (tmp_path / ".env").write_text( + "ODYSSEUS_ADMIN_USER=presetuser\nODYSSEUS_ADMIN_PASSWORD=fromenvfile12345\n", + encoding="utf-8-sig", + ) + + # Point setup at the temp dir and neutralize main()'s heavy steps. + monkeypatch.setattr(setup_module, "BASE_DIR", str(tmp_path)) + auth_path = tmp_path / "auth.json" + monkeypatch.setattr(setup_module, "AUTH_FILE", str(auth_path)) + monkeypatch.setattr(setup_module, "check_arch", lambda: None) + monkeypatch.setattr(setup_module, "create_dirs", lambda: None) + monkeypatch.setattr(setup_module, "create_env", lambda: None) + monkeypatch.setattr(setup_module, "check_deps", lambda: None) + monkeypatch.setattr(setup_module, "init_database", lambda: None) + # Force the non-interactive branch so the test never blocks on a prompt. + monkeypatch.setenv("ODYSSEUS_SKIP_ADMIN_PROMPT", "1") + + try: + setup_module.main() + finally: + # load_dotenv writes real os.environ entries; undo so sibling tests + # don't inherit them. + os.environ.pop("ODYSSEUS_ADMIN_USER", None) + os.environ.pop("ODYSSEUS_ADMIN_PASSWORD", None) + + data = json.loads(auth_path.read_text(encoding="utf-8")) + assert "presetuser" in data["users"], data + assert bcrypt.checkpw( + b"fromenvfile12345", data["users"]["presetuser"]["password_hash"].encode() + ), "admin password from .env was ignored; a random one was generated"