feat(admin): add Users management (list/create/reset/delete/toggle), seed test users; improve UI with modern styles
This commit is contained in:
104
main.py
104
main.py
@@ -54,6 +54,110 @@ def ensure_default_admin():
|
||||
if os.environ.get('SEED_ADMIN_DISABLED') != '1':
|
||||
ensure_default_admin()
|
||||
|
||||
|
||||
# Seed two test users (non-admin) for demos
|
||||
def ensure_test_users():
|
||||
tests = [
|
||||
('test1', 'test1@example.com', 'Пользователь 1'),
|
||||
('test2', 'test2@example.com', 'Пользователь 2'),
|
||||
]
|
||||
for username, email, full_name in tests:
|
||||
u = User.get_or_none(User.username == username)
|
||||
if not u:
|
||||
User.create(
|
||||
username=username,
|
||||
email=email,
|
||||
full_name=full_name,
|
||||
password_hash=generate_password_hash(os.environ.get('TEST_USER_PASSWORD', '1234')),
|
||||
is_admin=False,
|
||||
)
|
||||
|
||||
if os.environ.get('SEED_TEST_USERS_DISABLED') != '1':
|
||||
ensure_test_users()
|
||||
|
||||
|
||||
@app.route('/admin/users', methods=['GET', 'POST'])
|
||||
@admin_required
|
||||
def manage_users():
|
||||
if request.method == 'POST':
|
||||
# Create user
|
||||
username = request.form['username'].strip()
|
||||
email = request.form['email'].strip()
|
||||
full_name = request.form.get('full_name', '').strip()
|
||||
password = request.form['password']
|
||||
is_admin_flag = request.form.get('is_admin') == 'on'
|
||||
|
||||
if not username or not email or not password:
|
||||
flash('Заполните обязательные поля', 'danger')
|
||||
return redirect(url_for('manage_users'))
|
||||
if User.select().where((User.username == username) | (User.email == email)).exists():
|
||||
flash('Пользователь с таким логином или email уже существует', 'danger')
|
||||
return redirect(url_for('manage_users'))
|
||||
|
||||
User.create(
|
||||
username=username,
|
||||
email=email,
|
||||
full_name=full_name or None,
|
||||
password_hash=generate_password_hash(password),
|
||||
is_admin=is_admin_flag,
|
||||
)
|
||||
flash('Пользователь создан', 'success')
|
||||
return redirect(url_for('manage_users'))
|
||||
|
||||
users = User.select().order_by(User.id)
|
||||
return render_template('admin/users.html', users=users, title='Пользователи')
|
||||
|
||||
|
||||
@app.route('/admin/users/<int:user_id>/reset_password', methods=['POST'])
|
||||
@admin_required
|
||||
def admin_reset_password(user_id):
|
||||
user = User.get_or_none(User.id == user_id)
|
||||
if not user:
|
||||
flash('Пользователь не найден', 'danger')
|
||||
return redirect(url_for('manage_users'))
|
||||
new_password = request.form.get('new_password')
|
||||
if not new_password:
|
||||
flash('Укажите новый пароль', 'danger')
|
||||
return redirect(url_for('manage_users'))
|
||||
user.password_hash = generate_password_hash(new_password)
|
||||
user.save()
|
||||
flash('Пароль обновлён', 'success')
|
||||
return redirect(url_for('manage_users'))
|
||||
|
||||
|
||||
@app.route('/admin/users/<int:user_id>/toggle_admin', methods=['POST'])
|
||||
@admin_required
|
||||
def admin_toggle_admin(user_id):
|
||||
if current_user.id == user_id:
|
||||
flash('Нельзя менять свои собственные права', 'warning')
|
||||
return redirect(url_for('manage_users'))
|
||||
user = User.get_or_none(User.id == user_id)
|
||||
if not user:
|
||||
flash('Пользователь не найден', 'danger')
|
||||
return redirect(url_for('manage_users'))
|
||||
user.is_admin = not user.is_admin
|
||||
user.save()
|
||||
flash('Права обновлены', 'success')
|
||||
return redirect(url_for('manage_users'))
|
||||
|
||||
|
||||
@app.route('/admin/users/<int:user_id>/delete', methods=['POST'])
|
||||
@admin_required
|
||||
def admin_delete_user(user_id):
|
||||
if current_user.id == user_id:
|
||||
flash('Нельзя удалить самого себя', 'warning')
|
||||
return redirect(url_for('manage_users'))
|
||||
user = User.get_or_none(User.id == user_id)
|
||||
if not user:
|
||||
flash('Пользователь не найден', 'danger')
|
||||
return redirect(url_for('manage_users'))
|
||||
try:
|
||||
user.delete_instance(recursive=True)
|
||||
flash('Пользователь удалён', 'success')
|
||||
except Exception:
|
||||
flash('Не удалось удалить пользователя', 'danger')
|
||||
return redirect(url_for('manage_users'))
|
||||
|
||||
login_manager = LoginManager()
|
||||
login_manager.init_app(app)
|
||||
login_manager.login_view = 'login'
|
||||
|
||||
8
static/style.css
Normal file
8
static/style.css
Normal file
@@ -0,0 +1,8 @@
|
||||
/* Modern tweaks */
|
||||
:root { --brand-bg: #e9f6ff; }
|
||||
body { background-color: #f6f8fb; }
|
||||
.navbar { background: var(--brand-bg) !important; border-bottom: 1px solid rgba(0,0,0,0.05); }
|
||||
.card { border: 1px solid rgba(0,0,0,0.05); box-shadow: 0 2px 8px rgba(0,0,0,0.04); }
|
||||
.table thead th { font-weight: 600; color: #495057; }
|
||||
.btn { transition: all .2s ease; }
|
||||
.btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,0.08); }
|
||||
84
templates/admin/users.html
Normal file
84
templates/admin/users.html
Normal file
@@ -0,0 +1,84 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<h3 class="me-auto">Пользователи</h3>
|
||||
<a href="{{ url_for('manage_surveys') }}" class="btn btn-outline-secondary btn-sm">К опросам</a>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Создать пользователя</h5>
|
||||
<form method="post" action="{{ url_for('manage_users') }}" class="row gy-2" onsubmit="showLoading(this)">
|
||||
<div class="col-md-3">
|
||||
<input type="text" name="username" class="form-control" placeholder="Логин" required>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<input type="email" name="email" class="form-control" placeholder="Email" required>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<input type="text" name="full_name" class="form-control" placeholder="ФИО (необязательно)">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="text" name="password" class="form-control" placeholder="Пароль" required>
|
||||
</div>
|
||||
<div class="col-md-1 d-flex align-items-center">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="is_admin" id="is_admin">
|
||||
<label class="form-check-label" for="is_admin">Админ</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button id="submitBtn" type="submit" class="btn btn-primary">Создать</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Список пользователей</h5>
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Логин</th>
|
||||
<th>ФИО</th>
|
||||
<th>Email</th>
|
||||
<th>Роль</th>
|
||||
<th style="width:320px">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for u in users %}
|
||||
<tr>
|
||||
<td>{{ u.id }}</td>
|
||||
<td>{{ u.username }}</td>
|
||||
<td>{{ u.full_name or '—' }}</td>
|
||||
<td>{{ u.email }}</td>
|
||||
<td>
|
||||
{% if u.is_admin %}<span class="badge bg-danger">Админ</span>{% else %}<span class="badge bg-secondary">Пользователь</span>{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<form method="post" action="{{ url_for('admin_reset_password', user_id=u.id) }}" class="d-inline-flex align-items-center gap-2" onsubmit="showLoading(this)">
|
||||
<input type="text" name="new_password" class="form-control form-control-sm" placeholder="Новый пароль" required>
|
||||
<button class="btn btn-sm btn-outline-primary" type="submit">Сменить пароль</button>
|
||||
</form>
|
||||
<form method="post" action="{{ url_for('admin_toggle_admin', user_id=u.id) }}" class="d-inline" onsubmit="return confirm('Изменить права пользователя?');">
|
||||
<button class="btn btn-sm btn-outline-warning" type="submit">Переключить роль</button>
|
||||
</form>
|
||||
<form method="post" action="{{ url_for('admin_delete_user', user_id=u.id) }}" class="d-inline" onsubmit="return confirm('Удалить пользователя {{ u.username }}?');">
|
||||
<button class="btn btn-sm btn-outline-danger" type="submit" {% if u.id == current_user.id %}disabled{% endif %}>Удалить</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
<style>
|
||||
body {
|
||||
background-color: #f8f9fa;
|
||||
@@ -50,6 +51,11 @@
|
||||
</button>
|
||||
<div class="collapse navbar-collapse justify-content-end" id="navbarNav">
|
||||
<ul class="navbar-nav">
|
||||
{% if current_user.is_authenticated and current_user.is_admin %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if request.path.startswith('/admin/users') %} active{% endif %}" href="{{ url_for('manage_users') }}">Пользователи</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if request.path.startswith('/quiz') %} active{% endif %}" href="{{ url_for('choose_survey') }}">Опросы</a>
|
||||
</li>
|
||||
|
||||
Reference in New Issue
Block a user