1226 lines
51 KiB
PHP
1226 lines
51 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Filament\Admin\Pages;
|
|
|
|
use App\Filament\Admin\Widgets\Settings\DnssecTable;
|
|
use App\Filament\Admin\Widgets\Settings\NotificationLogTable;
|
|
use App\Models\DnsSetting;
|
|
use App\Services\Agent\AgentClient;
|
|
use BackedEnum;
|
|
use Exception;
|
|
use Filament\Actions\Action;
|
|
use Filament\Actions\Action as FormAction;
|
|
use Filament\Actions\Concerns\InteractsWithActions;
|
|
use Filament\Actions\Contracts\HasActions;
|
|
use Filament\Forms\Components\FileUpload;
|
|
use Filament\Forms\Components\TextInput;
|
|
use Filament\Forms\Components\Toggle;
|
|
use Filament\Forms\Concerns\InteractsWithForms;
|
|
use Filament\Forms\Contracts\HasForms;
|
|
use Filament\Notifications\Notification;
|
|
use Filament\Pages\Page;
|
|
use Filament\Schemas\Components\Actions;
|
|
use Filament\Schemas\Components\EmbeddedTable;
|
|
use Filament\Schemas\Components\Grid;
|
|
use Filament\Schemas\Components\Section;
|
|
use Filament\Schemas\Components\View;
|
|
use Filament\Schemas\Schema;
|
|
use Illuminate\Contracts\Support\Htmlable;
|
|
use Illuminate\Support\Facades\Artisan;
|
|
use Illuminate\Support\Facades\File;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Illuminate\Support\Facades\Response;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Livewire\Attributes\Url;
|
|
use Livewire\WithFileUploads;
|
|
|
|
class ServerSettings extends Page implements HasActions, HasForms
|
|
{
|
|
use InteractsWithActions;
|
|
use InteractsWithForms;
|
|
use WithFileUploads;
|
|
|
|
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-cog-6-tooth';
|
|
|
|
protected static ?int $navigationSort = 3;
|
|
|
|
protected static ?string $slug = 'server-settings';
|
|
|
|
public static function getNavigationLabel(): string
|
|
{
|
|
return __('Server Settings');
|
|
}
|
|
|
|
protected string $view = 'filament.admin.pages.server-settings';
|
|
|
|
// Form data arrays
|
|
public ?array $brandingData = [];
|
|
|
|
public ?array $hostnameData = [];
|
|
|
|
public ?array $dnsData = [];
|
|
|
|
public ?array $resolversData = [];
|
|
|
|
public ?array $quotaData = [];
|
|
|
|
public ?array $fileManagerData = [];
|
|
|
|
public ?array $emailData = [];
|
|
|
|
public ?array $notificationsData = [];
|
|
|
|
public ?array $phpFpmData = [];
|
|
|
|
// Version info (non-form)
|
|
public string $currentVersion = '';
|
|
|
|
public string $latestVersion = '';
|
|
|
|
public int $updatesAvailable = 0;
|
|
|
|
public bool $isChecking = false;
|
|
|
|
public bool $isUpgrading = false;
|
|
|
|
public string $upgradeLog = '';
|
|
|
|
public bool $isSystemdResolved = false;
|
|
|
|
public ?string $currentLogo = null;
|
|
|
|
#[Url(as: 'tab')]
|
|
public ?string $activeTab = 'general';
|
|
|
|
public function getLogoUrlProperty(): ?string
|
|
{
|
|
if ($this->currentLogo && Storage::disk('public')->exists($this->currentLogo)) {
|
|
return asset('storage/'.$this->currentLogo);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
protected function getAgent(): AgentClient
|
|
{
|
|
return new AgentClient;
|
|
}
|
|
|
|
public function getTitle(): string|Htmlable
|
|
{
|
|
return __('Server Settings');
|
|
}
|
|
|
|
protected function normalizeTabName(?string $tab): string
|
|
{
|
|
return match ($tab) {
|
|
'general', 'dns', 'storage', 'email', 'notifications', 'php-fpm' => $tab,
|
|
default => 'general',
|
|
};
|
|
}
|
|
|
|
public function setTab(string $tab): void
|
|
{
|
|
$this->activeTab = $this->normalizeTabName($tab);
|
|
}
|
|
|
|
public function mount(): void
|
|
{
|
|
$this->activeTab = $this->normalizeTabName($this->activeTab);
|
|
$settings = DnsSetting::getAll();
|
|
$hostname = gethostname() ?: 'localhost';
|
|
$serverIp = trim(shell_exec("hostname -I | awk '{print $1}'") ?? '') ?: '';
|
|
|
|
$this->currentLogo = $settings['custom_logo'] ?? null;
|
|
$this->isSystemdResolved = trim(shell_exec('systemctl is-active systemd-resolved 2>/dev/null') ?? '') === 'active';
|
|
|
|
// Load hostname from agent
|
|
$agentHostname = $hostname;
|
|
try {
|
|
$result = $this->getAgent()->send('server.info', []);
|
|
if ($result['success'] ?? false) {
|
|
$agentHostname = $result['info']['hostname'] ?? $hostname;
|
|
}
|
|
} catch (Exception $e) {
|
|
// Use default
|
|
}
|
|
|
|
// Load resolvers
|
|
$resolvers = ['', '', ''];
|
|
$searchDomain = '';
|
|
if (file_exists('/etc/resolv.conf')) {
|
|
$content = file_get_contents('/etc/resolv.conf');
|
|
$lines = explode("\n", $content);
|
|
$ns = [];
|
|
foreach ($lines as $line) {
|
|
$line = trim($line);
|
|
if (str_starts_with($line, 'nameserver ')) {
|
|
$ns[] = trim(substr($line, 11));
|
|
} elseif (str_starts_with($line, 'search ')) {
|
|
$searchDomain = trim(substr($line, 7));
|
|
}
|
|
}
|
|
$resolvers = [$ns[0] ?? '', $ns[1] ?? '', $ns[2] ?? ''];
|
|
}
|
|
|
|
// Fill form data
|
|
$this->brandingData = [
|
|
'panel_name' => $settings['panel_name'] ?? 'Jabali',
|
|
];
|
|
|
|
$this->hostnameData = [
|
|
'hostname' => $agentHostname,
|
|
];
|
|
|
|
$this->dnsData = [
|
|
'ns1' => $settings['ns1'] ?? "ns1.{$hostname}",
|
|
'ns1_ip' => $settings['ns1_ip'] ?? $serverIp,
|
|
'ns2' => $settings['ns2'] ?? "ns2.{$hostname}",
|
|
'ns2_ip' => $settings['ns2_ip'] ?? $serverIp,
|
|
'default_ip' => $settings['default_ip'] ?? $serverIp,
|
|
'default_ipv6' => $settings['default_ipv6'] ?? '',
|
|
'default_ttl' => $settings['default_ttl'] ?? '3600',
|
|
'admin_email' => $settings['admin_email'] ?? "admin.{$hostname}",
|
|
];
|
|
|
|
$this->resolversData = [
|
|
'resolver1' => $resolvers[0],
|
|
'resolver2' => $resolvers[1],
|
|
'resolver3' => $resolvers[2],
|
|
'search_domain' => $searchDomain,
|
|
];
|
|
|
|
$this->quotaData = [
|
|
'quotas_enabled' => (bool) ($settings['quotas_enabled'] ?? false),
|
|
'default_quota_mb' => (int) ($settings['default_quota_mb'] ?? 5120),
|
|
];
|
|
|
|
$this->fileManagerData = [
|
|
'max_upload_size_mb' => (int) ($settings['max_upload_size_mb'] ?? 100),
|
|
];
|
|
|
|
$this->emailData = [
|
|
'mail_hostname' => $settings['mail_hostname'] ?? "mail.{$hostname}",
|
|
'mail_default_quota_mb' => (int) ($settings['mail_default_quota_mb'] ?? 1024),
|
|
'max_mailboxes_per_domain' => (int) ($settings['max_mailboxes_per_domain'] ?? 10),
|
|
'webmail_url' => $settings['webmail_url'] ?? '/webmail',
|
|
'webmail_product_name' => $settings['webmail_product_name'] ?? 'Jabali Webmail',
|
|
];
|
|
|
|
$this->notificationsData = [
|
|
'admin_email_recipients' => $settings['admin_email_recipients'] ?? '',
|
|
'notify_ssl_errors' => (bool) ($settings['notify_ssl_errors'] ?? true),
|
|
'notify_backup_failures' => (bool) ($settings['notify_backup_failures'] ?? true),
|
|
'notify_backup_success' => (bool) ($settings['notify_backup_success'] ?? false),
|
|
'notify_disk_quota' => (bool) ($settings['notify_disk_quota'] ?? true),
|
|
'notify_login_failures' => (bool) ($settings['notify_login_failures'] ?? true),
|
|
'notify_ssh_logins' => (bool) ($settings['notify_ssh_logins'] ?? false),
|
|
'notify_system_updates' => (bool) ($settings['notify_system_updates'] ?? false),
|
|
'notify_service_health' => (bool) ($settings['notify_service_health'] ?? true),
|
|
'notify_high_load' => (bool) ($settings['notify_high_load'] ?? true),
|
|
'load_threshold' => (float) ($settings['load_threshold'] ?? 5.0),
|
|
'load_alert_minutes' => (int) ($settings['load_alert_minutes'] ?? 5),
|
|
];
|
|
|
|
$this->phpFpmData = [
|
|
'pm_max_children' => (int) ($settings['fpm_pm_max_children'] ?? 5),
|
|
'pm_max_requests' => (int) ($settings['fpm_pm_max_requests'] ?? 200),
|
|
'rlimit_files' => (int) ($settings['fpm_rlimit_files'] ?? 1024),
|
|
'process_priority' => (int) ($settings['fpm_process_priority'] ?? 0),
|
|
'request_terminate_timeout' => (int) ($settings['fpm_request_terminate_timeout'] ?? 300),
|
|
'memory_limit' => $settings['fpm_memory_limit'] ?? '512M',
|
|
];
|
|
|
|
$this->loadVersionInfo();
|
|
}
|
|
|
|
public function settingsForm(Schema $schema): Schema
|
|
{
|
|
return $schema
|
|
->schema([
|
|
View::make('filament.admin.components.server-settings-tabs-nav'),
|
|
...$this->getTabContent(),
|
|
]);
|
|
}
|
|
|
|
protected function getTabContent(): array
|
|
{
|
|
return match ($this->activeTab) {
|
|
'general' => $this->generalTabContent(),
|
|
'dns' => $this->dnsTabContent(),
|
|
'storage' => $this->storageTabContent(),
|
|
'email' => $this->emailTabContent(),
|
|
'notifications' => $this->notificationsTabContent(),
|
|
'php-fpm' => $this->phpFpmTabContent(),
|
|
default => $this->generalTabContent(),
|
|
};
|
|
}
|
|
|
|
protected function generalTabContent(): array
|
|
{
|
|
return [
|
|
Section::make(__('Panel Version & Updates'))
|
|
->description($this->currentVersion ?: __('Unknown'))
|
|
->icon('heroicon-o-arrow-up-tray')
|
|
->schema([
|
|
Actions::make([
|
|
FormAction::make('checkForUpdates')
|
|
->label(__('Check for Updates'))
|
|
->icon('heroicon-o-arrow-path')
|
|
->color('gray')
|
|
->action('checkForUpdates'),
|
|
FormAction::make('performUpgrade')
|
|
->label(__('Upgrade Now'))
|
|
->icon('heroicon-o-arrow-up-tray')
|
|
->color('success')
|
|
->requiresConfirmation()
|
|
->action('performUpgrade')
|
|
->visible(fn () => $this->updatesAvailable > 0),
|
|
]),
|
|
]),
|
|
Section::make(__('Panel Branding'))
|
|
->icon('heroicon-o-paint-brush')
|
|
->schema([
|
|
Grid::make(['default' => 1, 'md' => 2])->schema([
|
|
TextInput::make('brandingData.panel_name')
|
|
->label(__('Control Panel Name'))
|
|
->placeholder('Jabali')
|
|
->helperText(__('Appears in browser title and navigation'))
|
|
->required(),
|
|
]),
|
|
Actions::make([
|
|
FormAction::make('uploadLogo')
|
|
->label(__('Upload Logo'))
|
|
->icon('heroicon-o-arrow-up-tray')
|
|
->color('gray')
|
|
->form([
|
|
FileUpload::make('logo')
|
|
->label(__('Logo Image'))
|
|
->image()
|
|
->disk('public')
|
|
->directory('branding')
|
|
->visibility('public')
|
|
->acceptedFileTypes(['image/png', 'image/svg+xml', 'image/jpeg', 'image/webp'])
|
|
->maxSize(1024)
|
|
->required()
|
|
->helperText(__('SVG, PNG, JPEG or WebP, max 1MB')),
|
|
])
|
|
->action(function (array $data): void {
|
|
$this->uploadLogo($data);
|
|
}),
|
|
FormAction::make('removeLogo')
|
|
->label(__('Remove Logo'))
|
|
->color('danger')
|
|
->icon('heroicon-o-trash')
|
|
->requiresConfirmation()
|
|
->action(fn () => $this->removeLogo())
|
|
->visible(fn () => $this->currentLogo !== null),
|
|
FormAction::make('saveBranding')
|
|
->label(__('Save Branding'))
|
|
->action('saveBranding'),
|
|
]),
|
|
]),
|
|
Section::make(__('Server Hostname'))
|
|
->icon('heroicon-o-server')
|
|
->schema([
|
|
TextInput::make('hostnameData.hostname')
|
|
->label(__('Hostname'))
|
|
->placeholder('server.example.com')
|
|
->required(),
|
|
Actions::make([
|
|
FormAction::make('saveHostname')
|
|
->label(__('Save Hostname'))
|
|
->action('saveHostname'),
|
|
]),
|
|
]),
|
|
];
|
|
}
|
|
|
|
protected function dnsTabContent(): array
|
|
{
|
|
return [
|
|
Section::make(__('Nameservers'))
|
|
->icon('heroicon-o-server-stack')
|
|
->schema([
|
|
Grid::make(['default' => 1, 'md' => 2, 'lg' => 4])->schema([
|
|
TextInput::make('dnsData.ns1')->label(__('NS1 Hostname'))->placeholder('ns1.example.com'),
|
|
TextInput::make('dnsData.ns1_ip')->label(__('NS1 IP Address'))->placeholder('192.168.1.1'),
|
|
TextInput::make('dnsData.ns2')->label(__('NS2 Hostname'))->placeholder('ns2.example.com'),
|
|
TextInput::make('dnsData.ns2_ip')->label(__('NS2 IP Address'))->placeholder('192.168.1.2'),
|
|
]),
|
|
]),
|
|
Section::make(__('Zone Defaults'))
|
|
->schema([
|
|
Grid::make(['default' => 1, 'md' => 3])->schema([
|
|
TextInput::make('dnsData.default_ip')
|
|
->label(__('Default Server IP'))
|
|
->placeholder('192.168.1.1')
|
|
->helperText(__('Default A record IP for new zones')),
|
|
TextInput::make('dnsData.default_ipv6')
|
|
->label(__('Default IPv6'))
|
|
->placeholder('2001:db8::1')
|
|
->helperText(__('Default AAAA record IP for new zones'))
|
|
->rule('nullable|ipv6'),
|
|
TextInput::make('dnsData.default_ttl')
|
|
->label(__('Default TTL'))
|
|
->placeholder('3600'),
|
|
]),
|
|
TextInput::make('dnsData.admin_email')
|
|
->label(__('Admin Email (SOA)'))
|
|
->placeholder('admin.example.com')
|
|
->helperText(__('Use dots instead of @ (e.g., admin.example.com)')),
|
|
Actions::make([
|
|
FormAction::make('saveDns')
|
|
->label(__('Save DNS Settings'))
|
|
->action('saveDns'),
|
|
]),
|
|
]),
|
|
Section::make(__('DNS Resolvers'))
|
|
->description($this->isSystemdResolved ? __('systemd-resolved active') : null)
|
|
->icon('heroicon-o-signal')
|
|
->schema([
|
|
Grid::make(['default' => 1, 'md' => 2, 'lg' => 4])->schema([
|
|
TextInput::make('resolversData.resolver1')->label(__('Resolver 1'))->placeholder('8.8.8.8'),
|
|
TextInput::make('resolversData.resolver2')->label(__('Resolver 2'))->placeholder('8.8.4.4'),
|
|
TextInput::make('resolversData.resolver3')->label(__('Resolver 3'))->placeholder('1.1.1.1'),
|
|
TextInput::make('resolversData.search_domain')->label(__('Search Domain'))->placeholder('example.com'),
|
|
]),
|
|
Actions::make([
|
|
FormAction::make('saveResolvers')
|
|
->label(__('Save Resolvers'))
|
|
->action('saveResolvers'),
|
|
]),
|
|
]),
|
|
Section::make(__('DNSSEC'))
|
|
->description(__('DNS Security Extensions'))
|
|
->icon('heroicon-o-shield-check')
|
|
->schema([
|
|
EmbeddedTable::make(DnssecTable::class),
|
|
]),
|
|
];
|
|
}
|
|
|
|
protected function storageTabContent(): array
|
|
{
|
|
return [
|
|
Section::make(__('Disk Quotas'))
|
|
->icon('heroicon-o-chart-pie')
|
|
->schema([
|
|
Grid::make(['default' => 1, 'md' => 2])->schema([
|
|
Toggle::make('quotaData.quotas_enabled')
|
|
->label(__('Enable Disk Quotas'))
|
|
->helperText(__('When enabled, disk usage limits will be enforced for user accounts')),
|
|
TextInput::make('quotaData.default_quota_mb')
|
|
->label(__('Default Quota (MB)'))
|
|
->numeric()
|
|
->placeholder('5120')
|
|
->helperText(__('Default disk quota for new users (5120 MB = 5 GB)')),
|
|
]),
|
|
Actions::make([
|
|
FormAction::make('saveQuotaSettings')
|
|
->label(__('Save Quota Settings'))
|
|
->action('saveQuotaSettings'),
|
|
]),
|
|
]),
|
|
Section::make(__('File Manager'))
|
|
->icon('heroicon-o-folder')
|
|
->schema([
|
|
TextInput::make('fileManagerData.max_upload_size_mb')
|
|
->label(__('Max Upload Size (MB)'))
|
|
->numeric()
|
|
->minValue(1)
|
|
->maxValue(500)
|
|
->placeholder('100')
|
|
->helperText(__('Maximum file size users can upload (1-500 MB)')),
|
|
Actions::make([
|
|
FormAction::make('saveFileManagerSettings')
|
|
->label(__('Save'))
|
|
->action('saveFileManagerSettings'),
|
|
]),
|
|
]),
|
|
];
|
|
}
|
|
|
|
protected function emailTabContent(): array
|
|
{
|
|
return [
|
|
Section::make(__('Mail Server'))
|
|
->icon('heroicon-o-envelope')
|
|
->schema([
|
|
Grid::make(['default' => 1, 'md' => 2])->schema([
|
|
TextInput::make('emailData.mail_hostname')
|
|
->label(__('Mail Server Hostname'))
|
|
->placeholder('mail.example.com')
|
|
->helperText(__('The hostname used for mail server identification')),
|
|
TextInput::make('emailData.mail_default_quota_mb')
|
|
->label(__('Default Mailbox Quota (MB)'))
|
|
->numeric()
|
|
->minValue(100)
|
|
->maxValue(10240),
|
|
TextInput::make('emailData.max_mailboxes_per_domain')
|
|
->label(__('Max Mailboxes Per Domain'))
|
|
->numeric()
|
|
->minValue(1)
|
|
->maxValue(1000),
|
|
]),
|
|
]),
|
|
Section::make(__('Webmail'))
|
|
->icon('heroicon-o-globe-alt')
|
|
->schema([
|
|
Grid::make(['default' => 1, 'md' => 2])->schema([
|
|
TextInput::make('emailData.webmail_url')
|
|
->label(__('Webmail URL'))
|
|
->placeholder('/webmail')
|
|
->helperText(__('URL path for Roundcube webmail')),
|
|
TextInput::make('emailData.webmail_product_name')
|
|
->label(__('Webmail Product Name'))
|
|
->placeholder('Jabali Webmail')
|
|
->helperText(__('Name displayed on the webmail login page')),
|
|
]),
|
|
Actions::make([
|
|
FormAction::make('openWebmail')
|
|
->label(__('Open Webmail'))
|
|
->icon('heroicon-o-arrow-top-right-on-square')
|
|
->color('gray')
|
|
->url('/webmail', shouldOpenInNewTab: true),
|
|
FormAction::make('saveEmailSettings')
|
|
->label(__('Save Email Settings'))
|
|
->action('saveEmailSettings'),
|
|
]),
|
|
]),
|
|
];
|
|
}
|
|
|
|
protected function notificationsTabContent(): array
|
|
{
|
|
return [
|
|
Section::make(__('Admin Recipients'))
|
|
->icon('heroicon-o-user-group')
|
|
->schema([
|
|
TextInput::make('notificationsData.admin_email_recipients')
|
|
->label(__('Email Addresses'))
|
|
->placeholder('admin@example.com, alerts@example.com')
|
|
->helperText(__('Comma-separated list of email addresses to receive notifications')),
|
|
]),
|
|
Section::make(__('Notification Types'))
|
|
->icon('heroicon-o-bell-alert')
|
|
->schema([
|
|
Grid::make(['default' => 1, 'md' => 2])->schema([
|
|
Toggle::make('notificationsData.notify_ssl_errors')
|
|
->label(__('SSL Certificate Alerts'))
|
|
->helperText(__('Errors and expiring certificates')),
|
|
Toggle::make('notificationsData.notify_backup_failures')
|
|
->label(__('Backup Failures'))
|
|
->helperText(__('Failed scheduled backups')),
|
|
Toggle::make('notificationsData.notify_backup_success')
|
|
->label(__('Backup Success'))
|
|
->helperText(__('Successful backup completions')),
|
|
Toggle::make('notificationsData.notify_disk_quota')
|
|
->label(__('Disk Quota Warnings'))
|
|
->helperText(__('When users reach 90% quota')),
|
|
Toggle::make('notificationsData.notify_login_failures')
|
|
->label(__('Login Failure Alerts'))
|
|
->helperText(__('Brute force and Fail2ban alerts')),
|
|
Toggle::make('notificationsData.notify_ssh_logins')
|
|
->label(__('SSH Login Alerts'))
|
|
->helperText(__('Successful SSH login notifications')),
|
|
Toggle::make('notificationsData.notify_system_updates')
|
|
->label(__('System Updates Available'))
|
|
->helperText(__('When panel updates are available')),
|
|
Toggle::make('notificationsData.notify_service_health')
|
|
->label(__('Service Health Alerts'))
|
|
->helperText(__('Service failures and auto-restarts')),
|
|
]),
|
|
]),
|
|
Section::make(__('High Load Alerts'))
|
|
->icon('heroicon-o-cpu-chip')
|
|
->schema([
|
|
Grid::make(['default' => 1, 'md' => 3])->schema([
|
|
Toggle::make('notificationsData.notify_high_load')
|
|
->label(__('Enable High Load Alerts'))
|
|
->helperText(__('Alert when server load is high')),
|
|
TextInput::make('notificationsData.load_threshold')
|
|
->label(__('Load Threshold'))
|
|
->numeric()
|
|
->minValue(1)
|
|
->maxValue(100)
|
|
->step(0.5)
|
|
->placeholder('5')
|
|
->helperText(__('Alert when load exceeds this value')),
|
|
TextInput::make('notificationsData.load_alert_minutes')
|
|
->label(__('Alert After (minutes)'))
|
|
->numeric()
|
|
->minValue(1)
|
|
->maxValue(60)
|
|
->placeholder('5')
|
|
->helperText(__('Minutes of high load before alerting')),
|
|
]),
|
|
Actions::make([
|
|
FormAction::make('sendTestEmail')
|
|
->label(__('Send Test Email'))
|
|
->color('gray')
|
|
->action('sendTestEmail'),
|
|
FormAction::make('saveEmailNotificationSettings')
|
|
->label(__('Save Notification Settings'))
|
|
->action('saveEmailNotificationSettings'),
|
|
]),
|
|
]),
|
|
Section::make(__('Notification Log'))
|
|
->description(__('Last 30 days'))
|
|
->icon('heroicon-o-document-text')
|
|
->schema([
|
|
EmbeddedTable::make(NotificationLogTable::class),
|
|
]),
|
|
];
|
|
}
|
|
|
|
protected function phpFpmTabContent(): array
|
|
{
|
|
return [
|
|
Section::make(__('Default Pool Limits'))
|
|
->description(__('These settings apply to new user pools. Use "Apply to All" to update existing pools.'))
|
|
->icon('heroicon-o-adjustments-horizontal')
|
|
->schema([
|
|
Grid::make(['default' => 1, 'md' => 2, 'lg' => 3])->schema([
|
|
TextInput::make('phpFpmData.pm_max_children')
|
|
->label(__('Max Processes'))
|
|
->numeric()
|
|
->minValue(1)
|
|
->maxValue(50)
|
|
->helperText(__('Max PHP workers per user (1-50)')),
|
|
TextInput::make('phpFpmData.pm_max_requests')
|
|
->label(__('Max Requests'))
|
|
->numeric()
|
|
->minValue(50)
|
|
->maxValue(10000)
|
|
->helperText(__('Requests before worker recycle')),
|
|
TextInput::make('phpFpmData.memory_limit')
|
|
->label(__('Memory Limit'))
|
|
->placeholder('512M')
|
|
->helperText(__('PHP memory_limit (e.g., 512M, 1G)')),
|
|
]),
|
|
Grid::make(['default' => 1, 'md' => 2, 'lg' => 3])->schema([
|
|
TextInput::make('phpFpmData.rlimit_files')
|
|
->label(__('Open Files Limit'))
|
|
->numeric()
|
|
->minValue(256)
|
|
->maxValue(65536)
|
|
->helperText(__('Max open file descriptors')),
|
|
TextInput::make('phpFpmData.process_priority')
|
|
->label(__('Process Priority'))
|
|
->numeric()
|
|
->minValue(-20)
|
|
->maxValue(19)
|
|
->helperText(__('Nice value (-20 to 19, lower = higher priority)')),
|
|
TextInput::make('phpFpmData.request_terminate_timeout')
|
|
->label(__('Request Timeout (s)'))
|
|
->numeric()
|
|
->minValue(30)
|
|
->maxValue(3600)
|
|
->helperText(__('Kill slow requests after this time')),
|
|
]),
|
|
Actions::make([
|
|
FormAction::make('saveFpmSettings')
|
|
->label(__('Save Settings'))
|
|
->action('saveFpmSettings'),
|
|
FormAction::make('applyFpmToAll')
|
|
->label(__('Apply to All Users'))
|
|
->color('warning')
|
|
->icon('heroicon-o-arrow-path')
|
|
->requiresConfirmation()
|
|
->modalHeading(__('Apply FPM Settings to All Users'))
|
|
->modalDescription(__('This will update all existing PHP-FPM pool configurations with the current settings. PHP-FPM will be reloaded.'))
|
|
->action('applyFpmToAll'),
|
|
]),
|
|
]),
|
|
];
|
|
}
|
|
|
|
protected function getForms(): array
|
|
{
|
|
return [
|
|
'settingsForm',
|
|
];
|
|
}
|
|
|
|
public function saveBranding(): void
|
|
{
|
|
$data = $this->brandingData;
|
|
|
|
if (empty(trim($data['panel_name'] ?? ''))) {
|
|
Notification::make()->title(__('Panel name cannot be empty'))->danger()->send();
|
|
|
|
return;
|
|
}
|
|
|
|
DnsSetting::set('panel_name', trim($data['panel_name']));
|
|
DnsSetting::clearCache();
|
|
|
|
Notification::make()->title(__('Branding updated'))->body(__('Refresh to see changes.'))->success()->send();
|
|
}
|
|
|
|
public function uploadLogo(array $data): void
|
|
{
|
|
try {
|
|
$logo = $data['logo'] ?? null;
|
|
if (empty($logo)) {
|
|
Notification::make()->title(__('No file selected'))->warning()->send();
|
|
|
|
return;
|
|
}
|
|
|
|
// Filament FileUpload returns an array of stored file paths
|
|
$path = is_array($logo) ? ($logo[0] ?? null) : $logo;
|
|
|
|
if ($path) {
|
|
// Delete old logo if exists
|
|
if ($this->currentLogo && Storage::disk('public')->exists($this->currentLogo)) {
|
|
Storage::disk('public')->delete($this->currentLogo);
|
|
}
|
|
|
|
DnsSetting::set('custom_logo', $path);
|
|
DnsSetting::clearCache();
|
|
$this->currentLogo = $path;
|
|
|
|
Notification::make()->title(__('Logo uploaded'))->body(__('Refresh to see changes.'))->success()->send();
|
|
}
|
|
} catch (Exception $e) {
|
|
Notification::make()->title(__('Failed to upload logo'))->body($e->getMessage())->danger()->send();
|
|
}
|
|
}
|
|
|
|
public function removeLogo(): void
|
|
{
|
|
try {
|
|
if ($this->currentLogo && Storage::disk('public')->exists($this->currentLogo)) {
|
|
Storage::disk('public')->delete($this->currentLogo);
|
|
}
|
|
DnsSetting::set('custom_logo', null);
|
|
DnsSetting::clearCache();
|
|
$this->currentLogo = null;
|
|
Notification::make()->title(__('Logo removed'))->success()->send();
|
|
} catch (Exception $e) {
|
|
Notification::make()->title(__('Failed to remove logo'))->body($e->getMessage())->danger()->send();
|
|
}
|
|
}
|
|
|
|
public function saveHostname(): void
|
|
{
|
|
$hostname = $this->hostnameData['hostname'] ?? '';
|
|
|
|
if (empty(trim($hostname))) {
|
|
Notification::make()->title(__('Hostname cannot be empty'))->danger()->send();
|
|
|
|
return;
|
|
}
|
|
|
|
$result = $this->getAgent()->send('server.set_hostname', ['hostname' => $hostname]);
|
|
|
|
if (! ($result['success'] ?? false)) {
|
|
Notification::make()->title(__('Failed to update hostname'))->body($result['error'] ?? __('Unknown error'))->danger()->send();
|
|
|
|
return;
|
|
}
|
|
|
|
// Restart or reload affected services
|
|
$services = ['postfix', 'dovecot', 'nginx', 'php8.3-fpm', 'named'];
|
|
$updatedServices = [];
|
|
$failedServices = [];
|
|
|
|
foreach ($services as $service) {
|
|
$action = $this->shouldReloadService($service) ? 'reload' : 'restart';
|
|
$result = $this->getAgent()->send("service.{$action}", ['service' => $service]);
|
|
if ($result['success'] ?? false) {
|
|
$updatedServices[] = $service;
|
|
} else {
|
|
$failedServices[] = $service;
|
|
}
|
|
}
|
|
|
|
if (empty($failedServices)) {
|
|
Notification::make()
|
|
->title(__('Hostname updated'))
|
|
->body(__('Affected services have been restarted or reloaded.'))
|
|
->success()
|
|
->send();
|
|
} else {
|
|
Notification::make()
|
|
->title(__('Hostname updated'))
|
|
->body(__('Some services failed to restart or reload: :services. If you experience issues, a server reboot may help.', ['services' => implode(', ', $failedServices)]))
|
|
->warning()
|
|
->send();
|
|
}
|
|
}
|
|
|
|
protected function shouldReloadService(string $service): bool
|
|
{
|
|
if ($service === 'nginx') {
|
|
return true;
|
|
}
|
|
|
|
return preg_match('/^php(\d+\.\d+)?-fpm$/', $service) === 1;
|
|
}
|
|
|
|
public function saveDns(): void
|
|
{
|
|
$data = $this->dnsData;
|
|
|
|
DnsSetting::set('ns1', $data['ns1']);
|
|
DnsSetting::set('ns1_ip', $data['ns1_ip']);
|
|
DnsSetting::set('ns2', $data['ns2']);
|
|
DnsSetting::set('ns2_ip', $data['ns2_ip']);
|
|
DnsSetting::set('default_ip', $data['default_ip']);
|
|
DnsSetting::set('default_ipv6', $data['default_ipv6'] ?: null);
|
|
DnsSetting::set('default_ttl', $data['default_ttl']);
|
|
DnsSetting::set('admin_email', $data['admin_email']);
|
|
DnsSetting::clearCache();
|
|
|
|
$result = $this->getAgent()->send('server.create_zone', [
|
|
'hostname' => $this->hostnameData['hostname'],
|
|
'ns1' => $data['ns1'],
|
|
'ns1_ip' => $data['ns1_ip'],
|
|
'ns2' => $data['ns2'],
|
|
'ns2_ip' => $data['ns2_ip'],
|
|
'admin_email' => $data['admin_email'],
|
|
'server_ip' => $data['default_ip'],
|
|
'server_ipv6' => $data['default_ipv6'],
|
|
'ttl' => $data['default_ttl'],
|
|
]);
|
|
|
|
if ($result['success'] ?? false) {
|
|
Notification::make()->title(__('DNS settings saved'))->success()->send();
|
|
} else {
|
|
Notification::make()->title(__('Settings saved but zone creation failed'))->body($result['error'] ?? __('Unknown error'))->warning()->send();
|
|
}
|
|
}
|
|
|
|
public function saveResolvers(): void
|
|
{
|
|
$data = $this->resolversData;
|
|
|
|
try {
|
|
$nameservers = array_filter([
|
|
$data['resolver1'],
|
|
$data['resolver2'],
|
|
$data['resolver3'],
|
|
], fn ($ns) => ! empty(trim($ns ?? '')));
|
|
|
|
if (empty($nameservers)) {
|
|
Notification::make()->title(__('Failed to update DNS resolvers'))->body(__('At least one nameserver is required'))->danger()->send();
|
|
|
|
return;
|
|
}
|
|
|
|
$result = $this->getAgent()->send('server.set_resolvers', [
|
|
'nameservers' => array_values($nameservers),
|
|
'search_domains' => ! empty($data['search_domain']) ? [$data['search_domain']] : [],
|
|
]);
|
|
|
|
if ($result['success'] ?? false) {
|
|
Notification::make()->title(__('DNS resolvers updated'))->success()->send();
|
|
} else {
|
|
Notification::make()->title(__('Failed to update DNS resolvers'))->body($result['error'] ?? __('Unknown error'))->danger()->send();
|
|
}
|
|
} catch (Exception $e) {
|
|
Notification::make()->title(__('Failed to update DNS resolvers'))->body($e->getMessage())->danger()->send();
|
|
}
|
|
}
|
|
|
|
public function saveQuotaSettings(): void
|
|
{
|
|
$data = $this->quotaData;
|
|
$wasEnabled = (bool) DnsSetting::get('quotas_enabled', false);
|
|
|
|
DnsSetting::set('quotas_enabled', $data['quotas_enabled'] ? '1' : '0');
|
|
DnsSetting::set('default_quota_mb', (string) $data['default_quota_mb']);
|
|
DnsSetting::clearCache();
|
|
|
|
if ($data['quotas_enabled'] && ! $wasEnabled) {
|
|
try {
|
|
$result = $this->getAgent()->send('quota.enable', ['path' => '/home']);
|
|
if ($result['success'] ?? false) {
|
|
Notification::make()->title(__('Disk quotas enabled'))->body(__('Quota system has been initialized on /home'))->success()->send();
|
|
} else {
|
|
Notification::make()->title(__('Settings saved'))->body(__('Warning: Could not enable quota system on filesystem.'))->warning()->send();
|
|
}
|
|
} catch (Exception $e) {
|
|
Notification::make()->title(__('Settings saved'))->body(__('Warning: Could not enable quota system.'))->warning()->send();
|
|
}
|
|
}
|
|
|
|
Notification::make()->title(__('Quota settings saved'))->success()->send();
|
|
}
|
|
|
|
public function saveFileManagerSettings(): void
|
|
{
|
|
$data = $this->fileManagerData;
|
|
$size = max(1, min(500, (int) $data['max_upload_size_mb']));
|
|
|
|
DnsSetting::set('max_upload_size_mb', (string) $size);
|
|
DnsSetting::clearCache();
|
|
|
|
try {
|
|
$result = $this->getAgent()->send('server.set_upload_limits', ['size_mb' => $size]);
|
|
if ($result['success'] ?? false) {
|
|
Notification::make()->title(__('File manager settings saved'))->body(__('Server upload limits updated to :size MB', ['size' => $size]))->success()->send();
|
|
} else {
|
|
Notification::make()->title(__('Settings saved'))->body(__('Database updated but server config update had issues'))->warning()->send();
|
|
}
|
|
} catch (Exception $e) {
|
|
Notification::make()->title(__('Settings saved'))->body(__('Database updated but could not update server configs'))->warning()->send();
|
|
}
|
|
}
|
|
|
|
public function saveEmailSettings(): void
|
|
{
|
|
$data = $this->emailData;
|
|
|
|
DnsSetting::set('mail_hostname', $data['mail_hostname']);
|
|
DnsSetting::set('mail_default_quota_mb', (string) $data['mail_default_quota_mb']);
|
|
DnsSetting::set('max_mailboxes_per_domain', (string) $data['max_mailboxes_per_domain']);
|
|
DnsSetting::set('webmail_url', $data['webmail_url']);
|
|
DnsSetting::set('webmail_product_name', $data['webmail_product_name']);
|
|
DnsSetting::clearCache();
|
|
|
|
// Update Roundcube config
|
|
$configFile = '/etc/roundcube/config.inc.php';
|
|
if (file_exists($configFile)) {
|
|
try {
|
|
$content = file_get_contents($configFile);
|
|
$content = preg_replace(
|
|
"/\\\$config\['product_name'\]\s*=\s*'[^']*';/",
|
|
"\$config['product_name'] = '".addslashes($data['webmail_product_name'])."';",
|
|
$content
|
|
);
|
|
file_put_contents($configFile, $content);
|
|
} catch (Exception $e) {
|
|
// Silently fail
|
|
}
|
|
}
|
|
|
|
Notification::make()->title(__('Email settings saved'))->success()->send();
|
|
}
|
|
|
|
public function saveEmailNotificationSettings(): void
|
|
{
|
|
$data = $this->notificationsData;
|
|
|
|
if (! empty($data['admin_email_recipients'])) {
|
|
$emails = array_map('trim', explode(',', $data['admin_email_recipients']));
|
|
foreach ($emails as $email) {
|
|
if (! filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|
Notification::make()->title(__('Invalid recipient email'))->body(__(':email is not a valid email address', ['email' => $email]))->danger()->send();
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
DnsSetting::set('admin_email_recipients', $data['admin_email_recipients']);
|
|
DnsSetting::set('notify_ssl_errors', $data['notify_ssl_errors'] ? '1' : '0');
|
|
DnsSetting::set('notify_backup_failures', $data['notify_backup_failures'] ? '1' : '0');
|
|
DnsSetting::set('notify_backup_success', $data['notify_backup_success'] ? '1' : '0');
|
|
DnsSetting::set('notify_disk_quota', $data['notify_disk_quota'] ? '1' : '0');
|
|
DnsSetting::set('notify_login_failures', $data['notify_login_failures'] ? '1' : '0');
|
|
DnsSetting::set('notify_ssh_logins', $data['notify_ssh_logins'] ? '1' : '0');
|
|
DnsSetting::set('notify_system_updates', $data['notify_system_updates'] ? '1' : '0');
|
|
DnsSetting::set('notify_service_health', $data['notify_service_health'] ? '1' : '0');
|
|
DnsSetting::set('notify_high_load', $data['notify_high_load'] ? '1' : '0');
|
|
DnsSetting::set('load_threshold', (string) max(1, min(100, (float) ($data['load_threshold'] ?? 5))));
|
|
DnsSetting::set('load_alert_minutes', (string) max(1, min(60, (int) ($data['load_alert_minutes'] ?? 5))));
|
|
DnsSetting::clearCache();
|
|
|
|
Notification::make()->title(__('Notification settings saved'))->success()->send();
|
|
}
|
|
|
|
public function sendTestEmail(): void
|
|
{
|
|
$recipients = $this->notificationsData['admin_email_recipients'] ?? '';
|
|
|
|
if (empty($recipients)) {
|
|
Notification::make()->title(__('No recipients configured'))->body(__('Please add at least one admin email address'))->warning()->send();
|
|
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$recipientList = array_map('trim', explode(',', $recipients));
|
|
$hostname = gethostname() ?: 'localhost';
|
|
$sender = "webmaster@{$hostname}";
|
|
$subject = __('Test Email');
|
|
$message = __('This is a test email from your Jabali Panel at :hostname.', ['hostname' => $hostname]).
|
|
"\n\n".__('If you received this email, your admin notifications are working correctly.');
|
|
|
|
Mail::raw(
|
|
$message,
|
|
function ($mail) use ($recipientList, $sender, $subject) {
|
|
$mail->from($sender, 'Jabali Panel');
|
|
$mail->to($recipientList);
|
|
$mail->subject('[Jabali] '.$subject);
|
|
}
|
|
);
|
|
|
|
// Log the test email
|
|
\App\Models\NotificationLog::log(
|
|
'test',
|
|
$subject,
|
|
$message,
|
|
$recipientList,
|
|
'sent'
|
|
);
|
|
|
|
Notification::make()->title(__('Test email sent'))->body(__('Check your inbox for the test email'))->success()->send();
|
|
} catch (Exception $e) {
|
|
// Log the failed test email
|
|
\App\Models\NotificationLog::log(
|
|
'test',
|
|
__('Test Email'),
|
|
__('This is a test email from your Jabali Panel.'),
|
|
array_map('trim', explode(',', $recipients)),
|
|
'failed',
|
|
null,
|
|
$e->getMessage()
|
|
);
|
|
|
|
Notification::make()->title(__('Failed to send test email'))->body($e->getMessage())->danger()->send();
|
|
}
|
|
}
|
|
|
|
public function saveFpmSettings(): void
|
|
{
|
|
$data = $this->phpFpmData;
|
|
|
|
DnsSetting::set('fpm_pm_max_children', (string) $data['pm_max_children']);
|
|
DnsSetting::set('fpm_pm_max_requests', (string) $data['pm_max_requests']);
|
|
DnsSetting::set('fpm_rlimit_files', (string) $data['rlimit_files']);
|
|
DnsSetting::set('fpm_process_priority', (string) $data['process_priority']);
|
|
DnsSetting::set('fpm_request_terminate_timeout', (string) $data['request_terminate_timeout']);
|
|
DnsSetting::set('fpm_memory_limit', $data['memory_limit']);
|
|
DnsSetting::clearCache();
|
|
|
|
Notification::make()
|
|
->title(__('PHP-FPM settings saved'))
|
|
->body(__('New user pools will use these settings. Use "Apply to All" to update existing pools.'))
|
|
->success()
|
|
->send();
|
|
}
|
|
|
|
public function applyFpmToAll(): void
|
|
{
|
|
$data = $this->phpFpmData;
|
|
|
|
try {
|
|
$result = $this->getAgent()->send('php.update_all_pool_limits', [
|
|
'pm_max_children' => (int) $data['pm_max_children'],
|
|
'pm_max_requests' => (int) $data['pm_max_requests'],
|
|
'rlimit_files' => (int) $data['rlimit_files'],
|
|
'process_priority' => (int) $data['process_priority'],
|
|
'request_terminate_timeout' => (int) $data['request_terminate_timeout'],
|
|
'memory_limit' => $data['memory_limit'],
|
|
]);
|
|
|
|
if ($result['success'] ?? false) {
|
|
$updated = $result['updated'] ?? [];
|
|
$errors = $result['errors'] ?? [];
|
|
|
|
if (empty($errors)) {
|
|
Notification::make()
|
|
->title(__('FPM pools updated'))
|
|
->body(__(':count user pools updated. PHP-FPM will reload.', ['count' => count($updated)]))
|
|
->success()
|
|
->send();
|
|
} else {
|
|
Notification::make()
|
|
->title(__('Partial update'))
|
|
->body(__(':success pools updated, :errors failed', [
|
|
'success' => count($updated),
|
|
'errors' => count($errors),
|
|
]))
|
|
->warning()
|
|
->send();
|
|
}
|
|
} else {
|
|
Notification::make()
|
|
->title(__('Failed to update pools'))
|
|
->body($result['error'] ?? __('Unknown error'))
|
|
->danger()
|
|
->send();
|
|
}
|
|
} catch (Exception $e) {
|
|
Notification::make()
|
|
->title(__('Failed to update pools'))
|
|
->body($e->getMessage())
|
|
->danger()
|
|
->send();
|
|
}
|
|
}
|
|
|
|
protected function loadVersionInfo(): void
|
|
{
|
|
$versionFile = base_path('VERSION');
|
|
if (File::exists($versionFile)) {
|
|
$content = File::get($versionFile);
|
|
if (preg_match('/VERSION=(.+)/', $content, $matches)) {
|
|
$this->currentVersion = trim($matches[1]);
|
|
if (preg_match('/BUILD=(\d+)/', $content, $buildMatches)) {
|
|
$this->currentVersion .= ' ('.__('build').' '.trim($buildMatches[1]).')';
|
|
}
|
|
}
|
|
} else {
|
|
$this->currentVersion = __('Unknown');
|
|
}
|
|
}
|
|
|
|
public function checkForUpdates(): void
|
|
{
|
|
$this->isChecking = true;
|
|
$this->updatesAvailable = 0;
|
|
|
|
try {
|
|
$basePath = base_path();
|
|
|
|
if (! is_dir("{$basePath}/.git")) {
|
|
throw new Exception(__('Not a git repository.'));
|
|
}
|
|
|
|
exec("cd {$basePath} && timeout 30 git fetch origin main 2>&1", $fetchOutput, $fetchCode);
|
|
|
|
if ($fetchCode !== 0) {
|
|
throw new Exception(__('Failed to fetch from repository.'));
|
|
}
|
|
|
|
$behindCount = trim(shell_exec("cd {$basePath} && git rev-list HEAD..origin/main --count 2>&1") ?? '0');
|
|
$this->updatesAvailable = (int) $behindCount;
|
|
|
|
if ($this->updatesAvailable > 0) {
|
|
$this->latestVersion = trim(shell_exec("cd {$basePath} && git log origin/main -1 --format='%s' 2>&1") ?? '');
|
|
Notification::make()->title(__('Updates Available'))->body(__(':count update(s) available', ['count' => $this->updatesAvailable]))->warning()->send();
|
|
} else {
|
|
Notification::make()->title(__('Up to Date'))->body(__('Running the latest version'))->success()->send();
|
|
}
|
|
} catch (Exception $e) {
|
|
Notification::make()->title(__('Update Check Failed'))->body($e->getMessage())->danger()->send();
|
|
}
|
|
|
|
$this->isChecking = false;
|
|
}
|
|
|
|
public function performUpgrade(): void
|
|
{
|
|
$this->isUpgrading = true;
|
|
$this->upgradeLog = __('Starting upgrade...')."\n";
|
|
|
|
try {
|
|
$exitCode = Artisan::call('jabali:upgrade', ['--force' => true]);
|
|
$this->upgradeLog .= Artisan::output();
|
|
if ($exitCode !== 0) {
|
|
throw new Exception(__('Upgrade failed. Check the log for details.'));
|
|
}
|
|
$this->loadVersionInfo();
|
|
$this->updatesAvailable = 0;
|
|
Notification::make()->title(__('Upgrade Complete'))->body(__('Refresh to see changes.'))->success()->send();
|
|
} catch (Exception $e) {
|
|
$this->upgradeLog .= "\n".__('Error').': '.$e->getMessage();
|
|
Notification::make()->title(__('Upgrade Failed'))->body($e->getMessage())->danger()->send();
|
|
}
|
|
|
|
$this->isUpgrading = false;
|
|
}
|
|
|
|
protected function getHeaderActions(): array
|
|
{
|
|
return [
|
|
Action::make('export_config')
|
|
->label(__('Export'))
|
|
->icon('heroicon-o-arrow-down-tray')
|
|
->color('gray')
|
|
->action(fn () => $this->exportConfig()),
|
|
Action::make('import_config')
|
|
->label(__('Import'))
|
|
->icon('heroicon-o-arrow-up-tray')
|
|
->color('gray')
|
|
->modalHeading(__('Import Configuration'))
|
|
->modalDescription(__('Upload a previously exported configuration file. This will overwrite your current settings.'))
|
|
->modalIcon('heroicon-o-arrow-up-tray')
|
|
->modalIconColor('warning')
|
|
->modalSubmitActionLabel(__('Import'))
|
|
->form([
|
|
FileUpload::make('config_file')
|
|
->label(__('Configuration File'))
|
|
->acceptedFileTypes(['application/json'])
|
|
->required()
|
|
->maxSize(1024)
|
|
->helperText(__('Select a .json file exported from Jabali Panel')),
|
|
])
|
|
->action(fn (array $data) => $this->importConfig($data)),
|
|
];
|
|
}
|
|
|
|
public function exportConfig(): \Symfony\Component\HttpFoundation\StreamedResponse
|
|
{
|
|
$settings = DnsSetting::getAll();
|
|
unset($settings['custom_logo']);
|
|
|
|
$exportData = [
|
|
'version' => '1.0',
|
|
'exported_at' => now()->toIso8601String(),
|
|
'hostname' => gethostname(),
|
|
'settings' => $settings,
|
|
];
|
|
|
|
$filename = 'jabali-config-'.date('Y-m-d-His').'.json';
|
|
$content = json_encode($exportData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
|
|
|
Notification::make()->title(__('Configuration exported'))->success()->send();
|
|
|
|
return Response::streamDownload(function () use ($content) {
|
|
echo $content;
|
|
}, $filename, ['Content-Type' => 'application/json']);
|
|
}
|
|
|
|
public function importConfig(array $data): void
|
|
{
|
|
try {
|
|
if (empty($data['config_file'])) {
|
|
throw new Exception(__('No file uploaded'));
|
|
}
|
|
|
|
$filePath = Storage::disk('local')->path($data['config_file']);
|
|
|
|
if (! file_exists($filePath)) {
|
|
throw new Exception(__('Uploaded file not found'));
|
|
}
|
|
|
|
$content = file_get_contents($filePath);
|
|
$importData = json_decode($content, true);
|
|
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
throw new Exception(__('Invalid JSON file: :error', ['error' => json_last_error_msg()]));
|
|
}
|
|
|
|
if (! isset($importData['settings']) || ! is_array($importData['settings'])) {
|
|
throw new Exception(__('Invalid configuration file format'));
|
|
}
|
|
|
|
$imported = 0;
|
|
foreach ($importData['settings'] as $key => $value) {
|
|
if (in_array($key, ['custom_logo'])) {
|
|
continue;
|
|
}
|
|
DnsSetting::set($key, $value);
|
|
$imported++;
|
|
}
|
|
|
|
DnsSetting::clearCache();
|
|
Storage::disk('local')->delete($data['config_file']);
|
|
$this->mount();
|
|
|
|
Notification::make()->title(__('Configuration imported'))->body(__(':count settings imported successfully', ['count' => $imported]))->success()->send();
|
|
} catch (Exception $e) {
|
|
Notification::make()->title(__('Import failed'))->body($e->getMessage())->danger()->send();
|
|
}
|
|
}
|
|
}
|