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:
58
setup.py
58
setup.py
@@ -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,9 +108,14 @@ 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)
|
||||||
print(f" [ok] Initial admin user created ({username})")
|
|
||||||
print(f" Temporary password: {password}")
|
if sys.stdin.isatty() and not os.getenv("ODYSSEUS_ADMIN_PASSWORD"):
|
||||||
print(f" ** Change it after first login. Set ODYSSEUS_ADMIN_PASSWORD to choose your own. **")
|
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"
|
return "created"
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print(" [warn] bcrypt not installed — skipping admin user creation")
|
print(" [warn] bcrypt not installed — skipping admin user creation")
|
||||||
@@ -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":
|
||||||
|
|||||||
Reference in New Issue
Block a user