Introduces error log tracking with new ErrorLog model, controller, views, and migration. Adds admin UI for viewing, resolving, and deleting errors. Implements bulk actions for users and notification groups, refactors domain filtering/pagination, and centralizes admin access checks using Auth::requireAdmin().
492 lines
19 KiB
PHP
492 lines
19 KiB
PHP
<?php
|
|
|
|
namespace App\Controllers;
|
|
|
|
use Core\Controller;
|
|
use Core\Auth;
|
|
use App\Models\Setting;
|
|
|
|
class SettingsController extends Controller
|
|
{
|
|
private Setting $settingModel;
|
|
|
|
public function __construct()
|
|
{
|
|
Auth::requireAdmin();
|
|
$this->settingModel = new Setting();
|
|
}
|
|
|
|
public function index()
|
|
{
|
|
$settings = $this->settingModel->getAllAsKeyValue();
|
|
$appSettings = $this->settingModel->getAppSettings();
|
|
$emailSettings = $this->settingModel->getEmailSettings();
|
|
$captchaSettings = $this->settingModel->getCaptchaSettings();
|
|
|
|
// Predefined notification day options
|
|
$notificationPresets = [
|
|
'minimal' => [
|
|
'label' => 'Minimal (30, 7, 1 days)',
|
|
'value' => '30,7,1'
|
|
],
|
|
'standard' => [
|
|
'label' => 'Standard (60, 30, 21, 14, 7, 5, 3, 2, 1 days)',
|
|
'value' => '60,30,21,14,7,5,3,2,1'
|
|
],
|
|
'frequent' => [
|
|
'label' => 'Frequent (90, 60, 45, 30, 21, 14, 10, 7, 5, 3, 2, 1 days)',
|
|
'value' => '90,60,45,30,21,14,10,7,5,3,2,1'
|
|
],
|
|
'business' => [
|
|
'label' => 'Business Focused (60, 30, 14, 7, 3, 1 days)',
|
|
'value' => '60,30,14,7,3,1'
|
|
],
|
|
'conservative' => [
|
|
'label' => 'Conservative (30, 15, 7, 3, 1 days)',
|
|
'value' => '30,15,7,3,1'
|
|
],
|
|
'custom' => [
|
|
'label' => 'Custom',
|
|
'value' => 'custom'
|
|
]
|
|
];
|
|
|
|
// Check interval presets
|
|
$checkIntervalPresets = [
|
|
['label' => 'Every 6 hours', 'value' => 6],
|
|
['label' => 'Every 12 hours', 'value' => 12],
|
|
['label' => 'Daily (24 hours)', 'value' => 24],
|
|
['label' => 'Every 2 days (48 hours)', 'value' => 48],
|
|
['label' => 'Weekly (168 hours)', 'value' => 168]
|
|
];
|
|
|
|
$this->view('settings/index', [
|
|
'settings' => $settings,
|
|
'appSettings' => $appSettings,
|
|
'emailSettings' => $emailSettings,
|
|
'captchaSettings' => $captchaSettings,
|
|
'notificationPresets' => $notificationPresets,
|
|
'checkIntervalPresets' => $checkIntervalPresets,
|
|
'title' => 'Settings'
|
|
]);
|
|
}
|
|
|
|
public function update()
|
|
{
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
$this->redirect('/settings');
|
|
return;
|
|
}
|
|
|
|
// CSRF Protection
|
|
$this->verifyCsrf('/settings#monitoring');
|
|
|
|
try {
|
|
// Update notification days
|
|
$notificationPreset = $_POST['notification_preset'] ?? 'standard';
|
|
|
|
if ($notificationPreset === 'custom') {
|
|
// Custom days entered by user
|
|
$customDays = trim($_POST['custom_notification_days'] ?? '');
|
|
|
|
if (empty($customDays)) {
|
|
$_SESSION['error'] = 'Please enter notification days for custom preset';
|
|
$this->redirect('/settings#monitoring');
|
|
return;
|
|
}
|
|
|
|
// Validate custom days (comma-separated integers)
|
|
$daysArray = array_map('trim', explode(',', $customDays));
|
|
$daysArray = array_filter($daysArray, function($day) {
|
|
return is_numeric($day) && $day > 0;
|
|
});
|
|
|
|
if (empty($daysArray)) {
|
|
$_SESSION['error'] = 'Invalid notification days format. Use comma-separated numbers (e.g., 30,15,7,1)';
|
|
$this->redirect('/settings#monitoring');
|
|
return;
|
|
}
|
|
|
|
// Sort in descending order
|
|
rsort($daysArray, SORT_NUMERIC);
|
|
$notificationDays = implode(',', $daysArray);
|
|
} else {
|
|
// Use preset value
|
|
$notificationDays = $_POST['notification_days_before'] ?? '30,15,7,3,1';
|
|
}
|
|
|
|
// Update check interval
|
|
$checkInterval = (int)($_POST['check_interval_hours'] ?? 24);
|
|
|
|
if ($checkInterval < 1 || $checkInterval > 720) { // Max 30 days
|
|
$_SESSION['error'] = 'Check interval must be between 1 and 720 hours';
|
|
$this->redirect('/settings#monitoring');
|
|
return;
|
|
}
|
|
|
|
// Save settings
|
|
$this->settingModel->setValue('notification_days_before', $notificationDays);
|
|
$this->settingModel->setValue('check_interval_hours', $checkInterval);
|
|
|
|
$_SESSION['success'] = 'Settings updated successfully';
|
|
$this->redirect('/settings#monitoring');
|
|
|
|
} catch (\Exception $e) {
|
|
$_SESSION['error'] = 'Failed to update settings: ' . $e->getMessage();
|
|
$this->redirect('/settings#monitoring');
|
|
}
|
|
}
|
|
|
|
public function testCron()
|
|
{
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
$this->redirect('/settings');
|
|
return;
|
|
}
|
|
|
|
// CSRF Protection
|
|
$this->verifyCsrf('/settings');
|
|
|
|
// Update last check run time to show the test worked
|
|
$this->settingModel->updateLastCheckRun();
|
|
|
|
$_SESSION['info'] = 'Test notification sent (feature coming soon). Last check time updated.';
|
|
$this->redirect('/settings');
|
|
}
|
|
|
|
public function clearLogs()
|
|
{
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
$this->redirect('/settings');
|
|
return;
|
|
}
|
|
|
|
// CSRF Protection
|
|
$this->verifyCsrf('/settings#maintenance');
|
|
|
|
try {
|
|
// Clear notification logs older than 30 days
|
|
$stmt = $this->settingModel->db->prepare(
|
|
"DELETE FROM notification_logs WHERE sent_at < DATE_SUB(NOW(), INTERVAL 30 DAY)"
|
|
);
|
|
$stmt->execute();
|
|
$deleted = $stmt->rowCount();
|
|
|
|
$_SESSION['success'] = "Cleared $deleted old notification log(s)";
|
|
$this->redirect('/settings#maintenance');
|
|
|
|
} catch (\Exception $e) {
|
|
$_SESSION['error'] = 'Failed to clear logs: ' . $e->getMessage();
|
|
$this->redirect('/settings#maintenance');
|
|
}
|
|
}
|
|
|
|
public function updateApp()
|
|
{
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
$this->redirect('/settings');
|
|
return;
|
|
}
|
|
|
|
// CSRF Protection
|
|
$this->verifyCsrf('/settings#app');
|
|
|
|
try {
|
|
$appSettings = [
|
|
'app_name' => trim($_POST['app_name'] ?? 'Domain Monitor'),
|
|
'app_url' => trim($_POST['app_url'] ?? 'http://localhost:8000'),
|
|
'app_timezone' => trim($_POST['app_timezone'] ?? 'UTC')
|
|
];
|
|
|
|
// Validate app_name
|
|
if (empty($appSettings['app_name'])) {
|
|
$_SESSION['error'] = 'Application name is required';
|
|
$this->redirect('/settings#app');
|
|
return;
|
|
}
|
|
|
|
// Validate app_url
|
|
if (empty($appSettings['app_url']) || !filter_var($appSettings['app_url'], FILTER_VALIDATE_URL)) {
|
|
$_SESSION['error'] = 'Please enter a valid application URL';
|
|
$this->redirect('/settings#app');
|
|
return;
|
|
}
|
|
|
|
// Validate timezone
|
|
$validTimezones = timezone_identifiers_list();
|
|
if (!in_array($appSettings['app_timezone'], $validTimezones)) {
|
|
$_SESSION['error'] = 'Invalid timezone selected';
|
|
$this->redirect('/settings#app');
|
|
return;
|
|
}
|
|
|
|
// Update app settings
|
|
$this->settingModel->updateAppSettings($appSettings);
|
|
|
|
// Update registration settings
|
|
$registrationEnabled = isset($_POST['registration_enabled']) ? '1' : '0';
|
|
$requireEmailVerification = isset($_POST['require_email_verification']) ? '1' : '0';
|
|
|
|
$this->settingModel->setValue('registration_enabled', $registrationEnabled);
|
|
$this->settingModel->setValue('require_email_verification', $requireEmailVerification);
|
|
|
|
$_SESSION['success'] = 'Application settings updated successfully';
|
|
$this->redirect('/settings#app');
|
|
|
|
} catch (\Exception $e) {
|
|
$_SESSION['error'] = 'Failed to update application settings: ' . $e->getMessage();
|
|
$this->redirect('/settings#app');
|
|
}
|
|
}
|
|
|
|
public function updateEmail()
|
|
{
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
$this->redirect('/settings');
|
|
return;
|
|
}
|
|
|
|
// CSRF Protection
|
|
$this->verifyCsrf('/settings#email');
|
|
|
|
try {
|
|
$emailSettings = [
|
|
'mail_host' => trim($_POST['mail_host'] ?? ''),
|
|
'mail_port' => trim($_POST['mail_port'] ?? '2525'),
|
|
'mail_username' => trim($_POST['mail_username'] ?? ''),
|
|
'mail_password' => trim($_POST['mail_password'] ?? ''),
|
|
'mail_encryption' => trim($_POST['mail_encryption'] ?? 'tls'),
|
|
'mail_from_address' => trim($_POST['mail_from_address'] ?? ''),
|
|
'mail_from_name' => trim($_POST['mail_from_name'] ?? 'Domain Monitor')
|
|
];
|
|
|
|
// Validate required fields
|
|
if (empty($emailSettings['mail_host'])) {
|
|
$_SESSION['error'] = 'Mail host is required';
|
|
$this->redirect('/settings#email');
|
|
return;
|
|
}
|
|
|
|
if (empty($emailSettings['mail_from_address']) || !filter_var($emailSettings['mail_from_address'], FILTER_VALIDATE_EMAIL)) {
|
|
$_SESSION['error'] = 'Please enter a valid from email address';
|
|
$this->redirect('/settings#email');
|
|
return;
|
|
}
|
|
|
|
// Validate port
|
|
if (!is_numeric($emailSettings['mail_port']) || $emailSettings['mail_port'] < 1 || $emailSettings['mail_port'] > 65535) {
|
|
$_SESSION['error'] = 'Please enter a valid port number (1-65535)';
|
|
$this->redirect('/settings#email');
|
|
return;
|
|
}
|
|
|
|
$this->settingModel->updateEmailSettings($emailSettings);
|
|
$_SESSION['success'] = 'Email settings updated successfully';
|
|
$this->redirect('/settings#email');
|
|
|
|
} catch (\Exception $e) {
|
|
$_SESSION['error'] = 'Failed to update email settings: ' . $e->getMessage();
|
|
$this->redirect('/settings#email');
|
|
}
|
|
}
|
|
|
|
public function updateCaptcha()
|
|
{
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
$this->redirect('/settings');
|
|
return;
|
|
}
|
|
|
|
// CSRF Protection
|
|
$this->verifyCsrf('/settings#security');
|
|
|
|
try {
|
|
$captchaProvider = trim($_POST['captcha_provider'] ?? 'disabled');
|
|
$captchaSiteKey = trim($_POST['captcha_site_key'] ?? '');
|
|
$captchaSecretKey = trim($_POST['captcha_secret_key'] ?? '');
|
|
$recaptchaV3Threshold = trim($_POST['recaptcha_v3_score_threshold'] ?? '0.5');
|
|
|
|
// Validate provider
|
|
$validProviders = ['disabled', 'recaptcha_v2', 'recaptcha_v3', 'turnstile'];
|
|
if (!in_array($captchaProvider, $validProviders)) {
|
|
$_SESSION['error'] = 'Invalid CAPTCHA provider selected';
|
|
$this->redirect('/settings#security');
|
|
return;
|
|
}
|
|
|
|
// If CAPTCHA is enabled, validate keys
|
|
if ($captchaProvider !== 'disabled') {
|
|
if (empty($captchaSiteKey)) {
|
|
$_SESSION['error'] = 'Site key is required when CAPTCHA is enabled';
|
|
$this->redirect('/settings#security');
|
|
return;
|
|
}
|
|
|
|
if (empty($captchaSecretKey)) {
|
|
$_SESSION['error'] = 'Secret key is required when CAPTCHA is enabled';
|
|
$this->redirect('/settings#security');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Validate v3 score threshold
|
|
if ($captchaProvider === 'recaptcha_v3') {
|
|
$threshold = floatval($recaptchaV3Threshold);
|
|
if ($threshold < 0.0 || $threshold > 1.0) {
|
|
$_SESSION['error'] = 'reCAPTCHA v3 score threshold must be between 0.0 and 1.0';
|
|
$this->redirect('/settings#security');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Prepare settings array
|
|
$captchaSettings = [
|
|
'captcha_provider' => $captchaProvider,
|
|
'captcha_site_key' => $captchaSiteKey,
|
|
'recaptcha_v3_score_threshold' => $recaptchaV3Threshold
|
|
];
|
|
|
|
// Only update secret key if provided (to allow updating other settings without re-entering secret)
|
|
if (!empty($captchaSecretKey)) {
|
|
$captchaSettings['captcha_secret_key'] = $captchaSecretKey;
|
|
}
|
|
|
|
// Update CAPTCHA settings
|
|
$this->settingModel->updateCaptchaSettings($captchaSettings);
|
|
|
|
$_SESSION['success'] = 'CAPTCHA settings updated successfully';
|
|
$this->redirect('/settings#security');
|
|
|
|
} catch (\Exception $e) {
|
|
$_SESSION['error'] = 'Failed to update CAPTCHA settings: ' . $e->getMessage();
|
|
$this->redirect('/settings#security');
|
|
}
|
|
}
|
|
|
|
public function testEmail()
|
|
{
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
$this->redirect('/settings');
|
|
return;
|
|
}
|
|
|
|
// CSRF Protection
|
|
$this->verifyCsrf('/settings#email');
|
|
|
|
$testEmail = trim($_POST['test_email'] ?? '');
|
|
|
|
if (empty($testEmail) || !filter_var($testEmail, FILTER_VALIDATE_EMAIL)) {
|
|
$_SESSION['error'] = 'Please enter a valid email address';
|
|
$this->redirect('/settings#email');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Get current email settings
|
|
$emailSettings = $this->settingModel->getEmailSettings();
|
|
$appSettings = $this->settingModel->getAppSettings();
|
|
|
|
// Create PHPMailer instance
|
|
$mail = new \PHPMailer\PHPMailer\PHPMailer(true);
|
|
|
|
// Server settings
|
|
$mail->isSMTP();
|
|
$mail->Host = $emailSettings['mail_host'];
|
|
$mail->SMTPAuth = !empty($emailSettings['mail_username']);
|
|
$mail->Username = $emailSettings['mail_username'];
|
|
$mail->Password = $emailSettings['mail_password'];
|
|
$mail->SMTPSecure = $emailSettings['mail_encryption'];
|
|
$mail->Port = $emailSettings['mail_port'];
|
|
|
|
// Recipients
|
|
$mail->setFrom($emailSettings['mail_from_address'], $emailSettings['mail_from_name']);
|
|
$mail->addAddress($testEmail);
|
|
|
|
// Content
|
|
$mail->isHTML(true);
|
|
$mail->Subject = 'Test Email from ' . $appSettings['app_name'];
|
|
|
|
$appName = htmlspecialchars($appSettings['app_name']);
|
|
$appUrl = htmlspecialchars($appSettings['app_url']);
|
|
$currentTime = date('F j, Y g:i A');
|
|
|
|
$mail->Body = "
|
|
<html>
|
|
<head>
|
|
<style>
|
|
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
|
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
.header { background: #4A90E2; color: white; padding: 20px; border-radius: 5px 5px 0 0; }
|
|
.content { background: #f9f9f9; padding: 20px; border: 1px solid #ddd; }
|
|
.success { background: #d4edda; border: 1px solid #c3e6cb; color: #155724; padding: 12px; border-radius: 4px; margin: 15px 0; }
|
|
.info-table { width: 100%; margin-top: 15px; }
|
|
.info-table td { padding: 8px; border-bottom: 1px solid #ddd; }
|
|
.info-table td:first-child { font-weight: bold; width: 150px; }
|
|
.footer { background: #333; color: white; padding: 10px; text-align: center; font-size: 12px; border-radius: 0 0 5px 5px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class='container'>
|
|
<div class='header'>
|
|
<h2>✅ Email Test Successful!</h2>
|
|
</div>
|
|
<div class='content'>
|
|
<div class='success'>
|
|
<strong>Success!</strong> Your email configuration is working correctly.
|
|
</div>
|
|
<p>This is a test email from <strong>{$appName}</strong>.</p>
|
|
<p>If you're seeing this message, it means your SMTP settings are configured properly and emails are being delivered successfully.</p>
|
|
|
|
<table class='info-table'>
|
|
<tr>
|
|
<td>SMTP Host:</td>
|
|
<td>" . htmlspecialchars($emailSettings['mail_host']) . "</td>
|
|
</tr>
|
|
<tr>
|
|
<td>SMTP Port:</td>
|
|
<td>" . htmlspecialchars($emailSettings['mail_port']) . "</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Encryption:</td>
|
|
<td>" . htmlspecialchars($emailSettings['mail_encryption'] ?: 'None') . "</td>
|
|
</tr>
|
|
<tr>
|
|
<td>From Address:</td>
|
|
<td>" . htmlspecialchars($emailSettings['mail_from_address']) . "</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Test Time:</td>
|
|
<td>{$currentTime}</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
<div class='footer'>
|
|
<p>This is an automated test message from {$appName}</p>
|
|
<p style='margin-top: 5px;'><a href='{$appUrl}' style='color: #4A90E2;'>Visit Dashboard</a></p>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
";
|
|
|
|
$mail->AltBody = "Email Test Successful!\n\n" .
|
|
"This is a test email from {$appName}.\n" .
|
|
"Your SMTP configuration is working correctly.\n\n" .
|
|
"SMTP Host: {$emailSettings['mail_host']}\n" .
|
|
"SMTP Port: {$emailSettings['mail_port']}\n" .
|
|
"From: {$emailSettings['mail_from_address']}\n" .
|
|
"Test Time: {$currentTime}";
|
|
|
|
$mail->send();
|
|
|
|
$_SESSION['success'] = "Test email sent successfully to {$testEmail}. Please check your inbox.";
|
|
$this->redirect('/settings#email');
|
|
|
|
} catch (\Exception $e) {
|
|
$_SESSION['error'] = "Failed to send test email: " . $e->getMessage();
|
|
$this->redirect('/settings#email');
|
|
}
|
|
}
|
|
}
|
|
|