663fde3909
Full source for loveandrosary.com: slide-based Rosary/novena/Divine Mercy Chaplet presentation tool with multi-user roles, SVG bead ring, audio uploads, donate strip, and public session profiles. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
426 lines
22 KiB
PHP
426 lines
22 KiB
PHP
<?php
|
|
/**
|
|
* admin/users.php — User management. Requires admin role.
|
|
*/
|
|
require_once __DIR__ . '/../config/db.php';
|
|
require_once __DIR__ . '/../includes/auth.php';
|
|
require_once __DIR__ . '/../includes/mailer.php';
|
|
|
|
require_role('admin');
|
|
|
|
$pdo = get_pdo();
|
|
$user = current_user();
|
|
$uid = (int)$user['id'];
|
|
$is_super = has_role('superadmin');
|
|
$site_name = get_setting('site_name', APP_NAME);
|
|
$messages = [];
|
|
$errors = [];
|
|
|
|
// ── Handle actions ───────────────────────────────────────────────────────────
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
$action = $_POST['action'] ?? '';
|
|
|
|
// ── Create user ──────────────────────────────────────────────────────────
|
|
if ($action === 'create_user') {
|
|
$new_username = trim($_POST['new_username'] ?? '');
|
|
$new_display_name = trim($_POST['new_display_name'] ?? '');
|
|
$new_email = trim($_POST['new_email'] ?? '');
|
|
$new_password = $_POST['new_password'] ?? '';
|
|
$new_role = $_POST['new_role'] ?? 'user';
|
|
$new_limit = (int)($_POST['new_rosary_limit'] ?? 1);
|
|
|
|
// Validate role — admins can't set superadmin
|
|
$allowed_roles = $is_super ? ['user','superuser','admin','superadmin'] : ['user','superuser'];
|
|
if (!in_array($new_role, $allowed_roles, true)) $new_role = 'user';
|
|
|
|
if (!preg_match('/^[a-zA-Z0-9_]{3,30}$/', $new_username)) {
|
|
$errors[] = 'Invalid username.';
|
|
} elseif (!filter_var($new_email, FILTER_VALIDATE_EMAIL)) {
|
|
$errors[] = 'Invalid email.';
|
|
} elseif (strlen($new_password) < 8) {
|
|
$errors[] = 'Password must be at least 8 characters.';
|
|
} else {
|
|
$chk = $pdo->prepare('SELECT id FROM users WHERE username=? OR email=?');
|
|
$chk->execute([$new_username, $new_email]);
|
|
if ($chk->fetch()) {
|
|
$errors[] = 'Username or email already in use.';
|
|
} else {
|
|
$hash = password_hash($new_password, PASSWORD_BCRYPT);
|
|
$pdo->prepare("
|
|
INSERT INTO users (username,email,password_hash,display_name,role,rosary_limit,email_confirmed)
|
|
VALUES (?,?,?,?,?,?,1)
|
|
")->execute([$new_username, $new_email, $hash, $new_display_name ?: $new_username, $new_role, $new_limit]);
|
|
$messages[] = "User '{$new_username}' created.";
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Update user ──────────────────────────────────────────────────────────
|
|
if ($action === 'update_user') {
|
|
$target_id = (int)($_POST['target_id'] ?? 0);
|
|
$upd_display = trim($_POST['upd_display_name'] ?? '');
|
|
$upd_email = trim($_POST['upd_email'] ?? '');
|
|
$upd_role = $_POST['upd_role'] ?? '';
|
|
$upd_limit = (int)($_POST['upd_rosary_limit'] ?? 1);
|
|
|
|
// Fetch target to check if superadmin
|
|
$tgt = $pdo->prepare('SELECT * FROM users WHERE id=?');
|
|
$tgt->execute([$target_id]);
|
|
$tgt = $tgt->fetch();
|
|
|
|
if (!$tgt) {
|
|
$errors[] = 'User not found.';
|
|
} else {
|
|
// Role protection
|
|
$allowed_roles = $is_super ? ['user','superuser','admin','superadmin'] : ['user','superuser'];
|
|
if (!in_array($upd_role, $allowed_roles, true)) $upd_role = $tgt['role'];
|
|
// Non-superadmin cannot change superadmin's role
|
|
if (!$is_super && $tgt['role'] === 'superadmin') {
|
|
$errors[] = 'Cannot modify a superadmin account.';
|
|
} else {
|
|
if (!filter_var($upd_email, FILTER_VALIDATE_EMAIL)) {
|
|
$errors[] = 'Invalid email.';
|
|
} else {
|
|
$chk = $pdo->prepare('SELECT id FROM users WHERE email=? AND id!=?');
|
|
$chk->execute([$upd_email, $target_id]);
|
|
if ($chk->fetch()) {
|
|
$errors[] = 'Email already in use by another account.';
|
|
} else {
|
|
$pdo->prepare('UPDATE users SET display_name=?,email=?,role=?,rosary_limit=? WHERE id=?')
|
|
->execute([$upd_display, $upd_email, $upd_role, $upd_limit, $target_id]);
|
|
$messages[] = "User '{$tgt['username']}' updated.";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Reset password ───────────────────────────────────────────────────────
|
|
if ($action === 'reset_password') {
|
|
$target_id = (int)($_POST['target_id'] ?? 0);
|
|
$new_pass = $_POST['new_pass'] ?? '';
|
|
|
|
if (strlen($new_pass) < 8) {
|
|
$errors[] = 'New password must be at least 8 characters.';
|
|
} else {
|
|
$hash = password_hash($new_pass, PASSWORD_BCRYPT);
|
|
$pdo->prepare('UPDATE users SET password_hash=? WHERE id=?')->execute([$hash, $target_id]);
|
|
$messages[] = 'Password reset successfully.';
|
|
}
|
|
}
|
|
|
|
// ── Resend confirmation email ─────────────────────────────────────────────
|
|
if ($action === 'resend_confirmation') {
|
|
$target_id = (int)($_POST['target_id'] ?? 0);
|
|
$tgt = $pdo->prepare('SELECT * FROM users WHERE id = ? AND email_confirmed = 0');
|
|
$tgt->execute([$target_id]);
|
|
$tgt = $tgt->fetch();
|
|
|
|
if (!$tgt) {
|
|
$errors[] = 'User not found or already confirmed.';
|
|
} else {
|
|
$smtp_host = get_setting('smtp_host');
|
|
if ($smtp_host === '') {
|
|
// No SMTP — confirm directly
|
|
$pdo->prepare('UPDATE users SET email_confirmed = 1, confirm_token = NULL WHERE id = ?')
|
|
->execute([$target_id]);
|
|
$messages[] = "No SMTP configured — {$tgt['username']} has been confirmed directly.";
|
|
} else {
|
|
$token = bin2hex(random_bytes(32));
|
|
$site_url = rtrim(get_setting('site_url'), '/');
|
|
$site_name = get_setting('site_name', APP_NAME);
|
|
$link = $site_url . '/confirm?token=' . urlencode($token);
|
|
$disp = $tgt['display_name'] ?: $tgt['username'];
|
|
|
|
$pdo->prepare('UPDATE users SET confirm_token = ? WHERE id = ?')
|
|
->execute([$token, $target_id]);
|
|
|
|
$body_html = "
|
|
<h2 style='margin-top:0;color:#1e3a5f'>Confirm your email</h2>
|
|
<p>Hello, <strong>" . htmlspecialchars($disp) . "</strong>!</p>
|
|
<p>An administrator has resent your confirmation email for {$site_name}. Click the button below to confirm your email address and activate your account:</p>
|
|
<p style='text-align:center;margin:28px 0'>
|
|
<a href='" . htmlspecialchars($link) . "' style='display:inline-block;background:#1e3a5f;color:#fff;padding:12px 28px;border-radius:6px;text-decoration:none;font-weight:600'>Confirm Email</a>
|
|
</p>
|
|
<p style='color:#6b7280;font-size:13px'>Or copy this link: " . htmlspecialchars($link) . "</p>
|
|
<p style='color:#6b7280;font-size:13px'>If you did not register, ignore this email.</p>
|
|
";
|
|
$html = email_template('Confirm your email — ' . $site_name, $body_html);
|
|
$ok = send_email($tgt['email'], $disp, 'Confirm your email — ' . $site_name, $html);
|
|
|
|
if ($ok) {
|
|
$messages[] = "Confirmation email resent to {$tgt['email']}.";
|
|
} else {
|
|
$errors[] = 'Failed to send email. Check your SMTP settings.';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Delete user ──────────────────────────────────────────────────────────
|
|
if ($action === 'delete_user') {
|
|
$target_id = (int)($_POST['target_id'] ?? 0);
|
|
if ($target_id === $uid) {
|
|
$errors[] = 'You cannot delete your own account.';
|
|
} else {
|
|
$tgt = $pdo->prepare('SELECT role,username FROM users WHERE id=?');
|
|
$tgt->execute([$target_id]);
|
|
$tgt = $tgt->fetch();
|
|
if ($tgt && $tgt['role'] === 'superadmin' && !$is_super) {
|
|
$errors[] = 'Cannot delete a superadmin account.';
|
|
} elseif ($tgt) {
|
|
$pdo->prepare('DELETE FROM users WHERE id=?')->execute([$target_id]);
|
|
$messages[] = "User '{$tgt['username']}' deleted.";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Load all users with rosary counts ────────────────────────────────────────
|
|
$users = $pdo->query("
|
|
SELECT u.*,
|
|
(SELECT COUNT(*) FROM sessions WHERE user_id=u.id AND occasion != 'novena_deceased') +
|
|
(SELECT COUNT(*) FROM novena_groups WHERE user_id=u.id)
|
|
AS rosary_count
|
|
FROM users u
|
|
ORDER BY u.created_at DESC
|
|
")->fetchAll();
|
|
|
|
$role_labels = ['superadmin'=>'Superadmin','admin'=>'Admin','superuser'=>'Superuser','user'=>'User'];
|
|
$role_colors = [
|
|
'superadmin' => '#dc2626',
|
|
'admin' => '#ea580c',
|
|
'superuser' => '#2563eb',
|
|
'user' => '#6b7280',
|
|
];
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<link rel="icon" type="image/svg+xml" href="<?= BASE_URL ?>/favicon.svg">
|
|
<title>Users — <?= htmlspecialchars($site_name) ?></title>
|
|
<link rel="stylesheet" href="<?= BASE_URL ?>/assets/css/setup.css">
|
|
<style>
|
|
.role-badge{display:inline-block;padding:2px 10px;border-radius:99px;font-size:12px;font-weight:700;color:#fff}
|
|
.edit-panel{display:none;background:#f9fafb;border:1px solid #e5e7eb;border-radius:8px;padding:20px;margin-top:12px}
|
|
.edit-panel.open{display:block}
|
|
.user-row td{vertical-align:top;padding:12px 10px}
|
|
.mini-form{display:flex;flex-wrap:wrap;gap:12px;align-items:flex-end}
|
|
.mini-form .form-group{margin:0;min-width:160px}
|
|
.mini-form label{font-size:12px;font-weight:600;display:block;margin-bottom:4px;color:#374151}
|
|
.mini-form input,.mini-form select{font-size:14px;padding:6px 10px;border:1px solid #d1d5db;border-radius:6px;width:100%}
|
|
.create-panel{background:#fff;border:1px solid #e5e7eb;border-radius:8px;padding:24px;margin-bottom:28px;display:none}
|
|
.create-panel.open{display:block}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="admin-container">
|
|
|
|
<header class="admin-header">
|
|
<h1>✝ <?= htmlspecialchars($site_name) ?></h1>
|
|
<div class="header-actions">
|
|
<a href="<?= BASE_URL ?>/" class="btn btn-ghost" style="font-size:13px">← View Site</a>
|
|
<a href="<?= BASE_URL ?>/admin/" class="btn btn-ghost">Dashboard</a>
|
|
<?php if ($is_super): ?>
|
|
<a href="<?= BASE_URL ?>/admin/settings.php" class="btn btn-ghost">Settings</a>
|
|
<?php endif; ?>
|
|
<a href="<?= BASE_URL ?>/admin/profile.php" class="btn btn-ghost"><?= htmlspecialchars($user['display_name'] ?: $user['username']) ?></a>
|
|
<a href="<?= BASE_URL ?>/logout" class="btn btn-ghost">Logout</a>
|
|
</div>
|
|
</header>
|
|
|
|
<main>
|
|
<?php foreach ($messages as $m): ?>
|
|
<div class="alert alert-success">✓ <?= htmlspecialchars($m) ?></div>
|
|
<?php endforeach; ?>
|
|
<?php foreach ($errors as $e): ?>
|
|
<div class="alert alert-error"><?= htmlspecialchars($e) ?></div>
|
|
<?php endforeach; ?>
|
|
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px">
|
|
<h2 style="margin:0">Users (<?= count($users) ?>)</h2>
|
|
<button class="btn btn-primary" onclick="toggleCreate()">+ Create User</button>
|
|
</div>
|
|
|
|
<!-- Create User Panel -->
|
|
<div class="create-panel" id="create-panel">
|
|
<h3 style="margin:0 0 16px">Create New User</h3>
|
|
<form method="post">
|
|
<input type="hidden" name="action" value="create_user">
|
|
<div class="mini-form">
|
|
<div class="form-group">
|
|
<label>Username *</label>
|
|
<input type="text" name="new_username" pattern="[a-zA-Z0-9_]{3,30}" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Display Name</label>
|
|
<input type="text" name="new_display_name" maxlength="100">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Email *</label>
|
|
<input type="email" name="new_email" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Password * (min 8)</label>
|
|
<input type="password" name="new_password" minlength="8" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Role</label>
|
|
<select name="new_role">
|
|
<option value="user">User</option>
|
|
<option value="superuser">Superuser</option>
|
|
<option value="admin">Admin</option>
|
|
<?php if ($is_super): ?>
|
|
<option value="superadmin">Superadmin</option>
|
|
<?php endif; ?>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Rosary Limit (-1 = unlimited)</label>
|
|
<input type="number" name="new_rosary_limit" value="1" min="-1">
|
|
</div>
|
|
<div>
|
|
<button type="submit" class="btn btn-primary">Create</button>
|
|
<button type="button" class="btn btn-ghost" onclick="toggleCreate()">Cancel</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Users Table -->
|
|
<div class="sessions-table-wrap">
|
|
<table class="sessions-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Username</th>
|
|
<th>Display Name</th>
|
|
<th>Email</th>
|
|
<th>Role</th>
|
|
<th>Limit</th>
|
|
<th>Rosaries</th>
|
|
<th>Joined</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($users as $u): ?>
|
|
<tr class="user-row">
|
|
<td>
|
|
<strong><?= htmlspecialchars($u['username']) ?></strong>
|
|
<?= !$u['email_confirmed'] ? '<br><span style="font-size:11px;color:#d97706">Unconfirmed</span>' : '' ?>
|
|
</td>
|
|
<td><?= htmlspecialchars($u['display_name'] ?? '') ?></td>
|
|
<td style="font-size:13px"><?= htmlspecialchars($u['email']) ?></td>
|
|
<td>
|
|
<span class="role-badge" style="background:<?= $role_colors[$u['role']] ?? '#6b7280' ?>">
|
|
<?= htmlspecialchars($role_labels[$u['role']] ?? $u['role']) ?>
|
|
</span>
|
|
</td>
|
|
<td><?= $u['rosary_limit'] < 0 ? '∞' : (int)$u['rosary_limit'] ?></td>
|
|
<td><?= (int)$u['rosary_count'] ?></td>
|
|
<td style="font-size:12px"><?= date('M j, Y', strtotime($u['created_at'])) ?></td>
|
|
<td class="actions">
|
|
<?php $can_edit = $is_super || ($u['role'] !== 'superadmin' && $u['role'] !== 'admin'); ?>
|
|
<?php if ($can_edit || (int)$u['id'] !== $uid): ?>
|
|
<button class="btn btn-sm btn-secondary"
|
|
onclick="toggleEdit(<?= $u['id'] ?>)">Edit</button>
|
|
<?php endif; ?>
|
|
<?php if (!$u['email_confirmed']): ?>
|
|
<form method="post" style="display:inline">
|
|
<input type="hidden" name="action" value="resend_confirmation">
|
|
<input type="hidden" name="target_id" value="<?= $u['id'] ?>">
|
|
<button type="submit" class="btn btn-sm"
|
|
style="background:#d97706;color:#fff;border:none"
|
|
title="Resend confirmation email to <?= htmlspecialchars($u['email']) ?>">
|
|
Resend Email
|
|
</button>
|
|
</form>
|
|
<?php endif; ?>
|
|
<?php if ((int)$u['id'] !== $uid && ($is_super || $u['role'] !== 'superadmin')): ?>
|
|
<form method="post" style="display:inline"
|
|
onsubmit="return confirm('Delete user <?= htmlspecialchars(addslashes($u['username'])) ?>? Their sessions will remain.')">
|
|
<input type="hidden" name="action" value="delete_user">
|
|
<input type="hidden" name="target_id" value="<?= $u['id'] ?>">
|
|
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
|
|
</form>
|
|
<?php endif; ?>
|
|
|
|
<!-- Edit Panel -->
|
|
<div class="edit-panel" id="edit-<?= $u['id'] ?>">
|
|
<form method="post" style="margin-bottom:16px">
|
|
<input type="hidden" name="action" value="update_user">
|
|
<input type="hidden" name="target_id" value="<?= $u['id'] ?>">
|
|
<div class="mini-form">
|
|
<div class="form-group">
|
|
<label>Display Name</label>
|
|
<input type="text" name="upd_display_name" maxlength="100"
|
|
value="<?= htmlspecialchars($u['display_name'] ?? '') ?>">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Email</label>
|
|
<input type="email" name="upd_email" required
|
|
value="<?= htmlspecialchars($u['email']) ?>">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Role</label>
|
|
<select name="upd_role">
|
|
<option value="user" <?= $u['role']==='user' ?'selected':'' ?>>User</option>
|
|
<option value="superuser" <?= $u['role']==='superuser' ?'selected':'' ?>>Superuser</option>
|
|
<option value="admin" <?= $u['role']==='admin' ?'selected':'' ?>>Admin</option>
|
|
<?php if ($is_super): ?>
|
|
<option value="superadmin" <?= $u['role']==='superadmin'?'selected':'' ?>>Superadmin</option>
|
|
<?php endif; ?>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Rosary Limit (-1=unlimited)</label>
|
|
<input type="number" name="upd_rosary_limit" min="-1"
|
|
value="<?= (int)$u['rosary_limit'] ?>">
|
|
</div>
|
|
<div>
|
|
<button type="submit" class="btn btn-primary btn-sm">Save</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
<form method="post">
|
|
<input type="hidden" name="action" value="reset_password">
|
|
<input type="hidden" name="target_id" value="<?= $u['id'] ?>">
|
|
<div class="mini-form">
|
|
<div class="form-group">
|
|
<label>New Password (min 8)</label>
|
|
<input type="password" name="new_pass" minlength="8" required>
|
|
</div>
|
|
<div>
|
|
<button type="submit" class="btn btn-secondary btn-sm"
|
|
onclick="return confirm('Reset this user\'s password?')">
|
|
Reset Password
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<script>
|
|
function toggleCreate() {
|
|
var p = document.getElementById('create-panel');
|
|
p.classList.toggle('open');
|
|
}
|
|
function toggleEdit(id) {
|
|
var p = document.getElementById('edit-' + id);
|
|
p.classList.toggle('open');
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|