2025-10-08 18:54:34 +03:00
|
|
|
<?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'));
|
|
|
|
|
}
|
|
|
|
|
|
Upgraded to 1.1.0
1.1.0 (2025-10-09)
- **User Notifications System** - In-app notification center with 7 notification types, filtering, pagination
- **Advanced Session Management** - Database-backed sessions with geolocation (country, city, ISP)
- **Remote Session Control** - Terminate any device instantly with immediate logout validation
- **Enhanced Profile Page** - Sidebar navigation with 4 tabs, hash-based routing (#profile, #security, #sessions)
- **MVC Architecture Refactoring** - 3 new Helpers (Layout, Domain, Session), ~265 lines cleaned from views
- **Geolocation Tracking** - IP-based location detection using ip-api.com, country flags with flag-icons
- **Device Detection** - Browser & device type parsing (Chrome/Firefox/Safari, Desktop/Mobile/Tablet)
- **Auto-Detected Cron Paths** - Settings show actual installation paths (thanks @jadeops)
- **Welcome Notifications** - Sent to new users on registration or fresh install
- **Upgrade Notifications** - Admins notified on system updates with version & migration count
- **Web-Based Installer** - Replaces CLI, auto-generates encryption key, one-time password display
- **Web-Based Updater** - `/install/update` for running new migrations with smart detection
- **User Registration** - Full signup flow with email verification, password reset, resend verification
- **User Management** - CRUD for users with filtering, sorting, pagination (admin-only)
- **Remember Me** - 30-day secure tokens linked to sessions, cascade deletion on logout
- **Session Validator** - Middleware validates sessions on every request for instant remote logout
- **Consistent UI/UX** - Unified filtering, sorting, pagination across Domains, Users, Notifications, TLD Registry
- **Smart Migrations** - Consolidated schema for fresh installs, incremental for upgrades
- **XSS Protection** - htmlspecialchars() applied across all user-facing data (thanks @jadeops)
2025-10-09 18:02:46 +03:00
|
|
|
/**
|
|
|
|
|
* Get application version
|
|
|
|
|
*/
|
|
|
|
|
public function getAppVersion(): string
|
|
|
|
|
{
|
|
|
|
|
return $this->getValue('app_version', '1.1.0');
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-08 18:54:34 +03:00
|
|
|
/**
|
|
|
|
|
* 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'),
|
Upgraded to 1.1.0
1.1.0 (2025-10-09)
- **User Notifications System** - In-app notification center with 7 notification types, filtering, pagination
- **Advanced Session Management** - Database-backed sessions with geolocation (country, city, ISP)
- **Remote Session Control** - Terminate any device instantly with immediate logout validation
- **Enhanced Profile Page** - Sidebar navigation with 4 tabs, hash-based routing (#profile, #security, #sessions)
- **MVC Architecture Refactoring** - 3 new Helpers (Layout, Domain, Session), ~265 lines cleaned from views
- **Geolocation Tracking** - IP-based location detection using ip-api.com, country flags with flag-icons
- **Device Detection** - Browser & device type parsing (Chrome/Firefox/Safari, Desktop/Mobile/Tablet)
- **Auto-Detected Cron Paths** - Settings show actual installation paths (thanks @jadeops)
- **Welcome Notifications** - Sent to new users on registration or fresh install
- **Upgrade Notifications** - Admins notified on system updates with version & migration count
- **Web-Based Installer** - Replaces CLI, auto-generates encryption key, one-time password display
- **Web-Based Updater** - `/install/update` for running new migrations with smart detection
- **User Registration** - Full signup flow with email verification, password reset, resend verification
- **User Management** - CRUD for users with filtering, sorting, pagination (admin-only)
- **Remember Me** - 30-day secure tokens linked to sessions, cascade deletion on logout
- **Session Validator** - Middleware validates sessions on every request for instant remote logout
- **Consistent UI/UX** - Unified filtering, sorting, pagination across Domains, Users, Notifications, TLD Registry
- **Smart Migrations** - Consolidated schema for fresh installs, incremental for upgrades
- **XSS Protection** - htmlspecialchars() applied across all user-facing data (thanks @jadeops)
2025-10-09 18:02:46 +03:00
|
|
|
'app_timezone' => $this->getValue('app_timezone', 'UTC'),
|
|
|
|
|
'app_version' => $this->getAppVersion()
|
2025-10-08 18:54:34 +03:00
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
}
|
Add CSRF, CAPTCHA, and input validation improvements
Introduces CSRF protection to all sensitive controller actions, integrates configurable CAPTCHA (reCAPTCHA v2/v3, Turnstile) for authentication and registration flows, and centralizes input validation via a new InputValidator helper. Adds new helpers and services for CSRF and CAPTCHA, updates settings and migration for CAPTCHA configuration, and enhances logging and error handling in TLD registry import processes. Also improves validation for user, domain, group, and profile inputs throughout the application.
2025-10-10 00:04:12 +03:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get CAPTCHA settings
|
|
|
|
|
*/
|
|
|
|
|
public function getCaptchaSettings(): array
|
|
|
|
|
{
|
|
|
|
|
$encryptedSecret = $this->getValue('captcha_secret_key', '');
|
|
|
|
|
|
|
|
|
|
// Decrypt secret key if it's encrypted
|
|
|
|
|
$decryptedSecret = '';
|
|
|
|
|
if (!empty($encryptedSecret)) {
|
|
|
|
|
try {
|
|
|
|
|
$encryption = new \Core\Encryption();
|
|
|
|
|
$decryptedSecret = $encryption->decrypt($encryptedSecret);
|
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
|
// If decryption fails, it might be plaintext (migration scenario)
|
|
|
|
|
error_log("Failed to decrypt captcha_secret_key: " . $e->getMessage());
|
|
|
|
|
$decryptedSecret = $encryptedSecret;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'provider' => $this->getValue('captcha_provider', 'disabled'),
|
|
|
|
|
'site_key' => $this->getValue('captcha_site_key', ''),
|
|
|
|
|
'secret_key' => $decryptedSecret,
|
|
|
|
|
'score_threshold' => $this->getValue('recaptcha_v3_score_threshold', '0.5')
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update CAPTCHA settings
|
|
|
|
|
*/
|
|
|
|
|
public function updateCaptchaSettings(array $settings): bool
|
|
|
|
|
{
|
|
|
|
|
$result = true;
|
|
|
|
|
|
|
|
|
|
// Encrypt secret key before storing
|
|
|
|
|
if (isset($settings['captcha_secret_key']) && !empty($settings['captcha_secret_key'])) {
|
|
|
|
|
try {
|
|
|
|
|
$encryption = new \Core\Encryption();
|
|
|
|
|
$settings['captcha_secret_key'] = $encryption->encrypt($settings['captcha_secret_key']);
|
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
|
error_log("Failed to encrypt captcha_secret_key: " . $e->getMessage());
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ($settings as $key => $value) {
|
|
|
|
|
if (!$this->setValue($key, $value)) {
|
|
|
|
|
$result = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
|
}
|
2025-10-08 18:54:34 +03:00
|
|
|
}
|
|
|
|
|
|