""" TEE OFF ADMIN ACCESS BOOTSTRAP --------------------------------------------------------------------------- FUNKSJON: Oppretter eller oppdaterer én administrator uten å påvirke andre. Passord leses skjult fra terminalen, og 2FA kan genereres/roteres. STATUS: Trygg erstatning for create_admin.py når flere admins skal eksistere. --------------------------------------------------------------------------- """ import argparse import asyncio import getpass import sys import asyncpg import pyotp from passlib.hash import pbkdf2_sha256 from env_config import get_database_url DB_URL = get_database_url() def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Opprett eller oppdater en admin-bruker trygt.") parser.add_argument("--username", required=True, help="Brukernavn for admin-brukeren") parser.add_argument("--email", required=True, help="E-post for admin-brukeren") parser.add_argument( "--rotate-2fa", action="store_true", help="Generer en ny 2FA-hemmelighet selv om brukeren allerede har en", ) return parser.parse_args() async def bootstrap_admin() -> None: args = parse_args() username = args.username.strip() email = args.email.strip().lower() if not username: print("❌ Brukernavn kan ikke være tomt.") sys.exit(1) if "@" not in email: print("❌ E-postadressen ser ugyldig ut.") sys.exit(1) while True: password = getpass.getpass("Skriv inn passord: ") password_confirm = getpass.getpass("Gjenta passord: ") if password != password_confirm: print("❌ Passordene er ikke like. Prøv igjen.\n") continue if len(password) < 8: print("⚠️ Advarsel: Passordet bør være minst 8 tegn.") break password_hash = pbkdf2_sha256.hash(password) conn = None try: conn = await asyncpg.connect(DB_URL) existing = await conn.fetchrow( """ SELECT id, username, email, otp_secret FROM admins WHERE username = $1 OR email = $2 ORDER BY id ASC LIMIT 1 """, username, email, ) otp_secret = pyotp.random_base32() if (args.rotate_2fa or not existing or not existing["otp_secret"]) else existing["otp_secret"] if existing: await conn.execute( """ UPDATE admins SET username = $1, email = $2, password_hash = $3, otp_secret = $4 WHERE id = $5 """, username, email, password_hash, otp_secret, existing["id"], ) action = "oppdatert" else: await conn.execute( """ INSERT INTO admins (username, email, password_hash, otp_secret) VALUES ($1, $2, $3, $4) """, username, email, password_hash, otp_secret, ) action = "opprettet" except asyncpg.UniqueViolationError: print("❌ Brukernavn eller e-post er allerede i bruk av en annen admin.") sys.exit(1) except Exception as exc: print(f"❌ Kunne ikke bootstrappe admin-brukeren: {type(exc).__name__}") sys.exit(1) finally: if conn is not None: await conn.close() provisioning_uri = pyotp.TOTP(otp_secret).provisioning_uri( name=email or username, issuer_name="TeeOff.no", ) print("\n✅ ADMIN BRUKER KLAR") print("-" * 50) print(f"Brukeren '{username}' er {action}.") print("Passordet ble satt skjult i terminalen.") print("Legg inn denne 2FA-hemmeligheten i authenticator-appen:") print(f"2FA-nøkkel: {otp_secret}") print("Alternativt kan du bruke provisioning URI:") print(provisioning_uri) print("-" * 50 + "\n") if __name__ == "__main__": try: asyncio.run(bootstrap_admin()) except KeyboardInterrupt: print("\nAvbrutt.") sys.exit(0)