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>
276 lines
14 KiB
PHP
276 lines
14 KiB
PHP
<?php
|
|
/**
|
|
* admin/settings.php — Site settings. Requires superadmin role.
|
|
*/
|
|
require_once __DIR__ . '/../config/db.php';
|
|
require_once __DIR__ . '/../includes/auth.php';
|
|
require_once __DIR__ . '/../includes/mailer.php';
|
|
require_once __DIR__ . '/../includes/csrf.php';
|
|
|
|
require_role('superadmin');
|
|
|
|
$user = current_user();
|
|
$site_name = get_setting('site_name', APP_NAME);
|
|
$message = '';
|
|
$error = '';
|
|
|
|
// Save settings
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
csrf_verify();
|
|
$action = $_POST['action'] ?? 'save';
|
|
|
|
if ($action === 'save') {
|
|
// smtp_pass is handled separately: the form always renders it blank
|
|
// (see below), so a blank submission means "leave it unchanged",
|
|
// not "clear it".
|
|
$keys = ['site_name','site_url','smtp_host','smtp_port','smtp_user','smtp_from','smtp_from_name',
|
|
'donate_enabled','donate_type','donate_handle','donate_label'];
|
|
foreach ($keys as $k) {
|
|
if (isset($_POST[$k])) {
|
|
set_setting($k, trim($_POST[$k]));
|
|
}
|
|
}
|
|
if (!empty($_POST['smtp_pass'])) {
|
|
set_setting('smtp_pass', trim($_POST['smtp_pass']));
|
|
}
|
|
$message = 'Settings saved.';
|
|
$site_name = get_setting('site_name', APP_NAME); // refresh
|
|
}
|
|
|
|
if ($action === 'test_email') {
|
|
$to = $user['email'];
|
|
$tname = $user['display_name'] ?: $user['username'];
|
|
$body = email_template(
|
|
'Test Email — ' . $site_name,
|
|
"<h2 style='margin-top:0;color:#1e3a5f'>Test Email</h2>
|
|
<p>Hello, <strong>" . htmlspecialchars($tname) . "</strong>!</p>
|
|
<p>This is a test email from your {$site_name} installation. If you received this, your SMTP settings are working correctly.</p>"
|
|
);
|
|
$ok = send_email($to, $tname, 'Test Email — ' . $site_name, $body);
|
|
if ($ok) {
|
|
$message = 'Test email sent to ' . htmlspecialchars($to);
|
|
} else {
|
|
$error = 'Failed to send test email. Check your SMTP settings and server error log.';
|
|
}
|
|
}
|
|
}
|
|
|
|
$settings = [
|
|
'site_name' => get_setting('site_name', 'Rosary Presenter'),
|
|
'site_url' => get_setting('site_url'),
|
|
'smtp_host' => get_setting('smtp_host'),
|
|
'smtp_port' => get_setting('smtp_port', '587'),
|
|
'smtp_user' => get_setting('smtp_user'),
|
|
'smtp_pass_set' => get_setting('smtp_pass') !== '',
|
|
'smtp_from' => get_setting('smtp_from'),
|
|
'smtp_from_name' => get_setting('smtp_from_name', 'Rosary Presenter'),
|
|
'donate_enabled' => get_setting('donate_enabled', '0'),
|
|
'donate_type' => get_setting('donate_type', 'custom'),
|
|
'donate_handle' => get_setting('donate_handle', ''),
|
|
'donate_label' => get_setting('donate_label', ''),
|
|
];
|
|
?>
|
|
<!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>Settings — <?= htmlspecialchars($site_name) ?></title>
|
|
<link rel="stylesheet" href="<?= BASE_URL ?>/assets/css/setup.css">
|
|
<style>
|
|
.settings-section { background:#fff;border:1px solid #e5e7eb;border-radius:8px;padding:28px;margin-bottom:24px }
|
|
.settings-section h3 { margin:0 0 20px;font-size:16px;font-weight:700;color:#1e3a5f;border-bottom:2px solid #e5e7eb;padding-bottom:10px }
|
|
.pass-wrap { position:relative }
|
|
.pass-wrap input { padding-right:80px }
|
|
.pass-toggle { position:absolute;right:10px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;font-size:12px;color:#6b7280;font-weight:600 }
|
|
</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/builder.php" class="btn btn-ghost">✒ Builder</a>
|
|
<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/" class="btn btn-ghost">Dashboard</a>
|
|
<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 ($message): ?>
|
|
<div class="alert alert-success">✓ <?= htmlspecialchars($message) ?></div>
|
|
<?php endif; ?>
|
|
<?php if ($error): ?>
|
|
<div class="alert alert-error"><?= htmlspecialchars($error) ?></div>
|
|
<?php endif; ?>
|
|
|
|
<h2 style="margin-bottom:24px">Site Settings</h2>
|
|
|
|
<form method="post">
|
|
<?= csrf_field() ?>
|
|
<input type="hidden" name="action" value="save">
|
|
|
|
<div class="settings-section">
|
|
<h3>General</h3>
|
|
<div class="form-group">
|
|
<label for="site_name">Site Name</label>
|
|
<input type="text" id="site_name" name="site_name"
|
|
value="<?= htmlspecialchars($settings['site_name']) ?>" required>
|
|
<p class="help-text">Displayed in the browser tab, emails, and the site header.</p>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="site_url">Site URL</label>
|
|
<input type="url" id="site_url" name="site_url"
|
|
placeholder="https://yourdomain.com"
|
|
value="<?= htmlspecialchars($settings['site_url']) ?>">
|
|
<p class="help-text">Used for links in emails. Include protocol, no trailing slash.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="settings-section">
|
|
<h3>SMTP Email</h3>
|
|
<p class="help-text" style="margin-top:0;margin-bottom:20px">
|
|
Leave SMTP Host blank to use PHP's built-in <code>mail()</code> function.
|
|
For Gmail: host=smtp.gmail.com, port=587, use an App Password.
|
|
</p>
|
|
<div class="form-grid">
|
|
<div class="form-group">
|
|
<label for="smtp_host">SMTP Host</label>
|
|
<input type="text" id="smtp_host" name="smtp_host"
|
|
placeholder="smtp.gmail.com"
|
|
value="<?= htmlspecialchars($settings['smtp_host']) ?>">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="smtp_port">SMTP Port</label>
|
|
<input type="number" id="smtp_port" name="smtp_port"
|
|
placeholder="587" min="1" max="65535"
|
|
value="<?= htmlspecialchars($settings['smtp_port']) ?>">
|
|
<p class="help-text">587 for STARTTLS, 465 for SSL.</p>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="smtp_user">SMTP Username</label>
|
|
<input type="text" id="smtp_user" name="smtp_user"
|
|
autocomplete="off"
|
|
value="<?= htmlspecialchars($settings['smtp_user']) ?>">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="smtp_pass">SMTP Password</label>
|
|
<div class="pass-wrap">
|
|
<input type="password" id="smtp_pass" name="smtp_pass"
|
|
autocomplete="new-password"
|
|
placeholder="<?= $settings['smtp_pass_set'] ? '•••••••• (leave blank to keep current)' : '' ?>">
|
|
<button type="button" class="pass-toggle" onclick="togglePass()">Show</button>
|
|
</div>
|
|
<p class="help-text">
|
|
<?= $settings['smtp_pass_set'] ? '✓ A password is currently set. Leave blank to keep it.' : 'No password set.' ?>
|
|
</p>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="smtp_from">From Email</label>
|
|
<input type="email" id="smtp_from" name="smtp_from"
|
|
placeholder="noreply@yourdomain.com"
|
|
value="<?= htmlspecialchars($settings['smtp_from']) ?>">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="smtp_from_name">From Name</label>
|
|
<input type="text" id="smtp_from_name" name="smtp_from_name"
|
|
value="<?= htmlspecialchars($settings['smtp_from_name']) ?>">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="settings-section">
|
|
<h3>Donate Button</h3>
|
|
<p class="help-text" style="margin-top:0;margin-bottom:20px">
|
|
Show a small donation strip on the public home page. Choose a service, enter your handle or URL, and enable it when ready.
|
|
</p>
|
|
<div class="form-group">
|
|
<label style="display:flex;align-items:center;gap:10px;cursor:pointer">
|
|
<input type="checkbox" name="donate_enabled" value="1"
|
|
id="donate_enabled"
|
|
<?= $settings['donate_enabled'] ? 'checked' : '' ?>
|
|
style="width:18px;height:18px">
|
|
<span>Enable donate strip on public pages</span>
|
|
</label>
|
|
</div>
|
|
<div class="form-grid" id="donate-fields">
|
|
<div class="form-group">
|
|
<label for="donate_type">Service</label>
|
|
<select id="donate_type" name="donate_type" class="form-input" onchange="updateDonateHint()">
|
|
<option value="paypal" <?= $settings['donate_type'] === 'paypal' ? 'selected' : '' ?>>PayPal (paypal.me)</option>
|
|
<option value="venmo" <?= $settings['donate_type'] === 'venmo' ? 'selected' : '' ?>>Venmo</option>
|
|
<option value="buymeacoffee" <?= $settings['donate_type'] === 'buymeacoffee' ? 'selected' : '' ?>>Buy Me a Coffee</option>
|
|
<option value="custom" <?= $settings['donate_type'] === 'custom' ? 'selected' : '' ?>>Custom URL</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="donate_handle" id="donate_handle_label">Handle / URL</label>
|
|
<input type="text" id="donate_handle" name="donate_handle"
|
|
value="<?= htmlspecialchars($settings['donate_handle']) ?>"
|
|
placeholder="">
|
|
<p class="help-text" id="donate_hint"></p>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="donate_label">Button Label <span style="font-weight:400;color:#6b7280">(optional)</span></label>
|
|
<input type="text" id="donate_label" name="donate_label"
|
|
value="<?= htmlspecialchars($settings['donate_label']) ?>"
|
|
placeholder="Leave blank for default">
|
|
<p class="help-text">Overrides the default label for the selected service.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-actions">
|
|
<button type="submit" class="btn btn-primary">Save Settings</button>
|
|
</div>
|
|
</form>
|
|
|
|
<div class="settings-section" style="margin-top:24px">
|
|
<h3>Test Email</h3>
|
|
<p class="help-text">Send a test email to <strong><?= htmlspecialchars($user['email']) ?></strong> to verify your SMTP settings.</p>
|
|
<form method="post">
|
|
<?= csrf_field() ?>
|
|
<input type="hidden" name="action" value="test_email">
|
|
<button type="submit" class="btn btn-secondary">Send Test Email</button>
|
|
</form>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<script>
|
|
var donateHints = {
|
|
paypal: { label: 'PayPal.me username', hint: 'Enter your PayPal.me handle, e.g. YourName — link becomes paypal.me/YourName' },
|
|
venmo: { label: 'Venmo username', hint: 'Enter your Venmo handle without @, e.g. YourName — link becomes venmo.com/u/YourName' },
|
|
buymeacoffee: { label: 'Buy Me a Coffee username', hint: 'Enter your buymeacoffee.com handle, e.g. YourName' },
|
|
custom: { label: 'Full URL', hint: 'Enter the complete URL, e.g. https://example.com/donate' },
|
|
};
|
|
|
|
function updateDonateHint() {
|
|
var type = document.getElementById('donate_type').value;
|
|
var info = donateHints[type] || donateHints.custom;
|
|
document.getElementById('donate_handle_label').textContent = info.label;
|
|
document.getElementById('donate_hint').textContent = info.hint;
|
|
}
|
|
updateDonateHint();
|
|
|
|
function togglePass() {
|
|
var inp = document.getElementById('smtp_pass');
|
|
var btn = inp.nextElementSibling;
|
|
if (inp.type === 'password') {
|
|
inp.type = 'text';
|
|
btn.textContent = 'Hide';
|
|
} else {
|
|
inp.type = 'password';
|
|
btn.textContent = 'Show';
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|