Implemented Settings
Improved cronjob Fixed Views Added env encryption key for encrypting sensitive data in database.
This commit is contained in:
@@ -24,16 +24,83 @@ class DashboardController extends Controller
|
||||
{
|
||||
$stats = $this->domainModel->getStatistics();
|
||||
$recentDomains = $this->domainModel->getRecent(5); // Get 5 most recent domains
|
||||
$expiringThisMonth = $this->domainModel->getExpiringDomains(30); // Domains expiring within 30 days
|
||||
|
||||
// Get expiring threshold from settings
|
||||
$settingModel = new \App\Models\Setting();
|
||||
$notificationDays = $settingModel->getNotificationDays();
|
||||
$expiringThreshold = !empty($notificationDays) ? max($notificationDays) : 30;
|
||||
|
||||
// Get expiring domains limited to top 5
|
||||
$allExpiringDomains = $this->domainModel->getExpiringDomains($expiringThreshold);
|
||||
$expiringThisMonth = array_slice($allExpiringDomains, 0, 5);
|
||||
|
||||
$recentLogs = $this->logModel->getRecent(10);
|
||||
$groups = $this->groupModel->all();
|
||||
|
||||
// Check system status
|
||||
$systemStatus = $this->checkSystemStatus();
|
||||
|
||||
$this->view('dashboard/index', [
|
||||
'stats' => $stats,
|
||||
'recentDomains' => $recentDomains,
|
||||
'expiringThisMonth' => $expiringThisMonth,
|
||||
'expiringCount' => count($allExpiringDomains),
|
||||
'recentLogs' => $recentLogs,
|
||||
'groups' => $groups,
|
||||
'systemStatus' => $systemStatus,
|
||||
'title' => 'Dashboard'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check system status
|
||||
*/
|
||||
private function checkSystemStatus(): array
|
||||
{
|
||||
$status = [
|
||||
'database' => ['status' => 'offline', 'color' => 'red'],
|
||||
'whois' => ['status' => 'offline', 'color' => 'red'],
|
||||
'notifications' => ['status' => 'disabled', 'color' => 'gray']
|
||||
];
|
||||
|
||||
// Check database connection
|
||||
try {
|
||||
$pdo = \Core\Database::getConnection();
|
||||
$pdo->query("SELECT 1");
|
||||
$status['database'] = ['status' => 'online', 'color' => 'green'];
|
||||
} catch (\Exception $e) {
|
||||
$status['database'] = ['status' => 'offline', 'color' => 'red'];
|
||||
}
|
||||
|
||||
// Check WHOIS service (test with a known TLD)
|
||||
try {
|
||||
$whoisService = new \App\Services\WhoisService();
|
||||
// Quick test - just check if we can discover TLD servers
|
||||
$tldModel = new \App\Models\TldRegistry();
|
||||
$testTld = $tldModel->find(1); // Get first TLD
|
||||
if ($testTld) {
|
||||
$status['whois'] = ['status' => 'active', 'color' => 'green'];
|
||||
} else {
|
||||
$status['whois'] = ['status' => 'no data', 'color' => 'yellow'];
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$status['whois'] = ['status' => 'error', 'color' => 'red'];
|
||||
}
|
||||
|
||||
// Check if any notification groups have active channels
|
||||
try {
|
||||
$channelModel = new \App\Models\NotificationChannel();
|
||||
$activeChannels = $channelModel->where('is_active', 1);
|
||||
if (count($activeChannels) > 0) {
|
||||
$status['notifications'] = ['status' => 'enabled', 'color' => 'green'];
|
||||
} else {
|
||||
$status['notifications'] = ['status' => 'no channels', 'color' => 'yellow'];
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$status['notifications'] = ['status' => 'error', 'color' => 'red'];
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,11 @@ class DomainController extends Controller
|
||||
$page = max(1, (int)($_GET['page'] ?? 1));
|
||||
$perPage = max(10, min(100, (int)($_GET['per_page'] ?? 25))); // Between 10 and 100
|
||||
|
||||
// Get expiring threshold from settings
|
||||
$settingModel = new \App\Models\Setting();
|
||||
$notificationDays = $settingModel->getNotificationDays();
|
||||
$expiringThreshold = !empty($notificationDays) ? max($notificationDays) : 30;
|
||||
|
||||
// Get all domains with groups
|
||||
$domains = $this->domainModel->getAllWithGroups();
|
||||
|
||||
@@ -43,12 +48,12 @@ class DomainController extends Controller
|
||||
}
|
||||
|
||||
if (!empty($status)) {
|
||||
$domains = array_filter($domains, function($domain) use ($status) {
|
||||
$domains = array_filter($domains, function($domain) use ($status, $expiringThreshold) {
|
||||
if ($status === 'expiring_soon') {
|
||||
// Check if domain expires within 30 days
|
||||
// Check if domain expires within configured threshold
|
||||
if (!empty($domain['expiration_date'])) {
|
||||
$daysLeft = floor((strtotime($domain['expiration_date']) - time()) / 86400);
|
||||
return $daysLeft <= 30 && $daysLeft >= 0;
|
||||
return $daysLeft <= $expiringThreshold && $daysLeft >= 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
387
app/Controllers/SettingsController.php
Normal file
387
app/Controllers/SettingsController.php
Normal file
@@ -0,0 +1,387 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use Core\Controller;
|
||||
use App\Models\Setting;
|
||||
|
||||
class SettingsController extends Controller
|
||||
{
|
||||
private Setting $settingModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->settingModel = new Setting();
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$settings = $this->settingModel->getAllAsKeyValue();
|
||||
$appSettings = $this->settingModel->getAppSettings();
|
||||
$emailSettings = $this->settingModel->getEmailSettings();
|
||||
|
||||
// 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,
|
||||
'notificationPresets' => $notificationPresets,
|
||||
'checkIntervalPresets' => $checkIntervalPresets,
|
||||
'title' => 'Settings'
|
||||
]);
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
$this->redirect('/settings');
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
$this->settingModel->updateAppSettings($appSettings);
|
||||
$_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;
|
||||
}
|
||||
|
||||
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 testEmail()
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
$this->redirect('/settings');
|
||||
return;
|
||||
}
|
||||
|
||||
$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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
196
app/Models/Setting.php
Normal file
196
app/Models/Setting.php
Normal file
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Core\Model;
|
||||
|
||||
class Setting extends Model
|
||||
{
|
||||
protected static string $table = 'settings';
|
||||
|
||||
/**
|
||||
* Get setting by key
|
||||
*/
|
||||
public function getByKey(string $key): ?array
|
||||
{
|
||||
$stmt = $this->db->prepare("SELECT * FROM settings WHERE setting_key = ?");
|
||||
$stmt->execute([$key]);
|
||||
$result = $stmt->fetch();
|
||||
return $result ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get setting value by key
|
||||
*/
|
||||
public function getValue(string $key, $default = null)
|
||||
{
|
||||
$setting = $this->getByKey($key);
|
||||
return $setting ? $setting['setting_value'] : $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set or update setting value
|
||||
*/
|
||||
public function setValue(string $key, $value): bool
|
||||
{
|
||||
$existing = $this->getByKey($key);
|
||||
|
||||
if ($existing) {
|
||||
$stmt = $this->db->prepare("UPDATE settings SET setting_value = ?, updated_at = NOW() WHERE setting_key = ?");
|
||||
return $stmt->execute([$value, $key]);
|
||||
} else {
|
||||
$stmt = $this->db->prepare("INSERT INTO settings (setting_key, setting_value) VALUES (?, ?)");
|
||||
return $stmt->execute([$key, $value]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all settings as key-value pairs
|
||||
*/
|
||||
public function getAllAsKeyValue(): array
|
||||
{
|
||||
$settings = $this->all();
|
||||
$result = [];
|
||||
|
||||
foreach ($settings as $setting) {
|
||||
$result[$setting['setting_key']] = $setting['setting_value'];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get notification days as array
|
||||
*/
|
||||
public function getNotificationDays(): array
|
||||
{
|
||||
$value = $this->getValue('notification_days_before', '30,15,7,3,1');
|
||||
return array_map('intval', explode(',', $value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get check interval hours
|
||||
*/
|
||||
public function getCheckIntervalHours(): int
|
||||
{
|
||||
return (int)$this->getValue('check_interval_hours', 24);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update notification days
|
||||
*/
|
||||
public function updateNotificationDays(array $days): bool
|
||||
{
|
||||
$value = implode(',', array_map('intval', $days));
|
||||
return $this->setValue('notification_days_before', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update check interval
|
||||
*/
|
||||
public function updateCheckInterval(int $hours): bool
|
||||
{
|
||||
return $this->setValue('check_interval_hours', $hours);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last check run timestamp
|
||||
*/
|
||||
public function getLastCheckRun(): ?string
|
||||
{
|
||||
return $this->getValue('last_check_run');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update last check run timestamp
|
||||
*/
|
||||
public function updateLastCheckRun(): bool
|
||||
{
|
||||
return $this->setValue('last_check_run', date('Y-m-d H:i:s'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get application settings
|
||||
*/
|
||||
public function getAppSettings(): array
|
||||
{
|
||||
return [
|
||||
'app_name' => $this->getValue('app_name', 'Domain Monitor'),
|
||||
'app_url' => $this->getValue('app_url', 'http://localhost:8000'),
|
||||
'app_timezone' => $this->getValue('app_timezone', 'UTC')
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get email settings
|
||||
*/
|
||||
public function getEmailSettings(): array
|
||||
{
|
||||
$encryptedPassword = $this->getValue('mail_password', '');
|
||||
|
||||
// Decrypt password if it's encrypted
|
||||
$decryptedPassword = '';
|
||||
if (!empty($encryptedPassword)) {
|
||||
try {
|
||||
$encryption = new \Core\Encryption();
|
||||
$decryptedPassword = $encryption->decrypt($encryptedPassword);
|
||||
} catch (\Exception $e) {
|
||||
// If decryption fails, it might be plaintext (migration scenario)
|
||||
// Try to use as-is but log the issue
|
||||
error_log("Failed to decrypt mail_password: " . $e->getMessage());
|
||||
$decryptedPassword = $encryptedPassword;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'mail_host' => $this->getValue('mail_host', 'smtp.mailtrap.io'),
|
||||
'mail_port' => $this->getValue('mail_port', '2525'),
|
||||
'mail_username' => $this->getValue('mail_username', ''),
|
||||
'mail_password' => $decryptedPassword,
|
||||
'mail_encryption' => $this->getValue('mail_encryption', 'tls'),
|
||||
'mail_from_address' => $this->getValue('mail_from_address', 'noreply@domainmonitor.com'),
|
||||
'mail_from_name' => $this->getValue('mail_from_name', 'Domain Monitor')
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update application settings
|
||||
*/
|
||||
public function updateAppSettings(array $settings): bool
|
||||
{
|
||||
$result = true;
|
||||
foreach ($settings as $key => $value) {
|
||||
if (!$this->setValue($key, $value)) {
|
||||
$result = false;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update email settings
|
||||
*/
|
||||
public function updateEmailSettings(array $settings): bool
|
||||
{
|
||||
$result = true;
|
||||
|
||||
foreach ($settings as $key => $value) {
|
||||
// Encrypt mail_password before storing
|
||||
if ($key === 'mail_password' && !empty($value)) {
|
||||
try {
|
||||
$encryption = new \Core\Encryption();
|
||||
$value = $encryption->encrypt($value);
|
||||
} catch (\Exception $e) {
|
||||
error_log("Failed to encrypt mail_password: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->setValue($key, $value)) {
|
||||
$result = false;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Services\Channels;
|
||||
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
use App\Models\Setting;
|
||||
|
||||
class EmailChannel implements NotificationChannelInterface
|
||||
{
|
||||
@@ -12,23 +13,28 @@ class EmailChannel implements NotificationChannelInterface
|
||||
$mail = new PHPMailer(true);
|
||||
|
||||
try {
|
||||
// Get email settings from database
|
||||
$settingModel = new Setting();
|
||||
$emailSettings = $settingModel->getEmailSettings();
|
||||
$appSettings = $settingModel->getAppSettings();
|
||||
|
||||
// Server settings
|
||||
$mail->isSMTP();
|
||||
$mail->Host = $_ENV['MAIL_HOST'];
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = $_ENV['MAIL_USERNAME'];
|
||||
$mail->Password = $_ENV['MAIL_PASSWORD'];
|
||||
$mail->SMTPSecure = $_ENV['MAIL_ENCRYPTION'];
|
||||
$mail->Port = $_ENV['MAIL_PORT'];
|
||||
$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($_ENV['MAIL_FROM_ADDRESS'], $_ENV['MAIL_FROM_NAME']);
|
||||
$mail->setFrom($emailSettings['mail_from_address'], $emailSettings['mail_from_name']);
|
||||
$mail->addAddress($config['email']);
|
||||
|
||||
// Content
|
||||
$mail->isHTML(true);
|
||||
$mail->Subject = $this->getSubject($data);
|
||||
$mail->Body = $this->formatHtmlBody($message, $data);
|
||||
$mail->Body = $this->formatHtmlBody($message, $data, $appSettings);
|
||||
$mail->AltBody = strip_tags($message);
|
||||
|
||||
$mail->send();
|
||||
@@ -55,9 +61,18 @@ class EmailChannel implements NotificationChannelInterface
|
||||
return "Domain Monitor Alert";
|
||||
}
|
||||
|
||||
private function formatHtmlBody(string $message, array $data): string
|
||||
private function formatHtmlBody(string $message, array $data, array $appSettings): string
|
||||
{
|
||||
$messageHtml = nl2br(htmlspecialchars($message));
|
||||
$appName = htmlspecialchars($appSettings['app_name']);
|
||||
$appUrl = htmlspecialchars($appSettings['app_url']);
|
||||
|
||||
// Build domain link if domain ID is available
|
||||
$domainLink = '';
|
||||
if (isset($data['domain_id'])) {
|
||||
$domainUrl = rtrim($appUrl, '/') . '/domains/' . $data['domain_id'];
|
||||
$domainLink = "<p style='margin-top: 15px;'><a href='$domainUrl' class='button'>View Domain Details</a></p>";
|
||||
}
|
||||
|
||||
return "
|
||||
<html>
|
||||
@@ -74,13 +89,15 @@ class EmailChannel implements NotificationChannelInterface
|
||||
<body>
|
||||
<div class='container'>
|
||||
<div class='header'>
|
||||
<h2>🔔 Domain Monitor Alert</h2>
|
||||
<h2>🔔 {$appName} Alert</h2>
|
||||
</div>
|
||||
<div class='content'>
|
||||
<p>$messageHtml</p>
|
||||
$domainLink
|
||||
</div>
|
||||
<div class='footer'>
|
||||
<p>This is an automated message from Domain Monitor</p>
|
||||
<p>This is an automated message from {$appName}</p>
|
||||
<p style='margin-top: 5px;'><a href='$appUrl' style='color: #4A90E2;'>Visit Dashboard</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -93,6 +93,7 @@ class NotificationService
|
||||
$message,
|
||||
[
|
||||
'domain' => $domain['domain_name'],
|
||||
'domain_id' => $domain['id'],
|
||||
'days_left' => $daysLeft,
|
||||
'expiration_date' => $domain['expiration_date'],
|
||||
'registrar' => $domain['registrar']
|
||||
|
||||
@@ -679,11 +679,19 @@ class WhoisService
|
||||
{
|
||||
// Check if domain is available (not registered)
|
||||
foreach ($statusArray as $status) {
|
||||
if (stripos($status, 'AVAILABLE') !== false || stripos($status, 'FREE') !== false) {
|
||||
if (stripos($status, 'AVAILABLE') !== false ||
|
||||
stripos($status, 'FREE') !== false ||
|
||||
stripos($status, 'NO MATCH') !== false ||
|
||||
stripos($status, 'NOT FOUND') !== false) {
|
||||
return 'available';
|
||||
}
|
||||
}
|
||||
|
||||
// Also check if expiration date is null and no status indicates it's registered
|
||||
if ($expirationDate === null && empty($statusArray)) {
|
||||
return 'available';
|
||||
}
|
||||
|
||||
$days = $this->daysUntilExpiration($expirationDate);
|
||||
|
||||
if ($days === null) {
|
||||
|
||||
@@ -39,7 +39,8 @@ ob_start();
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-medium text-gray-500 uppercase tracking-wide">Expiring Soon</p>
|
||||
<p class="text-2xl font-semibold text-gray-900 mt-1"><?= $stats['expiring_soon'] ?? 0 ?></p>
|
||||
<p class="text-2xl font-semibold text-gray-900 mt-1"><?= $globalStats['expiring_soon'] ?? 0 ?></p>
|
||||
<p class="text-xs text-gray-400 mt-1">within <?= $globalStats['expiring_threshold'] ?? 30 ?> days</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-orange-50 rounded-lg flex items-center justify-center">
|
||||
<i class="fas fa-exclamation-triangle text-orange-600 text-lg"></i>
|
||||
@@ -64,25 +65,26 @@ ob_start();
|
||||
<!-- Content Grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||
<!-- Recent Domains -->
|
||||
<div class="lg:col-span-2 bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-900 flex items-center">
|
||||
<i class="fas fa-clock text-gray-400 mr-2 text-sm"></i>
|
||||
<div class="lg:col-span-2">
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div class="px-5 py-3 border-b border-gray-200">
|
||||
<h2 class="text-sm font-semibold text-gray-900 flex items-center">
|
||||
<i class="fas fa-clock text-gray-400 mr-2 text-xs"></i>
|
||||
Recent Domains
|
||||
</h2>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="p-4">
|
||||
<?php if (!empty($recentDomains)): ?>
|
||||
<div class="space-y-3">
|
||||
<div class="space-y-2">
|
||||
<?php foreach ($recentDomains as $domain): ?>
|
||||
<div class="flex items-center justify-between p-3 border border-gray-100 rounded-lg hover:border-gray-300 hover:shadow-sm transition-all duration-200">
|
||||
<div class="flex items-center space-x-3 flex-1 min-w-0">
|
||||
<div class="w-10 h-10 bg-gray-50 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<i class="fas fa-globe text-gray-400"></i>
|
||||
<div class="w-9 h-9 bg-gray-50 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<i class="fas fa-globe text-gray-400 text-sm"></i>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="font-medium text-gray-900 truncate"><?= htmlspecialchars($domain['domain_name']) ?></h3>
|
||||
<div class="flex items-center space-x-3 text-xs text-gray-500 mt-1">
|
||||
<h3 class="text-sm font-medium text-gray-900 truncate"><?= htmlspecialchars($domain['domain_name']) ?></h3>
|
||||
<div class="flex items-center space-x-3 text-xs text-gray-500 mt-0.5">
|
||||
<span class="flex items-center">
|
||||
<i class="far fa-calendar mr-1"></i>
|
||||
<?php if ($domain['expiration_date']): ?>
|
||||
@@ -102,10 +104,19 @@ ob_start();
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 flex-shrink-0">
|
||||
<?php
|
||||
$statusClass = $domain['status'] === 'active' ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-700';
|
||||
$status = $domain['status'] ?? 'active';
|
||||
$statusClasses = [
|
||||
'active' => 'bg-green-100 text-green-700',
|
||||
'expiring_soon' => 'bg-orange-100 text-orange-700',
|
||||
'expired' => 'bg-red-100 text-red-700',
|
||||
'error' => 'bg-red-100 text-red-700',
|
||||
'available' => 'bg-blue-100 text-blue-700'
|
||||
];
|
||||
$statusClass = $statusClasses[$status] ?? 'bg-gray-100 text-gray-700';
|
||||
$statusLabel = $status === 'expiring_soon' ? 'Expiring Soon' : ($status === 'available' ? 'Available' : ucfirst($status));
|
||||
?>
|
||||
<span class="px-2 py-1 rounded text-xs font-medium <?= $statusClass ?>">
|
||||
<?= ucfirst($domain['status']) ?>
|
||||
<?= $statusLabel ?>
|
||||
</span>
|
||||
<a href="/domains/<?= $domain['id'] ?>" class="text-gray-400 hover:text-primary">
|
||||
<i class="fas fa-chevron-right text-sm"></i>
|
||||
@@ -114,17 +125,17 @@ ob_start();
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div class="mt-4 pt-4 border-t border-gray-100 text-center">
|
||||
<div class="mt-3 pt-3 border-t border-gray-100 text-center">
|
||||
<a href="/domains" class="text-sm text-primary hover:text-primary-dark font-medium inline-flex items-center">
|
||||
View All Domains
|
||||
<i class="fas fa-arrow-right ml-2 text-xs"></i>
|
||||
</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="text-center py-10">
|
||||
<i class="fas fa-globe text-gray-300 text-5xl mb-3"></i>
|
||||
<p class="text-gray-500">No domains added yet</p>
|
||||
<a href="/domains/create" class="mt-3 inline-flex items-center px-5 py-2 bg-primary text-white text-sm rounded-lg hover:bg-primary-dark transition-colors duration-200">
|
||||
<div class="text-center py-8">
|
||||
<i class="fas fa-globe text-gray-300 text-4xl mb-3"></i>
|
||||
<p class="text-sm text-gray-600">No domains added yet</p>
|
||||
<a href="/domains/create" class="mt-3 inline-flex items-center px-4 py-2 bg-primary text-white text-sm rounded-lg hover:bg-primary-dark transition-colors duration-200">
|
||||
<i class="fas fa-plus mr-2"></i>
|
||||
Add Your First Domain
|
||||
</a>
|
||||
@@ -132,6 +143,7 @@ ob_start();
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar: Quick Actions & Stats -->
|
||||
<div class="space-y-4">
|
||||
@@ -174,55 +186,86 @@ ob_start();
|
||||
</h2>
|
||||
</div>
|
||||
<div class="p-4 space-y-3">
|
||||
<?php
|
||||
$statusColors = [
|
||||
'green' => 'text-green-600',
|
||||
'yellow' => 'text-yellow-600',
|
||||
'red' => 'text-red-600',
|
||||
'gray' => 'text-gray-600'
|
||||
];
|
||||
?>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-gray-600">Database</span>
|
||||
<span class="flex items-center text-green-600 font-medium">
|
||||
<span class="flex items-center <?= $statusColors[$systemStatus['database']['color']] ?> font-medium">
|
||||
<i class="fas fa-circle text-xs mr-1.5"></i>
|
||||
Online
|
||||
<?= ucfirst($systemStatus['database']['status']) ?>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-gray-600">WHOIS Service</span>
|
||||
<span class="flex items-center text-green-600 font-medium">
|
||||
<span class="text-gray-600">TLD Registry</span>
|
||||
<span class="flex items-center <?= $statusColors[$systemStatus['whois']['color']] ?> font-medium">
|
||||
<i class="fas fa-circle text-xs mr-1.5"></i>
|
||||
Active
|
||||
<?= ucfirst($systemStatus['whois']['status']) ?>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-gray-600">Notifications</span>
|
||||
<span class="flex items-center text-green-600 font-medium">
|
||||
<span class="flex items-center <?= $statusColors[$systemStatus['notifications']['color']] ?> font-medium">
|
||||
<i class="fas fa-circle text-xs mr-1.5"></i>
|
||||
Enabled
|
||||
<?= ucfirst($systemStatus['notifications']['status']) ?>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Expiring This Month -->
|
||||
<?php if (!empty($expiringThisMonth)): ?>
|
||||
<div class="bg-white rounded-lg border-l-4 border-orange-500 border-t border-r border-b border-gray-200 overflow-hidden">
|
||||
<div class="bg-orange-50 px-5 py-3 border-b border-orange-100">
|
||||
<h2 class="text-sm font-semibold text-orange-900 flex items-center">
|
||||
<i class="fas fa-exclamation-triangle mr-2 text-xs"></i>
|
||||
Expiring This Month
|
||||
<!-- Expiring Soon -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div class="px-5 py-3 border-b border-gray-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-sm font-semibold text-gray-900 flex items-center">
|
||||
<i class="fas fa-exclamation-triangle text-orange-500 mr-2 text-xs"></i>
|
||||
Expiring Soon
|
||||
</h2>
|
||||
<?php if (($expiringCount ?? 0) > 5): ?>
|
||||
<a href="/domains?status=expiring_soon" class="text-xs text-primary hover:text-primary-dark font-medium">
|
||||
View all <?= $expiringCount ?>
|
||||
<i class="fas fa-arrow-right ml-1"></i>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="p-4 space-y-2.5">
|
||||
</div>
|
||||
<?php if (!empty($expiringThisMonth)): ?>
|
||||
<div class="p-4 space-y-2">
|
||||
<?php foreach ($expiringThisMonth as $domain): ?>
|
||||
<div class="flex items-center justify-between p-2 hover:bg-gray-50 rounded transition-colors duration-150">
|
||||
<?php
|
||||
$daysLeft = floor((strtotime($domain['expiration_date']) - time()) / 86400);
|
||||
$urgencyClass = $daysLeft <= 7 ? 'text-red-600' : ($daysLeft <= 30 ? 'text-orange-600' : 'text-yellow-600');
|
||||
?>
|
||||
<div class="flex items-center justify-between p-3 border border-gray-100 rounded-lg hover:border-gray-300 hover:shadow-sm transition-all duration-200">
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-gray-900 truncate"><?= htmlspecialchars($domain['domain_name']) ?></p>
|
||||
<p class="text-xs text-gray-500"><?= date('M d, Y', strtotime($domain['expiration_date'])) ?></p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">
|
||||
<?= date('M d, Y', strtotime($domain['expiration_date'])) ?>
|
||||
<span class="<?= $urgencyClass ?> font-semibold ml-2">
|
||||
<?= $daysLeft ?> days
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<a href="/domains/<?= $domain['id'] ?>" class="ml-2 text-gray-400 hover:text-primary flex-shrink-0">
|
||||
<i class="fas fa-chevron-right text-xs"></i>
|
||||
<a href="/domains/<?= $domain['id'] ?>" class="text-gray-400 hover:text-primary">
|
||||
<i class="fas fa-chevron-right text-sm"></i>
|
||||
</a>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="p-6 text-center">
|
||||
<i class="fas fa-check-circle text-green-500 text-3xl mb-2"></i>
|
||||
<p class="text-sm text-gray-600">No domains expiring soon</p>
|
||||
<p class="text-xs text-gray-400 mt-1">within <?= $globalStats['expiring_threshold'] ?? 30 ?> days</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -19,24 +19,47 @@ if (!isset($globalStats)) {
|
||||
$activeResult = $activeStmt->fetch(\PDO::FETCH_ASSOC);
|
||||
$active = $activeResult['count'] ?? 0;
|
||||
|
||||
// Get expiring soon (within 30 days)
|
||||
$expiringSoonStmt = $pdo->query("SELECT COUNT(*) as count FROM domains WHERE expiration_date IS NOT NULL AND expiration_date <= DATE_ADD(NOW(), INTERVAL 30 DAY) AND expiration_date >= NOW()");
|
||||
// Get expiring soon - use the first notification threshold from settings
|
||||
$settingModel = new \App\Models\Setting();
|
||||
$notificationDays = $settingModel->getNotificationDays();
|
||||
$expiringThreshold = !empty($notificationDays) ? max($notificationDays) : 30; // Use the largest notification day
|
||||
|
||||
$expiringSoonStmt = $pdo->prepare("SELECT COUNT(*) as count FROM domains WHERE is_active = 1 AND expiration_date IS NOT NULL AND expiration_date <= DATE_ADD(NOW(), INTERVAL ? DAY) AND expiration_date >= NOW()");
|
||||
$expiringSoonStmt->execute([$expiringThreshold]);
|
||||
$expiringSoonResult = $expiringSoonStmt->fetch(\PDO::FETCH_ASSOC);
|
||||
$expiringSoon = $expiringSoonResult['count'] ?? 0;
|
||||
|
||||
$globalStats = [
|
||||
'total' => $total,
|
||||
'active' => $active,
|
||||
'expiring_soon' => $expiringSoon
|
||||
'expiring_soon' => $expiringSoon,
|
||||
'expiring_threshold' => $expiringThreshold
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
$globalStats = [
|
||||
'total' => 0,
|
||||
'active' => 0,
|
||||
'expiring_soon' => 0
|
||||
'expiring_soon' => 0,
|
||||
'expiring_threshold' => 30
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Get application settings from database
|
||||
if (!isset($appName)) {
|
||||
try {
|
||||
$settingModel = new \App\Models\Setting();
|
||||
$appSettings = $settingModel->getAppSettings();
|
||||
$appName = $appSettings['app_name'];
|
||||
$appTimezone = $appSettings['app_timezone'];
|
||||
|
||||
// Set PHP timezone
|
||||
date_default_timezone_set($appTimezone);
|
||||
} catch (\Exception $e) {
|
||||
$appName = 'Domain Monitor';
|
||||
date_default_timezone_set('UTC');
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@@ -49,7 +72,7 @@ if (!isset($globalStats)) {
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
|
||||
<!-- Title -->
|
||||
<title><?= $title ?? 'Domain Monitor' ?> - <?= $_ENV['APP_NAME'] ?? 'Domain Monitor' ?></title>
|
||||
<title><?= $title ?? 'Domain Monitor' ?> - <?= $appName ?></title>
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="w-9 h-9 bg-primary rounded-lg flex items-center justify-center mr-3">
|
||||
<i class="fas fa-globe text-white text-sm"></i>
|
||||
</div>
|
||||
<h1 class="text-sm font-semibold text-white"><?= $_ENV['APP_NAME'] ?? 'Domain Monitor' ?></h1>
|
||||
<h1 class="text-sm font-semibold text-white"><?= $appName ?? 'Domain Monitor' ?></h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -46,6 +46,17 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Section -->
|
||||
<div class="mt-4 pt-3 border-t border-gray-800">
|
||||
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider px-3 mb-1">System</p>
|
||||
<div class="space-y-0.5">
|
||||
<a href="/settings" class="sidebar-link flex items-center px-3 py-2 rounded-md text-gray-400 hover:bg-gray-800 hover:text-white transition-colors duration-150 <?= strpos($_SERVER['REQUEST_URI'], '/settings') !== false ? 'bg-primary text-white' : '' ?>">
|
||||
<i class="fas fa-cog text-xs mr-3 w-4"></i>
|
||||
<span class="text-sm">Settings</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Quick Stats Cards - Pinned to Bottom -->
|
||||
@@ -70,7 +81,7 @@
|
||||
<div class="w-7 h-7 bg-orange-500/20 rounded flex items-center justify-center mr-2.5">
|
||||
<i class="fas fa-exclamation-triangle text-orange-400 text-xs"></i>
|
||||
</div>
|
||||
<span class="text-gray-400 text-xs">Expiring</span>
|
||||
<span class="text-gray-400 text-xs" title="Within <?= $globalStats['expiring_threshold'] ?? 30 ?> days">Expiring</span>
|
||||
</div>
|
||||
<span class="text-orange-400 font-semibold text-sm"><?= $globalStats['expiring_soon'] ?? 0 ?></span>
|
||||
</div>
|
||||
|
||||
584
app/Views/settings/index.php
Normal file
584
app/Views/settings/index.php
Normal file
@@ -0,0 +1,584 @@
|
||||
<?php
|
||||
$title = 'Settings';
|
||||
$pageTitle = 'System Settings';
|
||||
$pageDescription = 'Configure application, email, and monitoring settings';
|
||||
$pageIcon = 'fas fa-cog';
|
||||
ob_start();
|
||||
|
||||
$currentNotificationDays = $settings['notification_days_before'] ?? '30,15,7,3,1';
|
||||
$currentCheckInterval = $settings['check_interval_hours'] ?? '24';
|
||||
$lastCheckRun = $settings['last_check_run'] ?? null;
|
||||
|
||||
// Get timezone list (popular ones first)
|
||||
$popularTimezones = [
|
||||
'UTC' => 'UTC',
|
||||
'America/New_York' => 'Eastern Time (US)',
|
||||
'America/Chicago' => 'Central Time (US)',
|
||||
'America/Denver' => 'Mountain Time (US)',
|
||||
'America/Los_Angeles' => 'Pacific Time (US)',
|
||||
'Europe/London' => 'London',
|
||||
'Europe/Paris' => 'Paris',
|
||||
'Asia/Tokyo' => 'Tokyo',
|
||||
'Australia/Sydney' => 'Sydney'
|
||||
];
|
||||
|
||||
// Determine which preset is selected
|
||||
$selectedPreset = 'custom';
|
||||
foreach ($notificationPresets as $key => $preset) {
|
||||
if ($preset['value'] === $currentNotificationDays) {
|
||||
$selectedPreset = $key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<!-- Tabs Navigation -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 mb-6">
|
||||
<div class="border-b border-gray-200">
|
||||
<nav class="flex -mb-px overflow-x-auto">
|
||||
<button onclick="switchTab('app')" id="tab-app" class="tab-button px-6 py-3 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap">
|
||||
<i class="fas fa-cog mr-2"></i>
|
||||
Application
|
||||
</button>
|
||||
<button onclick="switchTab('email')" id="tab-email" class="tab-button px-6 py-3 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap">
|
||||
<i class="fas fa-envelope mr-2"></i>
|
||||
Email
|
||||
</button>
|
||||
<button onclick="switchTab('monitoring')" id="tab-monitoring" class="tab-button px-6 py-3 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap">
|
||||
<i class="fas fa-bell mr-2"></i>
|
||||
Monitoring
|
||||
</button>
|
||||
<button onclick="switchTab('system')" id="tab-system" class="tab-button px-6 py-3 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap">
|
||||
<i class="fas fa-server mr-2"></i>
|
||||
System
|
||||
</button>
|
||||
<button onclick="switchTab('maintenance')" id="tab-maintenance" class="tab-button px-6 py-3 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap">
|
||||
<i class="fas fa-tools mr-2"></i>
|
||||
Maintenance
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content: Application Settings -->
|
||||
<div id="content-app" class="tab-content">
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Application Settings</h3>
|
||||
<p class="text-sm text-gray-600 mt-1">Configure basic application information</p>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="/settings/update-app" class="p-6">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="app_name" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Application Name
|
||||
</label>
|
||||
<input type="text" id="app_name" name="app_name" required
|
||||
value="<?= htmlspecialchars($appSettings['app_name']) ?>"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary">
|
||||
<p class="text-xs text-gray-500 mt-1">Name displayed in the interface</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="app_url" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Application URL
|
||||
</label>
|
||||
<input type="url" id="app_url" name="app_url" required
|
||||
value="<?= htmlspecialchars($appSettings['app_url']) ?>"
|
||||
placeholder="https://domains.example.com"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary">
|
||||
<p class="text-xs text-gray-500 mt-1">Base URL for the application (used in emails and links)</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="app_timezone" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Timezone
|
||||
</label>
|
||||
<select id="app_timezone" name="app_timezone" required
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary">
|
||||
<?php foreach ($popularTimezones as $tz => $label): ?>
|
||||
<option value="<?= htmlspecialchars($tz) ?>" <?= $appSettings['app_timezone'] === $tz ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($label) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
<option disabled>──────────</option>
|
||||
<?php
|
||||
$allTimezones = timezone_identifiers_list();
|
||||
foreach ($allTimezones as $tz):
|
||||
if (!isset($popularTimezones[$tz])):
|
||||
?>
|
||||
<option value="<?= htmlspecialchars($tz) ?>" <?= $appSettings['app_timezone'] === $tz ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($tz) ?>
|
||||
</option>
|
||||
<?php
|
||||
endif;
|
||||
endforeach;
|
||||
?>
|
||||
</select>
|
||||
<p class="text-xs text-gray-500 mt-1">Application timezone for dates and times</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between pt-6 mt-6 border-t border-gray-200">
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2.5 bg-primary text-white text-sm rounded-lg hover:bg-primary-dark transition-colors font-medium">
|
||||
<i class="fas fa-save mr-2"></i>
|
||||
Save Application Settings
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content: Email Settings -->
|
||||
<div id="content-email" class="tab-content hidden">
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Email Settings</h3>
|
||||
<p class="text-sm text-gray-600 mt-1">Configure SMTP server for sending notifications</p>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="/settings/update-email" class="p-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="mail_host" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
SMTP Host <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="text" id="mail_host" name="mail_host" required
|
||||
value="<?= htmlspecialchars($emailSettings['mail_host']) ?>"
|
||||
placeholder="smtp.mailtrap.io"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="mail_port" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
SMTP Port <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="number" id="mail_port" name="mail_port" required
|
||||
value="<?= htmlspecialchars($emailSettings['mail_port']) ?>"
|
||||
placeholder="2525"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="mail_encryption" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Encryption
|
||||
</label>
|
||||
<select id="mail_encryption" name="mail_encryption"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary">
|
||||
<option value="tls" <?= $emailSettings['mail_encryption'] === 'tls' ? 'selected' : '' ?>>TLS</option>
|
||||
<option value="ssl" <?= $emailSettings['mail_encryption'] === 'ssl' ? 'selected' : '' ?>>SSL</option>
|
||||
<option value="" <?= empty($emailSettings['mail_encryption']) ? 'selected' : '' ?>>None</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-2">
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3">
|
||||
<p class="text-xs text-gray-600">
|
||||
<i class="fas fa-info-circle text-gray-400 mr-1"></i>
|
||||
<strong>Protocol:</strong> This application uses SMTP (Simple Mail Transfer Protocol) for sending emails.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="mail_username" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
SMTP Username
|
||||
</label>
|
||||
<input type="text" id="mail_username" name="mail_username"
|
||||
value="<?= htmlspecialchars($emailSettings['mail_username']) ?>"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="mail_password" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
SMTP Password
|
||||
</label>
|
||||
<input type="password" id="mail_password" name="mail_password"
|
||||
value="<?= htmlspecialchars($emailSettings['mail_password']) ?>"
|
||||
placeholder="••••••••"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary">
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
<i class="fas fa-lock text-green-600 mr-1"></i>
|
||||
Encrypted before storing in database
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="mail_from_address" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
From Email <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="email" id="mail_from_address" name="mail_from_address" required
|
||||
value="<?= htmlspecialchars($emailSettings['mail_from_address']) ?>"
|
||||
placeholder="noreply@domainmonitor.com"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="mail_from_name" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
From Name
|
||||
</label>
|
||||
<input type="text" id="mail_from_name" name="mail_from_name"
|
||||
value="<?= htmlspecialchars($emailSettings['mail_from_name']) ?>"
|
||||
placeholder="Domain Monitor"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between pt-6 mt-6 border-t border-gray-200">
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2.5 bg-primary text-white text-sm rounded-lg hover:bg-primary-dark transition-colors font-medium">
|
||||
<i class="fas fa-save mr-2"></i>
|
||||
Save Email Settings
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Test Email Section -->
|
||||
<div class="px-6 pb-6">
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<h4 class="text-sm font-semibold text-gray-900 mb-1">Test Email Configuration</h4>
|
||||
<p class="text-sm text-gray-700 mb-3">
|
||||
Send a test email to verify your SMTP settings are configured correctly.
|
||||
</p>
|
||||
<form method="POST" action="/settings/test-email" id="testEmailForm" class="flex gap-2">
|
||||
<input type="email" name="test_email" id="test_email" required
|
||||
placeholder="Enter email address to receive test"
|
||||
class="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary text-sm">
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition-colors font-medium">
|
||||
<i class="fas fa-paper-plane mr-2"></i>
|
||||
Send Test Email
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content: Monitoring Settings -->
|
||||
<div id="content-monitoring" class="tab-content hidden">
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Monitoring Settings</h3>
|
||||
<p class="text-sm text-gray-600 mt-1">Configure notification schedules and check intervals</p>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="/settings/update" id="settingsForm" class="p-6">
|
||||
|
||||
<!-- Notification Settings -->
|
||||
<div class="mb-6">
|
||||
<h4 class="text-base font-semibold text-gray-900 mb-4 flex items-center">
|
||||
<i class="fas fa-bell text-primary mr-2"></i>
|
||||
Notification Schedule
|
||||
</h4>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="notification_preset" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Choose Preset
|
||||
</label>
|
||||
<select class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary"
|
||||
id="notification_preset" name="notification_preset">
|
||||
<?php foreach ($notificationPresets as $key => $preset): ?>
|
||||
<option value="<?= htmlspecialchars($key) ?>"
|
||||
data-value="<?= htmlspecialchars($preset['value']) ?>"
|
||||
<?= $selectedPreset === $key ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($preset['label']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<input type="hidden" id="notification_days_before" name="notification_days_before"
|
||||
value="<?= htmlspecialchars($currentNotificationDays) ?>">
|
||||
|
||||
<!-- Custom days input -->
|
||||
<div id="custom_days_container" style="display: <?= $selectedPreset === 'custom' ? 'block' : 'none' ?>;">
|
||||
<label for="custom_notification_days" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Custom Days
|
||||
</label>
|
||||
<input type="text"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary"
|
||||
id="custom_notification_days"
|
||||
name="custom_notification_days"
|
||||
value="<?= $selectedPreset === 'custom' ? htmlspecialchars($currentNotificationDays) : '' ?>"
|
||||
placeholder="e.g., 90,60,30,14,7,3,1">
|
||||
<p class="text-xs text-gray-500 mt-1">Comma-separated numbers (will be sorted automatically)</p>
|
||||
</div>
|
||||
|
||||
<!-- Preview -->
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||
<p class="text-sm text-gray-700">
|
||||
<i class="fas fa-info-circle text-blue-500 mr-1"></i>
|
||||
Alerts at: <span id="days_preview" class="font-semibold text-primary"><?= htmlspecialchars($currentNotificationDays) ?></span> days
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 my-6"></div>
|
||||
|
||||
<!-- Check Interval -->
|
||||
<div class="mb-6">
|
||||
<h4 class="text-base font-semibold text-gray-900 mb-4 flex items-center">
|
||||
<i class="fas fa-clock text-primary mr-2"></i>
|
||||
Domain Check Interval
|
||||
</h4>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="check_interval_hours" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Check Every
|
||||
</label>
|
||||
<select class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary"
|
||||
id="check_interval_hours" name="check_interval_hours">
|
||||
<?php foreach ($checkIntervalPresets as $preset): ?>
|
||||
<option value="<?= $preset['value'] ?>"
|
||||
<?= $currentCheckInterval == $preset['value'] ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($preset['label']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Last Check Run
|
||||
</label>
|
||||
<div class="px-3 py-2 bg-gray-50 border border-gray-200 rounded-lg">
|
||||
<?php if ($lastCheckRun): ?>
|
||||
<div class="flex items-center text-sm">
|
||||
<i class="fas fa-check-circle text-green-500 mr-2"></i>
|
||||
<span class="text-gray-700"><?= date('M d, Y H:i', strtotime($lastCheckRun)) ?></span>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="flex items-center text-sm">
|
||||
<i class="fas fa-minus-circle text-gray-400 mr-2"></i>
|
||||
<span class="text-gray-500">Never run</span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center justify-between pt-4 border-t border-gray-200">
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2.5 bg-primary text-white text-sm rounded-lg hover:bg-primary-dark transition-colors font-medium">
|
||||
<i class="fas fa-save mr-2"></i>
|
||||
Save Monitoring Settings
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content: System Information -->
|
||||
<div id="content-system" class="tab-content hidden">
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
|
||||
<h3 class="text-lg font-semibold text-gray-900">System Information</h3>
|
||||
<p class="text-sm text-gray-600 mt-1">Cron job configuration and log file locations</p>
|
||||
</div>
|
||||
|
||||
<div class="p-6 space-y-6">
|
||||
<!-- Cron Command -->
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-gray-900 mb-2 flex items-center">
|
||||
<i class="fas fa-terminal text-blue-500 mr-2"></i>
|
||||
Cron Job Command
|
||||
</h4>
|
||||
<div class="bg-gray-900 text-gray-100 px-4 py-3 rounded-lg font-mono text-sm">
|
||||
<code>php cron/check_domains.php</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Crontab Entry -->
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-gray-900 mb-2 flex items-center">
|
||||
<i class="fas fa-calendar-alt text-green-500 mr-2"></i>
|
||||
Recommended Crontab Entry
|
||||
</h4>
|
||||
<div class="bg-gray-900 text-gray-100 px-4 py-3 rounded-lg font-mono text-sm break-all">
|
||||
<code>0 */<?= $currentCheckInterval ?> * * * php /path/to/cron/check_domains.php</code>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">Update the path to match your server installation</p>
|
||||
</div>
|
||||
|
||||
<!-- Log Files -->
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-gray-900 mb-3 flex items-center">
|
||||
<i class="fas fa-file-alt text-orange-500 mr-2"></i>
|
||||
Log Files
|
||||
</h4>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900">Cron Log</p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">Domain check execution logs</p>
|
||||
</div>
|
||||
<code class="text-xs bg-gray-900 text-gray-100 px-2 py-1 rounded">logs/cron.log</code>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900">TLD Import Log</p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">TLD registry import logs</p>
|
||||
</div>
|
||||
<code class="text-xs bg-gray-900 text-gray-100 px-2 py-1 rounded">logs/tld_import_*.log</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content: Maintenance -->
|
||||
<div id="content-maintenance" class="tab-content hidden">
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Maintenance Tools</h3>
|
||||
<p class="text-sm text-gray-600 mt-1">Database cleanup and system maintenance</p>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<!-- Clear Logs -->
|
||||
<div class="mb-6">
|
||||
<h4 class="text-base font-semibold text-gray-900 mb-3 flex items-center">
|
||||
<i class="fas fa-trash-alt text-red-500 mr-2"></i>
|
||||
Clear Old Notification Logs
|
||||
</h4>
|
||||
|
||||
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-4">
|
||||
<div class="flex">
|
||||
<i class="fas fa-exclamation-triangle text-yellow-600 mt-0.5 mr-3"></i>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900">Warning</p>
|
||||
<p class="text-sm text-gray-700 mt-1">
|
||||
This will permanently delete all notification logs older than 30 days. This action cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="/settings/clear-logs" onsubmit="return confirm('Are you sure you want to clear logs older than 30 days? This action cannot be undone.')">
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2.5 bg-red-600 text-white text-sm rounded-lg hover:bg-red-700 transition-colors font-medium">
|
||||
<i class="fas fa-trash-alt mr-2"></i>
|
||||
Clear Old Logs
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Future maintenance tools can be added here -->
|
||||
<div class="border-t border-gray-200 pt-6 mt-6">
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div class="flex">
|
||||
<i class="fas fa-lightbulb text-blue-500 mt-0.5 mr-3"></i>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900">Database Optimization</p>
|
||||
<p class="text-sm text-gray-700 mt-1">
|
||||
Regular maintenance keeps your system running smoothly. Consider clearing old logs monthly.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Tab switching
|
||||
function switchTab(tabName) {
|
||||
// Hide all tabs
|
||||
document.querySelectorAll('.tab-content').forEach(tab => tab.classList.add('hidden'));
|
||||
document.querySelectorAll('.tab-button').forEach(btn => {
|
||||
btn.classList.remove('active', 'border-primary', 'text-primary');
|
||||
btn.classList.add('border-transparent', 'text-gray-500');
|
||||
});
|
||||
|
||||
// Show selected tab
|
||||
document.getElementById('content-' + tabName).classList.remove('hidden');
|
||||
const activeBtn = document.getElementById('tab-' + tabName);
|
||||
activeBtn.classList.add('active', 'border-primary', 'text-primary');
|
||||
activeBtn.classList.remove('border-transparent', 'text-gray-500');
|
||||
|
||||
// Update URL hash without scrolling
|
||||
history.replaceState(null, null, '#' + tabName);
|
||||
}
|
||||
|
||||
// Load tab from URL hash on page load
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
const hash = window.location.hash.substring(1); // Remove the #
|
||||
const validTabs = ['app', 'email', 'monitoring', 'system', 'maintenance'];
|
||||
|
||||
if (hash && validTabs.includes(hash)) {
|
||||
switchTab(hash);
|
||||
} else {
|
||||
// Default to first tab
|
||||
switchTab('app');
|
||||
}
|
||||
});
|
||||
|
||||
// Settings form logic
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const presetSelect = document.getElementById('notification_preset');
|
||||
if (!presetSelect) return;
|
||||
|
||||
const customContainer = document.getElementById('custom_days_container');
|
||||
const customInput = document.getElementById('custom_notification_days');
|
||||
const hiddenInput = document.getElementById('notification_days_before');
|
||||
const daysPreview = document.getElementById('days_preview');
|
||||
|
||||
presetSelect.addEventListener('change', function() {
|
||||
const selectedOption = this.options[this.selectedIndex];
|
||||
const value = selectedOption.dataset.value;
|
||||
|
||||
if (this.value === 'custom') {
|
||||
customContainer.style.display = 'block';
|
||||
customInput.required = true;
|
||||
if (customInput.value) {
|
||||
daysPreview.textContent = customInput.value;
|
||||
}
|
||||
} else {
|
||||
customContainer.style.display = 'none';
|
||||
customInput.required = false;
|
||||
hiddenInput.value = value;
|
||||
daysPreview.textContent = value;
|
||||
}
|
||||
});
|
||||
|
||||
customInput.addEventListener('input', function() {
|
||||
if (presetSelect.value === 'custom') {
|
||||
daysPreview.textContent = this.value || 'Not set';
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('settingsForm').addEventListener('submit', function(e) {
|
||||
if (presetSelect.value === 'custom') {
|
||||
const customValue = customInput.value.trim();
|
||||
|
||||
if (!customValue) {
|
||||
e.preventDefault();
|
||||
alert('Please enter custom notification days');
|
||||
customInput.focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!/^[\d,\s]+$/.test(customValue)) {
|
||||
e.preventDefault();
|
||||
alert('Custom days must contain only numbers and commas');
|
||||
customInput.focus();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
include __DIR__ . '/../layout/base.php';
|
||||
?>
|
||||
106
core/Encryption.php
Normal file
106
core/Encryption.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace Core;
|
||||
|
||||
class Encryption
|
||||
{
|
||||
private string $key;
|
||||
private string $cipher = 'AES-256-CBC';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$key = $_ENV['APP_ENCRYPTION_KEY'] ?? null;
|
||||
|
||||
if (empty($key)) {
|
||||
throw new \Exception('APP_ENCRYPTION_KEY is not set in .env file. Generate one using: php -r "echo base64_encode(random_bytes(32));"');
|
||||
}
|
||||
|
||||
// Decode the base64 key
|
||||
$this->key = base64_decode($key);
|
||||
|
||||
if (strlen($this->key) !== 32) {
|
||||
throw new \Exception('APP_ENCRYPTION_KEY must be 32 bytes (base64 encoded). Generate one using: php -r "echo base64_encode(random_bytes(32));"');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt a value
|
||||
*/
|
||||
public function encrypt(string $value): string
|
||||
{
|
||||
if (empty($value)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Generate a random IV (Initialization Vector)
|
||||
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length($this->cipher));
|
||||
|
||||
// Encrypt the value
|
||||
$encrypted = openssl_encrypt($value, $this->cipher, $this->key, 0, $iv);
|
||||
|
||||
if ($encrypted === false) {
|
||||
throw new \Exception('Encryption failed');
|
||||
}
|
||||
|
||||
// Combine IV and encrypted data, then base64 encode
|
||||
$result = base64_encode($iv . $encrypted);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt a value
|
||||
*/
|
||||
public function decrypt(string $encrypted): string
|
||||
{
|
||||
if (empty($encrypted)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Decode from base64
|
||||
$data = base64_decode($encrypted);
|
||||
|
||||
if ($data === false) {
|
||||
throw new \Exception('Invalid encrypted data');
|
||||
}
|
||||
|
||||
// Extract IV and encrypted data
|
||||
$ivLength = openssl_cipher_iv_length($this->cipher);
|
||||
$iv = substr($data, 0, $ivLength);
|
||||
$encryptedData = substr($data, $ivLength);
|
||||
|
||||
// Decrypt the value
|
||||
$decrypted = openssl_decrypt($encryptedData, $this->cipher, $this->key, 0, $iv);
|
||||
|
||||
if ($decrypted === false) {
|
||||
throw new \Exception('Decryption failed');
|
||||
}
|
||||
|
||||
return $decrypted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new encryption key (base64 encoded)
|
||||
* This should be run once and the result stored in .env
|
||||
*/
|
||||
public static function generateKey(): string
|
||||
{
|
||||
return base64_encode(random_bytes(32));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a value is encrypted (basic heuristic)
|
||||
*/
|
||||
public function isEncrypted(string $value): bool
|
||||
{
|
||||
if (empty($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Encrypted values are base64 encoded
|
||||
// They should be longer than typical plaintext passwords
|
||||
// and contain only base64 characters
|
||||
return preg_match('/^[A-Za-z0-9+\/]+=*$/', $value) && strlen($value) > 40;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ use Dotenv\Dotenv;
|
||||
use App\Models\Domain;
|
||||
use App\Models\NotificationChannel;
|
||||
use App\Models\NotificationLog;
|
||||
use App\Models\Setting;
|
||||
use App\Services\WhoisService;
|
||||
use App\Services\NotificationService;
|
||||
use Core\Database;
|
||||
@@ -32,9 +33,18 @@ new Database();
|
||||
$domainModel = new Domain();
|
||||
$channelModel = new NotificationChannel();
|
||||
$logModel = new NotificationLog();
|
||||
$settingModel = new Setting();
|
||||
$whoisService = new WhoisService();
|
||||
$notificationService = new NotificationService();
|
||||
|
||||
// Set timezone from settings
|
||||
try {
|
||||
$appSettings = $settingModel->getAppSettings();
|
||||
date_default_timezone_set($appSettings['app_timezone']);
|
||||
} catch (\Exception $e) {
|
||||
date_default_timezone_set('UTC');
|
||||
}
|
||||
|
||||
// Log file
|
||||
$logFile = __DIR__ . '/../logs/cron.log';
|
||||
|
||||
@@ -45,11 +55,28 @@ function logMessage(string $message) {
|
||||
echo "[$timestamp] $message\n";
|
||||
}
|
||||
|
||||
function formatElapsedTime(float $seconds): string {
|
||||
if ($seconds < 60) {
|
||||
return sprintf("%.2f seconds", $seconds);
|
||||
} elseif ($seconds < 3600) {
|
||||
$minutes = floor($seconds / 60);
|
||||
$remainingSeconds = $seconds - ($minutes * 60);
|
||||
return sprintf("%d minute%s %.2f seconds", $minutes, $minutes != 1 ? 's' : '', $remainingSeconds);
|
||||
} else {
|
||||
$hours = floor($seconds / 3600);
|
||||
$remainingMinutes = floor(($seconds - ($hours * 3600)) / 60);
|
||||
$remainingSeconds = $seconds - ($hours * 3600) - ($remainingMinutes * 60);
|
||||
return sprintf("%d hour%s %d minute%s %.2f seconds", $hours, $hours != 1 ? 's' : '', $remainingMinutes, $remainingMinutes != 1 ? 's' : '', $remainingSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
// Record start time
|
||||
$startTime = microtime(true);
|
||||
|
||||
logMessage("=== Starting domain check cron job ===");
|
||||
|
||||
// Get notification days from settings
|
||||
$notificationDays = explode(',', $_ENV['NOTIFICATION_DAYS_BEFORE'] ?? '30,15,7,3,1');
|
||||
$notificationDays = array_map('intval', $notificationDays);
|
||||
// Get notification days from database settings
|
||||
$notificationDays = $settingModel->getNotificationDays();
|
||||
|
||||
logMessage("Notification thresholds (days): " . implode(', ', $notificationDays));
|
||||
|
||||
@@ -180,12 +207,21 @@ foreach ($domains as $domain) {
|
||||
}
|
||||
}
|
||||
|
||||
// Update last check run timestamp
|
||||
$settingModel->updateLastCheckRun();
|
||||
|
||||
// Calculate elapsed time
|
||||
$endTime = microtime(true);
|
||||
$elapsedTime = $endTime - $startTime;
|
||||
$formattedTime = formatElapsedTime($elapsedTime);
|
||||
|
||||
// Summary
|
||||
logMessage("\n=== Cron job completed ===");
|
||||
logMessage("Domains checked: {$stats['checked']}");
|
||||
logMessage("Domains updated: {$stats['updated']}");
|
||||
logMessage("Notifications sent: {$stats['notifications_sent']}");
|
||||
logMessage("Errors: {$stats['errors']}");
|
||||
logMessage("Execution time: $formattedTime");
|
||||
logMessage("==========================\n");
|
||||
|
||||
exit(0);
|
||||
|
||||
@@ -10,6 +10,16 @@ $dotenv = Dotenv::createImmutable(__DIR__ . '/..');
|
||||
$dotenv->load();
|
||||
|
||||
try {
|
||||
// Check if encryption key is set
|
||||
if (empty($_ENV['APP_ENCRYPTION_KEY'])) {
|
||||
echo "⚠️ WARNING: APP_ENCRYPTION_KEY is not set in .env\n";
|
||||
echo " This key is required to encrypt sensitive data (like SMTP passwords).\n\n";
|
||||
echo " Generate one using:\n";
|
||||
echo " php scripts/generate-encryption-key.php\n\n";
|
||||
echo " Then add it to your .env file and run migrations again.\n\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$host = $_ENV['DB_HOST'];
|
||||
$port = $_ENV['DB_PORT'];
|
||||
$database = $_ENV['DB_DATABASE'];
|
||||
@@ -36,6 +46,7 @@ try {
|
||||
__DIR__ . '/migrations/004_create_tld_registry_table.sql',
|
||||
__DIR__ . '/migrations/005_update_tld_import_logs.sql',
|
||||
__DIR__ . '/migrations/006_add_complete_workflow_import_type.sql',
|
||||
__DIR__ . '/migrations/007_add_app_and_email_settings.sql',
|
||||
];
|
||||
|
||||
foreach ($migrationFiles as $migrationFile) {
|
||||
|
||||
@@ -28,7 +28,7 @@ CREATE TABLE IF NOT EXISTS domains (
|
||||
registrar VARCHAR(255),
|
||||
expiration_date DATE,
|
||||
last_checked TIMESTAMP NULL,
|
||||
status ENUM('active', 'expiring_soon', 'expired', 'error') DEFAULT 'active',
|
||||
status ENUM('active', 'expiring_soon', 'expired', 'error', 'available') DEFAULT 'active',
|
||||
whois_data JSON,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
17
database/migrations/007_add_app_and_email_settings.sql
Normal file
17
database/migrations/007_add_app_and_email_settings.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
-- Add application and email settings to database
|
||||
INSERT INTO settings (setting_key, setting_value) VALUES
|
||||
-- Application Settings
|
||||
('app_name', 'Domain Monitor'),
|
||||
('app_url', 'http://localhost:8000'),
|
||||
('app_timezone', 'UTC'),
|
||||
|
||||
-- Email Settings
|
||||
('mail_host', 'smtp.mailtrap.io'),
|
||||
('mail_port', '2525'),
|
||||
('mail_username', ''),
|
||||
('mail_password', ''),
|
||||
('mail_encryption', 'tls'),
|
||||
('mail_from_address', 'noreply@domainmonitor.com'),
|
||||
('mail_from_name', 'Domain Monitor')
|
||||
ON DUPLICATE KEY UPDATE setting_key=setting_key;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# Application
|
||||
APP_NAME="Domain Monitor"
|
||||
APP_ENV=development
|
||||
APP_URL=http://localhost:8000
|
||||
APP_TIMEZONE=UTC
|
||||
|
||||
# Security - Generate encryption key using: php -r "echo base64_encode(random_bytes(32));"
|
||||
# This key is used to encrypt sensitive data in the database (like SMTP passwords)
|
||||
# IMPORTANT: Never share this key or commit it to version control!
|
||||
APP_ENCRYPTION_KEY=
|
||||
|
||||
# Database
|
||||
DB_HOST=localhost
|
||||
@@ -16,17 +18,3 @@ SESSION_LIFETIME=1440
|
||||
SESSION_COOKIE_HTTPONLY=1
|
||||
SESSION_COOKIE_SECURE=0
|
||||
SESSION_COOKIE_SAMESITE=Strict
|
||||
|
||||
# Email Configuration
|
||||
MAIL_DRIVER=smtp
|
||||
MAIL_HOST=smtp.mailtrap.io
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=
|
||||
MAIL_PASSWORD=
|
||||
MAIL_ENCRYPTION=tls
|
||||
MAIL_FROM_ADDRESS=noreply@domainmonitor.com
|
||||
MAIL_FROM_NAME="Domain Monitor"
|
||||
|
||||
# Domain Check Settings
|
||||
CHECK_INTERVAL_HOURS=24
|
||||
NOTIFICATION_DAYS_BEFORE=60,30,21,14,7,5,3,2,1
|
||||
@@ -9,6 +9,7 @@ use App\Controllers\AuthController;
|
||||
use App\Controllers\DebugController;
|
||||
use App\Controllers\SearchController;
|
||||
use App\Controllers\TldRegistryController;
|
||||
use App\Controllers\SettingsController;
|
||||
|
||||
$router = Application::$router;
|
||||
|
||||
@@ -76,3 +77,12 @@ $router->get('/tld-registry/{id}/refresh', [TldRegistryController::class, 'refre
|
||||
$router->get('/tld-registry/import-logs', [TldRegistryController::class, 'importLogs']);
|
||||
$router->get('/api/tld-info', [TldRegistryController::class, 'apiGetTldInfo']);
|
||||
|
||||
// Settings
|
||||
$router->get('/settings', [SettingsController::class, 'index']);
|
||||
$router->post('/settings/update', [SettingsController::class, 'update']);
|
||||
$router->post('/settings/update-app', [SettingsController::class, 'updateApp']);
|
||||
$router->post('/settings/update-email', [SettingsController::class, 'updateEmail']);
|
||||
$router->post('/settings/test-email', [SettingsController::class, 'testEmail']);
|
||||
$router->post('/settings/test-cron', [SettingsController::class, 'testCron']);
|
||||
$router->post('/settings/clear-logs', [SettingsController::class, 'clearLogs']);
|
||||
|
||||
|
||||
36
scripts/generate-encryption-key.php
Normal file
36
scripts/generate-encryption-key.php
Normal file
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Generate Encryption Key
|
||||
*
|
||||
* This script generates a secure encryption key for the application.
|
||||
* The key is used to encrypt sensitive data in the database (like SMTP passwords).
|
||||
*
|
||||
* Usage: php scripts/generate-encryption-key.php
|
||||
*/
|
||||
|
||||
echo "===========================================\n";
|
||||
echo " Domain Monitor - Encryption Key Generator\n";
|
||||
echo "===========================================\n\n";
|
||||
|
||||
// Generate a secure 32-byte (256-bit) key
|
||||
$key = random_bytes(32);
|
||||
$encodedKey = base64_encode($key);
|
||||
|
||||
echo "Your encryption key has been generated:\n\n";
|
||||
echo "\033[1;32m$encodedKey\033[0m\n\n";
|
||||
|
||||
echo "Add this to your .env file:\n\n";
|
||||
echo "APP_ENCRYPTION_KEY=$encodedKey\n\n";
|
||||
|
||||
echo "⚠️ IMPORTANT SECURITY NOTES:\n";
|
||||
echo "-------------------------------------------\n";
|
||||
echo "1. Keep this key SECRET - never share it\n";
|
||||
echo "2. Never commit this key to version control\n";
|
||||
echo "3. If you lose this key, encrypted data cannot be recovered\n";
|
||||
echo "4. Don't change this key after encrypting data\n";
|
||||
echo "5. Store a backup of this key in a secure location\n\n";
|
||||
|
||||
echo "✅ Done! Copy the key to your .env file.\n";
|
||||
|
||||
Reference in New Issue
Block a user