Settings page (#486)

* remove old settings stuff

* add basic settings page

* add some settings

* add "test mail" button

* fix mail fields not updating

* fix phpstan

* fix default for "top navigation"

* force toggle buttons to be bool

* force toggle to be bool

* add class to view to allow customization

* add mailgun settings

* add notification settings

* add timeout settings

* organize tabs into sub-functions

* add more settings

* add backup settings

* add sections to mail settings

* add setting for trusted_proxies

* fix unsaved data alert not showing

* fix clear action

* Fix clear action v2

TagsInput expects an array, not a string, fails on saving when using `''`

* Add App favicon

* Remove defaults, collapse misc sections

* Move Save btn, Add API rate limit

* small cleanup

---------

Co-authored-by: notCharles <charles@pelican.dev>
This commit is contained in:
Boy132 2024-07-29 12:14:24 +02:00 committed by GitHub
parent d89af243a8
commit a58e159478
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 587 additions and 860 deletions

View File

@ -4,7 +4,6 @@ APP_KEY=
APP_TIMEZONE=UTC APP_TIMEZONE=UTC
APP_URL=http://panel.test APP_URL=http://panel.test
APP_LOCALE=en APP_LOCALE=en
APP_ENVIRONMENT_ONLY=true
LOG_CHANNEL=daily LOG_CHANNEL=daily
LOG_STACK=single LOG_STACK=single

View File

@ -32,7 +32,6 @@ jobs:
APP_KEY: ThisIsARandomStringForTests12345 APP_KEY: ThisIsARandomStringForTests12345
APP_TIMEZONE: UTC APP_TIMEZONE: UTC
APP_URL: http://localhost/ APP_URL: http://localhost/
APP_ENVIRONMENT_ONLY: "true"
CACHE_DRIVER: array CACHE_DRIVER: array
MAIL_MAILER: array MAIL_MAILER: array
SESSION_DRIVER: array SESSION_DRIVER: array
@ -106,7 +105,6 @@ jobs:
APP_KEY: ThisIsARandomStringForTests12345 APP_KEY: ThisIsARandomStringForTests12345
APP_TIMEZONE: UTC APP_TIMEZONE: UTC
APP_URL: http://localhost/ APP_URL: http://localhost/
APP_ENVIRONMENT_ONLY: "true"
CACHE_DRIVER: array CACHE_DRIVER: array
MAIL_MAILER: array MAIL_MAILER: array
SESSION_DRIVER: array SESSION_DRIVER: array
@ -170,7 +168,6 @@ jobs:
APP_KEY: ThisIsARandomStringForTests12345 APP_KEY: ThisIsARandomStringForTests12345
APP_TIMEZONE: UTC APP_TIMEZONE: UTC
APP_URL: http://localhost/ APP_URL: http://localhost/
APP_ENVIRONMENT_ONLY: "true"
CACHE_DRIVER: array CACHE_DRIVER: array
MAIL_MAILER: array MAIL_MAILER: array
SESSION_DRIVER: array SESSION_DRIVER: array

View File

@ -38,8 +38,7 @@ class AppSettingsCommand extends Command
{--queue= : The queue driver backend to use.} {--queue= : The queue driver backend to use.}
{--redis-host= : Redis host to use for connections.} {--redis-host= : Redis host to use for connections.}
{--redis-pass= : Password used to connect to redis.} {--redis-pass= : Password used to connect to redis.}
{--redis-port= : Port to connect to redis over.} {--redis-port= : Port to connect to redis over.}';
{--settings-ui= : Enable or disable the settings UI.}';
protected array $variables = []; protected array $variables = [];
@ -87,12 +86,6 @@ class AppSettingsCommand extends Command
array_key_exists($selected, self::QUEUE_DRIVERS) ? $selected : null array_key_exists($selected, self::QUEUE_DRIVERS) ? $selected : null
); );
if (!is_null($this->option('settings-ui'))) {
$this->variables['APP_ENVIRONMENT_ONLY'] = $this->option('settings-ui') == 'true' ? 'false' : 'true';
} else {
$this->variables['APP_ENVIRONMENT_ONLY'] = $this->confirm(__('commands.appsettings.comment.settings_ui'), true) ? 'false' : 'true';
}
// Make sure session cookies are set as "secure" when using HTTPS // Make sure session cookies are set as "secure" when using HTTPS
if (str_starts_with($this->variables['APP_URL'], 'https://')) { if (str_starts_with($this->variables['APP_URL'], 'https://')) {
$this->variables['SESSION_SECURE_COOKIE'] = 'true'; $this->variables['SESSION_SECURE_COOKIE'] = 'true';

View File

@ -1,10 +0,0 @@
<?php
namespace App\Filament\Clusters;
use Filament\Clusters\Cluster;
class Settings extends Cluster
{
protected static ?string $navigationIcon = 'tabler-settings';
}

View File

@ -0,0 +1,570 @@
<?php
namespace App\Filament\Pages;
use App\Models\Backup;
use App\Notifications\MailTested;
use App\Traits\Commands\EnvironmentWriterTrait;
use Exception;
use Filament\Actions\Action;
use Filament\Forms\Components\Actions\Action as FormAction;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Notifications\Notification;
use Filament\Pages\Concerns\HasUnsavedDataChangesAlert;
use Filament\Pages\Concerns\InteractsWithHeaderActions;
use Filament\Pages\Page;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Notification as MailNotification;
/**
* @property Form $form
*/
class Settings extends Page implements HasForms
{
use EnvironmentWriterTrait;
use HasUnsavedDataChangesAlert;
use InteractsWithForms;
use InteractsWithHeaderActions;
protected static ?string $navigationIcon = 'tabler-settings';
protected static ?string $navigationGroup = 'Advanced';
protected static string $view = 'filament.pages.settings';
public ?array $data = [];
public function mount(): void
{
$this->form->fill();
}
protected function getFormSchema(): array
{
return [
Tabs::make('Tabs')
->columns()
->persistTabInQueryString()
->tabs([
Tab::make('general')
->label('General')
->icon('tabler-home')
->schema($this->generalSettings()),
Tab::make('recaptcha')
->label('reCAPTCHA')
->icon('tabler-shield')
->schema($this->recaptchaSettings()),
Tab::make('mail')
->label('Mail')
->icon('tabler-mail')
->schema($this->mailSettings()),
Tab::make('backup')
->label('Backup')
->icon('tabler-box')
->schema($this->backupSettings()),
Tab::make('misc')
->label('Misc')
->icon('tabler-tool')
->schema($this->miscSettings()),
]),
];
}
private function generalSettings(): array
{
return [
TextInput::make('APP_NAME')
->label('App Name')
->required()
->default(env('APP_NAME', 'Pelican')),
TextInput::make('APP_FAVICON')
->label('App Favicon')
->hintIcon('tabler-question-mark')
->hintIconTooltip('Favicons should be placed in the public folder, located in the root panel directory.')
->required()
->default(env('APP_FAVICON', './pelican.ico')),
Toggle::make('APP_DEBUG')
->label('Enable Debug Mode?')
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('APP_DEBUG', (bool) $state))
->default(env('RECAPTCHA_ENABLED', config('recaptcha.enabled'))),
ToggleButtons::make('FILAMENT_TOP_NAVIGATION')
->label('Navigation')
->grouped()
->options([
false => 'Sidebar',
true => 'Topbar',
])
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('FILAMENT_TOP_NAVIGATION', (bool) $state))
->default(env('FILAMENT_TOP_NAVIGATION', config('panel.filament.top-navigation'))),
ToggleButtons::make('PANEL_USE_BINARY_PREFIX')
->label('Unit prefix')
->grouped()
->options([
false => 'Decimal Prefix (MB/ GB)',
true => 'Binary Prefix (MiB/ GiB)',
])
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_USE_BINARY_PREFIX', (bool) $state))
->default(env('PANEL_USE_BINARY_PREFIX', config('panel.use_binary_prefix'))),
ToggleButtons::make('APP_2FA_REQUIRED')
->label('2FA Requirement')
->grouped()
->options([
0 => 'Not required',
1 => 'Required for only Admins',
2 => 'Required for all Users',
])
->formatStateUsing(fn ($state): int => (int) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('APP_2FA_REQUIRED', (int) $state))
->default(env('APP_2FA_REQUIRED', config('panel.auth.2fa_required'))),
TagsInput::make('TRUSTED_PROXIES')
->label('Trusted Proxies')
->separator()
->splitKeys(['Tab', ' '])
->placeholder('New IP or IP Range')
->default(env('TRUSTED_PROXIES', config('trustedproxy.proxies')))
->hintActions([
FormAction::make('clear')
->label('Clear')
->color('danger')
->icon('tabler-trash')
->requiresConfirmation()
->action(fn (Set $set) => $set('TRUSTED_PROXIES', [])),
FormAction::make('cloudflare')
->label('Set to Cloudflare IPs')
->icon('tabler-brand-cloudflare')
->action(fn (Set $set) => $set('TRUSTED_PROXIES', [
'173.245.48.0/20',
'103.21.244.0/22',
'103.22.200.0/22',
'103.31.4.0/22',
'141.101.64.0/18',
'108.162.192.0/18',
'190.93.240.0/20',
'188.114.96.0/20',
'197.234.240.0/22',
'198.41.128.0/17',
'162.158.0.0/15',
'104.16.0.0/13',
'104.24.0.0/14',
'172.64.0.0/13',
'131.0.72.0/22',
])),
]),
];
}
private function recaptchaSettings(): array
{
return [
Toggle::make('RECAPTCHA_ENABLED')
->label('Enable reCAPTCHA?')
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->live()
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('RECAPTCHA_ENABLED', (bool) $state))
->default(env('RECAPTCHA_ENABLED', config('recaptcha.enabled'))),
TextInput::make('RECAPTCHA_DOMAIN')
->label('Domain')
->required()
->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED'))
->default(env('RECAPTCHA_DOMAIN', config('recaptcha.domain'))),
TextInput::make('RECAPTCHA_WEBSITE_KEY')
->label('Website Key')
->required()
->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED'))
->default(env('RECAPTCHA_WEBSITE_KEY', config('recaptcha.website_key'))),
TextInput::make('RECAPTCHA_SECRET_KEY')
->label('Secret Key')
->required()
->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED'))
->default(env('RECAPTCHA_SECRET_KEY', config('recaptcha.secret_key'))),
];
}
private function mailSettings(): array
{
return [
ToggleButtons::make('MAIL_MAILER')
->label('Mail Driver')
->columnSpanFull()
->grouped()
->options([
'log' => 'Print mails to Log',
'smtp' => 'SMTP Server',
'sendmail' => 'sendmail Binary',
'mailgun' => 'Mailgun',
'mandrill' => 'Mandrill',
'postmark' => 'Postmark',
])
->live()
->default(env('MAIL_MAILER', config('mail.default')))
->hintAction(
FormAction::make('test')
->label('Send Test Mail')
->icon('tabler-send')
->hidden(fn (Get $get) => $get('MAIL_MAILER') === 'log')
->action(function () {
try {
MailNotification::route('mail', auth()->user()->email)
->notify(new MailTested(auth()->user()));
Notification::make()
->title('Test Mail sent')
->success()
->send();
} catch (Exception $exception) {
Notification::make()
->title('Test Mail failed')
->body($exception->getMessage())
->danger()
->send();
}
})
),
Section::make('"From" Settings')
->description('Set the Address and Name used as "From" in mails.')
->columns()
->schema([
TextInput::make('MAIL_FROM_ADDRESS')
->label('From Address')
->required()
->email()
->default(env('MAIL_FROM_ADDRESS', config('mail.from.address'))),
TextInput::make('MAIL_FROM_NAME')
->label('From Name')
->required()
->default(env('MAIL_FROM_NAME', config('mail.from.name'))),
]),
Section::make('SMTP Configuration')
->columns()
->visible(fn (Get $get) => $get('MAIL_MAILER') === 'smtp')
->schema([
TextInput::make('MAIL_HOST')
->label('SMTP Host')
->required()
->default(env('MAIL_HOST', config('mail.mailers.smtp.host'))),
TextInput::make('MAIL_PORT')
->label('SMTP Port')
->required()
->numeric()
->minValue(1)
->maxValue(65535)
->default(env('MAIL_PORT', config('mail.mailers.smtp.port'))),
TextInput::make('MAIL_USERNAME')
->label('SMTP Username')
->required()
->default(env('MAIL_USERNAME', config('mail.mailers.smtp.username'))),
TextInput::make('MAIL_PASSWORD')
->label('SMTP Password')
->password()
->revealable()
->default(env('MAIL_PASSWORD')),
ToggleButtons::make('MAIL_ENCRYPTION')
->label('SMTP encryption')
->required()
->grouped()
->options(['tls' => 'TLS', 'ssl' => 'SSL', '' => 'None'])
->default(env('MAIL_ENCRYPTION', config('mail.mailers.smtp.encryption', 'tls'))),
]),
Section::make('Mailgun Configuration')
->columns()
->visible(fn (Get $get) => $get('MAIL_MAILER') === 'mailgun')
->schema([
TextInput::make('MAILGUN_DOMAIN')
->label('Mailgun Domain')
->required()
->default(env('MAILGUN_DOMAIN', config('services.mailgun.domain'))),
TextInput::make('MAILGUN_SECRET')
->label('Mailgun Secret')
->required()
->default(env('MAIL_USERNAME', config('services.mailgun.secret'))),
TextInput::make('MAILGUN_ENDPOINT')
->label('Mailgun Endpoint')
->required()
->default(env('MAILGUN_ENDPOINT', config('services.mailgun.endpoint'))),
]),
];
}
private function backupSettings(): array
{
return [
ToggleButtons::make('APP_BACKUP_DRIVER')
->label('Backup Driver')
->columnSpanFull()
->grouped()
->options([
Backup::ADAPTER_DAEMON => 'Wings',
Backup::ADAPTER_AWS_S3 => 'S3',
])
->live()
->default(env('APP_BACKUP_DRIVER', config('backups.default'))),
Section::make('Throttles')
->description('Configure how many backups can be created in a period. Set period to 0 to disable this throttle.')
->columns()
->schema([
TextInput::make('BACKUP_THROTTLE_LIMIT')
->label('Limit')
->required()
->numeric()
->minValue(1)
->default(config('backups.throttles.limit')),
TextInput::make('BACKUP_THROTTLE_PERIOD')
->label('Period')
->required()
->numeric()
->minValue(0)
->suffix('Seconds')
->default(config('backups.throttles.period')),
]),
Section::make('S3 Configuration')
->columns()
->visible(fn (Get $get) => $get('APP_BACKUP_DRIVER') === Backup::ADAPTER_AWS_S3)
->schema([
TextInput::make('AWS_DEFAULT_REGION')
->label('Default Region')
->required()
->default(config('backups.disks.s3.region')),
TextInput::make('AWS_ACCESS_KEY_ID')
->label('Access Key ID')
->required()
->default(config('backups.disks.s3.key')),
TextInput::make('AWS_SECRET_ACCESS_KEY')
->label('Secret Access Key')
->required()
->default(config('backups.disks.s3.secret')),
TextInput::make('AWS_BACKUPS_BUCKET')
->label('Bucket')
->required()
->default(config('backups.disks.s3.bucket')),
TextInput::make('AWS_ENDPOINT')
->label('Endpoint')
->required()
->default(config('backups.disks.s3.endpoint')),
Toggle::make('AWS_USE_PATH_STYLE_ENDPOINT')
->label('Use path style endpoint?')
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->live()
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('AWS_USE_PATH_STYLE_ENDPOINT', (bool) $state))
->default(env('AWS_USE_PATH_STYLE_ENDPOINT', config('backups.disks.s3.use_path_style_endpoint'))),
]),
];
}
private function miscSettings(): array
{
return [
Section::make('Automatic Allocation Creation')
->description('Toggle if Users can create allocations via the client area.')
->columns()
->collapsible()
->collapsed()
->schema([
Toggle::make('PANEL_CLIENT_ALLOCATIONS_ENABLED')
->label('Allow Users to create allocations?')
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->live()
->columnSpanFull()
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_CLIENT_ALLOCATIONS_ENABLED', (bool) $state))
->default(env('PANEL_CLIENT_ALLOCATIONS_ENABLED', config('panel.client_features.allocations.enabled'))),
TextInput::make('PANEL_CLIENT_ALLOCATIONS_RANGE_START')
->label('Starting Port')
->required()
->numeric()
->minValue(1024)
->maxValue(65535)
->visible(fn (Get $get) => $get('PANEL_CLIENT_ALLOCATIONS_ENABLED'))
->default(env('PANEL_CLIENT_ALLOCATIONS_RANGE_START')),
TextInput::make('PANEL_CLIENT_ALLOCATIONS_RANGE_END')
->label('Ending Port')
->required()
->numeric()
->minValue(1024)
->maxValue(65535)
->visible(fn (Get $get) => $get('PANEL_CLIENT_ALLOCATIONS_ENABLED'))
->default(env('PANEL_CLIENT_ALLOCATIONS_RANGE_END')),
]),
Section::make('Mail Notifications')
->description('Toggle which mail notifications should be sent to Users.')
->columns()
->collapsible()
->collapsed()
->schema([
Toggle::make('PANEL_SEND_INSTALL_NOTIFICATION')
->label('Server Installed')
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->live()
->columnSpanFull()
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_INSTALL_NOTIFICATION', (bool) $state))
->default(env('PANEL_SEND_INSTALL_NOTIFICATION', config('panel.email.send_install_notification'))),
Toggle::make('PANEL_SEND_REINSTALL_NOTIFICATION')
->label('Server Reinstalled')
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->live()
->columnSpanFull()
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_REINSTALL_NOTIFICATION', (bool) $state))
->default(env('PANEL_SEND_REINSTALL_NOTIFICATION', config('panel.email.send_reinstall_notification'))),
]),
Section::make('Connections')
->description('Timeouts used when making requests.')
->columns()
->collapsible()
->collapsed()
->schema([
TextInput::make('GUZZLE_TIMEOUT')
->label('Request Timeout')
->required()
->numeric()
->minValue(15)
->maxValue(60)
->suffix('Seconds')
->default(env('GUZZLE_TIMEOUT', config('panel.guzzle.timeout'))),
TextInput::make('GUZZLE_CONNECT_TIMEOUT')
->label('Connect Timeout')
->required()
->numeric()
->minValue(5)
->maxValue(60)
->suffix('Seconds')
->default(env('GUZZLE_CONNECT_TIMEOUT', config('panel.guzzle.connect_timeout'))),
]),
Section::make('Activity Logs')
->description('Configure how often old activity logs should be pruned and whether admin activities should be logged.')
->columns()
->collapsible()
->collapsed()
->schema([
TextInput::make('APP_ACTIVITY_PRUNE_DAYS')
->label('Prune age')
->required()
->numeric()
->minValue(1)
->maxValue(365)
->suffix('Days')
->default(env('APP_ACTIVITY_PRUNE_DAYS', config('activity.prune_days'))),
Toggle::make('APP_ACTIVITY_HIDE_ADMIN')
->label('Hide admin activities?')
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->live()
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('APP_ACTIVITY_HIDE_ADMIN', (bool) $state))
->default(env('APP_ACTIVITY_HIDE_ADMIN', config('activity.hide_admin_activity'))),
]),
Section::make('API')
->description('Defines the rate limit for the number of requests per minute that can be executed.')
->columns()
->collapsible()
->collapsed()
->schema([
TextInput::make('APP_API_CLIENT_RATELIMIT')
->label('Client API Rate Limit')
->required()
->numeric()
->minValue(1)
->suffix('Requests Per Minute')
->default(env('APP_API_CLIENT_RATELIMIT', config('http.rate_limit.client'))),
TextInput::make('APP_API_APPLICATION_RATELIMIT')
->label('Application API Rate Limit')
->required()
->numeric()
->minValue(1)
->suffix('Requests Per Minute')
->default(env('APP_API_APPLICATION_RATELIMIT', config('http.rate_limit.application'))),
]),
];
}
protected function getFormStatePath(): ?string
{
return 'data';
}
protected function hasUnsavedDataChangesAlert(): bool
{
return true;
}
public function save(): void
{
try {
$data = $this->form->getState();
$this->writeToEnvironment($data);
Artisan::call('config:clear');
Artisan::call('queue:restart');
$this->rememberData();
$this->redirect($this->getUrl());
Notification::make()
->title('Settings saved')
->success()
->send();
} catch (Exception $exception) {
Notification::make()
->title('Save failed')
->body($exception->getMessage())
->danger()
->send();
}
}
protected function getHeaderActions(): array
{
return [
Action::make('save')
->action('save')
->keyBindings(['mod+s']),
];
}
protected function getFormActions(): array
{
return [];
}
}

View File

@ -1,56 +0,0 @@
<?php
namespace App\Http\Controllers\Admin\Settings;
use App\Models\Setting;
use Illuminate\View\View;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\Contracts\Console\Kernel;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Settings\AdvancedSettingsFormRequest;
class AdvancedController extends Controller
{
/**
* AdvancedController constructor.
*/
public function __construct(
private AlertsMessageBag $alert,
private Kernel $kernel,
) {
}
/**
* Render advanced Panel settings UI.
*/
public function index(): View
{
$showRecaptchaWarning = false;
if (
config('recaptcha._shipped_secret_key') === config('recaptcha.secret_key')
|| config('recaptcha._shipped_website_key') === config('recaptcha.website_key')
) {
$showRecaptchaWarning = true;
}
return view('admin.settings.advanced', [
'showRecaptchaWarning' => $showRecaptchaWarning,
]);
}
/**
* @throws \App\Exceptions\Model\DataValidationException
*/
public function update(AdvancedSettingsFormRequest $request): RedirectResponse
{
foreach ($request->normalize() as $key => $value) {
Setting::set('settings::' . $key, $value);
}
$this->kernel->call('queue:restart');
$this->alert->success('Advanced settings have been updated successfully and the queue worker was restarted to apply these changes.')->flash();
return redirect()->route('admin.settings.advanced');
}
}

View File

@ -1,56 +0,0 @@
<?php
namespace App\Http\Controllers\Admin\Settings;
use App\Models\Setting;
use Illuminate\View\View;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\Contracts\Console\Kernel;
use App\Http\Controllers\Controller;
use App\Traits\Helpers\AvailableLanguages;
use App\Services\Helpers\SoftwareVersionService;
use App\Http\Requests\Admin\Settings\BaseSettingsFormRequest;
class IndexController extends Controller
{
use AvailableLanguages;
/**
* IndexController constructor.
*/
public function __construct(
private AlertsMessageBag $alert,
private Kernel $kernel,
private SoftwareVersionService $versionService,
) {
}
/**
* Render the UI for basic Panel settings.
*/
public function index(): View
{
return view('admin.settings.index', [
'version' => $this->versionService,
'languages' => $this->getAvailableLanguages(),
]);
}
/**
* Handle settings update.
*
* @throws \App\Exceptions\Model\DataValidationException
*/
public function update(BaseSettingsFormRequest $request): RedirectResponse
{
foreach ($request->normalize() as $key => $value) {
Setting::set('settings::' . $key, $value);
}
$this->kernel->call('queue:restart');
$this->alert->success('Panel settings have been updated successfully and the queue worker was restarted to apply these changes.')->flash();
return redirect()->route('admin.settings');
}
}

View File

@ -1,82 +0,0 @@
<?php
namespace App\Http\Controllers\Admin\Settings;
use App\Models\Setting;
use Illuminate\View\View;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Contracts\Console\Kernel;
use App\Notifications\MailTested;
use Illuminate\Support\Facades\Notification;
use App\Exceptions\DisplayException;
use App\Http\Controllers\Controller;
use App\Providers\SettingsServiceProvider;
use App\Http\Requests\Admin\Settings\MailSettingsFormRequest;
class MailController extends Controller
{
/**
* MailController constructor.
*/
public function __construct(
private Kernel $kernel,
) {
}
/**
* Render UI for editing mail settings. This UI should only display if
* the server is configured to send mail using SMTP.
*/
public function index(): View
{
return view('admin.settings.mail', [
'disabled' => config('mail.default') !== 'smtp',
]);
}
/**
* Handle request to update SMTP mail settings.
*
* @throws DisplayException
* @throws \App\Exceptions\Model\DataValidationException
*/
public function update(MailSettingsFormRequest $request): Response
{
if (config('mail.default') !== 'smtp') {
throw new DisplayException('This feature is only available if SMTP is the selected email driver for the Panel.');
}
$values = $request->normalize();
if (array_get($values, 'mail:mailers:smtp:password') === '!e') {
$values['mail:mailers:smtp:password'] = '';
}
foreach ($values as $key => $value) {
if (in_array($key, SettingsServiceProvider::getEncryptedKeys()) && !empty($value)) {
$value = encrypt($value);
}
Setting::set('settings::' . $key, $value);
}
$this->kernel->call('queue:restart');
return response('', 204);
}
/**
* Submit a request to send a test mail message.
*/
public function test(Request $request): Response
{
try {
Notification::route('mail', $request->user()->email)
->notify(new MailTested($request->user()));
} catch (\Exception $exception) {
return response($exception->getMessage(), 500);
}
return response('', 204);
}
}

View File

@ -24,62 +24,4 @@ class Setting extends Model
'key' => 'required|string|between:1,255', 'key' => 'required|string|between:1,255',
'value' => 'string', 'value' => 'string',
]; ];
private static array $cache = [];
private static array $databaseMiss = [];
/**
* Store a new persistent setting in the database.
*/
public static function set(string $key, string $value = null): void
{
// Clear item from the cache.
self::clearCache($key);
self::query()->updateOrCreate(['key' => $key], ['value' => $value ?? '']);
self::$cache[$key] = $value;
}
/**
* Retrieve a persistent setting from the database.
*/
public static function get(string $key, mixed $default = null): mixed
{
// If item has already been requested return it from the cache. If
// we already know it is missing, immediately return the default value.
if (array_key_exists($key, self::$cache)) {
return self::$cache[$key];
} elseif (array_key_exists($key, self::$databaseMiss)) {
return value($default);
}
$instance = self::query()->where('key', $key)->first();
if (is_null($instance)) {
self::$databaseMiss[$key] = true;
return value($default);
}
return self::$cache[$key] = $instance->value;
}
/**
* Remove a key from the database cache.
*/
public static function forget(string $key)
{
self::clearCache($key);
return self::query()->where('key', $key)->delete();
}
/**
* Remove a key from the cache.
*/
private static function clearCache(string $key): void
{
unset(self::$cache[$key], self::$databaseMiss[$key]);
}
} }

View File

@ -87,11 +87,6 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function register(): void public function register(): void
{ {
// Only load the settings service provider if the environment is configured to allow it.
if (!config('panel.load_environment_only', false) && $this->app->environment() !== 'testing') {
$this->app->register(SettingsServiceProvider::class);
}
$this->app->singleton('extensions.themes', function () { $this->app->singleton('extensions.themes', function () {
return new Theme(); return new Theme();
}); });

View File

@ -1,112 +0,0 @@
<?php
namespace App\Providers;
use App\Models\Setting;
use Exception;
use Psr\Log\LoggerInterface as Log;
use Illuminate\Database\QueryException;
use Illuminate\Support\ServiceProvider;
class SettingsServiceProvider extends ServiceProvider
{
/**
* An array of configuration keys to override with database values
* if they exist.
*/
protected array $keys = [
'app:name',
'app:locale',
'recaptcha:enabled',
'recaptcha:secret_key',
'recaptcha:website_key',
'panel:guzzle:timeout',
'panel:guzzle:connect_timeout',
'panel:console:count',
'panel:console:frequency',
'panel:auth:2fa_required',
'panel:client_features:allocations:enabled',
'panel:client_features:allocations:range_start',
'panel:client_features:allocations:range_end',
];
/**
* Keys specific to the mail driver that are only grabbed from the database
* when using the SMTP driver.
*/
protected array $emailKeys = [
'mail:mailers:smtp:host',
'mail:mailers:smtp:port',
'mail:mailers:smtp:encryption',
'mail:mailers:smtp:username',
'mail:mailers:smtp:password',
'mail:from:address',
'mail:from:name',
];
/**
* Keys that are encrypted and should be decrypted when set in the
* configuration array.
*/
protected static array $encrypted = [
'mail:mailers:smtp:password',
];
/**
* Boot the service provider.
*/
public function boot(Log $log): void
{
// Only set the email driver settings from the database if we
// are configured using SMTP as the driver.
if (config('mail.default') === 'smtp') {
$this->keys = array_merge($this->keys, $this->emailKeys);
}
try {
$values = Setting::all()->mapWithKeys(function ($setting) {
return [$setting->key => $setting->value];
})->toArray();
} catch (QueryException $exception) {
$log->notice('A query exception was encountered while trying to load settings from the database: ' . $exception->getMessage());
return;
}
foreach ($this->keys as $key) {
$value = array_get($values, 'settings::' . $key, config(str_replace(':', '.', $key)));
if (in_array($key, self::$encrypted)) {
try {
$value = decrypt($value);
} catch (Exception) {
// ignore
}
}
switch (strtolower($value)) {
case 'true':
case '(true)':
$value = true;
break;
case 'false':
case '(false)':
$value = false;
break;
case 'empty':
case '(empty)':
$value = '';
break;
case 'null':
case '(null)':
$value = null;
}
config()->set(str_replace(':', '.', $key), $value);
}
}
public static function getEncryptedKeys(): array
{
return self::$encrypted;
}
}

View File

@ -5,6 +5,7 @@ use Illuminate\Support\Facades\Facade;
return [ return [
'name' => env('APP_NAME', 'Pelican'), 'name' => env('APP_NAME', 'Pelican'),
'favicon' => env('APP_FAVICON', './pelican.ico'),
'version' => 'canary', 'version' => 'canary',

View File

@ -1,18 +1,6 @@
<?php <?php
return [ return [
/*
|--------------------------------------------------------------------------
| Restricted Environment
|--------------------------------------------------------------------------
|
| Set this environment variable to true to enable a restricted configuration
| setup on the panel. When set to true, configurations stored in the
| database will not be applied.
*/
'load_environment_only' => (bool) env('APP_ENVIRONMENT_ONLY', false),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Authentication | Authentication

View File

@ -6,7 +6,6 @@ return [
'author' => 'Provide the email address that eggs exported by this Panel should be from. This should be a valid email address.', 'author' => 'Provide the email address that eggs exported by this Panel should be from. This should be a valid email address.',
'url' => 'The application URL MUST begin with https:// or http:// depending on if you are using SSL or not. If you do not include the scheme your emails and other content will link to the wrong location.', 'url' => 'The application URL MUST begin with https:// or http:// depending on if you are using SSL or not. If you do not include the scheme your emails and other content will link to the wrong location.',
'timezone' => "The timezone should match one of PHP\'s supported timezones. If you are unsure, please reference https://php.net/manual/en/timezones.php.", 'timezone' => "The timezone should match one of PHP\'s supported timezones. If you are unsure, please reference https://php.net/manual/en/timezones.php.",
'settings_ui' => 'Enable UI based settings editor?',
], ],
'redis' => [ 'redis' => [
'note' => 'You\'ve selected the Redis driver for one or more options, please provide valid connection information below. In most cases you can use the defaults provided unless you have modified your setup.', 'note' => 'You\'ve selected the Redis driver for one or more options, please provide valid connection information below. In most cases you can use the defaults provided unless you have modified your setup.',

View File

@ -1,127 +0,0 @@
@extends('layouts.admin')
@include('partials/admin.settings.nav', ['activeTab' => 'advanced'])
@section('title')
Advanced Settings
@endsection
@section('content-header')
<h1>Advanced Settings<small>Configure advanced settings for Panel.</small></h1>
<ol class="breadcrumb">
<li><a href="{{ route('admin.index') }}">Admin</a></li>
<li class="active">Settings</li>
</ol>
@endsection
@section('content')
@yield('settings::nav')
<div class="row">
<div class="col-xs-12">
<form action="" method="POST">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">reCAPTCHA</h3>
</div>
<div class="box-body">
<div class="row">
<div class="form-group col-md-4">
<label class="control-label">Status</label>
<div>
<select class="form-control" name="recaptcha:enabled">
<option value="true">Enabled</option>
<option value="false" @if(old('recaptcha:enabled', config('recaptcha.enabled')) == '0') selected @endif>Disabled</option>
</select>
<p class="text-muted small">If enabled, login forms and password reset forms will do a silent captcha check and display a visible captcha if needed.</p>
</div>
</div>
<div class="form-group col-md-4">
<label class="control-label">Site Key</label>
<div>
<input type="text" required class="form-control" name="recaptcha:website_key" value="{{ old('recaptcha:website_key', config('recaptcha.website_key')) }}">
</div>
</div>
<div class="form-group col-md-4">
<label class="control-label">Secret Key</label>
<div>
<input type="text" required class="form-control" name="recaptcha:secret_key" value="{{ old('recaptcha:secret_key', config('recaptcha.secret_key')) }}">
<p class="text-muted small">Used for communication between your site and Google. Be sure to keep it a secret.</p>
</div>
</div>
</div>
@if($showRecaptchaWarning)
<div class="row">
<div class="col-xs-12">
<div class="alert alert-warning no-margin">
You are currently using reCAPTCHA keys that were shipped with this Panel. For improved security it is recommended to <a href="https://www.google.com/recaptcha/admin">generate new invisible reCAPTCHA keys</a> that tied specifically to your website.
</div>
</div>
</div>
@endif
</div>
</div>
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">HTTP Connections</h3>
</div>
<div class="box-body">
<div class="row">
<div class="form-group col-md-6">
<label class="control-label">Connection Timeout</label>
<div>
<input type="number" required class="form-control" name="panel:guzzle:connect_timeout" value="{{ old('panel:guzzle:connect_timeout', config('panel.guzzle.connect_timeout')) }}">
<p class="text-muted small">The amount of time in seconds to wait for a connection to be opened before throwing an error.</p>
</div>
</div>
<div class="form-group col-md-6">
<label class="control-label">Request Timeout</label>
<div>
<input type="number" required class="form-control" name="panel:guzzle:timeout" value="{{ old('panel:guzzle:timeout', config('panel.guzzle.timeout')) }}">
<p class="text-muted small">The amount of time in seconds to wait for a request to be completed before throwing an error.</p>
</div>
</div>
</div>
</div>
</div>
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">Automatic Allocation Creation</h3>
</div>
<div class="box-body">
<div class="row">
<div class="form-group col-md-4">
<label class="control-label">Status</label>
<div>
<select class="form-control" name="panel:client_features:allocations:enabled">
<option value="false">Disabled</option>
<option value="true" @if(old('panel:client_features:allocations:enabled', config('panel.client_features.allocations.enabled'))) selected @endif>Enabled</option>
</select>
<p class="text-muted small">If enabled users will have the option to automatically create new allocations for their server via the frontend.</p>
</div>
</div>
<div class="form-group col-md-4">
<label class="control-label">Starting Port</label>
<div>
<input type="number" class="form-control" name="panel:client_features:allocations:range_start" value="{{ old('panel:client_features:allocations:range_start', config('panel.client_features.allocations.range_start')) }}">
<p class="text-muted small">The starting port in the range that can be automatically allocated.</p>
</div>
</div>
<div class="form-group col-md-4">
<label class="control-label">Ending Port</label>
<div>
<input type="number" class="form-control" name="panel:client_features:allocations:range_end" value="{{ old('panel:client_features:allocations:range_end', config('panel.client_features.allocations.range_end')) }}">
<p class="text-muted small">The ending port in the range that can be automatically allocated.</p>
</div>
</div>
</div>
</div>
</div>
<div class="box box-primary">
<div class="box-footer">
{{ csrf_field() }}
<button type="submit" name="_method" value="PATCH" class="btn btn-sm btn-primary pull-right">Save</button>
</div>
</div>
</form>
</div>
</div>
@endsection

View File

@ -1,75 +0,0 @@
@extends('layouts.admin')
@include('partials/admin.settings.nav', ['activeTab' => 'basic'])
@section('title')
Settings
@endsection
@section('content-header')
<h1>Panel Settings<small>Configure Panel to your liking.</small></h1>
<ol class="breadcrumb">
<li><a href="{{ route('admin.index') }}">Admin</a></li>
<li class="active">Settings</li>
</ol>
@endsection
@section('content')
@yield('settings::nav')
<div class="row">
<div class="col-xs-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">Panel Settings</h3>
</div>
<form action="{{ route('admin.settings') }}" method="POST">
<div class="box-body">
<div class="row">
<div class="form-group col-md-4">
<label class="control-label">Company Name</label>
<div>
<input type="text" class="form-control" name="app:name" value="{{ old('app:name', config('app.name')) }}" />
<p class="text-muted"><small>This is the name that is used throughout the panel and in emails sent to clients.</small></p>
</div>
</div>
<div class="form-group col-md-4">
<label class="control-label">Require 2-Factor Authentication</label>
<div>
<div class="btn-group" data-toggle="buttons">
@php
$level = old('panel:auth:2fa_required', config('panel.auth.2fa_required'));
@endphp
<label class="btn btn-primary @if ($level == 0) active @endif">
<input type="radio" name="panel:auth:2fa_required" autocomplete="off" value="0" @if ($level == 0) checked @endif> Not Required
</label>
<label class="btn btn-primary @if ($level == 1) active @endif">
<input type="radio" name="panel:auth:2fa_required" autocomplete="off" value="1" @if ($level == 1) checked @endif> Admin Only
</label>
<label class="btn btn-primary @if ($level == 2) active @endif">
<input type="radio" name="panel:auth:2fa_required" autocomplete="off" value="2" @if ($level == 2) checked @endif> All Users
</label>
</div>
<p class="text-muted"><small>If enabled, any account falling into the selected grouping will be required to have 2-Factor authentication enabled to use the Panel.</small></p>
</div>
</div>
<div class="form-group col-md-4">
<label class="control-label">Default Language</label>
<div>
<select name="app:locale" class="form-control">
@foreach($languages as $key => $value)
<option value="{{ $key }}" @if(config('app.locale') === $key) selected @endif>{{ $value }}</option>
@endforeach
</select>
<p class="text-muted"><small>The default language to use when rendering UI components.</small></p>
</div>
</div>
</div>
</div>
<div class="box-footer">
{!! csrf_field() !!}
<button type="submit" name="_method" value="PATCH" class="btn btn-sm btn-primary pull-right">Save</button>
</div>
</form>
</div>
</div>
</div>
@endsection

View File

@ -1,202 +0,0 @@
@extends('layouts.admin')
@include('partials/admin.settings.nav', ['activeTab' => 'mail'])
@section('title')
Mail Settings
@endsection
@section('content-header')
<h1>Mail Settings<small>Configure how email sending should be handled.</small></h1>
<ol class="breadcrumb">
<li><a href="{{ route('admin.index') }}">Admin</a></li>
<li class="active">Settings</li>
</ol>
@endsection
@section('content')
@yield('settings::nav')
<div class="row">
<div class="col-xs-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">Email Settings</h3>
</div>
@if($disabled)
<div class="box-body">
<div class="row">
<div class="col-xs-12">
<div class="alert alert-info no-margin-bottom">
This interface is limited to instances using SMTP as the mail driver. Please either use <code>php artisan p:environment:mail</code> command to update your email settings, or set <code>MAIL_DRIVER=smtp</code> in your environment file.
</div>
</div>
</div>
</div>
@else
<form>
<div class="box-body">
<div class="row">
<div class="form-group col-md-6">
<label class="control-label">SMTP Host</label>
<div>
<input required type="text" class="form-control" name="mail:mailers:smtp:host" value="{{ old('mail:mailers:smtp:host', config('mail.mailers.smtp.host')) }}" />
<p class="text-muted small">Enter the SMTP server address that mail should be sent through.</p>
</div>
</div>
<div class="form-group col-md-2">
<label class="control-label">SMTP Port</label>
<div>
<input required type="number" class="form-control" name="mail:mailers:smtp:port" value="{{ old('mail:mailers:smtp:port', config('mail.mailers.smtp.port')) }}" />
<p class="text-muted small">Enter the SMTP server port that mail should be sent through.</p>
</div>
</div>
<div class="form-group col-md-4">
<label class="control-label">Encryption</label>
<div>
@php
$encryption = old('mail:mailers:smtp:encryption', config('mail.mailers.smtp.encryption'));
@endphp
<select name="mail:mailers:smtp:encryption" class="form-control">
<option value="" @if($encryption === '') selected @endif>None</option>
<option value="tls" @if($encryption === 'tls') selected @endif>Transport Layer Security (TLS)</option>
<option value="ssl" @if($encryption === 'ssl') selected @endif>Secure Sockets Layer (SSL)</option>
</select>
<p class="text-muted small">Select the type of encryption to use when sending mail.</p>
</div>
</div>
<div class="form-group col-md-6">
<label class="control-label">Username <span class="field-optional"></span></label>
<div>
<input type="text" class="form-control" name="mail:mailers:smtp:username" value="{{ old('mail:mailers:smtp:username', config('mail.mailers.smtp.username')) }}" />
<p class="text-muted small">The username to use when connecting to the SMTP server.</p>
</div>
</div>
<div class="form-group col-md-6">
<label class="control-label">Password <span class="field-optional"></span></label>
<div>
<input type="password" class="form-control" name="mail:mailers:smtp:password"/>
<p class="text-muted small">The password to use in conjunction with the SMTP username. Leave blank to continue using the existing password. To set the password to an empty value enter <code>!e</code> into the field.</p>
</div>
</div>
</div>
<div class="row">
<hr />
<div class="form-group col-md-6">
<label class="control-label">Mail From</label>
<div>
<input required type="email" class="form-control" name="mail:from:address" value="{{ old('mail:from:address', config('mail.from.address')) }}" />
<p class="text-muted small">Enter an email address that all outgoing emails will originate from.</p>
</div>
</div>
<div class="form-group col-md-6">
<label class="control-label">Mail From Name <span class="field-optional"></span></label>
<div>
<input type="text" class="form-control" name="mail:from:name" value="{{ old('mail:from:name', config('mail.from.name')) }}" />
<p class="text-muted small">The name that emails should appear to come from.</p>
</div>
</div>
</div>
</div>
<div class="box-footer">
{{ csrf_field() }}
<div class="pull-right">
<button type="button" id="testButton" class="btn btn-sm btn-success">Test</button>
<button type="button" id="saveButton" class="btn btn-sm btn-primary">Save</button>
</div>
</div>
</form>
@endif
</div>
</div>
</div>
@endsection
@section('footer-scripts')
@parent
<script>
function saveSettings() {
return $.ajax({
method: 'PATCH',
url: '/admin/settings/mail',
contentType: 'application/json',
data: JSON.stringify({
'mail:mailers:smtp:host': $('input[name="mail:mailers:smtp:host"]').val(),
'mail:mailers:smtp:port': $('input[name="mail:mailers:smtp:port"]').val(),
'mail:mailers:smtp:encryption': $('select[name="mail:mailers:smtp:encryption"]').val(),
'mail:mailers:smtp:username': $('input[name="mail:mailers:smtp:username"]').val(),
'mail:mailers:smtp:password': $('input[name="mail:mailers:smtp:password"]').val(),
'mail:from:address': $('input[name="mail:from:address"]').val(),
'mail:from:name': $('input[name="mail:from:name"]').val()
}),
headers: { 'X-CSRF-Token': $('input[name="_token"]').val() }
}).fail(function (jqXHR) {
showErrorDialog(jqXHR, 'save');
});
}
function testSettings() {
swal({
type: 'info',
title: 'Test Mail Settings',
text: 'Click "Test" to begin the test.',
showCancelButton: true,
confirmButtonText: 'Test',
closeOnConfirm: false,
showLoaderOnConfirm: true
}, function () {
$.ajax({
method: 'POST',
url: '/admin/settings/mail/test',
headers: { 'X-CSRF-TOKEN': $('input[name="_token"]').val() }
}).fail(function (jqXHR) {
showErrorDialog(jqXHR, 'test');
}).done(function () {
swal({
title: 'Success',
text: 'The test message was sent successfully.',
type: 'success'
});
});
});
}
function saveAndTestSettings() {
saveSettings().done(testSettings);
}
function showErrorDialog(jqXHR, verb) {
console.error(jqXHR);
var errorText = '';
if (!jqXHR.responseJSON) {
errorText = jqXHR.responseText;
} else if (jqXHR.responseJSON.error) {
errorText = jqXHR.responseJSON.error;
} else if (jqXHR.responseJSON.errors) {
$.each(jqXHR.responseJSON.errors, function (i, v) {
if (v.detail) {
errorText += v.detail + ' ';
}
});
}
swal({
title: 'Whoops!',
text: 'An error occurred while attempting to ' + verb + ' mail settings: ' + errorText,
type: 'error'
});
}
$(document).ready(function () {
$('#testButton').on('click', saveAndTestSettings);
$('#saveButton').on('click', function () {
saveSettings().done(function () {
swal({
title: 'Success',
text: 'Mail settings have been updated successfully and the queue worker was restarted to apply these changes.',
type: 'success'
});
});
});
});
</script>
@endsection

View File

@ -0,0 +1,15 @@
<x-filament-panels::page
@class([
'fi-page-settings'
])
>
<x-filament-panels::form
id="form"
:wire:key="$this->getId() . '.forms.' . $this->getFormStatePath()"
wire:submit="save"
>
{{ $this->form }}
</x-filament-panels::form>
<x-filament-panels::page.unsaved-data-changes-alert />
</x-filament-panels::page>

View File

@ -96,11 +96,6 @@
</a> </a>
</li> </li>
<li class="header">OTHER</li> <li class="header">OTHER</li>
<li class="{{ ! starts_with(Route::currentRouteName(), 'admin.settings') ?: 'active' }}">
<a href="{{ route('admin.settings')}}">
<i class="fa fa-wrench"></i> <span>Settings</span>
</a>
</li>
<li class="{{ ! starts_with(Route::currentRouteName(), 'admin.api') ?: 'active' }}"> <li class="{{ ! starts_with(Route::currentRouteName(), 'admin.api') ?: 'active' }}">
<a href="{{ route('admin.api.index')}}"> <a href="{{ route('admin.api.index')}}">
<i class="fa fa-gamepad"></i> <span>Application API</span> <i class="fa fa-gamepad"></i> <span>Application API</span>

View File

@ -1,16 +0,0 @@
@include('partials/admin.settings.notice')
@section('settings::nav')
@yield('settings::notice')
<div class="row">
<div class="col-xs-12">
<div class="nav-tabs-custom nav-tabs-floating">
<ul class="nav nav-tabs">
<li @if($activeTab === 'basic')class="active"@endif><a href="{{ route('admin.settings') }}">General</a></li>
<li @if($activeTab === 'mail')class="active"@endif><a href="{{ route('admin.settings.mail') }}">Mail</a></li>
<li @if($activeTab === 'advanced')class="active"@endif><a href="{{ route('admin.settings.advanced') }}">Advanced</a></li>
</ul>
</div>
</div>
</div>
@endsection

View File

@ -1,11 +0,0 @@
@section('settings::notice')
@if(config('panel.load_environment_only', false))
<div class="row">
<div class="col-xs-12">
<div class="alert alert-danger">
Your Panel is currently configured to read settings from the environment only. You will need to set <code>APP_ENVIRONMENT_ONLY=false</code> in your environment file in order to load settings dynamically.
</div>
</div>
</div>
@endif
@endsection

View File

@ -40,26 +40,6 @@ Route::prefix('databases')->group(function () {
Route::delete('/view/{host:id}', [Admin\DatabaseController::class, 'delete']); Route::delete('/view/{host:id}', [Admin\DatabaseController::class, 'delete']);
}); });
/*
|--------------------------------------------------------------------------
| Settings Controller Routes
|--------------------------------------------------------------------------
|
| Endpoint: /admin/settings
|
*/
Route::prefix('settings')->group(function () {
Route::get('/', [Admin\Settings\IndexController::class, 'index'])->name('admin.settings');
Route::get('/mail', [Admin\Settings\MailController::class, 'index'])->name('admin.settings.mail');
Route::get('/advanced', [Admin\Settings\AdvancedController::class, 'index'])->name('admin.settings.advanced');
Route::post('/mail/test', [Admin\Settings\MailController::class, 'test'])->name('admin.settings.mail.test');
Route::patch('/', [Admin\Settings\IndexController::class, 'update']);
Route::patch('/mail', [Admin\Settings\MailController::class, 'update']);
Route::patch('/advanced', [Admin\Settings\AdvancedController::class, 'update']);
});
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| User Controller Routes | User Controller Routes