Добавить CLI для управления пользователями (manage_users.py)
This commit is contained in:
@@ -0,0 +1,243 @@
|
|||||||
|
"""CLI для управления пользователями панели.
|
||||||
|
|
||||||
|
Публичная регистрация в панели закрыта — пользователей создаёт администратор
|
||||||
|
либо через этот скрипт, либо через эндпоинт /admin/users.
|
||||||
|
|
||||||
|
Запуск внутри контейнера backend:
|
||||||
|
docker compose exec backend python -m app.manage_users <команда> [аргументы]
|
||||||
|
|
||||||
|
Команды:
|
||||||
|
list — показать всех пользователей
|
||||||
|
create <email> <password> [role] — создать пользователя
|
||||||
|
role: admin | client (по умолчанию client)
|
||||||
|
activate <email> — активировать аккаунт (is_active=True)
|
||||||
|
deactivate <email> — деактивировать аккаунт (is_active=False)
|
||||||
|
set-role <email> <role> — сменить роль (admin | client)
|
||||||
|
reset-password <email> <new_password> — задать новый пароль
|
||||||
|
delete <email> — удалить пользователя (осторожно)
|
||||||
|
|
||||||
|
Примеры:
|
||||||
|
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())
|
||||||
Reference in New Issue
Block a user