Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(admin): settings ui #4866

Open
wants to merge 16 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
59 changes: 59 additions & 0 deletions app/Http/Controllers/Api/Application/SettingsController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

namespace Pterodactyl\Http\Controllers\Api\Application;

use Illuminate\Http\Response;
use Pterodactyl\Contracts\Repository\SettingsRepositoryInterface;
use Pterodactyl\Exceptions\Model\DataValidationException;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
use Illuminate\Contracts\Console\Kernel;
use Pterodactyl\Http\Requests\Api\Application\Settings\UpdateSettingsRequest;
use Pterodactyl\Services\Helpers\SettingsService;
use Pterodactyl\Traits\Helpers\AvailableLanguages;
use Pterodactyl\Transformers\Api\Application\SettingsTransformer;

class SettingsController extends ApplicationApiController
{
use AvailableLanguages;

/**
* VersionController constructor.
*
* @param Kernel $kernel
* @param SettingsService $settingsService
* @param SettingsRepositoryInterface $settingsRepository
*/
public function __construct(
private readonly Kernel $kernel,
private readonly SettingsService $settingsService,
private readonly SettingsRepositoryInterface $settingsRepository
) {
parent::__construct();
}

/**
* Returns current settings of panel.
*/
public function index(): array
{
return $this->fractal->item($this->settingsService->getCurrentSettings())->transformWith(SettingsTransformer::class)->toArray();
}

/**
* Handle settings update.
*
* @throws DataValidationException
* @throws RecordNotFoundException
*/
public function update(UpdateSettingsRequest $request): Response
{
$data = $request->validated();

foreach($data as $key => $value) {
$this->settingsRepository->set($key, $value);
}

$this->kernel->call('queue:clear');
return response()->noContent();
}
}
5 changes: 4 additions & 1 deletion app/Http/Middleware/RequireTwoFactorAuthentication.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Illuminate\Support\Str;
use Illuminate\Http\Request;
use Pterodactyl\Contracts\Repository\SettingsRepositoryInterface;
use Pterodactyl\Models\User;
use Pterodactyl\Exceptions\Http\TwoFactorAuthRequiredException;

Expand All @@ -18,6 +19,8 @@ class RequireTwoFactorAuthentication
*/
protected string $redirectRoute = '/account';

public function __construct(private readonly SettingsRepositoryInterface $settings) {}

/**
* Check the user state on the incoming request to determine if they should be allowed to
* proceed or not. This checks if the Panel is configured to require 2FA on an account in
Expand All @@ -42,7 +45,7 @@ public function handle(Request $request, \Closure $next): mixed
return $next($request);
}

$level = (int) config('pterodactyl.auth.2fa_required');
$level = (int) $this->settings->get('sfaEnabled', config('pterodactyl.auth.2fa_required'));
// If this setting is not configured, or the user is already using 2FA then we can just
// send them right through, nothing else needs to be checked.
//
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace Pterodactyl\Http\Requests\Api\Application\Settings;

use Illuminate\Validation\Rule;
use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest;
use Pterodactyl\Traits\Helpers\AvailableLanguages;

class UpdateSettingsRequest extends ApplicationApiRequest
{
use AvailableLanguages;

public function rules(): array
{
return [
// General
'appName' => 'sometimes|required|string|max:191',
'language' => ['sometimes', 'required', 'string', Rule::in(array_keys($this->getAvailableLanguages()))],

// Mail
'smtpHost' => 'sometimes|required|string',
'smtpPort' => 'sometimes|required|integer|between:1,65535',
'smtpEncryption' => ['sometimes', 'present', Rule::in([null, 'tls', 'ssl'])],
'smtpUsername' => 'sometimes|nullable|string|max:191',
'smtpPassword' => 'sometimes|nullable|string|max:191',
'smtpMailFrom' => 'sometimes|required|string|email',
'smtpMailFromName' => 'sometimes|nullable|string|max:191',

// Security
'recaptchaEnabled' => 'sometimes|required|boolean',
'recaptchaSiteKey' => 'sometimes|required_if:recaptchaEnabled,true|string|max:191',
'recaptchaSecretKey' => 'sometimes|required_if:recaptchaEnabled,true|string|max:191',
'sfaEnabled' => 'sometimes|required|integer|between:0,2',
];
}
}
12 changes: 8 additions & 4 deletions app/Http/ViewComposers/AssetComposer.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,24 @@
namespace Pterodactyl\Http\ViewComposers;

use Illuminate\View\View;
use Pterodactyl\Contracts\Repository\SettingsRepositoryInterface;

class AssetComposer
{
public function __construct(private SettingsRepositoryInterface $repository)
{
}
/**
* Provide access to the asset service in the views.
*/
public function compose(View $view): void
{
$view->with('siteConfiguration', [
'name' => config('app.name') ?? 'Pterodactyl',
'locale' => config('app.locale') ?? 'en',
'name' => $this->repository->get('appName', config('app.name')),
'locale' => $this->repository->get('language', config('app.locale')),
'recaptcha' => [
'enabled' => config('recaptcha.enabled', false),
'siteKey' => config('recaptcha.website_key') ?? '',
'enabled' => (bool)$this->repository->get('recaptchaEnabled', config('recaptcha.enabled', false)),
'siteKey' => $this->repository->get('recaptchaSiteKey',config('recaptcha.website_key') ?? ''),
],
]);
}
Expand Down
5 changes: 5 additions & 0 deletions app/Models/Setting.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
*/
class Setting extends Model
{
/**
* The resource name for this model when it is transformed into an
* API representation using fractal.
*/
public const RESOURCE_NAME = 'settings';
/**
* The table associated with the model.
*/
Expand Down
4 changes: 2 additions & 2 deletions app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ public function setUsernameAttribute(string $value)
public function avatarUrl(): Attribute
{
return Attribute::make(
get: fn () => 'https://www.gravatar.com/avatar/' . $this->md5 . '.jpg',
get: fn () => 'https://www.gravatar.com/avatar/' . $this->md5,
);
}

Expand All @@ -236,7 +236,7 @@ public function adminRoleName(): Attribute
public function md5(): Attribute
{
return Attribute::make(
get: fn () => md5(strtolower($this->email)),
get: fn () => md5(strtolower(trim($this->email))),
);
}

Expand Down
45 changes: 45 additions & 0 deletions app/Services/Helpers/SettingsService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace Pterodactyl\Services\Helpers;

use Pterodactyl\Repositories\Eloquent\SettingsRepository;
use Pterodactyl\Traits\Helpers\AvailableLanguages;
use Pterodactyl\Models\Setting;

class SettingsService
{
use AvailableLanguages;

public function __construct(private readonly SettingsRepository $repository) {}

/**
* Return the current version of the panel that is being used.
*/
public function getCurrentSettings(): array
{
return array(
'general' => [
'name' => $this->repository->get('appName', config('app.name')),
'language' => $this->repository->get('language', config('app.locale')),
'languages' => $this->getAvailableLanguages(true),
],
'mail' => [
'host' => $this->repository->get('smtpHost', config('mail.mailers.smtp.host')),
'port' => $this->repository->get('smtpPort', config('mail.mailers.smtp.port')),
'encryption' => $this->repository->get('smtpEncryption', config('mail.mailers.smtp.encryption')),
'username' => $this->repository->get('smtpUsername', config('mail.mailers.smtp.username')),
'password' => $this->repository->get('smtpPassword', config('mail.mailers.smtp.password')),
'from_address' => $this->repository->get('smtpMailFrom', config('mail.from.address')),
'from_name' => $this->repository->get('smtpMailFromName', config('mail.from.name')),
],
'security' => [
'recaptcha' => [
'enabled' => $this->repository->get('recaptchaEnabled' , config('recaptcha.enabled')),
'site_key' => $this->repository->get('recaptchaSiteKey', config('recaptcha.website_key')),
'secret_key' => $this->repository->get('recaptchaSecretKey', config('recaptcha.secret_key')),
],
'2fa_enabled' => $this->repository->get('sfaEnabled', config('pterodactyl.auth.2fa_required')),
],
);
}
}
48 changes: 48 additions & 0 deletions app/Transformers/Api/Application/SettingsTransformer.php
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@matthewpi is a good idea to use transformers for settings page, for me at least it seems more boilerplate code?

Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

namespace Pterodactyl\Transformers\Api\Application;

use Pterodactyl\Models\Setting;
use Pterodactyl\Transformers\Api\Transformer;

class SettingsTransformer extends Transformer
{
/**
* Return the resource name for the JSONAPI output.
*/
public function getResourceName(): string
{
return Setting::RESOURCE_NAME;
}

/**
* Return a generic transformed server variable array.
*/
public function transform(array $model): array
{
return [
'general' => [
'name' => $model['general']['name'],
'language' => $model['general']['language'],
'languages' => $model['general']['languages'],
],
'mail' => [
'host' => $model['mail']['host'],
'port' => $model['mail']['port'],
'from_address' => $model['mail']['from_address'],
'from_name' => $model['mail']['from_name'],
'encryption' => $model['mail']['encryption'],
'username' => $model['mail']['username'],
'password' => $model['mail']['password'],
],
'security' => [
'recaptcha' => [
'enabled' => $model['security']['recaptcha']['enabled'],
'site_key' => $model['security']['recaptcha']['site_key'],
'secret_key' => $model['security']['recaptcha']['secret_key'],
],
'2fa_enabled' => $model['security']['2fa_enabled'],
],
];
}
}
60 changes: 60 additions & 0 deletions resources/scripts/api/admin/settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Model } from '@/api/admin/index';
import http from '@/api/http';
import Transformers from '../definitions/admin/transformers';

export interface Settings {
general: GeneralSettings;
mail: MailSettings;
security: SecuritySettings;
}

export interface GeneralSettings {
name: string;
language: LanguageKey;
languages: Record<LanguageKey, 'string'>;
}

export type LanguageKey = 'en' | 'es' | 'ro';

export interface MailSettings {
host: string;
port: number;
username: string;
password: string;
encryption: string;
fromAddress: string;
fromName: string;
}

export interface SecuritySettings {
recaptcha: {
enabled: boolean;
siteKey: string;
secretKey: string;
};
'2faEnabled': boolean;
}

// export const getSettings = () => {
// return useSWR<Settings>('settings', async () => {
// const { data } = await http.get(`/api/application/settings`);

// return Transformers.toSettings(data);
// });
// };

export const getSettings = (): Promise<Settings> => {
return new Promise((resolve, reject) => {
http.get('/api/application/settings')
.then(({ data }) => resolve(Transformers.toSettings(data)))
.catch(reject);
});
};

export const updateSetting = <Type>(values: Type): Promise<void> => {
return new Promise((resolve, reject) => {
http.patch('/api/application/settings', { ...values })
.then(() => resolve())
.catch(reject);
});
};
27 changes: 27 additions & 0 deletions resources/scripts/api/definitions/admin/transformers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as Models from '@definitions/admin/models';
import { Location } from '@/api/admin/location';
import { Egg, EggVariable } from '@/api/admin/egg';
import { Nest } from '@/api/admin/nest';
import { Settings } from '@/api/admin/settings';

const isList = (data: FractalResponseList | FractalResponseData): data is FractalResponseList => data.object === 'list';

Expand Down Expand Up @@ -225,4 +226,30 @@ export default class Transformers {
eggs: transform(attributes.relationships?.eggs as FractalResponseList, this.toEgg),
},
});

static toSettings = ({ attributes }: FractalResponseData): Settings => ({
general: {
name: attributes.general.name,
language: attributes.general.language,
languages: attributes.general.languages,
},
mail: {
host: attributes.mail.host,
port: attributes.mail.port,
username: attributes.mail.username,
password: attributes.mail.password,
encryption: attributes.mail.encryption,
fromAddress: attributes.mail.from_address,
fromName: attributes.mail.from_name,
},
security: {
recaptcha: {
enabled: attributes.security.recaptcha.enabled,
siteKey: attributes.security.recaptcha.site_key,
secretKey: attributes.security.recaptcha.secret_key,
},
'2faEnabled': attributes.security['2fa_enabled'],
},
relationships: {},
});
}