Add user management to admin dashboard and refine button specificity
- Implement admin user management interface (, , ) with async CRUD and password updating - Add Users tab link to admin navigation across and - Use in to lower specificity for native button resets, allowing modifier classes like to take precedence
This commit is contained in:
1 parent
803d02d758
commit
e68b24e1c7
6 files changed
+569
-1
No files matched your search
@@ -79,6 +79,7 @@ $today = date('Y-m-d');
|
||||
<div class="admin-nav-links">
|
||||
<a href="index.php" class="active">Announcements</a>
|
||||
<a href="menu.php">Menu</a>
|
||||
<a href="users.php">Users</a>
|
||||
</div>
|
||||
<div class="admin-user">
|
||||
<span>Logged in as <strong><?= htmlspecialchars($user['username']) ?></strong></span>
|
||||
|
||||
@@ -165,6 +165,7 @@ $menuGrouped = get_full_menu(false);
|
||||
<div class="admin-nav-links">
|
||||
<a href="index.php">Announcements</a>
|
||||
<a href="menu.php" class="active">Menu</a>
|
||||
<a href="users.php">Users</a>
|
||||
</div>
|
||||
<div class="admin-user">
|
||||
<span>Logged in as <strong><?= htmlspecialchars($user['username']) ?></strong></span>
|
||||
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
// User Management admin page: create users, change passwords, delete users.
|
||||
// CSRF token and the current user's id are handed off from PHP via
|
||||
// <body data-csrf-token="..." data-current-user-id="...">.
|
||||
|
||||
const csrfToken = document.body.dataset.csrfToken;
|
||||
const currentUserId = document.body.dataset.currentUserId;
|
||||
|
||||
const userModal = document.getElementById('modal-user-backdrop');
|
||||
const passwordModal = document.getElementById('modal-password-backdrop');
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text ?? '';
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
async function postForm(formData) {
|
||||
const response = await fetch('users.php', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// Add User modal
|
||||
function openUserModal() {
|
||||
document.getElementById('form-user').reset();
|
||||
userModal.classList.add('is-open');
|
||||
document.getElementById('user-field-username').focus();
|
||||
}
|
||||
|
||||
function closeUserModal() {
|
||||
userModal.classList.remove('is-open');
|
||||
}
|
||||
|
||||
// Change Password modal
|
||||
function openPasswordModal(userId, username) {
|
||||
const isSelf = String(userId) === currentUserId;
|
||||
|
||||
document.getElementById('form-password').reset();
|
||||
document.getElementById('password-field-user-id').value = userId;
|
||||
document.getElementById('modal-password-title').textContent = isSelf
|
||||
? '🔑 Change Your Password'
|
||||
: `🔑 Change Password for ${username}`;
|
||||
|
||||
document.getElementById('password-current-group').style.display = isSelf ? '' : 'none';
|
||||
|
||||
passwordModal.classList.add('is-open');
|
||||
(isSelf ? document.getElementById('password-field-current') : document.getElementById('password-field-new')).focus();
|
||||
}
|
||||
|
||||
function closePasswordModal() {
|
||||
passwordModal.classList.remove('is-open');
|
||||
}
|
||||
|
||||
document.getElementById('btn-new-user').addEventListener('click', () => openUserModal());
|
||||
document.getElementById('btn-close-user-modal').addEventListener('click', closeUserModal);
|
||||
document.getElementById('btn-cancel-user').addEventListener('click', closeUserModal);
|
||||
document.getElementById('btn-close-password-modal').addEventListener('click', closePasswordModal);
|
||||
document.getElementById('btn-cancel-password').addEventListener('click', closePasswordModal);
|
||||
|
||||
userModal.addEventListener('click', (e) => { if (e.target === userModal) closeUserModal(); });
|
||||
passwordModal.addEventListener('click', (e) => { if (e.target === passwordModal) closePasswordModal(); });
|
||||
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeUserModal();
|
||||
closePasswordModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Create user
|
||||
document.getElementById('form-user').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const saveBtn = document.getElementById('btn-save-user');
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.textContent = 'Saving...';
|
||||
|
||||
try {
|
||||
const res = await postForm(new FormData(e.target));
|
||||
if (res.success) {
|
||||
closeUserModal();
|
||||
insertUserRow(res.user);
|
||||
} else {
|
||||
alert(res.error || 'Failed to create user.');
|
||||
}
|
||||
} catch {
|
||||
alert('Network error. Please try again.');
|
||||
} finally {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = 'Create User';
|
||||
}
|
||||
});
|
||||
|
||||
// Change password
|
||||
document.getElementById('form-password').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const saveBtn = document.getElementById('btn-save-password');
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.textContent = 'Saving...';
|
||||
|
||||
try {
|
||||
const res = await postForm(new FormData(e.target));
|
||||
if (res.success) {
|
||||
closePasswordModal();
|
||||
alert('Password updated.');
|
||||
} else {
|
||||
alert(res.error || 'Failed to update password.');
|
||||
}
|
||||
} catch {
|
||||
alert('Network error. Please try again.');
|
||||
} finally {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = 'Update Password';
|
||||
}
|
||||
});
|
||||
|
||||
// Delegate table actions: Change Password, Delete
|
||||
document.getElementById('users-tbody').addEventListener('click', async (e) => {
|
||||
const { target } = e;
|
||||
|
||||
if (target.classList.contains('btn-change-password')) {
|
||||
openPasswordModal(target.dataset.id, target.dataset.username);
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.classList.contains('btn-delete-user')) {
|
||||
if (!confirm('Are you sure you want to delete this user? This cannot be undone.')) return;
|
||||
const row = target.closest('tr');
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('action', 'delete_user');
|
||||
fd.append('user_id', target.dataset.id);
|
||||
fd.append('csrf_token', csrfToken);
|
||||
|
||||
try {
|
||||
const data = await postForm(fd);
|
||||
if (data.success && row) {
|
||||
row.style.transition = 'opacity 0.2s ease, transform 0.2s ease';
|
||||
row.style.opacity = '0';
|
||||
row.style.transform = 'translateX(-10px)';
|
||||
setTimeout(() => row.remove(), 200);
|
||||
} else if (!data.success) {
|
||||
alert(data.error || 'Failed to delete user.');
|
||||
}
|
||||
} catch {
|
||||
alert('Network error. Please try again.');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function insertUserRow(user) {
|
||||
const tbody = document.getElementById('users-tbody');
|
||||
|
||||
const tr = document.createElement('tr');
|
||||
tr.id = `user-row-${user.id}`;
|
||||
tr.dataset.userId = user.id;
|
||||
|
||||
// data-username is set via dataset below, never interpolated into the
|
||||
// HTML string — escapeHtml() only escapes for text-content positions,
|
||||
// not attribute values, so it can't safely build an attribute string.
|
||||
tr.innerHTML = `
|
||||
<td><strong>${escapeHtml(user.username)}</strong></td>
|
||||
<td>${escapeHtml(user.role.charAt(0).toUpperCase() + user.role.slice(1))}</td>
|
||||
<td class="font-mono item-sort-cell">${escapeHtml(user.created_at)}</td>
|
||||
<td><div class="admin-table-actions">
|
||||
<button type="button" class="btn btn-outline btn-sm btn-change-password" data-id="${user.id}">Change Password</button>
|
||||
<button type="button" class="btn btn-danger btn-sm btn-delete-user" data-id="${user.id}">Delete</button>
|
||||
</div></td>
|
||||
`;
|
||||
tr.querySelector('.btn-change-password').dataset.username = user.username;
|
||||
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
+316
@@ -0,0 +1,316 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../inc/auth.php';
|
||||
require_once __DIR__ . '/../inc/users.php';
|
||||
|
||||
require_auth();
|
||||
$user = get_logged_in_user();
|
||||
|
||||
const MIN_PASSWORD_LENGTH = 8;
|
||||
|
||||
// Role is a label only right now — every account has identical access
|
||||
// regardless of what's stored here. This just keeps the value to a known,
|
||||
// displayable set instead of trusting arbitrary POST input.
|
||||
const ALLOWED_ROLES = ['admin', 'manager', 'staff'];
|
||||
const DEFAULT_ROLE = 'admin';
|
||||
|
||||
/**
|
||||
* Safely read a trimmed string from a POST payload, guarding against
|
||||
* unexpected non-string input (e.g. a spoofed `name[]=` array field).
|
||||
*/
|
||||
function post_str(array $data, string $key, string $default = ''): string {
|
||||
$value = $data[$key] ?? $default;
|
||||
return is_string($value) ? trim($value) : $default;
|
||||
}
|
||||
|
||||
function post_role(array $data): string {
|
||||
$role = post_str($data, 'role', DEFAULT_ROLE);
|
||||
return in_array($role, ALLOWED_ROLES, true) ? $role : DEFAULT_ROLE;
|
||||
}
|
||||
|
||||
function validate_username(string $username): ?string {
|
||||
if ($username === '') {
|
||||
return 'Username is required.';
|
||||
}
|
||||
if (strlen($username) < 3 || strlen($username) > 32) {
|
||||
return 'Username must be between 3 and 32 characters.';
|
||||
}
|
||||
if (!preg_match('/^[a-zA-Z0-9_.-]+$/', $username)) {
|
||||
return 'Username can only contain letters, numbers, and . _ -';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function handle_create_user(array $post, array $currentUser): array {
|
||||
$username = post_str($post, 'username');
|
||||
$password = post_str($post, 'password');
|
||||
$confirm = post_str($post, 'confirm_password');
|
||||
|
||||
if ($error = validate_username($username)) {
|
||||
return ['success' => false, 'error' => $error];
|
||||
}
|
||||
if (username_exists($username)) {
|
||||
return ['success' => false, 'error' => 'That username is already taken.'];
|
||||
}
|
||||
if (strlen($password) < MIN_PASSWORD_LENGTH) {
|
||||
return ['success' => false, 'error' => 'Password must be at least ' . MIN_PASSWORD_LENGTH . ' characters.'];
|
||||
}
|
||||
if ($password !== $confirm) {
|
||||
return ['success' => false, 'error' => 'Passwords do not match.'];
|
||||
}
|
||||
|
||||
$newId = create_user($username, $password, post_role($post));
|
||||
return ['success' => true, 'user' => get_user_by_id($newId)];
|
||||
}
|
||||
|
||||
function handle_change_password(array $post, array $currentUser): array {
|
||||
$targetId = (int) ($post['user_id'] ?? 0);
|
||||
$target = $targetId > 0 ? get_user_by_id($targetId) : null;
|
||||
if (!$target) {
|
||||
return ['success' => false, 'error' => 'User not found.'];
|
||||
}
|
||||
|
||||
$newPassword = post_str($post, 'new_password');
|
||||
$confirm = post_str($post, 'confirm_password');
|
||||
$isSelf = $targetId === (int) $currentUser['id'];
|
||||
|
||||
if ($isSelf) {
|
||||
$currentPassword = post_str($post, 'current_password');
|
||||
if (!verify_user_password($targetId, $currentPassword)) {
|
||||
return ['success' => false, 'error' => 'Your current password is incorrect.'];
|
||||
}
|
||||
}
|
||||
|
||||
if (strlen($newPassword) < MIN_PASSWORD_LENGTH) {
|
||||
return ['success' => false, 'error' => 'Password must be at least ' . MIN_PASSWORD_LENGTH . ' characters.'];
|
||||
}
|
||||
if ($newPassword !== $confirm) {
|
||||
return ['success' => false, 'error' => 'Passwords do not match.'];
|
||||
}
|
||||
|
||||
set_user_password($targetId, $newPassword);
|
||||
return ['success' => true];
|
||||
}
|
||||
|
||||
function handle_delete_user(array $post, array $currentUser): array {
|
||||
$targetId = (int) ($post['user_id'] ?? 0);
|
||||
if ($targetId <= 0 || !get_user_by_id($targetId)) {
|
||||
return ['success' => false, 'error' => 'User not found.'];
|
||||
}
|
||||
if ($targetId === (int) $currentUser['id']) {
|
||||
return ['success' => false, 'error' => "You can't delete your own account while logged in as it."];
|
||||
}
|
||||
if (count_users() <= 1) {
|
||||
return ['success' => false, 'error' => 'At least one user account must remain.'];
|
||||
}
|
||||
|
||||
delete_user($targetId);
|
||||
return ['success' => true];
|
||||
}
|
||||
|
||||
function handle_users_action(string $action, array $post, array $currentUser): array {
|
||||
return match ($action) {
|
||||
'create_user' => handle_create_user($post, $currentUser),
|
||||
'change_password' => handle_change_password($post, $currentUser),
|
||||
'delete_user' => handle_delete_user($post, $currentUser),
|
||||
default => ['success' => false, 'error' => 'Unknown action'],
|
||||
};
|
||||
}
|
||||
|
||||
// Handle AJAX actions (JSON response)
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$csrfToken = $_POST['csrf_token'] ?? null;
|
||||
if (!verify_csrf_token(is_string($csrfToken) ? $csrfToken : null)) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'error' => 'Session expired or invalid security token. Please refresh.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$action = is_string($_POST['action'] ?? null) ? $_POST['action'] : '';
|
||||
echo json_encode(handle_users_action($action, $_POST, $user));
|
||||
exit;
|
||||
}
|
||||
|
||||
$users = get_all_users();
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>User Management | The Old Fitz Admin</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,400;0,9..144,600;1,9..144,400&family=Karla:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
</head>
|
||||
|
||||
<body data-csrf-token="<?= htmlspecialchars(get_csrf_token(), ENT_QUOTES, 'UTF-8') ?>" data-current-user-id="<?= (int) $user['id'] ?>">
|
||||
|
||||
<nav class="admin-nav">
|
||||
<div class="container admin-nav-inner">
|
||||
<a href="index.php" class="admin-brand">
|
||||
<img src="../assets/knot.png" alt="" class="knot">
|
||||
<span>The Old Fitz Admin</span>
|
||||
</a>
|
||||
<div class="admin-nav-links">
|
||||
<a href="index.php">Announcements</a>
|
||||
<a href="menu.php">Menu</a>
|
||||
<a href="users.php" class="active">Users</a>
|
||||
</div>
|
||||
<div class="admin-user">
|
||||
<span>Logged in as <strong><?= htmlspecialchars($user['username']) ?></strong></span>
|
||||
<a href="../" target="_blank" rel="noopener">View Public Site ↗</a>
|
||||
<a href="logout.php">Sign Out</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="dashboard-layout">
|
||||
<div class="container">
|
||||
<div class="dash-header">
|
||||
<p class="eyebrow">Access</p>
|
||||
<h1>User Management</h1>
|
||||
</div>
|
||||
|
||||
<div class="admin-toolbar">
|
||||
<button type="button" class="btn btn-primary btn-sm" id="btn-new-user">+ Add User</button>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="admin-table-wrap">
|
||||
<table class="admin-table" id="users-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Role</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="users-tbody">
|
||||
<?php foreach ($users as $row): ?>
|
||||
<tr id="user-row-<?= (int) $row['id'] ?>" data-user-id="<?= (int) $row['id'] ?>">
|
||||
<td>
|
||||
<strong><?= htmlspecialchars($row['username']) ?></strong>
|
||||
<?php if ((int) $row['id'] === (int) $user['id']): ?>
|
||||
<span class="muted"> (you)</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= htmlspecialchars(ucfirst($row['role'])) ?></td>
|
||||
<td class="font-mono item-sort-cell"><?= htmlspecialchars($row['created_at']) ?></td>
|
||||
<td>
|
||||
<div class="admin-table-actions">
|
||||
<button type="button" class="btn btn-outline btn-sm btn-change-password"
|
||||
data-id="<?= (int) $row['id'] ?>"
|
||||
data-username="<?= htmlspecialchars($row['username']) ?>">
|
||||
Change Password
|
||||
</button>
|
||||
<?php if ((int) $row['id'] !== (int) $user['id']): ?>
|
||||
<button type="button" class="btn btn-danger btn-sm btn-delete-user" data-id="<?= (int) $row['id'] ?>">
|
||||
Delete
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Modal Backdrop: Add User -->
|
||||
<div class="admin-modal-backdrop" id="modal-user-backdrop" role="dialog" aria-modal="true" aria-labelledby="modal-user-title">
|
||||
<div class="admin-modal">
|
||||
<div class="modal-header">
|
||||
<h2 id="modal-user-title">✨ Add User</h2>
|
||||
<button type="button" class="modal-close" aria-label="Close dialog" id="btn-close-user-modal">×</button>
|
||||
</div>
|
||||
<form id="form-user">
|
||||
<input type="hidden" name="action" value="create_user">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="form-group full-width">
|
||||
<label for="user-field-username">Username</label>
|
||||
<input type="text" id="user-field-username" name="username" class="form-control" placeholder="e.g. jsmith" autocomplete="off" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group full-width">
|
||||
<label for="user-field-role">Role</label>
|
||||
<select id="user-field-role" name="role" class="form-control">
|
||||
<option value="admin">Admin</option>
|
||||
<option value="manager">Manager</option>
|
||||
<option value="staff">Staff</option>
|
||||
</select>
|
||||
<p class="muted field-hint">For your own reference only — every account has the same access.</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="user-field-password">Password</label>
|
||||
<input type="password" id="user-field-password" name="password" class="form-control" autocomplete="new-password" minlength="8" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="user-field-confirm">Confirm Password</label>
|
||||
<input type="password" id="user-field-confirm" name="confirm_password" class="form-control" autocomplete="new-password" minlength="8" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary" id="btn-save-user">Create User</button>
|
||||
<button type="button" class="btn btn-outline" id="btn-cancel-user">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Backdrop: Change Password -->
|
||||
<div class="admin-modal-backdrop" id="modal-password-backdrop" role="dialog" aria-modal="true" aria-labelledby="modal-password-title">
|
||||
<div class="admin-modal">
|
||||
<div class="modal-header">
|
||||
<h2 id="modal-password-title">🔑 Change Password</h2>
|
||||
<button type="button" class="modal-close" aria-label="Close dialog" id="btn-close-password-modal">×</button>
|
||||
</div>
|
||||
<form id="form-password">
|
||||
<input type="hidden" name="action" value="change_password">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
||||
<input type="hidden" name="user_id" id="password-field-user-id" value="0">
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="form-group full-width" id="password-current-group">
|
||||
<label for="password-field-current">Current Password</label>
|
||||
<input type="password" id="password-field-current" name="current_password" class="form-control" autocomplete="current-password">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password-field-new">New Password</label>
|
||||
<input type="password" id="password-field-new" name="new_password" class="form-control" autocomplete="new-password" minlength="8" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password-field-confirm">Confirm New Password</label>
|
||||
<input type="password" id="password-field-confirm" name="confirm_password" class="form-control" autocomplete="new-password" minlength="8" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary" id="btn-save-password">Update Password</button>
|
||||
<button type="button" class="btn btn-outline" id="btn-cancel-password">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="users.js" defer></script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Admin user management helpers.
|
||||
*
|
||||
* Every read here explicitly selects columns and leaves password_hash
|
||||
* out, so it can never accidentally end up in a JSON response or a
|
||||
* data-* attribute the way a full row dump would.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/db.php';
|
||||
|
||||
function get_all_users(): array {
|
||||
$pdo = get_db();
|
||||
$stmt = $pdo->query("SELECT id, username, role, created_at FROM users ORDER BY username ASC");
|
||||
return $stmt->fetchAll() ?: [];
|
||||
}
|
||||
|
||||
function get_user_by_id(int $id): ?array {
|
||||
$pdo = get_db();
|
||||
$stmt = $pdo->prepare("SELECT id, username, role, created_at FROM users WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$res = $stmt->fetch();
|
||||
return $res ?: null;
|
||||
}
|
||||
|
||||
function count_users(): int {
|
||||
$pdo = get_db();
|
||||
return (int) $pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();
|
||||
}
|
||||
|
||||
function username_exists(string $username, ?int $excludeId = null): bool {
|
||||
$pdo = get_db();
|
||||
if ($excludeId !== null) {
|
||||
$stmt = $pdo->prepare("SELECT 1 FROM users WHERE username = ? AND id != ?");
|
||||
$stmt->execute([$username, $excludeId]);
|
||||
} else {
|
||||
$stmt = $pdo->prepare("SELECT 1 FROM users WHERE username = ?");
|
||||
$stmt->execute([$username]);
|
||||
}
|
||||
return (bool) $stmt->fetchColumn();
|
||||
}
|
||||
|
||||
function create_user(string $username, string $password, string $role): int {
|
||||
$pdo = get_db();
|
||||
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||
$stmt = $pdo->prepare("INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)");
|
||||
$stmt->execute([$username, $hash, $role]);
|
||||
return (int) $pdo->lastInsertId();
|
||||
}
|
||||
|
||||
function set_user_password(int $id, string $newPassword): bool {
|
||||
$pdo = get_db();
|
||||
$hash = password_hash($newPassword, PASSWORD_DEFAULT);
|
||||
$stmt = $pdo->prepare("UPDATE users SET password_hash = ? WHERE id = ?");
|
||||
return $stmt->execute([$hash, $id]);
|
||||
}
|
||||
|
||||
function verify_user_password(int $id, string $password): bool {
|
||||
$pdo = get_db();
|
||||
$stmt = $pdo->prepare("SELECT password_hash FROM users WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$hash = $stmt->fetchColumn();
|
||||
return is_string($hash) && password_verify($password, $hash);
|
||||
}
|
||||
|
||||
function delete_user(int $id): bool {
|
||||
$pdo = get_db();
|
||||
$stmt = $pdo->prepare("DELETE FROM users WHERE id = ?");
|
||||
return $stmt->execute([$id]);
|
||||
}
|
||||
+4
-1
@@ -470,7 +470,10 @@
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
button.btn {
|
||||
/* :where() keeps this native-button reset at zero specificity, so
|
||||
modifier classes like .btn-primary's background can actually win
|
||||
instead of losing to button.btn's higher (0,1,1) specificity */
|
||||
:where(button.btn) {
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user