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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>
This commit is contained in:
Wes Huber
2026-06-01 21:14:37 -07:00
committed by GitHub
parent 5268a546bc
commit ccc0b9ab0c

View File

@@ -43,6 +43,33 @@ def init_database():
print(" [ok] Database initialized") 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(): def create_default_admin():
"""Create an initial admin user if none exists.""" """Create an initial admin user if none exists."""
auth_path = os.path.join(DATA_DIR, "auth.json") auth_path = os.path.join(DATA_DIR, "auth.json")
@@ -54,8 +81,22 @@ def create_default_admin():
import bcrypt import bcrypt
import json import json
username = os.getenv("ODYSSEUS_ADMIN_USER", "admin").strip().lower() or "admin" # Priority: env vars > interactive prompt > random password
password = os.getenv("ODYSSEUS_ADMIN_PASSWORD") or __import__("secrets").token_urlsafe(18) 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() hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
auth_data = { auth_data = {
"users": { "users": {
@@ -67,7 +108,12 @@ def create_default_admin():
} }
with open(auth_path, "w", encoding="utf-8") as f: with open(auth_path, "w", encoding="utf-8") as f:
json.dump(auth_data, f, indent=2) json.dump(auth_data, f, indent=2)
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})") print(f" [ok] Initial admin user created ({username})")
if not os.getenv("ODYSSEUS_ADMIN_PASSWORD"):
print(f" Temporary password: {password}") print(f" Temporary password: {password}")
print(f" ** Change it after first login. Set ODYSSEUS_ADMIN_PASSWORD to choose your own. **") print(f" ** Change it after first login. Set ODYSSEUS_ADMIN_PASSWORD to choose your own. **")
return "created" return "created"
@@ -160,7 +206,7 @@ def main():
# Cleaned, action-focused final instruction strings # Cleaned, action-focused final instruction strings
if admin_status == "created": 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": elif admin_status == "exists":
print("Login with your existing admin credentials.\n") print("Login with your existing admin credentials.\n")
elif admin_status == "skipped": elif admin_status == "skipped":