Introducing Persistent Settings: a new Pterodactyl Feature

in php, laravel, panel

In previous versions of Pterodactyl you had very few configuration options available in the Admin CP. For the most part this was done because I was moving quickly to implement other features and abused Laravel's excellent configuration system to keep track of things. There were many instances were users had to ask around to figure out what environment variable they needed to change in order to toggle a feature on or off.

No more though! Pterodactyl@0.7 introduces a much improved Admin UI that allows for more detailed configuration of your Panel without having to dig into the environment file to change things.

On our standard settings screen you'll find a few general settings that most users will want to configure to their liking. The first is your standard company name, the second is 2-FA requirements for accounts, and the third is the default Panel language (which now auto-loads new languages into the list). The two-factor settings toggle was a highly requested feature and allows you to improve overall security by requiring 2FA be enabled for all admin accounts or even for all accounts on the system.

General Settings Overview

On the next tab we have another UI to address a pain point for the Panel: mail settings! Currently this UI only renders if you have configured SMTP as the mail sending method, but down the road I hope to open this up to support other options. You'll still be able to configure all of your email settings via the CLI command as well: php artisan p:environment:mail. This new UI also automatically restarts your queue worker so that you don't forget and keep sending using old details.

Mail Settings Overview

The final tab contains more advanced configuration options for reCAPTCHA, console data settings, and HTTP connection timeouts. I hope this new settings UI makes the Panel that much easier to use and lowers the barrier to entry for more common tasks.

A Deeper Dive

For those of you interested in the technical aspects of how this was made possible, keep on reading! For everyone else, you probably want to turn around — we're looking at code now.

The Service Provider

At the heart of this change lies a new service provider: SettingsServiceProvider.php. This provider sits in the application stack and is called before all of the other application specific service providers are called. If you dig into this file, the first thing you'll notice is an array of configuation keys which point to configuration file values we want to overwrite with values we have stored in the database. However, if you look in the boot() function you'll notice the first line does a check to determine if we should load from the database of not.

if ($config->get('pterodactyl.load_environment_only', false)) {
    return;
}

This line allows us to add APP_ENVIRONMENT_ONLY=true to our .env file or environment and when set to true it will prevent any of this service provider from booting. This is used in our tests to avoid trying to hit the database on unit tests, and since we don't need to control configuration values through the Panel. Moving on through this function you'll notice we grab all of the Setting models stored in the database and use a handy collection to map them into the correct format before passing that array into a foreach loop. The service provider then uses Config::set($key, $value); to set runtime configuration values that the rest of the application can make use of by calling config($key).

The Interface

When starting this change I was initially using a settings package available for Laravel, but quickly realized it was excessive for what I was needing, and was reportedly broken in Laravel 5.5, which is the next upgrade step for this Panel. I whipped up a simple interface using the custom repository classes I have written and added a Setting model to use as the backing layer.

interface SettingsRepositoryInterface extends RepositoryInterface
{
    /**
     * Store a new persistent setting in the database.
     *
     * @param string $key
     * @param string $value
     * @return mixed
     */
    public function set(string $key, string $value);

    /**
     * Retrieve a persistent setting from the database.
     *
     * @param string $key
     * @param mixed  $default
     * @return mixed
     */
    public function get(string $key, $default);

    /**
     * Remove a key from the database cache.
     *
     * @param string $key
     * @return mixed
     */
    public function forget(string $key);
}

This quick addition now means that anywhere settings need to be loaded we can inject SettingsRepositoryInterface and get access to these variables above. For the most part, there is nowhere that is actually calling this interface to get settings, other than the service provider. However, all of the controllers that manage settings are injecting it to make use of the set function. You'll also notice the strict type-hints that are being employed, I think they make the code much more readable, and avoid instances where invalid keys or values might be provided.

The Controller

For the most part the IndexController and AdvancedController do nothing special with the submitted values: they get them from the request (after passing them through a FormRequest and validating them) and then loop through and set the values using the interface above.

The MailController does a little more though as we need to encrypt the password before trying to store it in the database, and we also need to allow setting a password to a blank value easily. We also check to determine if the mail driver is currently set to SMTP, if not: throw an error. This controller also injects the \Illuminate\Contracts\Console\Kernel interface so that we can call $this->kernel->call('queue:restart') which will tell the queue workers to die and restart.

Comments