File size: 6,033 Bytes
da2ad52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7cf23f7
da2ad52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7435134
6e91d61
7435134
da2ad52
 
 
c7fa337
 
 
 
da2ad52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7cf23f7
 
 
 
 
da2ad52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6e91d61
 
 
 
 
 
da2ad52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7cf23f7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
da2ad52
 
 
 
 
 
 
7cf23f7
da2ad52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
"""
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()))