504dfc52f5
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled
Login is now by email, not username. username still exists internally (JWT sub claim, audit logs, Jira actor attribution, SSO provisioning all still key off it) but is now always kept equal to email everywhere a user is created or their email changes — never a separately-chosen value. - User.email is now unique + NOT NULL (migration b067 backfills any missing/blank email from username first, so existing rows — notably the seeded admin, which historically had none — never violate it). - /auth/login and the (unused but updated for consistency) authenticate_user() now query by email. - create_user (legacy, unreferenced but kept) and create_user_without_password both derive username from email. - update_user keeps username in sync when email changes, and rejects duplicate emails. - seed.py reads ADMIN_EMAIL (new env var, wired through install.sh and docker-compose.prod.yml) for the initial admin; falls back to an email-shaped ADMIN_USERNAME or a placeholder that's flagged for the operator to fix. - admin_config.py's import bundle now matches/creates users by email, skipping (not crashing on) entries with no email. - sso_service.py always sets username = email for SSO-provisioned users. - LoginPage/auth.ts updated to email input/copy (wire field name stays 'username' — that's the OAuth2PasswordRequestForm spec, not the value).
147 lines
5.3 KiB
Python
147 lines
5.3 KiB
Python
"""Seed script — creates the initial admin user if it does not already exist.
|
|
|
|
On first run the admin credentials are generated securely:
|
|
- Email (the unique login identifier) is read from ``ADMIN_EMAIL`` env var.
|
|
Falls back to ``ADMIN_USERNAME`` if it's already email-shaped, otherwise
|
|
to a placeholder that MUST be changed before this account can receive
|
|
any webhook email (password reset, notifications, etc).
|
|
- Password is read from ``ADMIN_PASSWORD`` env var. When the variable is
|
|
**not set**, a cryptographically random 16-character password is generated
|
|
automatically and printed to the startup logs so the operator can copy it.
|
|
|
|
Usage:
|
|
python -m app.seed
|
|
"""
|
|
|
|
# Import os
|
|
import os
|
|
|
|
# Import secrets
|
|
import secrets
|
|
|
|
# Import string
|
|
import string
|
|
|
|
# Import hash_password from app.auth
|
|
from app.auth import hash_password
|
|
|
|
# Import SessionLocal from app.database
|
|
from app.database import SessionLocal
|
|
|
|
# Import User from app.models.user
|
|
from app.models.user import User
|
|
|
|
# Characters for auto-generated passwords (alphanumeric + safe symbols)
|
|
_PW_ALPHABET = string.ascii_letters + string.digits + "!@#$%&*-_+"
|
|
|
|
|
|
# Define function _generate_password
|
|
def _generate_password(length: int = 16) -> str:
|
|
"""Return a cryptographically random password of *length* characters."""
|
|
# Return "".join(secrets.choice(_PW_ALPHABET) for _ in range(length))
|
|
return "".join(secrets.choice(_PW_ALPHABET) for _ in range(length))
|
|
|
|
|
|
# Define function seed_admin
|
|
def seed_admin() -> None:
|
|
"""Create the initial admin user when it is missing.
|
|
|
|
Reads ``ADMIN_USERNAME`` and ``ADMIN_PASSWORD`` from the environment.
|
|
If ``ADMIN_PASSWORD`` is empty or unset a secure random password is
|
|
generated and displayed in the logs.
|
|
"""
|
|
# Assign db = SessionLocal()
|
|
db = SessionLocal()
|
|
# Attempt the following; catch errors below
|
|
try:
|
|
# Assign admin_username = os.environ.get("ADMIN_USERNAME", "admin").strip() or "admin"
|
|
admin_username = os.environ.get("ADMIN_USERNAME", "admin").strip() or "admin"
|
|
|
|
# Email is the unique login identifier — prefer ADMIN_EMAIL, then an
|
|
# already email-shaped ADMIN_USERNAME, then a placeholder that needs
|
|
# fixing later (Users page) before this account can receive email.
|
|
admin_email = os.environ.get("ADMIN_EMAIL", "").strip()
|
|
if not admin_email:
|
|
admin_email = admin_username if "@" in admin_username else f"{admin_username}@localhost"
|
|
|
|
# Assign existing = db.query(User).filter(User.email == admin_email).first()
|
|
existing = db.query(User).filter(User.email == admin_email).first()
|
|
# Check: existing
|
|
if existing:
|
|
# Call print()
|
|
print(f"Admin user '{admin_email}' already exists — skipping.")
|
|
# Return control to caller
|
|
return
|
|
|
|
# Assign admin_password = os.environ.get("ADMIN_PASSWORD", "").strip()
|
|
admin_password = os.environ.get("ADMIN_PASSWORD", "").strip()
|
|
# Assign password_was_generated = False
|
|
password_was_generated = False
|
|
|
|
# Check: not admin_password
|
|
if not admin_password:
|
|
# Assign admin_password = _generate_password()
|
|
admin_password = _generate_password()
|
|
# Assign password_was_generated = True
|
|
password_was_generated = True
|
|
|
|
# Assign admin = User(
|
|
admin = User(
|
|
# Keyword argument: username
|
|
username=admin_email,
|
|
# Keyword argument: email
|
|
email=admin_email,
|
|
# Keyword argument: hashed_password
|
|
hashed_password=hash_password(admin_password),
|
|
# Keyword argument: role
|
|
role="admin",
|
|
)
|
|
# Stage new record(s) for database insertion
|
|
db.add(admin)
|
|
# Commit all pending changes to the database
|
|
db.commit()
|
|
|
|
# ── Display credentials in startup logs ──────────────────────
|
|
print()
|
|
# Call print()
|
|
print("=" * 60)
|
|
# Call print()
|
|
print(" AEGIS — Initial Admin User Created")
|
|
# Call print()
|
|
print("=" * 60)
|
|
# Call print()
|
|
print(f" Login (email) : {admin_email}")
|
|
if "@localhost" in admin_email:
|
|
print(" ** No ADMIN_EMAIL was set — using a placeholder. **")
|
|
print(" ** Update it in Users before relying on emailed links. **")
|
|
# Check: password_was_generated
|
|
if password_was_generated:
|
|
# Call print()
|
|
print(f" Password : {admin_password}")
|
|
# Call print()
|
|
print()
|
|
# Call print()
|
|
print(" ** This password was auto-generated because")
|
|
# Call print()
|
|
print(" ADMIN_PASSWORD was not set in the environment. **")
|
|
# Call print()
|
|
print(" ** Save it now — it will NOT be shown again. **")
|
|
# Fallback: handle remaining cases
|
|
else:
|
|
# Call print()
|
|
print(" Password : (set via ADMIN_PASSWORD env var)")
|
|
# Call print()
|
|
print("=" * 60)
|
|
# Call print()
|
|
print()
|
|
# Always execute this cleanup block
|
|
finally:
|
|
# Close the database session
|
|
db.close()
|
|
|
|
|
|
# Check: __name__ == "__main__"
|
|
if __name__ == "__main__":
|
|
# Call seed_admin()
|
|
seed_admin()
|