Files
Aegis/backend/app/seed.py
T
kitos 07403cbd9d
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
fix(seed): don't create a duplicate admin on restart after email migration
seed_admin()'s existence check matched on a freshly-computed email/username
identifier — after the email-unique-identifier migration backfills an
older install's admin email to its bare username (e.g. 'administrator'),
that no longer matches the '...@localhost' placeholder computed when
ADMIN_EMAIL isn't set, so every container restart created a new duplicate
admin. Now skips seeding whenever ANY admin role already exists.
2026-07-23 15:24:59 +02:00

153 lines
5.7 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"
# Skip if ANY admin already exists — not just one matching this
# specific username/email. Matching only on the computed identifier
# is fragile: an install predating ADMIN_EMAIL has a user whose
# email was migration-backfilled to its bare username (e.g.
# "administrator"), which won't match a freshly-computed
# "...@localhost" placeholder, so every restart would otherwise
# create a new duplicate admin account.
existing = db.query(User).filter(User.role == "admin").first()
# Check: existing
if existing:
# Call print()
print(f"An admin user already exists ('{existing.email}') — 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()