diff --git a/app/Http/Controllers/Api/Application/SettingsController.php b/app/Http/Controllers/Api/Application/SettingsController.php new file mode 100644 index 0000000000..0413459335 --- /dev/null +++ b/app/Http/Controllers/Api/Application/SettingsController.php @@ -0,0 +1,59 @@ +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(); + } +} diff --git a/app/Http/Middleware/RequireTwoFactorAuthentication.php b/app/Http/Middleware/RequireTwoFactorAuthentication.php index a6110edb16..bd0a73e46e 100644 --- a/app/Http/Middleware/RequireTwoFactorAuthentication.php +++ b/app/Http/Middleware/RequireTwoFactorAuthentication.php @@ -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; @@ -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 @@ -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. // diff --git a/app/Http/Requests/Api/Application/Settings/UpdateSettingsRequest.php b/app/Http/Requests/Api/Application/Settings/UpdateSettingsRequest.php new file mode 100644 index 0000000000..6136e96cd6 --- /dev/null +++ b/app/Http/Requests/Api/Application/Settings/UpdateSettingsRequest.php @@ -0,0 +1,36 @@ + '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', + ]; + } +} diff --git a/app/Http/ViewComposers/AssetComposer.php b/app/Http/ViewComposers/AssetComposer.php index 2bbb39d5ce..91e7489151 100644 --- a/app/Http/ViewComposers/AssetComposer.php +++ b/app/Http/ViewComposers/AssetComposer.php @@ -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') ?? ''), ], ]); } diff --git a/app/Models/Setting.php b/app/Models/Setting.php index b8812bc665..56e516c53e 100644 --- a/app/Models/Setting.php +++ b/app/Models/Setting.php @@ -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. */ diff --git a/app/Models/User.php b/app/Models/User.php index 3446bf8071..58934b76b0 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -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, ); } @@ -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))), ); } diff --git a/app/Services/Helpers/SettingsService.php b/app/Services/Helpers/SettingsService.php new file mode 100644 index 0000000000..6fb38edf2f --- /dev/null +++ b/app/Services/Helpers/SettingsService.php @@ -0,0 +1,45 @@ + [ + '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')), + ], + ); + } +} diff --git a/app/Transformers/Api/Application/SettingsTransformer.php b/app/Transformers/Api/Application/SettingsTransformer.php new file mode 100644 index 0000000000..4a9198854f --- /dev/null +++ b/app/Transformers/Api/Application/SettingsTransformer.php @@ -0,0 +1,48 @@ + [ + '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'], + ], + ]; + } +} diff --git a/resources/scripts/api/admin/settings.ts b/resources/scripts/api/admin/settings.ts new file mode 100644 index 0000000000..2d19c5b2ab --- /dev/null +++ b/resources/scripts/api/admin/settings.ts @@ -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; +} + +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', async () => { +// const { data } = await http.get(`/api/application/settings`); + +// return Transformers.toSettings(data); +// }); +// }; + +export const getSettings = (): Promise => { + return new Promise((resolve, reject) => { + http.get('/api/application/settings') + .then(({ data }) => resolve(Transformers.toSettings(data))) + .catch(reject); + }); +}; + +export const updateSetting = (values: Type): Promise => { + return new Promise((resolve, reject) => { + http.patch('/api/application/settings', { ...values }) + .then(() => resolve()) + .catch(reject); + }); +}; diff --git a/resources/scripts/api/definitions/admin/transformers.ts b/resources/scripts/api/definitions/admin/transformers.ts index d298341e83..6cbd8e694e 100644 --- a/resources/scripts/api/definitions/admin/transformers.ts +++ b/resources/scripts/api/definitions/admin/transformers.ts @@ -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'; @@ -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: {}, + }); } diff --git a/resources/scripts/components/admin/settings/AdvancedSettings.tsx b/resources/scripts/components/admin/settings/AdvancedSettings.tsx new file mode 100644 index 0000000000..8089655e69 --- /dev/null +++ b/resources/scripts/components/admin/settings/AdvancedSettings.tsx @@ -0,0 +1,90 @@ +import { Form, Formik } from 'formik'; +import tw from 'twin.macro'; + +import AdminBox from '@/components/admin/AdminBox'; +import Field, { FieldRow } from '@/components/elements/Field'; +import SelectField from '@/components/elements/SelectField'; +import { Button } from '@/components/elements/button'; + +export default () => { + const submit = () => { + // + }; + + return ( + +
+
+ + + + + + + + + + + + + +
+
+
+ +
+
+
+
+ ); +}; diff --git a/resources/scripts/components/admin/settings/GeneralSettings.tsx b/resources/scripts/components/admin/settings/GeneralSettings.tsx index eb4ece2c8c..1d7fdf5ff1 100644 --- a/resources/scripts/components/admin/settings/GeneralSettings.tsx +++ b/resources/scripts/components/admin/settings/GeneralSettings.tsx @@ -1,37 +1,94 @@ -import { Form, Formik } from 'formik'; +import { Form, Formik, FormikHelpers } from 'formik'; import tw from 'twin.macro'; import AdminBox from '@/components/admin/AdminBox'; import Field, { FieldRow } from '@/components/elements/Field'; +import { Actions } from 'easy-peasy'; +import { ApplicationStore } from '@/state'; +import SelectField from '@/components/elements/SelectField'; +import { Context } from './SettingsRouter'; +import { Button } from '@/components/elements/button'; +import { LanguageKey, updateSetting } from '@/api/admin/settings'; +import { useStoreActions } from '@/state/hooks'; +import { SiteSettings } from '@/state/settings'; -export default () => { - const submit = () => { - // +type Values = { + appName: string; + language: LanguageKey; +}; + +export default function GeneralSettings() { + const { name: appName, languages, language } = Context.useStoreState(state => state.settings!.general); + const setSettings = useStoreActions(actions => actions.settings!.setSettings); + + const { addFlash, clearFlashes, clearAndAddHttpError } = useStoreActions( + (actions: Actions) => actions.flashes, + ); + + const submit = async (values: Values, { setSubmitting }: FormikHelpers) => { + clearFlashes('admin:settings'); + setSubmitting(true); + + try { + await updateSetting(values); + setSettings({ name: values.appName, locale: values.language } as SiteSettings); + addFlash({ type: 'success', message: 'Successfully updated settings.', key: 'admin:settings' }); + setTimeout(() => clearFlashes('admin:settings'), 2000); + } catch (error) { + console.error(error); + clearAndAddHttpError({ key: 'admin:settings', error }); + } finally { + setSubmitting(false); + } }; return ( - -
-
- - - - - - - - - - - -
-
+ + {({ isSubmitting, isValid }) => ( +
+
+ + + + + + + + { + return { + value: lang, + label: languages[lang as LanguageKey], + }; + })} + /> + + +
+
+
+ +
+
+
+ )}
); -}; +} diff --git a/resources/scripts/components/admin/settings/MailSettings.tsx b/resources/scripts/components/admin/settings/MailSettings.tsx index 1e63eecc2c..8a85fc86d0 100644 --- a/resources/scripts/components/admin/settings/MailSettings.tsx +++ b/resources/scripts/components/admin/settings/MailSettings.tsx @@ -1,28 +1,62 @@ -import { Form, Formik } from 'formik'; +import { Form, Formik, FormikHelpers } from 'formik'; import tw from 'twin.macro'; import AdminBox from '@/components/admin/AdminBox'; -import Button from '@/components/elements/Button'; +import { Button } from '@/components/elements/button'; import Field, { FieldRow } from '@/components/elements/Field'; import Label from '@/components/elements/Label'; -import Select from '@/components/elements/Select'; +import { Context } from './SettingsRouter'; +import SelectField from '@/components/elements/SelectField'; +import { updateSetting } from '@/api/admin/settings'; +import { Actions, useStoreActions } from 'easy-peasy'; +import { ApplicationStore } from '@/state'; + +interface Values { + smtpHost: string; + smtpPort: number; + smtpEncryption: string; + smtpUsername: string; + smtpPassword: string; + smtpMailFrom: string; + smtpMailFromName: string; +} export default () => { - const submit = () => { - // + const { host, port, encryption, username, password, fromAddress, fromName } = Context.useStoreState( + state => state.settings!.mail, + ); + + const { addFlash, clearFlashes, clearAndAddHttpError } = useStoreActions( + (actions: Actions) => actions.flashes, + ); + + const submit = async (values: Values, { setSubmitting }: FormikHelpers) => { + clearFlashes('admin:settings'); + setSubmitting(true); + + try { + await updateSetting(values); + addFlash({ type: 'success', message: 'Successfully updated settings.', key: 'admin:settings' }); + setTimeout(() => clearFlashes('admin:settings'), 2000); + } catch (error) { + console.error(error); + clearAndAddHttpError({ key: 'admin:settings', error }); + } finally { + setSubmitting(false); + } }; return ( {({ isSubmitting, isValid }) => ( @@ -45,25 +79,28 @@ export default () => { />
- +
{ {
-
diff --git a/resources/scripts/components/admin/settings/SecuritySettings.tsx b/resources/scripts/components/admin/settings/SecuritySettings.tsx new file mode 100644 index 0000000000..bed4b3c0a0 --- /dev/null +++ b/resources/scripts/components/admin/settings/SecuritySettings.tsx @@ -0,0 +1,120 @@ +import { Form, Formik, FormikHelpers } from 'formik'; +import tw from 'twin.macro'; + +import AdminBox from '@/components/admin/AdminBox'; +import Field, { FieldRow } from '@/components/elements/Field'; +import SelectField from '@/components/elements/SelectField'; +import { Context } from './SettingsRouter'; +import { Button } from '@/components/elements/button'; +import { ApplicationStore } from '@/state'; +import { Actions, useStoreActions } from 'easy-peasy'; +import { updateSetting } from '@/api/admin/settings'; + +interface Values { + recaptchaEnabled: string; + recaptchaSiteKey: string; + recaptchaSecretKey: string; + sfaEnabled: string; +} + +export default () => { + const security = Context.useStoreState(state => state.settings!.security); + const { enabled: recaptchaStatus, siteKey, secretKey } = security.recaptcha; + + const { addFlash, clearFlashes, clearAndAddHttpError } = useStoreActions( + (actions: Actions) => actions.flashes, + ); + + const submit = async (values: Values, { setSubmitting }: FormikHelpers) => { + clearFlashes('admin:settings'); + setSubmitting(true); + + try { + console.log(values); + await updateSetting(values); + addFlash({ type: 'success', message: 'Successfully updated settings.', key: 'admin:settings' }); + setTimeout(() => clearFlashes('admin:settings'), 2000); + } catch (error) { + console.error(error); + clearAndAddHttpError({ key: 'admin:settings', error }); + } finally { + setSubmitting(false); + } + }; + + return ( + +
+
+ + + + + + + + + + + + + + + +
+
+
+ +
+
+
+
+ ); +}; diff --git a/resources/scripts/components/admin/settings/SettingsContainer.tsx b/resources/scripts/components/admin/settings/SettingsContainer.tsx deleted file mode 100644 index 3fab069569..0000000000 --- a/resources/scripts/components/admin/settings/SettingsContainer.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { AdjustmentsIcon, ChipIcon, CodeIcon, MailIcon, ShieldCheckIcon } from '@heroicons/react/outline'; -import { Route, Routes } from 'react-router-dom'; -import tw from 'twin.macro'; - -import AdminContentBlock from '@/components/admin/AdminContentBlock'; -import MailSettings from '@/components/admin/settings/MailSettings'; -import FlashMessageRender from '@/components/FlashMessageRender'; -import { SubNavigation, SubNavigationLink } from '@/components/admin/SubNavigation'; -import GeneralSettings from '@/components/admin/settings/GeneralSettings'; - -export default () => { - return ( - -
-
-

Settings

-

- Configure and manage settings for Pterodactyl. -

-
-
- - - - - - - - - - - - - - - - - - - - - - - } /> - } /> - Security

} /> - Features

} /> - Advanced

} /> -
-
- ); -}; diff --git a/resources/scripts/components/admin/settings/SettingsRouter.tsx b/resources/scripts/components/admin/settings/SettingsRouter.tsx new file mode 100644 index 0000000000..32313f1ccf --- /dev/null +++ b/resources/scripts/components/admin/settings/SettingsRouter.tsx @@ -0,0 +1,112 @@ +import { ChipIcon, CodeIcon, MailIcon, ShieldCheckIcon } from '@heroicons/react/outline'; +import { Route, Routes } from 'react-router-dom'; +import tw from 'twin.macro'; + +import AdminContentBlock from '@/components/admin/AdminContentBlock'; +import MailSettings from '@/components/admin/settings/MailSettings'; +import FlashMessageRender from '@/components/FlashMessageRender'; +import { SubNavigation, SubNavigationLink } from '@/components/admin/SubNavigation'; +import GeneralSettings from '@/components/admin/settings/GeneralSettings'; +import SecuritySettings from '@/components/admin/settings/SecuritySettings'; +import AdvancedSettings from './AdvancedSettings'; +import { Settings, getSettings } from '@/api/admin/settings'; +import { useEffect, useState } from 'react'; +import { Action, Actions, action, createContextStore, useStoreActions } from 'easy-peasy'; +import { ApplicationStore } from '@/state'; +import Spinner from '@/components/elements/Spinner'; + +interface ctx { + settings: Settings | undefined; + setSettings: Action; +} + +export const Context = createContextStore({ + settings: undefined, + + setSettings: action((state, payload) => { + state.settings = payload; + }), +}); + +const SettingsRouter = () => { + const { clearFlashes, clearAndAddHttpError } = useStoreActions( + (actions: Actions) => actions.flashes, + ); + const [loading, setLoading] = useState(true); + + const settings = Context.useStoreState(state => state.settings); + const setSettings = Context.useStoreActions(actions => actions.setSettings); + + useEffect(() => { + clearFlashes('settings'); + + getSettings() + .then(settings => setSettings(settings)) + .catch(error => { + console.error(error); + clearAndAddHttpError({ key: 'settings', error }); + }) + .then(() => setLoading(false)); + }, []); + + if (loading || settings === undefined) { + return ( + + + +
+ +
+
+ ); + } + + return ( + +
+
+

Settings

+

+ Configure and manage settings for Pterodactyl. +

+
+
+ + + + + + + + + + + + + + {/* + + */} + + + + + + + } /> + } /> + } /> + {/* Features

} /> */} + } /> +
+
+ ); +}; + +export default () => { + return ( + + + + ); +}; diff --git a/resources/scripts/components/admin/users/UsersContainer.tsx b/resources/scripts/components/admin/users/UsersContainer.tsx index 0cb3398f7d..791f49e2ee 100644 --- a/resources/scripts/components/admin/users/UsersContainer.tsx +++ b/resources/scripts/components/admin/users/UsersContainer.tsx @@ -5,7 +5,7 @@ import { NavLink } from 'react-router-dom'; import { useGetUsers } from '@/api/admin/users'; import type { UUID } from '@/api/definitions'; import { Transition } from '@/components/elements/transitions'; -import { Button } from '@/components/elements/button/index'; +import { Button } from '@/components/elements/button'; import Checkbox from '@/components/elements/inputs/Checkbox'; import InputField from '@/components/elements/inputs/InputField'; import UserTableRow from '@/components/admin/users/UserTableRow'; diff --git a/resources/scripts/components/dashboard/forms/ConfigureTwoFactorForm.tsx b/resources/scripts/components/dashboard/forms/ConfigureTwoFactorForm.tsx index 059d1474e4..14f6beeaaf 100644 --- a/resources/scripts/components/dashboard/forms/ConfigureTwoFactorForm.tsx +++ b/resources/scripts/components/dashboard/forms/ConfigureTwoFactorForm.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { useStoreState } from 'easy-peasy'; import { ApplicationStore } from '@/state'; import tw from 'twin.macro'; -import { Button } from '@/components/elements/button/index'; +import { Button } from '@/components/elements/button'; import SetupTOTPDialog from '@/components/dashboard/forms/SetupTOTPDialog'; import RecoveryTokensDialog from '@/components/dashboard/forms/RecoveryTokensDialog'; import DisableTOTPDialog from '@/components/dashboard/forms/DisableTOTPDialog'; diff --git a/resources/scripts/components/dashboard/forms/DisableTOTPDialog.tsx b/resources/scripts/components/dashboard/forms/DisableTOTPDialog.tsx index fe5039b794..fe623759ab 100644 --- a/resources/scripts/components/dashboard/forms/DisableTOTPDialog.tsx +++ b/resources/scripts/components/dashboard/forms/DisableTOTPDialog.tsx @@ -2,7 +2,7 @@ import { useContext, useEffect, useState } from 'react'; import * as React from 'react'; import asDialog from '@/hoc/asDialog'; import { Dialog, DialogWrapperContext } from '@/components/elements/dialog'; -import { Button } from '@/components/elements/button/index'; +import { Button } from '@/components/elements/button'; import { Input } from '@/components/elements/inputs'; import Tooltip from '@/components/elements/tooltip/Tooltip'; import disableAccountTwoFactor from '@/api/account/disableAccountTwoFactor'; diff --git a/resources/scripts/components/dashboard/forms/RecoveryTokensDialog.tsx b/resources/scripts/components/dashboard/forms/RecoveryTokensDialog.tsx index c5b090450a..f18debfec3 100644 --- a/resources/scripts/components/dashboard/forms/RecoveryTokensDialog.tsx +++ b/resources/scripts/components/dashboard/forms/RecoveryTokensDialog.tsx @@ -1,5 +1,5 @@ import { Dialog, DialogProps } from '@/components/elements/dialog'; -import { Button } from '@/components/elements/button/index'; +import { Button } from '@/components/elements/button'; import CopyOnClick from '@/components/elements/CopyOnClick'; import { Alert } from '@/components/elements/alert'; diff --git a/resources/scripts/components/dashboard/forms/SetupTOTPDialog.tsx b/resources/scripts/components/dashboard/forms/SetupTOTPDialog.tsx index f117a222c7..73e55bbb9f 100644 --- a/resources/scripts/components/dashboard/forms/SetupTOTPDialog.tsx +++ b/resources/scripts/components/dashboard/forms/SetupTOTPDialog.tsx @@ -5,7 +5,7 @@ import getTwoFactorTokenData, { TwoFactorTokenData } from '@/api/account/getTwoF import { useFlashKey } from '@/plugins/useFlash'; import tw from 'twin.macro'; import QRCode from 'qrcode.react'; -import { Button } from '@/components/elements/button/index'; +import { Button } from '@/components/elements/button'; import Spinner from '@/components/elements/Spinner'; import { Input } from '@/components/elements/inputs'; import CopyOnClick from '@/components/elements/CopyOnClick'; diff --git a/resources/scripts/components/dashboard/forms/UpdateEmailAddressForm.tsx b/resources/scripts/components/dashboard/forms/UpdateEmailAddressForm.tsx index 56632e1f4f..d5d0f33adb 100644 --- a/resources/scripts/components/dashboard/forms/UpdateEmailAddressForm.tsx +++ b/resources/scripts/components/dashboard/forms/UpdateEmailAddressForm.tsx @@ -7,7 +7,7 @@ import Field from '@/components/elements/Field'; import { httpErrorToHuman } from '@/api/http'; import { ApplicationStore } from '@/state'; import tw from 'twin.macro'; -import { Button } from '@/components/elements/button/index'; +import { Button } from '@/components/elements/button'; interface Values { email: string; diff --git a/resources/scripts/components/dashboard/forms/UpdatePasswordForm.tsx b/resources/scripts/components/dashboard/forms/UpdatePasswordForm.tsx index 34dcd64e68..cd673534d8 100644 --- a/resources/scripts/components/dashboard/forms/UpdatePasswordForm.tsx +++ b/resources/scripts/components/dashboard/forms/UpdatePasswordForm.tsx @@ -8,7 +8,7 @@ import updateAccountPassword from '@/api/account/updateAccountPassword'; import { httpErrorToHuman } from '@/api/http'; import { ApplicationStore } from '@/state'; import tw from 'twin.macro'; -import { Button } from '@/components/elements/button/index'; +import { Button } from '@/components/elements/button'; interface Values { current: string; diff --git a/resources/scripts/components/elements/Input.tsx b/resources/scripts/components/elements/Input.tsx index 3b6ccedc6e..6b58bce1c0 100644 --- a/resources/scripts/components/elements/Input.tsx +++ b/resources/scripts/components/elements/Input.tsx @@ -45,7 +45,7 @@ const inputStyle = css` & + .input-help { ${tw`mt-1 text-xs`}; - ${props => (props.hasError ? tw`text-red-200` : tw`text-neutral-200`)}; + ${props => (props.hasError ? tw`text-red-200` : tw`text-neutral-400`)}; } &:required, diff --git a/resources/scripts/components/elements/activity/ActivityLogMetaButton.tsx b/resources/scripts/components/elements/activity/ActivityLogMetaButton.tsx index 6665b772fe..04d7e94219 100644 --- a/resources/scripts/components/elements/activity/ActivityLogMetaButton.tsx +++ b/resources/scripts/components/elements/activity/ActivityLogMetaButton.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { ClipboardListIcon } from '@heroicons/react/outline'; import { Dialog } from '@/components/elements/dialog'; -import { Button } from '@/components/elements/button/index'; +import { Button } from '@/components/elements/button'; export default ({ meta }: { meta: Record }) => { const [open, setOpen] = useState(false); diff --git a/resources/scripts/components/elements/dialog/ConfirmationDialog.tsx b/resources/scripts/components/elements/dialog/ConfirmationDialog.tsx index 3f2cd7217c..195733b7e8 100644 --- a/resources/scripts/components/elements/dialog/ConfirmationDialog.tsx +++ b/resources/scripts/components/elements/dialog/ConfirmationDialog.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { Dialog, RenderDialogProps } from './'; -import { Button } from '@/components/elements/button/index'; +import { Button } from '@/components/elements/button'; type ConfirmationProps = Omit & { children: React.ReactNode; diff --git a/resources/scripts/components/elements/dialog/Dialog.tsx b/resources/scripts/components/elements/dialog/Dialog.tsx index 6612d1871e..26563988d1 100644 --- a/resources/scripts/components/elements/dialog/Dialog.tsx +++ b/resources/scripts/components/elements/dialog/Dialog.tsx @@ -1,7 +1,7 @@ import { useRef, useState } from 'react'; import * as React from 'react'; import { Dialog as HDialog } from '@headlessui/react'; -import { Button } from '@/components/elements/button/index'; +import { Button } from '@/components/elements/button'; import { XIcon } from '@heroicons/react/solid'; import { AnimatePresence, motion } from 'framer-motion'; import { DialogContext, IconPosition, RenderDialogProps, styles } from './'; diff --git a/resources/scripts/components/elements/table/PaginationFooter.tsx b/resources/scripts/components/elements/table/PaginationFooter.tsx index 43b5c9d48b..9240899687 100644 --- a/resources/scripts/components/elements/table/PaginationFooter.tsx +++ b/resources/scripts/components/elements/table/PaginationFooter.tsx @@ -1,6 +1,6 @@ import { PaginationDataSet } from '@/api/http'; import classNames from 'classnames'; -import { Button } from '@/components/elements/button/index'; +import { Button } from '@/components/elements/button'; import { ChevronDoubleLeftIcon, ChevronDoubleRightIcon } from '@heroicons/react/solid'; interface Props { diff --git a/resources/scripts/components/server/console/PowerButtons.tsx b/resources/scripts/components/server/console/PowerButtons.tsx index 446fada52e..5fde279bb8 100644 --- a/resources/scripts/components/server/console/PowerButtons.tsx +++ b/resources/scripts/components/server/console/PowerButtons.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; import * as React from 'react'; -import { Button } from '@/components/elements/button/index'; +import { Button } from '@/components/elements/button'; import Can from '@/components/elements/Can'; import { ServerContext } from '@/state/server'; import { PowerAction } from '@/components/server/console/ServerConsoleContainer'; diff --git a/resources/scripts/components/server/files/FileManagerContainer.tsx b/resources/scripts/components/server/files/FileManagerContainer.tsx index aa97904de9..e2242fd14a 100644 --- a/resources/scripts/components/server/files/FileManagerContainer.tsx +++ b/resources/scripts/components/server/files/FileManagerContainer.tsx @@ -11,7 +11,7 @@ import NewDirectoryButton from '@/components/server/files/NewDirectoryButton'; import { NavLink, useLocation } from 'react-router-dom'; import Can from '@/components/elements/Can'; import { ServerError } from '@/components/elements/ScreenBlock'; -import { Button } from '@/components/elements/button/index'; +import { Button } from '@/components/elements/button'; import { ServerContext } from '@/state/server'; import useFileManagerSwr from '@/plugins/useFileManagerSwr'; // import FileManagerStatus from '@/components/server/files/FileManagerStatus'; diff --git a/resources/scripts/components/server/files/FileManagerStatus.tsx b/resources/scripts/components/server/files/FileManagerStatus.tsx index 1b065d031c..efb4fce66b 100644 --- a/resources/scripts/components/server/files/FileManagerStatus.tsx +++ b/resources/scripts/components/server/files/FileManagerStatus.tsx @@ -2,7 +2,7 @@ import { CloudUploadIcon, XIcon } from '@heroicons/react/solid'; import { useSignal } from '@preact/signals-react'; import { useContext, useEffect } from 'react'; -import { Button } from '@/components/elements/button/index'; +import { Button } from '@/components/elements/button'; import { Dialog, DialogWrapperContext } from '@/components/elements/dialog'; import Tooltip from '@/components/elements/tooltip/Tooltip'; import Code from '@/components/elements/Code'; diff --git a/resources/scripts/components/server/files/NewDirectoryButton.tsx b/resources/scripts/components/server/files/NewDirectoryButton.tsx index f92c6f602d..3bf7fdc9a9 100644 --- a/resources/scripts/components/server/files/NewDirectoryButton.tsx +++ b/resources/scripts/components/server/files/NewDirectoryButton.tsx @@ -6,7 +6,7 @@ import { join } from 'pathe'; import { object, string } from 'yup'; import createDirectory from '@/api/server/files/createDirectory'; import tw from 'twin.macro'; -import { Button } from '@/components/elements/button/index'; +import { Button } from '@/components/elements/button'; import { FileObject } from '@/api/server/files/loadDirectory'; import { useFlashKey } from '@/plugins/useFlash'; import useFileManagerSwr from '@/plugins/useFileManagerSwr'; diff --git a/resources/scripts/components/server/files/UploadButton.tsx b/resources/scripts/components/server/files/UploadButton.tsx index b3c6d94041..f21b16bf30 100644 --- a/resources/scripts/components/server/files/UploadButton.tsx +++ b/resources/scripts/components/server/files/UploadButton.tsx @@ -5,7 +5,7 @@ import { useEffect, useRef } from 'react'; import tw from 'twin.macro'; import getFileUploadUrl from '@/api/server/files/getFileUploadUrl'; -import { Button } from '@/components/elements/button/index'; +import { Button } from '@/components/elements/button'; import { ModalMask } from '@/components/elements/Modal'; import Portal from '@/components/elements/Portal'; import FadeTransition from '@/components/elements/transitions/FadeTransition'; diff --git a/resources/scripts/components/server/network/AllocationRow.tsx b/resources/scripts/components/server/network/AllocationRow.tsx index 117910e90e..ac31d9def6 100644 --- a/resources/scripts/components/server/network/AllocationRow.tsx +++ b/resources/scripts/components/server/network/AllocationRow.tsx @@ -6,7 +6,7 @@ import { faNetworkWired } from '@fortawesome/free-solid-svg-icons'; import InputSpinner from '@/components/elements/InputSpinner'; import { Textarea } from '@/components/elements/Input'; import Can from '@/components/elements/Can'; -import { Button } from '@/components/elements/button/index'; +import { Button } from '@/components/elements/button'; import GreyRowBox from '@/components/elements/GreyRowBox'; import { Allocation } from '@/api/server/getServer'; import styled from 'styled-components'; diff --git a/resources/scripts/components/server/network/DeleteAllocationButton.tsx b/resources/scripts/components/server/network/DeleteAllocationButton.tsx index 5dd5b7c656..c112552d54 100644 --- a/resources/scripts/components/server/network/DeleteAllocationButton.tsx +++ b/resources/scripts/components/server/network/DeleteAllocationButton.tsx @@ -7,7 +7,7 @@ import deleteServerAllocation from '@/api/server/network/deleteServerAllocation' import getServerAllocations from '@/api/swr/getServerAllocations'; import { useFlashKey } from '@/plugins/useFlash'; import { Dialog } from '@/components/elements/dialog'; -import { Button } from '@/components/elements/button/index'; +import { Button } from '@/components/elements/button'; interface Props { allocation: number; diff --git a/resources/scripts/components/server/schedules/DeleteScheduleButton.tsx b/resources/scripts/components/server/schedules/DeleteScheduleButton.tsx index d6b028556d..facada9e11 100644 --- a/resources/scripts/components/server/schedules/DeleteScheduleButton.tsx +++ b/resources/scripts/components/server/schedules/DeleteScheduleButton.tsx @@ -4,7 +4,7 @@ import { ServerContext } from '@/state/server'; import { Actions, useStoreActions } from 'easy-peasy'; import { ApplicationStore } from '@/state'; import { httpErrorToHuman } from '@/api/http'; -import { Button } from '@/components/elements/button/index'; +import { Button } from '@/components/elements/button'; import { Dialog } from '@/components/elements/dialog'; import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; diff --git a/resources/scripts/components/server/schedules/EditScheduleModal.tsx b/resources/scripts/components/server/schedules/EditScheduleModal.tsx index 8ab8536565..6a3e3b8b1e 100644 --- a/resources/scripts/components/server/schedules/EditScheduleModal.tsx +++ b/resources/scripts/components/server/schedules/EditScheduleModal.tsx @@ -9,7 +9,7 @@ import { httpErrorToHuman } from '@/api/http'; import FlashMessageRender from '@/components/FlashMessageRender'; import useFlash from '@/plugins/useFlash'; import tw from 'twin.macro'; -import { Button } from '@/components/elements/button/index'; +import { Button } from '@/components/elements/button'; import ModalContext from '@/context/ModalContext'; import asModal from '@/hoc/asModal'; import Switch from '@/components/elements/Switch'; diff --git a/resources/scripts/components/server/schedules/NewTaskButton.tsx b/resources/scripts/components/server/schedules/NewTaskButton.tsx index 186b0a2ff5..a732f46121 100644 --- a/resources/scripts/components/server/schedules/NewTaskButton.tsx +++ b/resources/scripts/components/server/schedules/NewTaskButton.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { Schedule } from '@/api/server/schedules/getServerSchedules'; import TaskDetailsModal from '@/components/server/schedules/TaskDetailsModal'; -import { Button } from '@/components/elements/button/index'; +import { Button } from '@/components/elements/button'; interface Props { schedule: Schedule; diff --git a/resources/scripts/components/server/schedules/RunScheduleButton.tsx b/resources/scripts/components/server/schedules/RunScheduleButton.tsx index 7c1b4a69bc..be9d6c09fe 100644 --- a/resources/scripts/components/server/schedules/RunScheduleButton.tsx +++ b/resources/scripts/components/server/schedules/RunScheduleButton.tsx @@ -1,6 +1,6 @@ import { useCallback, useState } from 'react'; import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; -import { Button } from '@/components/elements/button/index'; +import { Button } from '@/components/elements/button'; import triggerScheduleExecution from '@/api/server/schedules/triggerScheduleExecution'; import { ServerContext } from '@/state/server'; import useFlash from '@/plugins/useFlash'; diff --git a/resources/scripts/components/server/schedules/ScheduleContainer.tsx b/resources/scripts/components/server/schedules/ScheduleContainer.tsx index f644653e05..c8ba18fad2 100644 --- a/resources/scripts/components/server/schedules/ScheduleContainer.tsx +++ b/resources/scripts/components/server/schedules/ScheduleContainer.tsx @@ -10,7 +10,7 @@ import Can from '@/components/elements/Can'; import useFlash from '@/plugins/useFlash'; import tw from 'twin.macro'; import GreyRowBox from '@/components/elements/GreyRowBox'; -import { Button } from '@/components/elements/button/index'; +import { Button } from '@/components/elements/button'; import ServerContentBlock from '@/components/elements/ServerContentBlock'; import { Link } from 'react-router-dom'; diff --git a/resources/scripts/components/server/schedules/ScheduleEditContainer.tsx b/resources/scripts/components/server/schedules/ScheduleEditContainer.tsx index 840b3583e4..d7603f24a3 100644 --- a/resources/scripts/components/server/schedules/ScheduleEditContainer.tsx +++ b/resources/scripts/components/server/schedules/ScheduleEditContainer.tsx @@ -11,7 +11,7 @@ import useFlash from '@/plugins/useFlash'; import { ServerContext } from '@/state/server'; import PageContentBlock from '@/components/elements/PageContentBlock'; import tw from 'twin.macro'; -import { Button } from '@/components/elements/button/index'; +import { Button } from '@/components/elements/button'; import ScheduleTaskRow from '@/components/server/schedules/ScheduleTaskRow'; import isEqual from 'react-fast-compare'; import { format } from 'date-fns'; diff --git a/resources/scripts/components/server/schedules/TaskDetailsModal.tsx b/resources/scripts/components/server/schedules/TaskDetailsModal.tsx index a52e59a33b..e13e5fef41 100644 --- a/resources/scripts/components/server/schedules/TaskDetailsModal.tsx +++ b/resources/scripts/components/server/schedules/TaskDetailsModal.tsx @@ -12,7 +12,7 @@ import FormikFieldWrapper from '@/components/elements/FormikFieldWrapper'; import tw from 'twin.macro'; import Label from '@/components/elements/Label'; import { Textarea } from '@/components/elements/Input'; -import { Button } from '@/components/elements/button/index'; +import { Button } from '@/components/elements/button'; import Select from '@/components/elements/Select'; import ModalContext from '@/context/ModalContext'; import asModal from '@/hoc/asModal'; diff --git a/resources/scripts/components/server/settings/ReinstallServerBox.tsx b/resources/scripts/components/server/settings/ReinstallServerBox.tsx index 0a5a157232..5bf2073e42 100644 --- a/resources/scripts/components/server/settings/ReinstallServerBox.tsx +++ b/resources/scripts/components/server/settings/ReinstallServerBox.tsx @@ -6,7 +6,7 @@ import { Actions, useStoreActions } from 'easy-peasy'; import { ApplicationStore } from '@/state'; import { httpErrorToHuman } from '@/api/http'; import tw from 'twin.macro'; -import { Button } from '@/components/elements/button/index'; +import { Button } from '@/components/elements/button'; import { Dialog } from '@/components/elements/dialog'; export default () => { diff --git a/resources/scripts/components/server/settings/RenameServerBox.tsx b/resources/scripts/components/server/settings/RenameServerBox.tsx index 34d0508793..475b209c9f 100644 --- a/resources/scripts/components/server/settings/RenameServerBox.tsx +++ b/resources/scripts/components/server/settings/RenameServerBox.tsx @@ -8,7 +8,7 @@ import { object, string } from 'yup'; import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; import { ApplicationStore } from '@/state'; import { httpErrorToHuman } from '@/api/http'; -import { Button } from '@/components/elements/button/index'; +import { Button } from '@/components/elements/button'; import tw from 'twin.macro'; import Label from '@/components/elements/Label'; import FormikFieldWrapper from '@/components/elements/FormikFieldWrapper'; diff --git a/resources/scripts/components/server/settings/SettingsContainer.tsx b/resources/scripts/components/server/settings/SettingsContainer.tsx index e86aa07d99..0277c1a136 100644 --- a/resources/scripts/components/server/settings/SettingsContainer.tsx +++ b/resources/scripts/components/server/settings/SettingsContainer.tsx @@ -12,7 +12,7 @@ import ServerContentBlock from '@/components/elements/ServerContentBlock'; import isEqual from 'react-fast-compare'; import CopyOnClick from '@/components/elements/CopyOnClick'; import { ip } from '@/lib/formatters'; -import { Button } from '@/components/elements/button/index'; +import { Button } from '@/components/elements/button'; export default () => { const username = useStoreState(state => state.user.data!.username); diff --git a/resources/scripts/components/server/users/AddSubuserButton.tsx b/resources/scripts/components/server/users/AddSubuserButton.tsx index 42bfa6a4f7..561ef68c12 100644 --- a/resources/scripts/components/server/users/AddSubuserButton.tsx +++ b/resources/scripts/components/server/users/AddSubuserButton.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; import EditSubuserModal from '@/components/server/users/EditSubuserModal'; -import { Button } from '@/components/elements/button/index'; +import { Button } from '@/components/elements/button'; export default () => { const [visible, setVisible] = useState(false); diff --git a/resources/scripts/routers/AdminRouter.tsx b/resources/scripts/routers/AdminRouter.tsx index 4f918a06e4..e7285af8c7 100644 --- a/resources/scripts/routers/AdminRouter.tsx +++ b/resources/scripts/routers/AdminRouter.tsx @@ -18,7 +18,6 @@ import tw from 'twin.macro'; import CollapsedIcon from '@/assets/images/pterodactyl.svg'; import OverviewContainer from '@/components/admin/overview/OverviewContainer'; -import SettingsContainer from '@/components/admin/settings/SettingsContainer'; import DatabasesContainer from '@/components/admin/databases/DatabasesContainer'; import NewDatabaseContainer from '@/components/admin/databases/NewDatabaseContainer'; import DatabaseEditContainer from '@/components/admin/databases/DatabaseEditContainer'; @@ -46,6 +45,7 @@ import type { ApplicationStore } from '@/state'; import Sidebar from '@/components/admin/Sidebar'; // import useUserPersistedState from '@/plugins/useUserPersistedState'; import UsersContainer from '@/components/admin/users/UsersContainer'; +import SettingsRouter from '@/components/admin/settings/SettingsRouter'; function AdminRouter() { const email = useStoreState((state: ApplicationStore) => state.user.data!.email); @@ -145,7 +145,7 @@ function AdminRouter() {
} /> - } /> + } /> } /> } /> } /> diff --git a/resources/scripts/state/settings.ts b/resources/scripts/state/settings.ts index 20dbbdc6e0..3233be8f0c 100644 --- a/resources/scripts/state/settings.ts +++ b/resources/scripts/state/settings.ts @@ -6,7 +6,7 @@ export interface SiteSettings { recaptcha: { enabled: boolean; siteKey: string; - }; + } | null; } export interface SettingsStore { diff --git a/routes/api-application.php b/routes/api-application.php index 717739dda7..59b0062f5d 100644 --- a/routes/api-application.php +++ b/routes/api-application.php @@ -4,7 +4,10 @@ use Pterodactyl\Http\Controllers\Api\Application; Route::get('/version', [Application\VersionController::class, '__invoke']); - +Route::group(['prefix' => '/settings'], function () { + Route::get('/', [Application\SettingsController::class, 'index']); + Route::patch('/', [Application\SettingsController::class, 'update']); +}); /* |-------------------------------------------------------------------------- | Database Controller Routes