Final release: Added theme color customization, security hardening, and UI refinements

This commit is contained in:
Michael Howard 2026-04-30 10:54:12 -05:00
parent 2a5c12a0a8
commit 87f0786a0a
15 changed files with 91 additions and 32 deletions

1
.gitignore vendored
View File

@ -17,6 +17,7 @@ temp_restore_*/
# Logs # Logs
error_log error_log
access_log access_log
*.log
# Debug and utility scripts # Debug and utility scripts
debug_db.php debug_db.php

20
.htaccess Normal file
View File

@ -0,0 +1,20 @@
# Disable Directory Listing
Options -Indexes
# Protect sensitive files
<FilesMatch "^\.">
Order allow,deny
Deny from all
</FilesMatch>
# Protect config files
<FilesMatch "(db|config)\.php">
Order allow,deny
Deny from all
</FilesMatch>
# Protect SQL files
<FilesMatch "\.sql$">
Order allow,deny
Deny from all
</FilesMatch>

View File

@ -1,4 +1,3 @@
<<<<<<< HEAD
# Podcast Server 🚀 # Podcast Server 🚀
A lightweight, professional-grade podcast hosting and management platform designed for churches and small organizations. Built with PHP 8 and MySQL, this system provides a seamless way to host audio content, track analytics, and engage listeners via push notifications. A lightweight, professional-grade podcast hosting and management platform designed for churches and small organizations. Built with PHP 8 and MySQL, this system provides a seamless way to host audio content, track analytics, and engage listeners via push notifications.
@ -84,8 +83,3 @@ Feel free to fork this project and submit pull requests to the [Linology Git](ht
## 📄 License ## 📄 License
This project is licensed under the MIT License. This project is licensed under the MIT License.
=======
# Podcast-server
A lightweight, professional-grade podcast hosting and management platform designed for churches and small organizations. Built with PHP 8 and MySQL, this system provides a seamless way to host audio content, track analytics, and engage listeners via push notifications.
>>>>>>> de0edf94110f2fe94fafd5cb8cf258079f336584

View File

@ -2,11 +2,6 @@
require_once '../includes/db.php'; require_once '../includes/db.php';
require_once '../includes/functions.php'; require_once '../includes/functions.php';
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
ini_set('log_errors', 1);
ini_set('error_log', 'backup_debug.log');
set_time_limit(300); // 5 minutes set_time_limit(300); // 5 minutes
ini_set('memory_limit', '512M'); ini_set('memory_limit', '512M');

1
admin/backups/index.php Normal file
View File

@ -0,0 +1 @@
<?php header('Location: ../../index.php'); exit;

View File

@ -11,6 +11,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
updateSetting($pdo, 'site_title', $_POST['site_title']); updateSetting($pdo, 'site_title', $_POST['site_title']);
updateSetting($pdo, 'footer_copyright', $_POST['footer_copyright']); updateSetting($pdo, 'footer_copyright', $_POST['footer_copyright']);
updateSetting($pdo, 'footer_powered_by', $_POST['footer_powered_by']); updateSetting($pdo, 'footer_powered_by', $_POST['footer_powered_by']);
updateSetting($pdo, 'theme_primary_color', $_POST['theme_primary_color']);
updateSetting($pdo, 'theme_bg_color', $_POST['theme_bg_color']);
updateSetting($pdo, 'theme_text_color', $_POST['theme_text_color']);
$success = "Site settings updated!"; $success = "Site settings updated!";
} }
@ -67,9 +70,25 @@ $site_title = getSetting($pdo, 'site_title');
<input type="text" id="footer_copyright" name="footer_copyright" value="<?php echo htmlspecialchars(getSetting($pdo, 'footer_copyright')); ?>" required> <input type="text" id="footer_copyright" name="footer_copyright" value="<?php echo htmlspecialchars(getSetting($pdo, 'footer_copyright')); ?>" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="footer_powered_by">"Powered By" Text</label> <label for="footer_powered_by">Other</label>
<input type="text" id="footer_powered_by" name="footer_powered_by" value="<?php echo htmlspecialchars(getSetting($pdo, 'footer_powered_by')); ?>" required> <input type="text" id="footer_powered_by" name="footer_powered_by" value="<?php echo htmlspecialchars(getSetting($pdo, 'footer_powered_by')); ?>" required>
</div> </div>
<h3 style="margin: 2rem 0 1rem; border-bottom: 1px solid var(--glass-border); padding-bottom: 0.5rem;">Theme Colors</h3>
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem; margin-bottom: 1.5rem;">
<div class="form-group">
<label for="theme_primary_color">Primary Color</label>
<input type="color" id="theme_primary_color" name="theme_primary_color" value="<?php echo htmlspecialchars(getSetting($pdo, 'theme_primary_color') ?: '#6366f1'); ?>" style="height: 45px; cursor: pointer;">
</div>
<div class="form-group">
<label for="theme_bg_color">Background Color</label>
<input type="color" id="theme_bg_color" name="theme_bg_color" value="<?php echo htmlspecialchars(getSetting($pdo, 'theme_bg_color') ?: '#0f172a'); ?>" style="height: 45px; cursor: pointer;">
</div>
<div class="form-group">
<label for="theme_text_color">Text Color</label>
<input type="color" id="theme_text_color" name="theme_text_color" value="<?php echo htmlspecialchars(getSetting($pdo, 'theme_text_color') ?: '#f8fafc'); ?>" style="height: 45px; cursor: pointer;">
</div>
</div>
<button type="submit" name="update_site" class="btn btn-primary" style="width: 100%;">Update Settings</button> <button type="submit" name="update_site" class="btn btn-primary" style="width: 100%;">Update Settings</button>
</form> </form>

View File

@ -127,7 +127,7 @@ nav {
.hero { .hero {
height: 400px; height: 400px;
background-size: cover; background-size: cover;
background-position: center 20%; /* Adjusted to show more of the top/center */ background-position: top center; /* Anchored to top to prevent cutting off */
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -136,6 +136,7 @@ nav {
border-radius: 0 0 40px 40px; border-radius: 0 0 40px 40px;
margin-bottom: 3rem; margin-bottom: 3rem;
background-repeat: no-repeat; background-repeat: no-repeat;
overflow: hidden; /* Prevent image bleeding */
} }
.hero::after { .hero::after {
@ -145,7 +146,7 @@ nav {
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: linear-gradient(to bottom, rgba(15, 23, 42, 0.4), var(--bg-dark)); background: linear-gradient(to bottom, rgba(15, 23, 42, 0.05), rgba(15, 23, 42, 0.3));
} }
.hero-content { .hero-content {
@ -265,6 +266,12 @@ input:focus, textarea:focus {
.hero h1 { .hero h1 {
font-size: 2.5rem; font-size: 2.5rem;
} }
.hero {
height: auto;
min-height: 180px;
padding: 2.5rem 1.5rem;
border-radius: 0 0 24px 24px;
}
.nav-links { .nav-links {
display: none; display: none;
} }

View File

@ -1,5 +1,5 @@
<footer style="text-align: center; padding: 3rem; color: var(--text-muted); border-top: 1px solid var(--glass-border); margin-top: 3rem;"> <footer style="text-align: center; padding: 3rem; color: var(--text-muted); border-top: 1px solid var(--glass-border); margin-top: 3rem;">
<p>&copy; <?php echo date('Y'); ?> <?php echo htmlspecialchars(getSetting($pdo, 'footer_copyright')); ?>. Powered by <?php echo htmlspecialchars(getSetting($pdo, 'footer_powered_by')); ?>.</p> <p>&copy; <?php echo date('Y'); ?> <?php echo parseFooterText(getSetting($pdo, 'footer_copyright')); ?>. <?php echo parseFooterText(getSetting($pdo, 'footer_powered_by')); ?></p>
</footer> </footer>
<script src="<?php echo PROJECT_ROOT_URL; ?>/assets/js/main.js"></script> <script src="<?php echo PROJECT_ROOT_URL; ?>/assets/js/main.js"></script>
</body> </body>

View File

@ -43,7 +43,14 @@ function requireRole($role) {
function logActivity($user_id, $action, $details = null) { function logActivity($user_id, $action, $details = null) {
global $pdo; global $pdo;
$username = $_SESSION['admin_username'] ?? 'GUEST'; $username = $_SESSION['admin_username'] ?? 'GUEST';
$ip = $_SERVER['REMOTE_ADDR'] ?? 'UNKNOWN'; $ip = 'UNKNOWN';
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0];
} elseif (!empty($_SERVER['REMOTE_ADDR'])) {
$ip = $_SERVER['REMOTE_ADDR'];
}
$stmt = $pdo->prepare("INSERT INTO activity_log (user_id, username, action, details, ip_address) VALUES (?, ?, ?, ?, ?)"); $stmt = $pdo->prepare("INSERT INTO activity_log (user_id, username, action, details, ip_address) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$user_id, $username, $action, $details, $ip]); $stmt->execute([$user_id, $username, $action, $details, $ip]);
@ -121,4 +128,15 @@ function uploadImage($file) {
function formatDate($date) { function formatDate($date) {
return date("F j, Y", strtotime($date)); return date("F j, Y", strtotime($date));
} }
/**
* Parse text for URLs and convert to clickable links
*/
function parseFooterText($text) {
$escaped = htmlspecialchars($text);
// Regex for URLs
$pattern = '/(https?:\/\/[^\s]+)/';
$replacement = '<a href="$1" target="_blank" style="color: var(--primary-color); text-decoration: none; font-weight: 600;">$1</a>';
return preg_replace($pattern, $replacement, $escaped);
}
?> ?>

View File

@ -12,13 +12,25 @@ $banner_image = getSetting($pdo, 'banner_image');
<title><?php echo htmlspecialchars($site_title); ?></title> <title><?php echo htmlspecialchars($site_title); ?></title>
<link rel="stylesheet" href="<?php echo PROJECT_ROOT_URL; ?>/assets/css/style.css"> <link rel="stylesheet" href="<?php echo PROJECT_ROOT_URL; ?>/assets/css/style.css">
<link rel="icon" type="image/png" href="<?php echo PROJECT_ROOT_URL; ?>/assets/uploads/images/icon.png"> <link rel="icon" type="image/png" href="<?php echo PROJECT_ROOT_URL; ?>/assets/uploads/images/icon.png">
<style>
:root {
<?php
$pColor = getSetting($pdo, 'theme_primary_color') ?: '#6366f1';
$bgColor = getSetting($pdo, 'theme_bg_color') ?: '#0f172a';
$tColor = getSetting($pdo, 'theme_text_color') ?: '#f8fafc';
?>
--primary-color: <?php echo $pColor; ?>;
--bg-dark: <?php echo $bgColor; ?>;
--text-main: <?php echo $tColor; ?>;
--primary-hover: <?php echo $pColor; ?>dd; /* Slightly lighter/transparent for hover */
}
</style>
</head> </head>
<body> <body>
<nav> <nav>
<a href="<?php echo PROJECT_ROOT_URL; ?>/" class="logo"><?php echo htmlspecialchars($site_title); ?></a> <a href="<?php echo PROJECT_ROOT_URL; ?>/" class="logo"><?php echo htmlspecialchars($site_title); ?></a>
<div class="nav-links"> <div class="nav-links">
<a href="<?php echo PROJECT_ROOT_URL; ?>/">Home</a> <a href="<?php echo PROJECT_ROOT_URL; ?>/">Home</a>
<a href="<?php echo PROJECT_ROOT_URL; ?>/subscribe.php">How to Subscribe</a>
<button id="notify-btn" class="notify-btn">🔔 Notify Me</button> <button id="notify-btn" class="notify-btn">🔔 Notify Me</button>
<?php if (isAdmin()): ?> <?php if (isAdmin()): ?>
<a href="<?php echo PROJECT_ROOT_URL; ?>/admin/dashboard.php">Dashboard</a> <a href="<?php echo PROJECT_ROOT_URL; ?>/admin/dashboard.php">Dashboard</a>

1
includes/index.php Normal file
View File

@ -0,0 +1 @@
<?php header('Location: ../index.php'); exit;

View File

@ -26,14 +26,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
logActivity(null, 'LISTENER_STARTED', "Episode: $title (Session: $session_id)"); logActivity(null, 'LISTENER_STARTED', "Episode: $title (Session: $session_id)");
} }
file_put_contents('debug.log', date('[Y-m-d H:i:s] ') . "SUCCESS: Logged play for episode $episode_id ($duration s, session: $session_id)" . PHP_EOL, FILE_APPEND);
echo json_encode(['status' => 'success']); echo json_encode(['status' => 'success']);
} catch (PDOException $e) { } catch (PDOException $e) {
file_put_contents('debug.log', date('[Y-m-d H:i:s] ') . "DB ERROR: " . $e->getMessage() . PHP_EOL, FILE_APPEND);
echo json_encode(['status' => 'error', 'message' => 'Database error']); echo json_encode(['status' => 'error', 'message' => 'Database error']);
} }
} else { } else {
file_put_contents('debug.log', date('[Y-m-d H:i:s] ') . "INVALID DATA: ID=$episode_id, DUR=$duration" . PHP_EOL, FILE_APPEND);
echo json_encode(['status' => 'error', 'message' => 'Invalid episode ID']); echo json_encode(['status' => 'error', 'message' => 'Invalid episode ID']);
} }
} }

View File

@ -18,17 +18,7 @@ if ($banner_image === 'default-banner.jpg') {
?> ?>
<div class="hero" style="background-image: url('<?php echo $banner_url; ?>');"> <div class="hero" style="background-image: url('<?php echo $banner_url; ?>');">
<div class="hero-content">
<h1><?php echo htmlspecialchars($site_title); ?></h1>
<p>Listen to our latest messages and sermons.</p>
<div style="margin-top: 2rem; display: flex; gap: 1rem; justify-content: center;">
<a href="subscribe.php" class="btn btn-primary">How to Subscribe</a>
<a href="feed.php" class="btn" style="background: rgba(255,255,255,0.1); backdrop-filter: blur(10px); border: 1px solid var(--glass-border);">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; margin-right: 0.5rem;"><path d="M4 11a9 9 0 0 1 9 9"></path><path d="M4 4a16 16 0 0 1 16 16"></path><circle cx="5" cy="19" r="1"></circle></svg>
RSS Feed
</a>
</div>
</div>
</div> </div>
<div class="container"> <div class="container">

1
sql/index.php Normal file
View File

@ -0,0 +1 @@
<?php header('Location: ../index.php'); exit;

View File

@ -44,7 +44,10 @@ INSERT IGNORE INTO settings (`key`, `value`) VALUES
('site_title', 'Our Church Podcast'), ('site_title', 'Our Church Podcast'),
('banner_image', 'default-banner.jpg'), ('banner_image', 'default-banner.jpg'),
('footer_copyright', 'Our Church'), ('footer_copyright', 'Our Church'),
('footer_powered_by', 'Antigravity Podcast Server'); ('footer_powered_by', 'Antigravity Podcast Server'),
('theme_primary_color', '#6366f1'),
('theme_bg_color', '#0f172a'),
('theme_text_color', '#f8fafc');
-- Subscriptions table (Push Notifications) -- Subscriptions table (Push Notifications)
CREATE TABLE IF NOT EXISTS subscriptions ( CREATE TABLE IF NOT EXISTS subscriptions (