""" Crée ou met à jour le compte administrateur initial. Aucun utilisateur n'était créé nulle part dans le projet : le modèle `User` et la table `users` existaient depuis la migration 0b21fb6a3651 sans qu'aucun code ne les alimente. Une fois l'authentification branchée, sans ce script il devient impossible de se connecter à sa propre instance. Idempotent : relancé, il ne duplique rien et ne modifie que ce qui doit l'être. Usage: python scripts/create_admin.py python scripts/create_admin.py --email admin@jurix.cm --username admin python scripts/create_admin.py --reset-password python scripts/create_admin.py --role superadmin python scripts/create_admin.py --promouvoir # compte ordinaire deja inscrit Les valeurs par défaut viennent de .env : ADMIN_EMAIL, ADMIN_USERNAME, ADMIN_PASSWORD. Si aucun mot de passe n'est fourni ni par option ni par l'environnement, il est demandé de façon interactive — jamais de valeur par défaut en dur. """ import argparse import asyncio import getpass import os import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent)) from dotenv import load_dotenv from pydantic import ValidationError from sqlalchemy import select load_dotenv() from app.core.auth import hash_password from app.core.database import AsyncSessionLocal from app.models.user import User from app.schemas.user import UserCreate def parse_args() -> argparse.Namespace: p = argparse.ArgumentParser(description="Crée ou met à jour un administrateur JuriX") p.add_argument("--email", default=os.getenv("ADMIN_EMAIL", "admin@jurix.cm")) p.add_argument("--username", default=os.getenv("ADMIN_USERNAME", "admin")) p.add_argument("--password", default=os.getenv("ADMIN_PASSWORD")) p.add_argument( "--role", default=os.getenv("ADMIN_ROLE", "superadmin"), choices=["admin", "superadmin"], ) p.add_argument( "--reset-password", action="store_true", help="Réinitialise le mot de passe d'un compte existant", ) p.add_argument( "--promouvoir", action="store_true", help="Promeut un compte ORDINAIRE existant ; son mot de passe est alors remplacé", ) return p.parse_args() async def main() -> int: args = parse_args() password = args.password if not password: password = getpass.getpass(f"Mot de passe pour {args.email} : ") if not password: print("❌ Aucun mot de passe fourni.", file=sys.stderr) return 1 # On valide via UserCreate pour que le script et l'API appliquent # exactement la même politique de mot de passe. try: UserCreate( email=args.email, username=args.username, password=password, role=args.role, ) except ValidationError as e: # Jamais str(e) : pydantic y recopie la valeur refusee, donc le mot de # passe, en clair dans le terminal et dans la sortie du deploiement. for erreur in e.errors(include_input=False, include_url=False): champ = ".".join(str(partie) for partie in erreur["loc"]) print(f"❌ Données invalides, {champ} : {erreur['msg']}", file=sys.stderr) return 1 try: async with AsyncSessionLocal() as db: existing = ( await db.execute(select(User).where(User.email == args.email.lower())) ).scalar_one_or_none() if existing is None: db.add( User( email=args.email.lower(), username=args.username.lower(), hashed_password=hash_password(password), role=args.role, is_active=True, is_verified=True, ) ) await db.commit() print(f"✅ Compte créé : {args.email} (rôle {args.role})") return 0 # Un compte ordinaire deja inscrit a cette adresse n'est jamais promu # en silence. Le site est public avant la creation de # l'administrateur (demarrer.sh, etape 6) : quiconque s'inscrivait # sous ADMIN_EMAIL devenait superadmin, avec SON mot de passe. ordinaire = existing.role not in ("admin", "superadmin") if ordinaire and not args.promouvoir: print( f"❌ {args.email} est déjà un compte ordinaire (rôle {existing.role}) : " "il n'est pas promu sans confirmation. S'il est bien à vous, relancez avec " "--promouvoir (son mot de passe sera remplacé) ; sinon, choisissez un autre " "ADMIN_EMAIL.", file=sys.stderr, ) return 1 changed = [] if existing.role != args.role: existing.role = args.role changed.append("rôle") if not existing.is_active: existing.is_active = True changed.append("réactivation") if args.reset_password or ordinaire: existing.hashed_password = hash_password(password) changed.append("mot de passe") if changed: await db.commit() print(f"✅ Compte mis à jour : {args.email} ({', '.join(changed)})") else: print( f"ℹ️ Compte déjà conforme : {args.email} (rôle {existing.role}). " "Utilisez --reset-password pour changer le mot de passe." ) return 0 except Exception as e: print(f"❌ Base de données inaccessible : {e}", file=sys.stderr) print(" Vérifiez DATABASE_URL dans .env et que les migrations sont appliquées.", file=sys.stderr) return 1 if __name__ == "__main__": sys.exit(asyncio.run(main()))