From ccc0b9ab0c861c31623e2dd2ff50e40f7c65489e Mon Sep 17 00:00:00 2001 From: Wes Huber Date: Mon, 1 Jun 2026 21:14:37 -0700 Subject: [PATCH] Setup: prompt for first-run admin credentials * feat(setup): prompt for admin credentials interactively on first run When setup.py runs in a terminal (TTY) without env vars set, it now asks the user to choose a username and password instead of generating a random one that scrolls off-screen. Includes confirmation prompt to catch typos. Existing behavior is preserved: - ODYSSEUS_ADMIN_USER + ODYSSEUS_ADMIN_PASSWORD env vars take priority - Non-interactive contexts (Docker, CI) still get a random password - ODYSSEUS_SKIP_ADMIN_PROMPT=1 opts out of the interactive prompt - Re-runs still skip if auth.json already exists Co-Authored-By: Claude Opus 4.6 (1M context) * fix(macos): use venv Python for pip install and uvicorn launch On PEP 668 systems (newer Homebrew Python), pip install outside a venv is rejected. The script creates a venv but then called the system $PY for pip and uvicorn. Switch to ./venv/bin/python for both. Co-Authored-By: Claude Opus 4.6 (1M context) * Revert "fix(macos): use venv Python for pip install and uvicorn launch" This reverts commit 7a1be956659d86183da2edcde2114eb363efd3e4. --------- Co-authored-by: Claude Opus 4.6 (1M context) --- setup.py | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index fe670fd..84ba322 100644 --- a/setup.py +++ b/setup.py @@ -43,6 +43,33 @@ def init_database(): print(" [ok] Database initialized") +def _prompt_admin_credentials(): + """Interactively ask for admin username and password when running in a terminal.""" + import getpass + + print() + print(" Set up your admin account:") + print(" (Press Enter to accept defaults)") + print() + + username = input(" Username [admin]: ").strip().lower() + if not username: + username = "admin" + + while True: + password = getpass.getpass(" Password: ") + if not password: + print(" Password cannot be empty.") + continue + confirm = getpass.getpass(" Confirm password: ") + if password != confirm: + print(" Passwords don't match. Try again.") + continue + break + + return username, password + + def create_default_admin(): """Create an initial admin user if none exists.""" auth_path = os.path.join(DATA_DIR, "auth.json") @@ -54,8 +81,22 @@ def create_default_admin(): import bcrypt import json - username = os.getenv("ODYSSEUS_ADMIN_USER", "admin").strip().lower() or "admin" - password = os.getenv("ODYSSEUS_ADMIN_PASSWORD") or __import__("secrets").token_urlsafe(18) + # Priority: env vars > interactive prompt > random password + username = os.getenv("ODYSSEUS_ADMIN_USER", "").strip().lower() + password = os.getenv("ODYSSEUS_ADMIN_PASSWORD", "").strip() + + if username and password: + # Both provided via env — use them directly + pass + elif sys.stdin.isatty() and not os.getenv("ODYSSEUS_SKIP_ADMIN_PROMPT"): + # Interactive terminal — ask the user + username, password = _prompt_admin_credentials() + else: + # Non-interactive (Docker, CI) — fall back to generated password + username = username or "admin" + password = password or __import__("secrets").token_urlsafe(18) + + username = username or "admin" hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() auth_data = { "users": { @@ -67,9 +108,14 @@ def create_default_admin(): } with open(auth_path, "w", encoding="utf-8") as f: json.dump(auth_data, f, indent=2) - print(f" [ok] Initial admin user created ({username})") - print(f" Temporary password: {password}") - print(f" ** Change it after first login. Set ODYSSEUS_ADMIN_PASSWORD to choose your own. **") + + if sys.stdin.isatty() and not os.getenv("ODYSSEUS_ADMIN_PASSWORD"): + print(f" [ok] Admin account created ({username})") + else: + print(f" [ok] Initial admin user created ({username})") + if not os.getenv("ODYSSEUS_ADMIN_PASSWORD"): + print(f" Temporary password: {password}") + print(f" ** Change it after first login. Set ODYSSEUS_ADMIN_PASSWORD to choose your own. **") return "created" except ImportError: print(" [warn] bcrypt not installed — skipping admin user creation") @@ -160,7 +206,7 @@ def main(): # Cleaned, action-focused final instruction strings if admin_status == "created": - print("Login with the admin username and temporary password printed above.\n") + print("Login with your admin credentials.\n") elif admin_status == "exists": print("Login with your existing admin credentials.\n") elif admin_status == "skipped":