feat: auth & admin
This commit is contained in:
+55
-3
@@ -1,22 +1,74 @@
|
||||
"""Management CLI (``mcma``). Admin/seed commands land here in later steps.
|
||||
"""Management CLI (``mcma``).
|
||||
|
||||
For now it exposes ``mcma version``. ``mcma create-admin`` arrives with auth
|
||||
(plan §11 step 3).
|
||||
Commands:
|
||||
* ``mcma version`` — print the backend version.
|
||||
* ``mcma create-admin`` — create the first (or another) superuser. Private
|
||||
instance, so there is no public sign-up — bootstrap
|
||||
admins here (plan §11 step 3).
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import getpass
|
||||
|
||||
from app import __version__
|
||||
|
||||
|
||||
async def _create_admin(username: str, password: str) -> None:
|
||||
from app.application.user_service import UserService
|
||||
from app.core.security import Argon2PasswordHasher
|
||||
from app.infrastructure.db import session_scope
|
||||
from app.infrastructure.db.repositories import (
|
||||
SqlAlchemyRefreshTokenRepository,
|
||||
SqlAlchemyUserRepository,
|
||||
)
|
||||
|
||||
async with session_scope() as session:
|
||||
service = UserService(
|
||||
users=SqlAlchemyUserRepository(session),
|
||||
refresh_tokens=SqlAlchemyRefreshTokenRepository(session),
|
||||
hasher=Argon2PasswordHasher(),
|
||||
)
|
||||
user = await service.create_user(username=username, password=password, is_superuser=True)
|
||||
print(f"Created admin {user.username!r} ({user.id}).")
|
||||
|
||||
|
||||
def _cmd_create_admin(args: argparse.Namespace) -> None:
|
||||
username: str = args.username or input("Username: ").strip()
|
||||
if not username:
|
||||
raise SystemExit("Username is required.")
|
||||
password: str = args.password or getpass.getpass("Password: ")
|
||||
if len(password) < 8:
|
||||
raise SystemExit("Password must be at least 8 characters.")
|
||||
if args.password is None and getpass.getpass("Confirm password: ") != password:
|
||||
raise SystemExit("Passwords do not match.")
|
||||
|
||||
from app.domain.errors import AlreadyExistsError
|
||||
|
||||
try:
|
||||
asyncio.run(_create_admin(username, password))
|
||||
except AlreadyExistsError as exc:
|
||||
raise SystemExit(str(exc)) from exc
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(prog="mcma", description="mcma-backend management CLI")
|
||||
sub = parser.add_subparsers(dest="command")
|
||||
|
||||
sub.add_parser("version", help="Print the backend version")
|
||||
|
||||
admin = sub.add_parser("create-admin", help="Create a superuser")
|
||||
admin.add_argument("username", nargs="?", help="Username (prompted if omitted)")
|
||||
admin.add_argument(
|
||||
"--password",
|
||||
help="Password (prompted securely if omitted; avoid on shared shells)",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
if args.command == "version":
|
||||
print(__version__)
|
||||
elif args.command == "create-admin":
|
||||
_cmd_create_admin(args)
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user