feat: admin can create users from /admin/users page
Adds a dialog form on the users list. Validates email uniqueness, password length. Creates user as active with confirmed email. System role can assign admin role; admin role can only create users. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -101,6 +101,75 @@ async def admin_user_detail(user_id: int, request: Request, db: Session = Depend
|
|||||||
return _render(request, "admin/user_detail.html", {"user": admin, "target": target})
|
return _render(request, "admin/user_detail.html", {"user": admin, "target": target})
|
||||||
|
|
||||||
|
|
||||||
|
# ── Create user ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@router.post("/users/create")
|
||||||
|
async def admin_create_user(request: Request, db: Session = Depends(get_db)):
|
||||||
|
try:
|
||||||
|
admin = _admin_user(request, db)
|
||||||
|
except Exception:
|
||||||
|
return RedirectResponse("/login", 303)
|
||||||
|
|
||||||
|
form = await request.form()
|
||||||
|
first_name = str(form.get("first_name", "")).strip()
|
||||||
|
last_name = str(form.get("last_name", "")).strip()
|
||||||
|
email = str(form.get("email", "")).strip().lower()
|
||||||
|
phone = str(form.get("phone", "")).strip() or None
|
||||||
|
password = str(form.get("password", ""))
|
||||||
|
role_str = str(form.get("role", "user"))
|
||||||
|
|
||||||
|
errors = []
|
||||||
|
if not first_name:
|
||||||
|
errors.append("Имя обязательно")
|
||||||
|
if not email:
|
||||||
|
errors.append("Email обязателен")
|
||||||
|
if not password or len(password) < 8:
|
||||||
|
errors.append("Пароль должен содержать минимум 8 символов")
|
||||||
|
if role_str not in ("user", "admin") and admin.role != UserRoleEnum.system:
|
||||||
|
role_str = "user"
|
||||||
|
|
||||||
|
if not errors:
|
||||||
|
existing = db.query(User).filter(User.email == email).first()
|
||||||
|
if existing:
|
||||||
|
errors.append("Пользователь с таким email уже существует")
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
# Re-render list page with errors and dialog open
|
||||||
|
q = db.query(User)
|
||||||
|
total = q.count()
|
||||||
|
users = q.order_by(User.created_at.desc()).limit(PAGE_SIZE).all()
|
||||||
|
return _render(request, "admin/users.html", {
|
||||||
|
"user": admin,
|
||||||
|
"users": users,
|
||||||
|
"search": "",
|
||||||
|
"status_filter": "",
|
||||||
|
"role_filter": "",
|
||||||
|
"page": 1,
|
||||||
|
"total_pages": max(1, (total + PAGE_SIZE - 1) // PAGE_SIZE),
|
||||||
|
"total": total,
|
||||||
|
"create_errors": errors,
|
||||||
|
})
|
||||||
|
|
||||||
|
try:
|
||||||
|
role = UserRoleEnum(role_str)
|
||||||
|
except ValueError:
|
||||||
|
role = UserRoleEnum.user
|
||||||
|
|
||||||
|
new_user = User(
|
||||||
|
first_name=first_name,
|
||||||
|
last_name=last_name,
|
||||||
|
email=email,
|
||||||
|
phone=phone,
|
||||||
|
password_hash=hash_password(password),
|
||||||
|
role=role,
|
||||||
|
status=UserStatusEnum.active,
|
||||||
|
is_email_confirmed=True,
|
||||||
|
)
|
||||||
|
db.add(new_user)
|
||||||
|
db.commit()
|
||||||
|
return RedirectResponse(f"/admin/users/{new_user.id}?success=saved", 303)
|
||||||
|
|
||||||
|
|
||||||
# ── View-as ───────────────────────────────────────────────────────────────────
|
# ── View-as ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@router.post("/users/{user_id}/view-as")
|
@router.post("/users/{user_id}/view-as")
|
||||||
|
|||||||
@@ -4,9 +4,62 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="d-flex justify-between align-center mb-3">
|
<div class="d-flex justify-between align-center mb-3">
|
||||||
<h1 style="font-size:1.3rem; margin:0;"><i class="bi bi-people me-2"></i>Пользователи</h1>
|
<h1 style="font-size:1.3rem; margin:0;"><i class="bi bi-people me-2"></i>Пользователи</h1>
|
||||||
<span class="text-muted small">Всего: {{ total }}</span>
|
<button onclick="document.getElementById('create-user-dialog').showModal()" class="sm">
|
||||||
|
<i class="bi bi-person-plus me-1"></i>Создать пользователя
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<dialog id="create-user-dialog">
|
||||||
|
<article>
|
||||||
|
<header>
|
||||||
|
<button aria-label="Закрыть" rel="prev" onclick="document.getElementById('create-user-dialog').close()"></button>
|
||||||
|
<h3>Создать пользователя</h3>
|
||||||
|
</header>
|
||||||
|
<form method="post" action="/admin/users/create">
|
||||||
|
<div class="row gap-2 mb-2">
|
||||||
|
<div class="col">
|
||||||
|
<label for="cu_first_name">Имя
|
||||||
|
<input type="text" id="cu_first_name" name="first_name" required>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<label for="cu_last_name">Фамилия
|
||||||
|
<input type="text" id="cu_last_name" name="last_name">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label for="cu_email">Email
|
||||||
|
<input type="email" id="cu_email" name="email" required>
|
||||||
|
</label>
|
||||||
|
<label for="cu_phone">Телефон
|
||||||
|
<input type="tel" id="cu_phone" name="phone" placeholder="+7 (999) 999-99-99">
|
||||||
|
</label>
|
||||||
|
<label for="cu_password">Пароль
|
||||||
|
<input type="password" id="cu_password" name="password" minlength="8" required>
|
||||||
|
</label>
|
||||||
|
{% if user.role == 'system' %}
|
||||||
|
<label for="cu_role">Роль
|
||||||
|
<select id="cu_role" name="role">
|
||||||
|
<option value="user" selected>Пользователь</option>
|
||||||
|
<option value="admin">Администратор</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
{% endif %}
|
||||||
|
<footer class="d-flex gap-2 justify-end">
|
||||||
|
<button type="button" class="outline secondary" onclick="document.getElementById('create-user-dialog').close()">Отмена</button>
|
||||||
|
<button type="submit">Создать</button>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</article>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
{% if create_errors %}
|
||||||
|
<div role="alert" class="alert alert-danger mb-3">
|
||||||
|
{% for e in create_errors %}<p>{{ e }}</p>{% endfor %}
|
||||||
|
</div>
|
||||||
|
<script>document.addEventListener('DOMContentLoaded', () => document.getElementById('create-user-dialog').showModal());</script>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<article class="card mb-3">
|
<article class="card mb-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="get" action="/admin/users" class="d-flex gap-2 flex-wrap align-center">
|
<form method="get" action="/admin/users" class="d-flex gap-2 flex-wrap align-center">
|
||||||
|
|||||||
Reference in New Issue
Block a user