diff --git a/backend/app/manage_users.py b/backend/app/manage_users.py new file mode 100644 index 0000000..3c4a65d --- /dev/null +++ b/backend/app/manage_users.py @@ -0,0 +1,243 @@ +"""CLI для управления пользователями панели. + +Публичная регистрация в панели закрыта — пользователей создаёт администратор +либо через этот скрипт, либо через эндпоинт /admin/users. + +Запуск внутри контейнера backend: + docker compose exec backend python -m app.manage_users <команда> [аргументы] + +Команды: + list — показать всех пользователей + create [role] — создать пользователя + role: admin | client (по умолчанию client) + activate — активировать аккаунт (is_active=True) + deactivate — деактивировать аккаунт (is_active=False) + set-role — сменить роль (admin | client) + reset-password — задать новый пароль + delete — удалить пользователя (осторожно) + +Примеры: + docker compose exec backend python -m app.manage_users list + docker compose exec backend python -m app.manage_users create user@example.com 'Str0ng!Pass' client + docker compose exec backend python -m app.manage_users set-role user@example.com admin + docker compose exec backend python -m app.manage_users deactivate user@example.com +""" + +import argparse +import getpass +import sys +from typing import Iterable + +from app import models +from app.database import SessionLocal +from app.security import hash_password + +VALID_ROLES = ("admin", "client") + + +def _normalize_email(email: str) -> str: + return email.lower().strip() + + +def list_users() -> Iterable[models.User]: + db = SessionLocal() + try: + users = db.query(models.User).order_by(models.User.created_at.desc()).all() + if not users: + print("В базе нет пользователей.") + return + + print(f"{'ID':>4} {'EMAIL':<32} {'ROLE':<7} {'ACTIVE':<6} CREATED_AT") + print("-" * 78) + for u in users: + print( + f"{u.id:>4} {u.email:<32} {u.role.value:<7} " + f"{'да' if u.is_active else 'нет':<6} {u.created_at:%Y-%m-%d %H:%M}" + ) + finally: + db.close() + + +def create_user(email: str, password: str, role: str) -> None: + if role not in VALID_ROLES: + print(f"❌ Неверная роль: {role}. Допустимо: {', '.join(VALID_ROLES)}") + sys.exit(1) + + email = _normalize_email(email) + if len(password) < 6: + print("❌ Пароль слишком короткий (минимум 6 символов).") + sys.exit(1) + + db = SessionLocal() + try: + if db.query(models.User).filter(models.User.email == email).first(): + print(f"❌ Пользователь {email} уже существует.") + sys.exit(1) + + user = models.User( + email=email, + password_hash=hash_password(password), + role=models.Role(role), + is_active=True, + ) + db.add(user) + db.commit() + db.refresh(user) + print(f"✅ Создан пользователь {email} (role={role}, id={user.id})") + finally: + db.close() + + +def set_active(email: str, active: bool) -> None: + email = _normalize_email(email) + db = SessionLocal() + try: + user = db.query(models.User).filter(models.User.email == email).first() + if not user: + print(f"❌ Пользователь {email} не найден.") + sys.exit(1) + user.is_active = active + db.commit() + state = "активирован" if active else "деактивирован" + print(f"✅ {email}: {state}") + finally: + db.close() + + +def set_role(email: str, role: str) -> None: + if role not in VALID_ROLES: + print(f"❌ Неверная роль: {role}. Допустимо: {', '.join(VALID_ROLES)}") + sys.exit(1) + + email = _normalize_email(email) + db = SessionLocal() + try: + user = db.query(models.User).filter(models.User.email == email).first() + if not user: + print(f"❌ Пользователь {email} не найден.") + sys.exit(1) + # Защита от случайного понижения единственного админа. + if user.role == models.Role.admin and role == "client": + admins = ( + db.query(models.User) + .filter(models.User.role == models.Role.admin, models.User.is_active.is_(True)) + .count() + ) + if admins <= 1: + print("❌ Нельзя понизить единственного активного администратора. Сначала назначьте другого admin.") + sys.exit(1) + user.role = models.Role(role) + db.commit() + print(f"✅ {email}: роль изменена на {role}") + finally: + db.close() + + +def reset_password(email: str, new_password: str) -> None: + if len(new_password) < 6: + print("❌ Пароль слишком короткий (минимум 6 символов).") + sys.exit(1) + + email = _normalize_email(email) + db = SessionLocal() + try: + user = db.query(models.User).filter(models.User.email == email).first() + if not user: + print(f"❌ Пользователь {email} не найден.") + sys.exit(1) + user.password_hash = hash_password(new_password) + db.commit() + print(f"✅ {email}: пароль изменён") + finally: + db.close() + + +def delete_user(email: str) -> None: + email = _normalize_email(email) + db = SessionLocal() + try: + user = db.query(models.User).filter(models.User.email == email).first() + if not user: + print(f"❌ Пользователь {email} не найден.") + sys.exit(1) + # Защита от удаления единственного админа. + if user.role == models.Role.admin: + admins = ( + db.query(models.User) + .filter(models.User.role == models.Role.admin, models.User.is_active.is_(True)) + .count() + ) + if admins <= 1: + print("❌ Нельзя удалить единственного активного администратора.") + sys.exit(1) + db.delete(user) + db.commit() + print(f"✅ Пользователь {email} удалён") + finally: + db.close() + + +def build_parser() -> argparse.ArgumentParser: + p = argparse.ArgumentParser( + prog="manage_users", + description="Управление пользователями Proxmox VPS Panel.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__, + ) + sub = p.add_subparsers(dest="command", required=True) + + sub.add_parser("list", help="Список всех пользователей") + + pc = sub.add_parser("create", help="Создать пользователя") + pc.add_argument("email") + pc.add_argument("password", nargs="?", help="Если не задан — спросит интерактивно") + pc.add_argument("role", nargs="?", default="client", choices=VALID_ROLES) + + pa = sub.add_parser("activate", help="Активировать аккаунт") + pa.add_argument("email") + + pd = sub.add_parser("deactivate", help="Деактивировать аккаунт") + pd.add_argument("email") + + pr = sub.add_parser("set-role", help="Сменить роль пользователя") + pr.add_argument("email") + pr.add_argument("role", choices=VALID_ROLES) + + pp = sub.add_parser("reset-password", help="Сменить пароль пользователя") + pp.add_argument("email") + pp.add_argument("new_password", nargs="?") + + pdel = sub.add_parser("delete", help="Удалить пользователя") + pdel.add_argument("email") + + return p + + +def main(argv: list[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + + if args.command == "list": + list_users() + elif args.command == "create": + password = args.password or getpass.getpass("Пароль для нового пользователя: ") + create_user(args.email, password, args.role) + elif args.command == "activate": + set_active(args.email, True) + elif args.command == "deactivate": + set_active(args.email, False) + elif args.command == "set-role": + set_role(args.email, args.role) + elif args.command == "reset-password": + password = args.new_password or getpass.getpass("Новый пароль: ") + reset_password(args.email, password) + elif args.command == "delete": + delete_user(args.email) + else: + parser.print_help() + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main())