222 lines
8.4 KiB
HTML
222 lines
8.4 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}Users — Website Checker{% endblock %}
|
||
{% block content %}
|
||
<div class="page-header">
|
||
<h1 class="page-title">User Management</h1>
|
||
<div class="flex gap-1 items-center">
|
||
<div class="search-wrap">
|
||
<input type="text" id="user-search" placeholder="Search users…" autocomplete="off">
|
||
<span class="search-icon">🔍</span>
|
||
</div>
|
||
<button class="btn btn-primary" onclick="openModal('modal-create-user')">+ Add User</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<table class="table table-hover">
|
||
<thead>
|
||
<tr>
|
||
<th>ID</th><th>Username</th><th>Full Name</th><th>Email</th>
|
||
<th>Role</th><th>Active</th><th>Created</th><th>Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for u in users %}
|
||
<tr>
|
||
<td>{{ u.id }}</td>
|
||
<td><strong>{{ u.username }}</strong></td>
|
||
<td>{{ u.full_name or '—' }}</td>
|
||
<td>{{ u.email or '—' }}</td>
|
||
<td><span class="badge badge-{{ 'accent' if u.role == 'admin' else 'neutral' }}">{{ u.role }}</span></td>
|
||
<td><span class="badge badge-{{ 'success' if u.is_active else 'danger' }}">{{ 'Yes' if u.is_active else 'No' }}</span></td>
|
||
<td class="text-muted">{{ u.created_at.strftime('%Y-%m-%d') if u.created_at else '—' }}</td>
|
||
<td class="actions">
|
||
<button class="btn btn-ghost btn-sm js-edit-user"
|
||
data-user="{{ u|tojson|e }}">✎ Edit</button>
|
||
{% if u.email %}
|
||
<button class="btn btn-ghost btn-sm js-send-reset"
|
||
data-id="{{ u.id }}" data-username="{{ u.username }}" data-email="{{ u.email }}"
|
||
title="Send password reset link to {{ u.email }}">✉ Reset</button>
|
||
{% endif %}
|
||
<form method="post" action="{{ url_for('admin_users.delete', user_id=u.id) }}"
|
||
style="display:inline">
|
||
<button class="btn btn-danger btn-sm js-delete-user" type="button"
|
||
data-username="{{ u.username }}">✕</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
{% else %}
|
||
<tr><td colspan="8" class="text-center text-muted">No users found.</td></tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Create User Modal -->
|
||
<div class="modal-overlay" id="modal-create-user">
|
||
<div class="modal-dialog">
|
||
<div class="modal-header">
|
||
<h3>Add User</h3>
|
||
<button class="modal-close" onclick="closeModal('modal-create-user')">✕</button>
|
||
</div>
|
||
<form method="post" action="{{ url_for('admin_users.create') }}">
|
||
<div class="modal-body">
|
||
<div class="form-group">
|
||
<label class="form-label">Username *</label>
|
||
<input class="form-control" name="username" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Full Name</label>
|
||
<input class="form-control" name="full_name">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Email</label>
|
||
<input class="form-control" type="email" name="email">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Role</label>
|
||
<select class="form-control" name="role">
|
||
<option value="user">User</option>
|
||
<option value="admin">Admin</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Password *</label>
|
||
<input class="form-control" type="password" name="password" id="create-user-pw" required>
|
||
<div class="pw-strength-wrap">
|
||
<div class="pw-strength-bar"><div class="pw-strength-fill" id="create-user-pw-fill"></div></div>
|
||
<small class="pw-strength-label" id="create-user-pw-label"></small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-ghost" onclick="closeModal('modal-create-user')">Cancel</button>
|
||
<button type="submit" class="btn btn-primary">Create User</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Edit User Modal -->
|
||
<div class="modal-overlay" id="modal-edit-user">
|
||
<div class="modal-dialog">
|
||
<div class="modal-header">
|
||
<h3>Edit User</h3>
|
||
<button class="modal-close" onclick="closeModal('modal-edit-user')">✕</button>
|
||
</div>
|
||
<form method="post" id="edit-user-form">
|
||
<div class="modal-body">
|
||
<div class="form-group">
|
||
<label class="form-label">Username *</label>
|
||
<input class="form-control" name="username" id="edit-username" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Full Name</label>
|
||
<input class="form-control" name="full_name" id="edit-full-name">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Email</label>
|
||
<input class="form-control" type="email" name="email" id="edit-email">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Role</label>
|
||
<select class="form-control" name="role" id="edit-role">
|
||
<option value="user">User</option>
|
||
<option value="admin">Admin</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Active</label>
|
||
<select class="form-control" name="is_active" id="edit-is-active">
|
||
<option value="1">Yes</option>
|
||
<option value="0">No</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">New Password (leave blank to keep current)</label>
|
||
<input class="form-control" type="password" name="password">
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-ghost" onclick="closeModal('modal-edit-user')">Cancel</button>
|
||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
/* User search */
|
||
(function() {
|
||
var timer = null;
|
||
document.getElementById('user-search').addEventListener('input', function() {
|
||
var val = this.value;
|
||
clearTimeout(timer);
|
||
timer = setTimeout(function() {
|
||
var q = val.trim().toLowerCase();
|
||
document.querySelectorAll('tbody tr').forEach(function(row) {
|
||
row.style.display = !q || row.textContent.toLowerCase().includes(q) ? '' : 'none';
|
||
});
|
||
}, 250);
|
||
});
|
||
}());
|
||
|
||
/* Delete user — safe confirm via data-attribute */
|
||
document.querySelectorAll('.js-delete-user').forEach(function(btn) {
|
||
btn.addEventListener('click', function() {
|
||
if (confirm('Delete user ' + btn.dataset.username + '?')) {
|
||
btn.closest('form').submit();
|
||
}
|
||
});
|
||
});
|
||
|
||
/* Edit user — data-user attribute avoids single-quote apostrophe breakage */
|
||
document.querySelectorAll('.js-edit-user').forEach(function(btn) {
|
||
btn.addEventListener('click', function() {
|
||
openEditUser(JSON.parse(btn.dataset.user));
|
||
});
|
||
});
|
||
|
||
function openEditUser(u) {
|
||
document.getElementById('edit-username').value = u.username;
|
||
document.getElementById('edit-full-name').value = u.full_name || '';
|
||
document.getElementById('edit-email').value = u.email || '';
|
||
document.getElementById('edit-role').value = u.role;
|
||
document.getElementById('edit-is-active').value = u.is_active ? '1' : '0';
|
||
document.getElementById('edit-user-form').action =
|
||
'/admin/users/' + u.id + '/edit';
|
||
openModal('modal-edit-user');
|
||
}
|
||
|
||
document.querySelectorAll('.js-send-reset').forEach(function(btn) {
|
||
btn.addEventListener('click', function() {
|
||
var username = btn.dataset.username;
|
||
var email = btn.dataset.email;
|
||
if (!confirm('Send a password reset link to ' + username + ' (' + email + ')?')) return;
|
||
btn.disabled = true;
|
||
btn.textContent = '…';
|
||
fetch('/admin/users/' + btn.dataset.id + '/send-reset', {
|
||
method: 'POST',
|
||
headers: { 'X-CSRFToken': getCsrfToken() },
|
||
}).then(function(r) { return r.json(); })
|
||
.then(function(d) {
|
||
if (d.ok) {
|
||
btn.textContent = '✓ Sent';
|
||
btn.style.color = 'var(--success, #16a34a)';
|
||
} else {
|
||
alert('Error: ' + d.error);
|
||
btn.disabled = false;
|
||
btn.textContent = '✉ Reset';
|
||
}
|
||
})
|
||
.catch(function() {
|
||
alert('Request failed. Please try again.');
|
||
btn.disabled = false;
|
||
btn.textContent = '✉ Reset';
|
||
});
|
||
});
|
||
});
|
||
</script>
|
||
{% endblock %}
|
||
{% block scripts %}<script>attachPasswordStrength('create-user-pw', 'create-user-pw-fill', 'create-user-pw-label');</script>{% endblock %}
|