Auto commit: 2025-12-19T15:08:47.391Z

This commit is contained in:
Flatlogic Bot 2025-12-19 15:08:47 +00:00
parent 30eacc0737
commit 40a0d866cd
10 changed files with 396 additions and 24 deletions

28
admin/auth.php Normal file
View File

@ -0,0 +1,28 @@
<?php
session_start();
$admin_timeout = 3600; // 1 hour
// Check if user is logged in and is an admin
if (!isset($_SESSION['user_id']) || $_SESSION['user_role'] !== 'admin') {
header('Location: ../login.php');
exit();
}
// Session timeout logic
if (isset($_SESSION['last_activity']) && (time() - $_SESSION['last_activity'] > $admin_timeout)) {
session_unset();
session_destroy();
header('Location: ../login.php?timeout');
exit();
}
// IP binding logic
if (isset($_SESSION['admin_ip_address']) && $_SESSION['admin_ip_address'] !== $_SERVER['REMOTE_ADDR']) {
session_unset();
session_destroy();
header('Location: ../login.php?ip_changed');
exit();
}
$_SESSION['last_activity'] = time(); // Update last activity time

View File

@ -1,10 +1,5 @@
<?php <?php
session_start(); require_once __DIR__ . '/auth.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_role'] !== 'admin') {
header("Location: ../login.php");
exit;
}
require_once __DIR__ . '/../db/config.php'; require_once __DIR__ . '/../db/config.php';
$pdo = db(); $pdo = db();
@ -78,6 +73,7 @@ while ($row = $stmt->fetch()) {
<a class="nav-link active" href="categories.php">Categories</a> <a class="nav-link active" href="categories.php">Categories</a>
<a class="nav-link" href="users.php">Users</a> <a class="nav-link" href="users.php">Users</a>
<a class="nav-link" href="links.php">Links</a> <a class="nav-link" href="links.php">Links</a>
<a class="nav-link" href="settings.php">Settings</a>
</nav> </nav>
</aside> </aside>
</div> </div>

View File

@ -1,10 +1,5 @@
<?php <?php
session_start(); require_once __DIR__ . '/auth.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_role'] !== 'admin') {
header("Location: ../login.php");
exit;
}
require_once __DIR__ . '/../db/config.php'; require_once __DIR__ . '/../db/config.php';
@ -44,6 +39,7 @@ $category_count = $pdo->query("SELECT count(*) FROM categories")->fetchColumn();
<a class="nav-link" href="categories.php">Categories</a> <a class="nav-link" href="categories.php">Categories</a>
<a class="nav-link" href="users.php">Users</a> <a class="nav-link" href="users.php">Users</a>
<a class="nav-link" href="links.php">Links</a> <a class="nav-link" href="links.php">Links</a>
<a class="nav-link" href="settings.php">Settings</a>
</nav> </nav>
</aside> </aside>
</div> </div>

View File

@ -1,10 +1,5 @@
<?php <?php
session_start(); require_once __DIR__ . '/auth.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_role'] !== 'admin') {
header("Location: ../login.php");
exit;
}
// Debug block for POST data - visible only to admin // Debug block for POST data - visible only to admin
@ -184,6 +179,7 @@ $subcategories = $pdo->query("SELECT sc.id, sc.name AS subcategory_name, c.name
<a class="nav-link" href="categories.php">Categories</a> <a class="nav-link" href="categories.php">Categories</a>
<a class="nav-link" href="users.php">Users</a> <a class="nav-link" href="users.php">Users</a>
<a class="nav-link active" href="links.php">Links</a> <a class="nav-link active" href="links.php">Links</a>
<a class="nav-link" href="settings.php">Settings</a>
</nav> </nav>
</aside> </aside>
</div> </div>

154
admin/settings.php Normal file
View File

@ -0,0 +1,154 @@
<?php
require_once __DIR__ . '/auth.php';
require_once __DIR__ . '/../includes/Settings.php';
$allSettings = Settings::getAllWithMetadata();
// Handle form submission to update settings
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// CSRF Protection (consider adding this if not already in auth.php or a global handler)
// if (!isset($_POST['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
// // Handle CSRF token mismatch
// $_SESSION['error_message'] = 'CSRF token validation failed.';
// header('Location: settings.php');
// exit;
// }
foreach ($allSettings as $setting) {
$key = $setting['setting_key'];
$newValue = $_POST[$key] ?? null;
// Special handling for checkboxes: if not present in POST, it means unchecked
if ($setting['setting_type'] === 'checkbox') {
$newValue = isset($_POST[$key]) ? '1' : '0';
}
// Update the setting using the Settings class
Settings::set(
$key,
$newValue,
$setting['setting_type'],
$setting['default_value'],
$setting['validation_rules'],
$setting['description']
);
}
Settings::clearCache(); // Clear cache after updating settings
$_SESSION['success_message'] = 'Settings updated successfully.';
header('Location: settings.php');
exit;
}
// Re-fetch settings after potential update
$allSettings = Settings::getAllWithMetadata();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Manage Settings - Admin Panel</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="../assets/css/custom.css?v=<?php echo time(); ?>">
</head>
<body>
<header class="header">
<h1><a href="/" style="text-decoration: none; color: inherit;">Admin Panel</a></h1>
<div class="auth-links">
<a href="../index.php">View Site</a>
<a href="../logout.php">Logout</a>
</div>
</header>
<div class="container my-4">
<div class="row">
<div class="col-md-3">
<aside class="admin-nav">
<h3>Menu</h3>
<nav class="nav flex-column">
<a class="nav-link" href="index.php">Dashboard</a>
<a class="nav-link" href="categories.php">Categories</a>
<a class="nav-link" href="users.php">Users</a>
<a class="nav-link" href="links.php">Links</a>
<a class="nav-link active" href="settings.php">Settings</a>
</nav>
</aside>
</div>
<div class="col-md-9">
<main class="content">
<h2>Manage Settings</h2>
<?php if (isset($_SESSION['success_message'])): ?>
<div class="alert alert-success">
<?php echo $_SESSION['success_message']; unset($_SESSION['success_message']); ?>
</div>
<?php endif; ?>
<?php if (isset($_SESSION['error_message'])): ?>
<div class="alert alert-danger">
<?php echo $_SESSION['error_message']; unset($_SESSION['error_message']); ?>
</div>
<?php endif; ?>
<form action="settings.php" method="POST">
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token'] ?? ''; // Assuming CSRF token is available globally or generated here ?>">
<?php foreach ($allSettings as $setting): ?>
<div class="mb-3">
<label for="<?php echo htmlspecialchars($setting['setting_key']); ?>" class="form-label">
<?php echo htmlspecialchars($setting['description'] ?: ucwords(str_replace('_', ' ', $setting['setting_key']))); ?>
</label>
<?php
$currentValue = Settings::get($setting['setting_key'], $setting['default_value']);
switch ($setting['setting_type']) {
case 'text':
case 'string':
echo '<input type="text" class="form-control" id="' . htmlspecialchars($setting['setting_key']) . '" name="' . htmlspecialchars($setting['setting_key']) . '" value="' . htmlspecialchars($currentValue) . '">';
break;
case 'number':
case 'integer':
echo '<input type="number" class="form-control" id="' . htmlspecialchars($setting['setting_key']) . '" name="' . htmlspecialchars($setting['setting_key']) . '" value="' . htmlspecialchars($currentValue) . '">';
break;
case 'textarea':
echo '<textarea class="form-control" id="' . htmlspecialchars($setting['setting_key']) . '" name="' . htmlspecialchars($setting['setting_key']) . '" rows="3">' . htmlspecialchars($currentValue) . '</textarea>';
break;
case 'checkbox':
case 'boolean':
echo '<div class="form-check form-switch">';
echo '<input class="form-check-input" type="checkbox" id="' . htmlspecialchars($setting['setting_key']) . '" name="' . htmlspecialchars($setting['setting_key']) . '" value="1" ' . ($currentValue ? 'checked' : '') . '>';
echo '<label class="form-check-label" for="' . htmlspecialchars($setting['setting_key']) . '"></label>';
echo '</div>';
break;
case 'dropdown':
case 'select':
$options = json_decode($setting['validation_rules'], true)['options'] ?? []; // Assuming options are stored in validation_rules as JSON
echo '<select class="form-select" id="' . htmlspecialchars($setting['setting_key']) . '" name="' . htmlspecialchars($setting['setting_key']) . '>';
foreach ($options as $optionValue => $optionLabel) {
echo '<option value="' . htmlspecialchars($optionValue) . '" ' . ($currentValue == $optionValue ? 'selected' : '') . '>' . htmlspecialchars($optionLabel) . '</option>';
}
echo '</select>';
break;
// Add more types as needed (e.g., color, date, email)
default:
echo '<input type="text" class="form-control" id="' . htmlspecialchars($setting['setting_key']) . '" name="' . htmlspecialchars($setting['setting_key']) . '" value="' . htmlspecialchars($currentValue) . '">';
break;
}
?>
<?php if (!empty($setting['description'])): ?>
<div class="form-text"><?php echo htmlspecialchars($setting['description']); ?></div>
<?php endif; ?>
</div>
<?php endforeach; ?>
<button type="submit" class="btn btn-primary">Save Settings</button>
</form>
</main>
</div>
</div>
</div>
<footer class="footer">
<p>&copy; <?php echo date("Y"); ?> <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'Web Directory'); ?>. All Rights Reserved.</p>
</footer>
</body>
</html>

View File

@ -1,10 +1,5 @@
<?php <?php
session_start(); require_once __DIR__ . '/auth.php';
if (!isset($_SESSION['user_id']) || $_SESSION['user_role'] !== 'admin') {
header("Location: ../login.php");
exit;
}
require_once __DIR__ . '/../db/config.php'; require_once __DIR__ . '/../db/config.php';
$pdo = db(); $pdo = db();
@ -54,6 +49,7 @@ $users = $pdo->query("SELECT id, username, role, created_at FROM users ORDER BY
<a class="nav-link" href="categories.php">Categories</a> <a class="nav-link" href="categories.php">Categories</a>
<a class="nav-link active" href="users.php">Users</a> <a class="nav-link active" href="users.php">Users</a>
<a class="nav-link" href="links.php">Links</a> <a class="nav-link" href="links.php">Links</a>
<a class="nav-link" href="settings.php">Settings</a>
</nav> </nav>
</aside> </aside>
</div> </div>

View File

@ -0,0 +1,13 @@
-- db/migrations/003_create_settings_table.sql
CREATE TABLE IF NOT EXISTS settings (
id INT AUTO_INCREMENT PRIMARY KEY,
setting_key VARCHAR(255) NOT NULL UNIQUE,
setting_value TEXT,
setting_type VARCHAR(50) NOT NULL DEFAULT 'text',
default_value TEXT NULL,
validation_rules TEXT NULL,
description TEXT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

View File

@ -0,0 +1,10 @@
-- db/migrations/004_add_initial_settings.sql
-- Insert initial settings if they do not already exist
INSERT IGNORE INTO settings (setting_key, setting_value, setting_type, default_value, description)
VALUES
('site_title', 'My Web Directory', 'text', 'My Web Directory', 'The main title of the website.'),
('maintenance_mode', '0', 'checkbox', '0', 'Enable or disable maintenance mode for the site.'),
('items_per_page', '10', 'number', '10', 'Number of items to display per page in listings.'),
('admin_email', 'admin@example.com', 'text', 'admin@example.com', 'The email address for administrative notifications.'),
('tar_pit_delay', '0', 'number', '0', 'Delay (in seconds) for tar pit defense against brute-force attacks.');

178
includes/Settings.php Normal file
View File

@ -0,0 +1,178 @@
<?php
require_once __DIR__ . '/../db/config.php';
class Settings
{
private static $cache = [];
/**
* Get a setting value.
*
* @param string $key The setting key.
* @param mixed $default The default value to return if the setting is not found.
* @return mixed The setting value or the default value.
*/
public static function get(string $key, $default = null)
{
if (isset(self::$cache[$key])) {
return self::$cache[$key];
}
try {
$pdo = db();
$stmt = $pdo->prepare("SELECT setting_value, setting_type FROM settings WHERE setting_key = ?");
$stmt->execute([$key]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row) {
$value = self::castValue($row['setting_value'], $row['setting_type']);
self::$cache[$key] = $value;
return $value;
}
} catch (PDOException $e) {
error_log("Error getting setting '{$key}': " . $e->getMessage());
}
return $default;
}
/**
* Set or update a setting value.
*
* @param string $key The setting key.
* @param mixed $value The setting value.
* @param string $type The type of the setting (e.g., 'text', 'checkbox', 'number').
* @param mixed $defaultValue The default value for the setting.
* @param string|null $validationRules JSON string of validation rules.
* @param string|null $description A description for the setting.
* @return bool True on success, false on failure.
*/
public static function set(
string $key,
$value,
string $type = 'text',
$defaultValue = null,
?string $validationRules = null,
?string $description = null
): bool {
try {
$pdo = db();
// Check if setting exists
$stmt = $pdo->prepare("SELECT COUNT(*) FROM settings WHERE setting_key = ?");
$stmt->execute([$key]);
$exists = $stmt->fetchColumn();
$stringValue = self::valueToString($value);
$defaultValue = self::valueToString($defaultValue);
if ($exists) {
$stmt = $pdo->prepare("UPDATE settings SET setting_value = ?, setting_type = ?, default_value = ?, validation_rules = ?, description = ?, updated_at = NOW() WHERE setting_key = ?");
$stmt->execute([$stringValue, $type, $defaultValue, $validationRules, $description, $key]);
} else {
$stmt = $pdo->prepare("INSERT INTO settings (setting_key, setting_value, setting_type, default_value, validation_rules, description) VALUES (?, ?, ?, ?, ?, ?)");
$stmt->execute([$key, $stringValue, $type, $defaultValue, $validationRules, $description]);
}
// Clear cache for this key
unset(self::$cache[$key]);
return true;
} catch (PDOException $e) {
error_log("Error setting '{$key}': " . $e->getMessage());
return false;
}
}
/**
* Casts a string value to its appropriate PHP type based on the setting type.
*
* @param string $value The string value from the database.
* @param string $type The declared type of the setting.
* @return mixed The type-casted value.
*/
private static function castValue(string $value, string $type)
{
switch ($type) {
case 'boolean':
case 'checkbox':
return (bool)$value;
case 'integer':
case 'number':
return (int)$value;
case 'float':
return (float)$value;
case 'json':
return json_decode($value, true);
case 'array': // Simple comma-separated array
return explode(',', $value);
default:
return $value;
}
}
/**
* Converts a PHP value to its string representation for storage.
*
* @param mixed $value The PHP value.
* @return string The string representation.
*/
private static function valueToString($value): string
{
if (is_bool($value)) {
return $value ? '1' : '0';
} elseif (is_array($value) || is_object($value)) {
return json_encode($value);
} else {
return (string)$value;
}
}
/**
* Clears the entire settings cache.
*/
public static function clearCache(): void
{
self::$cache = [];
}
/**
* Fetches all settings from the database.
*
* @return array An associative array of all settings.
*/
public static function getAll(): array
{
try {
$pdo = db();
$stmt = $pdo->query("SELECT setting_key, setting_value, setting_type FROM settings");
$allSettings = $stmt->fetchAll(PDO::FETCH_ASSOC);
$result = [];
foreach ($allSettings as $setting) {
$result[$setting['setting_key']] = self::castValue($setting['setting_value'], $setting['setting_type']);
}
return $result;
} catch (PDOException $e) {
error_log("Error getting all settings: " . $e->getMessage());
return [];
}
}
/**
* Fetches all settings with their metadata from the database.
*
* @return array An associative array of all settings including metadata.
*/
public static function getAllWithMetadata(): array
{
try {
$pdo = db();
$stmt = $pdo->query("SELECT * FROM settings");
return $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
error_log("Error getting all settings with metadata: " . $e->getMessage());
return [];
}
}
}

View File

@ -31,6 +31,11 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") {
$_SESSION['user_id'] = $user['id']; $_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username']; $_SESSION['username'] = $user['username'];
$_SESSION['user_role'] = $user['role']; $_SESSION['user_role'] = $user['role'];
// For admin panel security features
if ($user['role'] === 'admin') {
$_SESSION['admin_ip_address'] = $_SERVER['REMOTE_ADDR'];
$_SESSION['last_activity'] = time();
}
header("Location: index.php"); header("Location: index.php");
exit; exit;
} else { } else {