mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-30 00:22:10 -04:00
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) <noreply@anthropic.com>
This commit is contained in:
@@ -239,6 +239,15 @@ def check_arch():
|
|||||||
def main():
|
def main():
|
||||||
print("\n=== Odysseus Setup ===\n")
|
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
|
# Fail fast with a clear message if the CPU architecture is wrong (Apple
|
||||||
# Silicon under an x86/Rosetta Python) before importing anything native.
|
# Silicon under an x86/Rosetta Python) before importing anything native.
|
||||||
check_arch()
|
check_arch()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import importlib.util
|
import importlib.util
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
from pathlib import Path
|
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"))
|
data = json.loads(auth_path.read_text(encoding="utf-8"))
|
||||||
assert "adminuser" in data["users"]
|
assert "adminuser" in data["users"]
|
||||||
assert "AdminUser" not 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"
|
||||||
|
|||||||
Reference in New Issue
Block a user