9622f9aeca
Six fixes from a codebase review: - Consolidate the ad hoc install.php + migrate_v2..v6.php chain into one canonical schema.sql (structure reference) plus a simplified install.php that creates all tables and seeds site_settings, the superadmin account, and the standard prayer library. The six migrate_v*.php scripts are deleted — their cumulative effect is now fully captured in schema.sql. - Delete the two root-level setup.php/novena_group.php files that existed only to redirect to their admin/ equivalents of the same name; confirmed unreferenced by any link or .htaccess rule. - Decouple includes/build_slides.php from data/prayers.php's implicit `global $opening, $mysteries, ...` contract. data/prayers.php now explicitly returns its arrays; build_slides.php loads them through a small memoized get_prayer_data() and destructures them by key. - Add CSRF protection (includes/csrf.php: csrf_token/csrf_field/csrf_verify) across every POST-handling endpoint — 10 form pages and 7 API endpoints — plus token wiring in the JS/inline scripts that call the FormData- and JSON-body API endpoints (builder.js, setup.js, and the inline scripts in admin/audio.php, admin/novena_group.php, and index.php). - Stop round-tripping the SMTP password in plaintext through the settings form: the field now renders blank with a "currently set" hint, and a blank submission leaves the stored password unchanged instead of clearing it. - Add login rate-limiting: users.failed_login_attempts / locked_until columns, is_locked_out()/record_login_failure()/record_login_success() helpers in includes/auth.php, and lockout handling in login.php (5 failed attempts locks the account for 15 minutes). README documents the one manual ALTER TABLE needed to add these columns to an existing production database. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
265 lines
12 KiB
PHP
265 lines
12 KiB
PHP
<?php
|
|
/**
|
|
* admin/index.php — Dashboard. Shows sessions for current user (or all for admin+).
|
|
*/
|
|
require_once __DIR__ . '/../config/db.php';
|
|
require_once __DIR__ . '/../includes/auth.php';
|
|
require_once __DIR__ . '/../includes/csrf.php';
|
|
|
|
require_auth();
|
|
|
|
$pdo = get_pdo();
|
|
$user = current_user();
|
|
$uid = (int)$user['id'];
|
|
$is_admin = has_role('admin');
|
|
$site_name = get_setting('site_name', APP_NAME);
|
|
|
|
// Handle deletions
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
csrf_verify();
|
|
if (isset($_POST['delete_group_id'])) {
|
|
$gid = (int)$_POST['delete_group_id'];
|
|
// Verify ownership or admin
|
|
if ($is_admin) {
|
|
$pdo->prepare('DELETE FROM sessions WHERE novena_group_id = ?')->execute([$gid]);
|
|
$pdo->prepare('DELETE FROM novena_groups WHERE id = ?')->execute([$gid]);
|
|
} else {
|
|
$pdo->prepare('DELETE FROM sessions WHERE novena_group_id = ? AND (SELECT user_id FROM novena_groups WHERE id = ?) = ?')
|
|
->execute([$gid, $gid, $uid]);
|
|
$pdo->prepare('DELETE FROM novena_groups WHERE id = ? AND user_id = ?')->execute([$gid, $uid]);
|
|
}
|
|
header('Location: ' . BASE_URL . '/admin/');
|
|
exit;
|
|
}
|
|
|
|
if (isset($_POST['delete_id'])) {
|
|
$id = (int)$_POST['delete_id'];
|
|
if ($is_admin) {
|
|
$pdo->prepare('DELETE FROM sessions WHERE id = ?')->execute([$id]);
|
|
} else {
|
|
$pdo->prepare('DELETE FROM sessions WHERE id = ? AND user_id = ?')->execute([$id, $uid]);
|
|
}
|
|
header('Location: ' . BASE_URL . '/admin/');
|
|
exit;
|
|
}
|
|
}
|
|
|
|
// Load sessions
|
|
if ($is_admin) {
|
|
$regular = $pdo->query("
|
|
SELECT s.*, 'session' AS row_type, u.username, u.display_name
|
|
FROM sessions s
|
|
LEFT JOIN users u ON u.id = s.user_id
|
|
WHERE s.novena_group_id IS NULL
|
|
ORDER BY s.created_at DESC
|
|
")->fetchAll();
|
|
|
|
$novena_groups = $pdo->query("
|
|
SELECT ng.*, 'novena_group' AS row_type, COUNT(s.id) AS day_count,
|
|
u.username, u.display_name
|
|
FROM novena_groups ng
|
|
LEFT JOIN sessions s ON s.novena_group_id = ng.id
|
|
LEFT JOIN users u ON u.id = ng.user_id
|
|
GROUP BY ng.id
|
|
ORDER BY ng.created_at DESC
|
|
")->fetchAll();
|
|
} else {
|
|
$regular = $pdo->prepare("
|
|
SELECT s.*, 'session' AS row_type, u.username, u.display_name
|
|
FROM sessions s
|
|
LEFT JOIN users u ON u.id = s.user_id
|
|
WHERE s.novena_group_id IS NULL AND s.user_id = ?
|
|
ORDER BY s.created_at DESC
|
|
");
|
|
$regular->execute([$uid]);
|
|
$regular = $regular->fetchAll();
|
|
|
|
$ng_stmt = $pdo->prepare("
|
|
SELECT ng.*, 'novena_group' AS row_type, COUNT(s.id) AS day_count,
|
|
u.username, u.display_name
|
|
FROM novena_groups ng
|
|
LEFT JOIN sessions s ON s.novena_group_id = ng.id
|
|
LEFT JOIN users u ON u.id = ng.user_id
|
|
WHERE ng.user_id = ?
|
|
GROUP BY ng.id
|
|
ORDER BY ng.created_at DESC
|
|
");
|
|
$ng_stmt->execute([$uid]);
|
|
$novena_groups = $ng_stmt->fetchAll();
|
|
}
|
|
|
|
$all_rows = array_merge($regular, $novena_groups);
|
|
usort($all_rows, fn($a, $b) => strcmp($b['created_at'], $a['created_at']));
|
|
|
|
$occasion_labels = [
|
|
'novena_deceased' => 'Novena for Deceased',
|
|
'divine_mercy_novena' => 'Divine Mercy Novena',
|
|
'general_rosary' => 'General Rosary',
|
|
'memorial' => 'Memorial',
|
|
'custom' => 'Custom Sequence',
|
|
];
|
|
$mystery_labels = [
|
|
'sorrowful' => 'Sorrowful',
|
|
'joyful' => 'Joyful',
|
|
'glorious' => 'Glorious',
|
|
'luminous' => 'Luminous',
|
|
'by_day_of_week' => 'By Day',
|
|
];
|
|
|
|
$novena_created = isset($_GET['novena_created']) ? (int)$_GET['novena_created'] : 0;
|
|
?>
|
|
<!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>Dashboard — <?= htmlspecialchars($site_name) ?></title>
|
|
<link rel="stylesheet" href="<?= BASE_URL ?>/assets/css/setup.css">
|
|
</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>
|
|
<?php if (has_role('superuser')): ?>
|
|
<a href="<?= BASE_URL ?>/admin/builder.php" class="btn btn-ghost">✒ Builder</a>
|
|
<?php endif; ?>
|
|
<?php if (has_role('admin')): ?>
|
|
<a href="<?= BASE_URL ?>/admin/prayers.php" class="btn btn-ghost">Prayers</a>
|
|
<a href="<?= BASE_URL ?>/admin/users.php" class="btn btn-ghost">Users</a>
|
|
<a href="<?= BASE_URL ?>/admin/audio.php" class="btn btn-ghost">Audio</a>
|
|
<?php endif; ?>
|
|
<?php if (has_role('superadmin')): ?>
|
|
<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 if ($novena_created > 0): ?>
|
|
<div class="alert alert-success">
|
|
✓ <?= $novena_created ?> novena day sessions created.
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px">
|
|
<h2 style="margin:0"><?= $is_admin ? 'All Sessions' : 'My Sessions' ?></h2>
|
|
<div style="display:flex;gap:8px">
|
|
<?php if (has_role('superuser')): ?>
|
|
<a href="<?= BASE_URL ?>/admin/builder.php" class="btn btn-secondary">✒ Build Custom</a>
|
|
<?php endif; ?>
|
|
<a href="<?= BASE_URL ?>/admin/setup.php" class="btn btn-primary">+ New Session</a>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if (empty($all_rows)): ?>
|
|
<div class="empty-state">
|
|
<p>No sessions yet.</p>
|
|
<a href="<?= BASE_URL ?>/admin/setup.php" class="btn btn-primary">Create Your First Session</a>
|
|
</div>
|
|
<?php else: ?>
|
|
<div class="sessions-table-wrap">
|
|
<table class="sessions-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Occasion</th>
|
|
<th>Mysteries</th>
|
|
<?php if ($is_admin): ?><th>Owner</th><?php endif; ?>
|
|
<th>Public</th>
|
|
<th>Created</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($all_rows as $row): ?>
|
|
|
|
<?php if ($row['row_type'] === 'novena_group'): ?>
|
|
<tr class="novena-group-row">
|
|
<td class="session-name">
|
|
<span class="novena-group-icon">✝</span>
|
|
<?= htmlspecialchars($row['name']) ?>
|
|
<?php if ($row['subject_name']): ?>
|
|
<span class="subject-name"><?= htmlspecialchars($row['subject_name']) ?></span>
|
|
<?php endif; ?>
|
|
<span class="novena-badge">9-Day Novena</span>
|
|
</td>
|
|
<td><?= ($row['mystery_set'] === 'chaplet') ? 'Divine Mercy Novena' : 'Novena for Deceased' ?></td>
|
|
<td><?= htmlspecialchars($mystery_labels[$row['mystery_set']] ?? $row['mystery_set']) ?></td>
|
|
<?php if ($is_admin): ?>
|
|
<td><?= htmlspecialchars($row['display_name'] ?: ($row['username'] ?? '—')) ?></td>
|
|
<?php endif; ?>
|
|
<td><?= $row['is_public'] ? '<span style="color:#15803d">✓ Yes</span>' : '<span style="color:#9ca3af">No</span>' ?></td>
|
|
<td><?= date('M j, Y', strtotime($row['created_at'])) ?></td>
|
|
<td class="actions">
|
|
<a href="<?= BASE_URL ?>/admin/novena_group.php?id=<?= $row['id'] ?>"
|
|
class="btn btn-sm btn-primary">View Days →</a>
|
|
<?php if ($is_admin || (int)$row['user_id'] === $uid): ?>
|
|
<form method="post" style="display:inline"
|
|
onsubmit="return confirm('Delete this entire novena (all 9 days)?')">
|
|
<?= csrf_field() ?>
|
|
<input type="hidden" name="delete_group_id" value="<?= $row['id'] ?>">
|
|
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
|
|
</form>
|
|
<?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
|
|
<?php else: ?>
|
|
<tr>
|
|
<td class="session-name">
|
|
<?= htmlspecialchars($row['name']) ?>
|
|
<?php if ($row['subject_name']): ?>
|
|
<span class="subject-name"><?= htmlspecialchars($row['subject_name']) ?></span>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td><?= htmlspecialchars($occasion_labels[$row['occasion']] ?? $row['occasion']) ?></td>
|
|
<td><?= htmlspecialchars($mystery_labels[$row['mystery_set']] ?? $row['mystery_set']) ?></td>
|
|
<?php if ($is_admin): ?>
|
|
<td><?= htmlspecialchars($row['display_name'] ?: ($row['username'] ?? '—')) ?></td>
|
|
<?php endif; ?>
|
|
<td><?= $row['is_public'] ? '<span style="color:#15803d">✓ Yes</span>' : '<span style="color:#9ca3af">No</span>' ?></td>
|
|
<td><?= date('M j, Y', strtotime($row['created_at'])) ?></td>
|
|
<td class="actions">
|
|
<?php
|
|
$slug_val = $row['slug'] ?? '';
|
|
$owner = $row['username'] ?? '';
|
|
$present_url = ($slug_val && $owner)
|
|
? BASE_URL . '/' . rawurlencode($owner) . '/' . rawurlencode($slug_val)
|
|
: BASE_URL . '/present.php?id=' . $row['id'];
|
|
?>
|
|
<a href="<?= htmlspecialchars($present_url) ?>"
|
|
target="_blank"
|
|
class="btn btn-sm btn-primary">Present</a>
|
|
<?php if ($is_admin || (int)$row['user_id'] === $uid): ?>
|
|
<?php $edit_url = ($row['occasion'] === 'custom')
|
|
? BASE_URL . '/admin/builder.php?id=' . $row['id']
|
|
: BASE_URL . '/admin/setup.php?id=' . $row['id']; ?>
|
|
<a href="<?= htmlspecialchars($edit_url) ?>"
|
|
class="btn btn-sm btn-secondary">Edit</a>
|
|
<form method="post" style="display:inline"
|
|
onsubmit="return confirm('Delete this session?')">
|
|
<?= csrf_field() ?>
|
|
<input type="hidden" name="delete_id" value="<?= $row['id'] ?>">
|
|
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
|
|
</form>
|
|
<?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
<?php endif; ?>
|
|
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?php endif; ?>
|
|
</main>
|
|
</div>
|
|
</body>
|
|
</html>
|