mirror of
https://github.com/pyrohost/pyrodactyl.git
synced 2026-04-05 19:51:59 +02:00
feat(captcha): Remove Recaptcha support in favor of others
- Remove ReCaptcha support and all related files
- Adds support for the following captchas
- Cloudflare Turnstile
- Hcaptcha
- Friendly Captcha
- Implement new captcha middleware and controller
- Add frontend components for new captcha options
- Update settings configuration and views
Closes #169
This commit is contained in:
@@ -9,133 +9,133 @@ use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
|
||||
interface RepositoryInterface
|
||||
{
|
||||
/**
|
||||
* Return an identifier or Model object to be used by the repository.
|
||||
*/
|
||||
public function model(): string;
|
||||
/**
|
||||
* Return an identifier or Model object to be used by the repository.
|
||||
*/
|
||||
public function model(): string;
|
||||
|
||||
/**
|
||||
* Return the model being used for this repository instance.
|
||||
*/
|
||||
public function getModel(): Model;
|
||||
/**
|
||||
* Return the model being used for this repository instance.
|
||||
*/
|
||||
public function getModel(): Model;
|
||||
|
||||
/**
|
||||
* Returns an instance of a query builder.
|
||||
*/
|
||||
public function getBuilder(): Builder;
|
||||
/**
|
||||
* Returns an instance of a query builder.
|
||||
*/
|
||||
public function getBuilder(): Builder;
|
||||
|
||||
/**
|
||||
* Returns the columns to be selected or returned by the query.
|
||||
*/
|
||||
public function getColumns(): array;
|
||||
/**
|
||||
* Returns the columns to be selected or returned by the query.
|
||||
*/
|
||||
public function getColumns(): array;
|
||||
|
||||
/**
|
||||
* An array of columns to filter the response by.
|
||||
*/
|
||||
public function setColumns(array|string $columns = ['*']): self;
|
||||
/**
|
||||
* An array of columns to filter the response by.
|
||||
*/
|
||||
public function setColumns(array|string $columns = ['*']): self;
|
||||
|
||||
/**
|
||||
* Stop repository update functions from returning a fresh
|
||||
* model when changes are committed.
|
||||
*/
|
||||
public function withoutFreshModel(): self;
|
||||
/**
|
||||
* Stop repository update functions from returning a fresh
|
||||
* model when changes are committed.
|
||||
*/
|
||||
public function withoutFreshModel(): self;
|
||||
|
||||
/**
|
||||
* Return a fresh model with a repository updates a model.
|
||||
*/
|
||||
public function withFreshModel(): self;
|
||||
/**
|
||||
* Return a fresh model with a repository updates a model.
|
||||
*/
|
||||
public function withFreshModel(): self;
|
||||
|
||||
/**
|
||||
* Set whether the repository should return a fresh model
|
||||
* when changes are committed.
|
||||
*/
|
||||
public function setFreshModel(bool $fresh = true): self;
|
||||
/**
|
||||
* Set whether the repository should return a fresh model
|
||||
* when changes are committed.
|
||||
*/
|
||||
public function setFreshModel(bool $fresh = true): self;
|
||||
|
||||
/**
|
||||
* Create a new model instance and persist it to the database.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
*/
|
||||
public function create(array $fields, bool $validate = true, bool $force = false): mixed;
|
||||
/**
|
||||
* Create a new model instance and persist it to the database.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
*/
|
||||
public function create(array $fields, bool $validate = true, bool $force = false): mixed;
|
||||
|
||||
/**
|
||||
* Find a model that has the specific ID passed.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function find(int $id): mixed;
|
||||
/**
|
||||
* Find a model that has the specific ID passed.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function find(int $id): mixed;
|
||||
|
||||
/**
|
||||
* Find a model matching an array of where clauses.
|
||||
*/
|
||||
public function findWhere(array $fields): Collection;
|
||||
/**
|
||||
* Find a model matching an array of where clauses.
|
||||
*/
|
||||
public function findWhere(array $fields): Collection;
|
||||
|
||||
/**
|
||||
* Find and return the first matching instance for the given fields.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function findFirstWhere(array $fields): mixed;
|
||||
/**
|
||||
* Find and return the first matching instance for the given fields.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function findFirstWhere(array $fields): mixed;
|
||||
|
||||
/**
|
||||
* Return a count of records matching the passed arguments.
|
||||
*/
|
||||
public function findCountWhere(array $fields): int;
|
||||
/**
|
||||
* Return a count of records matching the passed arguments.
|
||||
*/
|
||||
public function findCountWhere(array $fields): int;
|
||||
|
||||
/**
|
||||
* Delete a given record from the database.
|
||||
*/
|
||||
public function delete(int $id): int;
|
||||
/**
|
||||
* Delete a given record from the database.
|
||||
*/
|
||||
public function delete(int $id): int;
|
||||
|
||||
/**
|
||||
* Delete records matching the given attributes.
|
||||
*/
|
||||
public function deleteWhere(array $attributes): int;
|
||||
/**
|
||||
* Delete records matching the given attributes.
|
||||
*/
|
||||
public function deleteWhere(array $attributes): int;
|
||||
|
||||
/**
|
||||
* Update a given ID with the passed array of fields.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function update(int $id, array $fields, bool $validate = true, bool $force = false): mixed;
|
||||
/**
|
||||
* Update a given ID with the passed array of fields.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function update(int $id, array $fields, bool $validate = true, bool $force = false): mixed;
|
||||
|
||||
/**
|
||||
* Perform a mass update where matching records are updated using whereIn.
|
||||
* This does not perform any model data validation.
|
||||
*/
|
||||
public function updateWhereIn(string $column, array $values, array $fields): int;
|
||||
/**
|
||||
* Perform a mass update where matching records are updated using whereIn.
|
||||
* This does not perform any model data validation.
|
||||
*/
|
||||
public function updateWhereIn(string $column, array $values, array $fields): int;
|
||||
|
||||
/**
|
||||
* Update a record if it exists in the database, otherwise create it.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
*/
|
||||
public function updateOrCreate(array $where, array $fields, bool $validate = true, bool $force = false): mixed;
|
||||
/**
|
||||
* Update a record if it exists in the database, otherwise create it.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
*/
|
||||
public function updateOrCreate(array $where, array $fields, bool $validate = true, bool $force = false): mixed;
|
||||
|
||||
/**
|
||||
* Return all records associated with the given model.
|
||||
*/
|
||||
public function all(): Collection;
|
||||
/**
|
||||
* Return all records associated with the given model.
|
||||
*/
|
||||
public function all(): Collection;
|
||||
|
||||
/**
|
||||
* Return a paginated result set using a search term if set on the repository.
|
||||
*/
|
||||
public function paginated(int $perPage): LengthAwarePaginator;
|
||||
/**
|
||||
* Return a paginated result set using a search term if set on the repository.
|
||||
*/
|
||||
public function paginated(int $perPage): LengthAwarePaginator;
|
||||
|
||||
/**
|
||||
* Insert a single or multiple records into the database at once skipping
|
||||
* validation and mass assignment checking.
|
||||
*/
|
||||
public function insert(array $data): bool;
|
||||
/**
|
||||
* Insert a single or multiple records into the database at once skipping
|
||||
* validation and mass assignment checking.
|
||||
*/
|
||||
public function insert(array $data): bool;
|
||||
|
||||
/**
|
||||
* Insert multiple records into the database and ignore duplicates.
|
||||
*/
|
||||
public function insertIgnore(array $values): bool;
|
||||
/**
|
||||
* Insert multiple records into the database and ignore duplicates.
|
||||
*/
|
||||
public function insertIgnore(array $values): bool;
|
||||
|
||||
/**
|
||||
* Get the amount of entries in the database.
|
||||
*/
|
||||
public function count(): int;
|
||||
/**
|
||||
* Get the amount of entries in the database.
|
||||
*/
|
||||
public function count(): int;
|
||||
}
|
||||
|
||||
@@ -14,49 +14,41 @@ use Pterodactyl\Http\Requests\Admin\Settings\AdvancedSettingsFormRequest;
|
||||
|
||||
class AdvancedController extends Controller
|
||||
{
|
||||
/**
|
||||
* AdvancedController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private AlertsMessageBag $alert,
|
||||
private ConfigRepository $config,
|
||||
private Kernel $kernel,
|
||||
private SettingsRepositoryInterface $settings,
|
||||
private ViewFactory $view,
|
||||
) {
|
||||
/**
|
||||
* AdvancedController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private AlertsMessageBag $alert,
|
||||
private ConfigRepository $config,
|
||||
private Kernel $kernel,
|
||||
private SettingsRepositoryInterface $settings,
|
||||
private ViewFactory $view,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Render advanced Panel settings UI.
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
return $this->view->make('admin.settings.advanced');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update advanced settings.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function update(AdvancedSettingsFormRequest $request): RedirectResponse
|
||||
{
|
||||
foreach ($request->normalize() as $key => $value) {
|
||||
$this->settings->set('settings::' . $key, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render advanced Panel settings UI.
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
$showRecaptchaWarning = false;
|
||||
if (
|
||||
$this->config->get('recaptcha._shipped_secret_key') === $this->config->get('recaptcha.secret_key')
|
||||
|| $this->config->get('recaptcha._shipped_website_key') === $this->config->get('recaptcha.website_key')
|
||||
) {
|
||||
$showRecaptchaWarning = true;
|
||||
}
|
||||
$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 $this->view->make('admin.settings.advanced', [
|
||||
'showRecaptchaWarning' => $showRecaptchaWarning,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function update(AdvancedSettingsFormRequest $request): RedirectResponse
|
||||
{
|
||||
foreach ($request->normalize() as $key => $value) {
|
||||
$this->settings->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');
|
||||
}
|
||||
}
|
||||
return redirect()->route('admin.settings.advanced');
|
||||
}
|
||||
}
|
||||
109
app/Http/Controllers/Admin/Settings/CaptchaController.php
Normal file
109
app/Http/Controllers/Admin/Settings/CaptchaController.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
namespace Pterodactyl\Http\Controllers\Admin\Settings;
|
||||
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Prologue\Alerts\AlertsMessageBag;
|
||||
use Pterodactyl\Http\Controllers\Controller;
|
||||
use Pterodactyl\Contracts\Repository\SettingsRepositoryInterface;
|
||||
use Pterodactyl\Http\Requests\Admin\Settings\CaptchaSettingsFormRequest;
|
||||
|
||||
class CaptchaController extends Controller
|
||||
{
|
||||
/**
|
||||
* @var \Prologue\Alerts\AlertsMessageBag
|
||||
*/
|
||||
protected $alert;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\SettingsRepositoryInterface
|
||||
*/
|
||||
protected $settings;
|
||||
|
||||
/**
|
||||
* CaptchaController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
AlertsMessageBag $alert,
|
||||
SettingsRepositoryInterface $settings
|
||||
) {
|
||||
$this->alert = $alert;
|
||||
$this->settings = $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render CAPTCHA settings page.
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
return view('admin.settings.captcha', [
|
||||
'providers' => [
|
||||
'none' => 'Disabled',
|
||||
'hcaptcha' => 'hCaptcha',
|
||||
'mcaptcha' => 'mCaptcha',
|
||||
'turnstile' => 'Cloudflare Turnstile',
|
||||
'friendly' => 'Friendly Captcha',
|
||||
'recaptcha' => 'Recaptcha V3'
|
||||
],
|
||||
'current' => [
|
||||
'driver' => $this->settings->get('settings::captcha:driver', 'none'),
|
||||
'hcaptcha' => [
|
||||
'site_key' => $this->settings->get('settings::captcha:hcaptcha:site_key', ''),
|
||||
'secret_key' => $this->settings->get('settings::captcha:hcaptcha:secret_key', ''),
|
||||
],
|
||||
'mcaptcha' => [
|
||||
'site_key' => $this->settings->get('settings::captcha:mcaptcha:site_key', ''),
|
||||
'secret_key' => $this->settings->get('settings::captcha:mcaptcha:secret_key', ''),
|
||||
'endpoint' => $this->settings->get('settings::captcha:mcaptcha:endpoint', ''),
|
||||
],
|
||||
'turnstile' => [
|
||||
'site_key' => $this->settings->get('settings::captcha:turnstile:site_key', ''),
|
||||
'secret_key' => $this->settings->get('settings::captcha:turnstile:secret_key', ''),
|
||||
],
|
||||
'friendly' => [
|
||||
'site_key' => $this->settings->get('settings::captcha:friendly:site_key', ''),
|
||||
'secret_key' => $this->settings->get('settings::captcha:friendly:secret_key', ''),
|
||||
],
|
||||
'recaptcha' => [
|
||||
'site_key' => $this->settings->get('settings::captcha:recaptcha:site_key', ''),
|
||||
'secret_key' => $this->settings->get('settings::captcha:friendly:secret_key', ''),
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle CAPTCHA settings update.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function update(CaptchaSettingsFormRequest $request): RedirectResponse
|
||||
{
|
||||
$data = $request->validated();
|
||||
$driver = $data['driver'];
|
||||
|
||||
// Save the driver
|
||||
$this->settings->set('settings::captcha:driver', $driver);
|
||||
|
||||
// Clear all provider settings first
|
||||
$providers = ['hcaptcha', 'mcaptcha', 'turnstile', 'friendly', 'recaptcha'];
|
||||
foreach ($providers as $provider) {
|
||||
$this->settings->set("settings::captcha:{$provider}:site_key", '');
|
||||
$this->settings->set("settings::captcha:{$provider}:secret_key", '');
|
||||
if ($provider === 'mcaptcha') {
|
||||
$this->settings->set("settings::captcha:{$provider}:endpoint", '');
|
||||
}
|
||||
}
|
||||
|
||||
// Save the selected provider's config if enabled
|
||||
if ($driver !== 'none' && isset($data[$driver])) {
|
||||
foreach ($data[$driver] as $key => $value) {
|
||||
$this->settings->set("settings::captcha:{$driver}:{$key}", $value);
|
||||
}
|
||||
}
|
||||
|
||||
$this->alert->success('CAPTCHA settings have been updated successfully.')->flash();
|
||||
return redirect()->route('admin.settings.captcha');
|
||||
}
|
||||
}
|
||||
@@ -15,46 +15,46 @@ use Pterodactyl\Http\Requests\Admin\Settings\BaseSettingsFormRequest;
|
||||
|
||||
class IndexController extends Controller
|
||||
{
|
||||
use AvailableLanguages;
|
||||
use AvailableLanguages;
|
||||
|
||||
/**
|
||||
* IndexController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private AlertsMessageBag $alert,
|
||||
private Kernel $kernel,
|
||||
private SettingsRepositoryInterface $settings,
|
||||
private SoftwareVersionService $versionService,
|
||||
private ViewFactory $view,
|
||||
) {
|
||||
/**
|
||||
* IndexController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private AlertsMessageBag $alert,
|
||||
private Kernel $kernel,
|
||||
private SettingsRepositoryInterface $settings,
|
||||
private SoftwareVersionService $versionService,
|
||||
private ViewFactory $view,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the UI for basic Panel settings.
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
return $this->view->make('admin.settings.index', [
|
||||
'version' => $this->versionService,
|
||||
'languages' => $this->getAvailableLanguages(true),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle settings update.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function update(BaseSettingsFormRequest $request): RedirectResponse
|
||||
{
|
||||
foreach ($request->normalize() as $key => $value) {
|
||||
$this->settings->set('settings::' . $key, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the UI for basic Panel settings.
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
return $this->view->make('admin.settings.index', [
|
||||
'version' => $this->versionService,
|
||||
'languages' => $this->getAvailableLanguages(true),
|
||||
]);
|
||||
}
|
||||
$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();
|
||||
|
||||
/**
|
||||
* Handle settings update.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function update(BaseSettingsFormRequest $request): RedirectResponse
|
||||
{
|
||||
foreach ($request->normalize() as $key => $value) {
|
||||
$this->settings->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');
|
||||
}
|
||||
return redirect()->route('admin.settings');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,121 +17,122 @@ use Pterodactyl\Http\Requests\Api\Client\Servers\Settings\ReinstallServerRequest
|
||||
|
||||
class SettingsController extends ClientApiController
|
||||
{
|
||||
/**
|
||||
* SettingsController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private ServerRepository $repository,
|
||||
private ReinstallServerService $reinstallServerService,
|
||||
) {
|
||||
parent::__construct();
|
||||
/**
|
||||
* SettingsController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private ServerRepository $repository,
|
||||
private ReinstallServerService $reinstallServerService,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames a server.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function rename(RenameServerRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
$name = $request->input('name');
|
||||
$description = $request->has('description') ? (string) $request->input('description') : $server->description;
|
||||
$this->repository->update($server->id, [
|
||||
'name' => $name,
|
||||
'description' => $description,
|
||||
]);
|
||||
|
||||
if ($server->name !== $name) {
|
||||
Activity::event('server:settings.rename')
|
||||
->property(['old' => $server->name, 'new' => $name])
|
||||
->log();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames a server.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function rename(RenameServerRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
$name = $request->input('name');
|
||||
$description = $request->has('description') ? (string) $request->input('description') : $server->description;
|
||||
$this->repository->update($server->id, [
|
||||
'name' => $name,
|
||||
'description' => $description,
|
||||
]);
|
||||
|
||||
if ($server->name !== $name) {
|
||||
Activity::event('server:settings.rename')
|
||||
->property(['old' => $server->name, 'new' => $name])
|
||||
->log();
|
||||
}
|
||||
|
||||
if ($server->description !== $description) {
|
||||
Activity::event('server:settings.description')
|
||||
->property(['old' => $server->description, 'new' => $description])
|
||||
->log();
|
||||
}
|
||||
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
if ($server->description !== $description) {
|
||||
Activity::event('server:settings.description')
|
||||
->property(['old' => $server->description, 'new' => $description])
|
||||
->log();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reinstalls the server on the daemon.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function reinstall(ReinstallServerRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
$this->reinstallServerService->handle($server);
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
Activity::event('server:reinstall')->log();
|
||||
/**
|
||||
* Reinstalls the server on the daemon.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function reinstall(ReinstallServerRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
$this->reinstallServerService->handle($server);
|
||||
|
||||
return new JsonResponse([], Response::HTTP_ACCEPTED);
|
||||
Activity::event('server:reinstall')->log();
|
||||
|
||||
return new JsonResponse([], Response::HTTP_ACCEPTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the Docker image in use by the server.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function dockerImage(SetDockerImageRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
if (!in_array($request->input('docker_image'), array_values($server->egg->docker_images))) {
|
||||
throw new BadRequestHttpException('The requested Docker image is not allowed for this server.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the Docker image in use by the server.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function dockerImage(SetDockerImageRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
if (!in_array($request->input('docker_image'), array_values($server->egg->docker_images))) {
|
||||
throw new BadRequestHttpException('The requested Docker image is not allowed for this server.');
|
||||
}
|
||||
$original = $server->image;
|
||||
$server->forceFill(['image' => $request->input('docker_image')])->saveOrFail();
|
||||
|
||||
$original = $server->image;
|
||||
$server->forceFill(['image' => $request->input('docker_image')])->saveOrFail();
|
||||
|
||||
if ($original !== $server->image) {
|
||||
Activity::event('server:startup.image')
|
||||
->property(['old' => $original, 'new' => $request->input('docker_image')])
|
||||
->log();
|
||||
}
|
||||
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
if ($original !== $server->image) {
|
||||
Activity::event('server:startup.image')
|
||||
->property(['old' => $original, 'new' => $request->input('docker_image')])
|
||||
->log();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset Startup Command
|
||||
*/
|
||||
private function resetStartupCommand(Server $server): JsonResponse
|
||||
{
|
||||
$server->startup = $server->egg->startup;
|
||||
$server->save();
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
/**
|
||||
* Reset Startup Command
|
||||
*/
|
||||
private function resetStartupCommand(Server $server): JsonResponse
|
||||
{
|
||||
$server->startup = $server->egg->startup;
|
||||
$server->save();
|
||||
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the egg for a server.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function changeEgg(SetEggRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
$eggId = $request->input('egg_id');
|
||||
$nestId = $request->input('nest_id');
|
||||
$originalEggId = $server->egg_id;
|
||||
$originalNestId = $server->nest_id;
|
||||
|
||||
// Check if the new Egg and Nest IDs are different from the current ones
|
||||
if ($originalEggId !== $eggId || $originalNestId !== $nestId) {
|
||||
// Update the server's Egg and Nest IDs
|
||||
$server->egg_id = $eggId;
|
||||
$server->nest_id = $nestId;
|
||||
$server->save();
|
||||
|
||||
// Log an activity event for the Egg change
|
||||
Activity::event('server:settings.egg')
|
||||
->property(['original_egg_id' => $originalEggId, 'new_egg_id' => $eggId, 'original_nest_id' => $originalNestId, 'new_nest_id' => $nestId])
|
||||
->log();
|
||||
|
||||
// Reset the server's startup command
|
||||
$this->resetStartupCommand($server);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the egg for a server.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function changeEgg(SetEggRequest $request, Server $server): JsonResponse {
|
||||
$eggId = $request->input('egg_id');
|
||||
$nestId = $request->input('nest_id');
|
||||
$originalEggId = $server->egg_id;
|
||||
$originalNestId = $server->nest_id;
|
||||
|
||||
// Check if the new Egg and Nest IDs are different from the current ones
|
||||
if ($originalEggId !== $eggId || $originalNestId !== $nestId) {
|
||||
// Update the server's Egg and Nest IDs
|
||||
$server->egg_id = $eggId;
|
||||
$server->nest_id = $nestId;
|
||||
$server->save();
|
||||
|
||||
// Log an activity event for the Egg change
|
||||
Activity::event('server:settings.egg')
|
||||
->property(['original_egg_id' => $originalEggId, 'new_egg_id' => $eggId, 'original_nest_id' => $originalNestId, 'new_nest_id' => $nestId])
|
||||
->log();
|
||||
|
||||
// Reset the server's startup command
|
||||
$this->resetStartupCommand($server);
|
||||
}
|
||||
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use Illuminate\Session\Middleware\StartSession;
|
||||
use Pterodactyl\Http\Middleware\EncryptCookies;
|
||||
use Pterodactyl\Http\Middleware\Api\IsValidJson;
|
||||
use Pterodactyl\Http\Middleware\VerifyCsrfToken;
|
||||
use Pterodactyl\Http\Middleware\VerifyReCaptcha;
|
||||
use Pterodactyl\Http\Middleware\VerifyCaptcha;
|
||||
use Illuminate\Routing\Middleware\ThrottleRequests;
|
||||
use Pterodactyl\Http\Middleware\LanguageMiddleware;
|
||||
use Illuminate\Foundation\Http\Kernel as HttpKernel;
|
||||
@@ -36,66 +36,66 @@ use Pterodactyl\Http\Middleware\Api\Application\AuthenticateApplicationUser;
|
||||
|
||||
class Kernel extends HttpKernel
|
||||
{
|
||||
/**
|
||||
* The application's global HTTP middleware stack.
|
||||
*/
|
||||
protected $middleware = [
|
||||
TrustProxies::class,
|
||||
HandleCors::class,
|
||||
PreventRequestsDuringMaintenance::class,
|
||||
ValidatePostSize::class,
|
||||
TrimStrings::class,
|
||||
ConvertEmptyStringsToNull::class,
|
||||
];
|
||||
/**
|
||||
* The application's global HTTP middleware stack.
|
||||
*/
|
||||
protected $middleware = [
|
||||
TrustProxies::class,
|
||||
HandleCors::class,
|
||||
PreventRequestsDuringMaintenance::class,
|
||||
ValidatePostSize::class,
|
||||
TrimStrings::class,
|
||||
ConvertEmptyStringsToNull::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* The application's route middleware groups.
|
||||
*/
|
||||
protected $middlewareGroups = [
|
||||
'web' => [
|
||||
EncryptCookies::class,
|
||||
AddQueuedCookiesToResponse::class,
|
||||
StartSession::class,
|
||||
ShareErrorsFromSession::class,
|
||||
VerifyCsrfToken::class,
|
||||
SubstituteBindings::class,
|
||||
LanguageMiddleware::class,
|
||||
],
|
||||
'api' => [
|
||||
EnsureStatefulRequests::class,
|
||||
'auth:sanctum',
|
||||
IsValidJson::class,
|
||||
TrackAPIKey::class,
|
||||
RequireTwoFactorAuthentication::class,
|
||||
AuthenticateIPAccess::class,
|
||||
],
|
||||
'application-api' => [
|
||||
SubstituteBindings::class,
|
||||
AuthenticateApplicationUser::class,
|
||||
],
|
||||
'client-api' => [
|
||||
SubstituteClientBindings::class,
|
||||
RequireClientApiKey::class,
|
||||
],
|
||||
'daemon' => [
|
||||
SubstituteBindings::class,
|
||||
DaemonAuthenticate::class,
|
||||
],
|
||||
];
|
||||
/**
|
||||
* The application's route middleware groups.
|
||||
*/
|
||||
protected $middlewareGroups = [
|
||||
'web' => [
|
||||
EncryptCookies::class,
|
||||
AddQueuedCookiesToResponse::class,
|
||||
StartSession::class,
|
||||
ShareErrorsFromSession::class,
|
||||
VerifyCsrfToken::class,
|
||||
SubstituteBindings::class,
|
||||
LanguageMiddleware::class,
|
||||
],
|
||||
'api' => [
|
||||
EnsureStatefulRequests::class,
|
||||
'auth:sanctum',
|
||||
IsValidJson::class,
|
||||
TrackAPIKey::class,
|
||||
RequireTwoFactorAuthentication::class,
|
||||
AuthenticateIPAccess::class,
|
||||
],
|
||||
'application-api' => [
|
||||
SubstituteBindings::class,
|
||||
AuthenticateApplicationUser::class,
|
||||
],
|
||||
'client-api' => [
|
||||
SubstituteClientBindings::class,
|
||||
RequireClientApiKey::class,
|
||||
],
|
||||
'daemon' => [
|
||||
SubstituteBindings::class,
|
||||
DaemonAuthenticate::class,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* The application's route middleware.
|
||||
*/
|
||||
protected $middlewareAliases = [
|
||||
'auth' => Authenticate::class,
|
||||
'auth.basic' => AuthenticateWithBasicAuth::class,
|
||||
'auth.session' => AuthenticateSession::class,
|
||||
'guest' => RedirectIfAuthenticated::class,
|
||||
'csrf' => VerifyCsrfToken::class,
|
||||
'throttle' => ThrottleRequests::class,
|
||||
'can' => Authorize::class,
|
||||
'bindings' => SubstituteBindings::class,
|
||||
'recaptcha' => VerifyReCaptcha::class,
|
||||
'node.maintenance' => MaintenanceMiddleware::class,
|
||||
];
|
||||
/**
|
||||
* The application's route middleware.
|
||||
*/
|
||||
protected $middlewareAliases = [
|
||||
'auth' => Authenticate::class,
|
||||
'auth.basic' => AuthenticateWithBasicAuth::class,
|
||||
'auth.session' => AuthenticateSession::class,
|
||||
'guest' => RedirectIfAuthenticated::class,
|
||||
'csrf' => VerifyCsrfToken::class,
|
||||
'throttle' => ThrottleRequests::class,
|
||||
'can' => Authorize::class,
|
||||
'bindings' => SubstituteBindings::class,
|
||||
'captcha' => VerifyCaptcha::class,
|
||||
'node.maintenance' => MaintenanceMiddleware::class,
|
||||
];
|
||||
}
|
||||
|
||||
207
app/Http/Middleware/VerifyCaptcha.php
Normal file
207
app/Http/Middleware/VerifyCaptcha.php
Normal file
@@ -0,0 +1,207 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Middleware;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Pterodactyl\Events\Auth\FailedCaptcha;
|
||||
use Illuminate\Contracts\Config\Repository;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Psr\Http\Client\ClientExceptionInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
class VerifyCaptcha
|
||||
{
|
||||
private const PROVIDER_ENDPOINTS = [
|
||||
'turnstile' => 'https://challenges.cloudflare.com/turnstile/v0/siteverify',
|
||||
'hcaptcha' => 'https://hcaptcha.com/siteverify',
|
||||
'friendly' => 'https://api.friendlycaptcha.com/api/v1/siteverify',
|
||||
'mcaptcha' => 'https://mcaptcha.org/api/siteverify',
|
||||
];
|
||||
|
||||
private const PROVIDER_FIELDS = [
|
||||
'turnstile' => 'cf-turnstile-response',
|
||||
'hcaptcha' => 'h-captcha-response',
|
||||
'friendly' => 'frc-captcha-response',
|
||||
'mcaptcha' => 'mcaptcha-response',
|
||||
];
|
||||
|
||||
|
||||
public function __construct(
|
||||
private Dispatcher $dispatcher,
|
||||
private Repository $config,
|
||||
private Client $client
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(Request $request, \Closure $next): mixed
|
||||
{
|
||||
$driver = $this->config->get('captcha.driver');
|
||||
|
||||
if (!$this->shouldVerifyCaptcha($driver)) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$fieldName = self::PROVIDER_FIELDS[$driver];
|
||||
$captchaResponse = $this->getCaptchaResponseFromRequest($request, $fieldName);
|
||||
|
||||
|
||||
|
||||
if (empty($captchaResponse)) {
|
||||
\Log::warning('CAPTCHA Middleware - Missing response token', [
|
||||
'expected_field' => $fieldName,
|
||||
'available_fields' => array_keys($request->all()),
|
||||
]);
|
||||
$this->logAndTriggerFailure($request, $driver, 'missing_response');
|
||||
throw new HttpException(400, 'Please complete the CAPTCHA challenge.');
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $this->verifyWithProvider($driver, $captchaResponse, $request->ip());
|
||||
|
||||
if ($this->isResponseValid($result, $request, $driver)) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$this->logAndTriggerFailure($request, $driver, 'verification_failed', $result);
|
||||
throw new HttpException(400, 'CAPTCHA verification failed. Please try again.');
|
||||
|
||||
} catch (ClientExceptionInterface $e) {
|
||||
$this->logAndTriggerFailure($request, $driver, 'service_error');
|
||||
\Log::error('CAPTCHA service error', ['error' => $e->getMessage()]);
|
||||
throw new HttpException(503, 'CAPTCHA service unavailable. Please try again later.');
|
||||
} catch (\Exception $e) {
|
||||
$this->logAndTriggerFailure($request, $driver, 'unexpected_error');
|
||||
\Log::error('CAPTCHA unexpected error', ['error' => $e->getMessage()]);
|
||||
throw new HttpException(500, 'An unexpected error occurred during CAPTCHA verification.');
|
||||
}
|
||||
}
|
||||
|
||||
private function shouldVerifyCaptcha(?string $driver): bool
|
||||
{
|
||||
return $driver && array_key_exists($driver, self::PROVIDER_FIELDS);
|
||||
}
|
||||
|
||||
private function getCaptchaResponseFromRequest(Request $request, string $fieldName): ?string
|
||||
{
|
||||
|
||||
if ($request->isJson()) {
|
||||
$data = $request->json()->all();
|
||||
return $data['captchaData'] ?? $data[$fieldName] ?? null;
|
||||
}
|
||||
|
||||
$response = $request->input($fieldName) ?? $request->input('captchaData');
|
||||
|
||||
if (empty($response) && in_array($request->method(), ['POST', 'PUT', 'PATCH'])) {
|
||||
$rawInput = file_get_contents('php://input');
|
||||
if (!empty($rawInput)) {
|
||||
parse_str($rawInput, $parsed);
|
||||
$response = $parsed[$fieldName] ?? $parsed['captchaData'] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function verifyWithProvider(string $driver, string $response, string $remoteIp): \stdClass
|
||||
{
|
||||
$secretKey = $this->config->get("captcha.{$driver}.secret_key");
|
||||
|
||||
if (empty($secretKey)) {
|
||||
throw new \RuntimeException("No secret key configured for CAPTCHA driver: {$driver}");
|
||||
}
|
||||
|
||||
$params = ['secret' => $secretKey];
|
||||
|
||||
if ($driver === 'turnstile') {
|
||||
$params['response'] = $response;
|
||||
$params['remoteip'] = $remoteIp;
|
||||
} elseif ($driver === 'hcaptcha') {
|
||||
$params['response'] = $response;
|
||||
$params['remoteip'] = $remoteIp;
|
||||
} elseif ($driver === 'friendly') {
|
||||
$params['solution'] = $response;
|
||||
$params['sitekey'] = $this->config->get("captcha.{$driver}.site_key");
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
$res = $this->client->post(self::PROVIDER_ENDPOINTS[$driver], [
|
||||
'timeout' => $this->config->get('captcha.timeout', 5),
|
||||
'json' => $params,
|
||||
]);
|
||||
|
||||
$body = $res->getBody()->getContents();
|
||||
$result = json_decode($body);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
\Log::error('Invalid JSON response from CAPTCHA provider', [
|
||||
'provider' => $driver,
|
||||
'response_body' => $body,
|
||||
'json_error' => json_last_error_msg()
|
||||
]);
|
||||
throw new \RuntimeException("Invalid JSON response from {$driver} CAPTCHA provider");
|
||||
}
|
||||
|
||||
return $result;
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('CAPTCHA verification error', [
|
||||
'provider' => $driver,
|
||||
'error' => $e->getMessage(),
|
||||
'response' => $e->getResponse() ? $e->getResponse()->getBody()->getContents() : null
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function isResponseValid(\stdClass $result, Request $request, string $driver): bool
|
||||
{
|
||||
if (!($result->success ?? false)) {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
switch ($driver) {
|
||||
case 'turnstile':
|
||||
if ($this->config->get('captcha.verify_domain', false)) {
|
||||
$expectedHost = parse_url($request->url(), PHP_URL_HOST);
|
||||
if (($result->hostname ?? null) !== $expectedHost) {
|
||||
\Log::warning('Domain verification failed', [
|
||||
'expected' => $expectedHost,
|
||||
'actual' => $result->hostname ?? 'null'
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function logAndTriggerFailure(
|
||||
Request $request,
|
||||
string $driver,
|
||||
string $reason,
|
||||
?\stdClass $result = null
|
||||
): void {
|
||||
$errorCodes = $result->{'error-codes'} ?? [];
|
||||
|
||||
\Log::warning("CAPTCHA verification failed", [
|
||||
'driver' => $driver,
|
||||
'reason' => $reason,
|
||||
'ip' => $request->ip(),
|
||||
'path' => $request->path(),
|
||||
'method' => $request->method(),
|
||||
'error_codes' => $errorCodes,
|
||||
]);
|
||||
|
||||
$this->dispatcher->dispatch(new FailedCaptcha(
|
||||
$request->ip(),
|
||||
$driver,
|
||||
$reason,
|
||||
$errorCodes
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Middleware;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Pterodactyl\Events\Auth\FailedCaptcha;
|
||||
use Illuminate\Contracts\Config\Repository;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
class VerifyReCaptcha
|
||||
{
|
||||
/**
|
||||
* VerifyReCaptcha constructor.
|
||||
*/
|
||||
public function __construct(private Dispatcher $dispatcher, private Repository $config)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*/
|
||||
public function handle(Request $request, \Closure $next): mixed
|
||||
{
|
||||
if (!$this->config->get('recaptcha.enabled')) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
if ($request->filled('g-recaptcha-response')) {
|
||||
$client = new Client();
|
||||
$res = $client->post($this->config->get('recaptcha.domain'), [
|
||||
'form_params' => [
|
||||
'secret' => $this->config->get('recaptcha.secret_key'),
|
||||
'response' => $request->input('g-recaptcha-response'),
|
||||
],
|
||||
]);
|
||||
|
||||
if ($res->getStatusCode() === 200) {
|
||||
$result = json_decode($res->getBody());
|
||||
|
||||
if ($result->success && (!$this->config->get('recaptcha.verify_domain') || $this->isResponseVerified($result, $request))) {
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->dispatcher->dispatch(
|
||||
new FailedCaptcha(
|
||||
$request->ip(),
|
||||
!empty($result) ? ($result->hostname ?? null) : null
|
||||
)
|
||||
);
|
||||
|
||||
throw new HttpException(Response::HTTP_BAD_REQUEST, 'Failed to validate reCAPTCHA data.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the response from the recaptcha servers was valid.
|
||||
*/
|
||||
private function isResponseVerified(\stdClass $result, Request $request): bool
|
||||
{
|
||||
if (!$this->config->get('recaptcha.verify_domain')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$url = parse_url($request->url());
|
||||
|
||||
return $result->hostname === array_get($url, 'host');
|
||||
}
|
||||
}
|
||||
@@ -6,45 +6,39 @@ use Pterodactyl\Http\Requests\Admin\AdminFormRequest;
|
||||
|
||||
class AdvancedSettingsFormRequest extends AdminFormRequest
|
||||
{
|
||||
/**
|
||||
* Return all the rules to apply to this request's data.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'recaptcha:enabled' => 'required|in:true,false',
|
||||
'recaptcha:secret_key' => 'required|string|max:191',
|
||||
'recaptcha:website_key' => 'required|string|max:191',
|
||||
'pterodactyl:guzzle:timeout' => 'required|integer|between:1,60',
|
||||
'pterodactyl:guzzle:connect_timeout' => 'required|integer|between:1,60',
|
||||
'pterodactyl:client_features:allocations:enabled' => 'required|in:true,false',
|
||||
'pterodactyl:client_features:allocations:range_start' => [
|
||||
'nullable',
|
||||
'required_if:pterodactyl:client_features:allocations:enabled,true',
|
||||
'integer',
|
||||
'between:1024,65535',
|
||||
],
|
||||
'pterodactyl:client_features:allocations:range_end' => [
|
||||
'nullable',
|
||||
'required_if:pterodactyl:client_features:allocations:enabled,true',
|
||||
'integer',
|
||||
'between:1024,65535',
|
||||
'gt:pterodactyl:client_features:allocations:range_start',
|
||||
],
|
||||
];
|
||||
}
|
||||
/**
|
||||
* Return all the rules to apply to this request's data.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'pterodactyl:guzzle:timeout' => 'required|integer|between:1,60',
|
||||
'pterodactyl:guzzle:connect_timeout' => 'required|integer|between:1,60',
|
||||
'pterodactyl:client_features:allocations:enabled' => 'required|in:true,false',
|
||||
'pterodactyl:client_features:allocations:range_start' => [
|
||||
'nullable',
|
||||
'required_if:pterodactyl:client_features:allocations:enabled,true',
|
||||
'integer',
|
||||
'between:1024,65535',
|
||||
],
|
||||
'pterodactyl:client_features:allocations:range_end' => [
|
||||
'nullable',
|
||||
'required_if:pterodactyl:client_features:allocations:enabled,true',
|
||||
'integer',
|
||||
'between:1024,65535',
|
||||
'gt:pterodactyl:client_features:allocations:range_start',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'recaptcha:enabled' => 'reCAPTCHA Enabled',
|
||||
'recaptcha:secret_key' => 'reCAPTCHA Secret Key',
|
||||
'recaptcha:website_key' => 'reCAPTCHA Website Key',
|
||||
'pterodactyl:guzzle:timeout' => 'HTTP Request Timeout',
|
||||
'pterodactyl:guzzle:connect_timeout' => 'HTTP Connection Timeout',
|
||||
'pterodactyl:client_features:allocations:enabled' => 'Auto Create Allocations Enabled',
|
||||
'pterodactyl:client_features:allocations:range_start' => 'Starting Port',
|
||||
'pterodactyl:client_features:allocations:range_end' => 'Ending Port',
|
||||
];
|
||||
}
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'pterodactyl:guzzle:timeout' => 'HTTP Request Timeout',
|
||||
'pterodactyl:guzzle:connect_timeout' => 'HTTP Connection Timeout',
|
||||
'pterodactyl:client_features:allocations:enabled' => 'Auto Create Allocations Enabled',
|
||||
'pterodactyl:client_features:allocations:range_start' => 'Starting Port',
|
||||
'pterodactyl:client_features:allocations:range_end' => 'Ending Port',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
namespace Pterodactyl\Http\Requests\Admin\Settings;
|
||||
|
||||
use Pterodactyl\Http\Requests\Admin\AdminFormRequest;
|
||||
|
||||
class CaptchaSettingsFormRequest extends AdminFormRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = [
|
||||
'driver' => 'required|in:none,hcaptcha,mcaptcha,turnstile,friendly,recaptcha',
|
||||
];
|
||||
|
||||
// Only apply validation rules for the selected driver
|
||||
$driver = $this->input('driver');
|
||||
if ($driver !== 'none') {
|
||||
$rules[$driver] = 'required|array';
|
||||
|
||||
if ($driver === 'hcaptcha') {
|
||||
$rules['hcaptcha.site_key'] = 'required|string';
|
||||
$rules['hcaptcha.secret_key'] = 'required|string';
|
||||
} elseif ($driver === 'mcaptcha') {
|
||||
$rules['mcaptcha.site_key'] = 'required|string';
|
||||
$rules['mcaptcha.secret_key'] = 'required|string';
|
||||
$rules['mcaptcha.endpoint'] = 'required|url';
|
||||
} elseif ($driver === 'turnstile') {
|
||||
$rules['turnstile.site_key'] = 'required|string';
|
||||
$rules['turnstile.secret_key'] = 'required|string';
|
||||
} elseif ($driver === 'friendly') {
|
||||
$rules['friendly.site_key'] = 'required|string';
|
||||
$rules['friendly.secret_key'] = 'required|string';
|
||||
} elseif ($driver === 'recaptcha') {
|
||||
$rules['recaptcha.site_key'] = 'required|string';
|
||||
$rules['recaptcha.secret_key'] = 'required|string';
|
||||
}
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'hcaptcha.site_key' => 'hCaptcha Site Key',
|
||||
'hcaptcha.secret_key' => 'hCaptcha Secret Key',
|
||||
'mcaptcha.site_key' => 'mCaptcha Site Key',
|
||||
'mcaptcha.secret_key' => 'mCaptcha Secret Key',
|
||||
'mcaptcha.endpoint' => 'mCaptcha Endpoint',
|
||||
'turnstile.site_key' => 'Turnstile Site Key',
|
||||
'turnstile.secret_key' => 'Turnstile Secret Key',
|
||||
'proton.site_key' => 'Proton Site Key',
|
||||
'proton.secret_key' => 'Proton Secret Key',
|
||||
'friendly.site_key' => 'Friendly Site Key',
|
||||
'friendly.secret_key' => 'Friendly Secret Key',
|
||||
'recaptcha.site_key' => 'Recaptcha Site Key',
|
||||
'recaptcha.secret_key' => 'Recaptcha Secret Key',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -7,18 +7,32 @@ use Pterodactyl\Services\Helpers\AssetHashService;
|
||||
|
||||
class AssetComposer
|
||||
{
|
||||
/**
|
||||
* 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',
|
||||
'recaptcha' => [
|
||||
'enabled' => config('recaptcha.enabled', false),
|
||||
'siteKey' => config('recaptcha.website_key') ?? '',
|
||||
],
|
||||
]);
|
||||
}
|
||||
/**
|
||||
* Provide access to the asset service in the views.
|
||||
*/
|
||||
public function compose(View $view): void
|
||||
{
|
||||
$view->with('siteConfiguration', [
|
||||
'name' => config('app.name') ?? 'Pyrodactyl',
|
||||
'locale' => config('app.locale') ?? 'en',
|
||||
'captcha' => [
|
||||
'driver' => config('captcha.driver', 'none'),
|
||||
'turnstile' => [
|
||||
'siteKey' => config('captcha.turnstile.site_key', '')
|
||||
],
|
||||
'hcaptcha' => [
|
||||
'siteKey' => config('captcha.hcaptcha.site_key', '')
|
||||
],
|
||||
'mcaptcha' => [
|
||||
'siteKey' => config('captcha.mcaptcha.site_key', '')
|
||||
],
|
||||
'friendly' => [
|
||||
'siteKey' => config('captcha.friendly.site_key', '')
|
||||
],
|
||||
'recaptcha' => [
|
||||
'siteKey' => config('captcha.recaptcha.site_key', '')
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,102 +12,111 @@ use Pterodactyl\Contracts\Repository\SettingsRepositoryInterface;
|
||||
|
||||
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',
|
||||
'pterodactyl:guzzle:timeout',
|
||||
'pterodactyl:guzzle:connect_timeout',
|
||||
'pterodactyl:console:count',
|
||||
'pterodactyl:console:frequency',
|
||||
'pterodactyl:auth:2fa_required',
|
||||
'pterodactyl:client_features:allocations:enabled',
|
||||
'pterodactyl:client_features:allocations:range_start',
|
||||
'pterodactyl:client_features:allocations:range_end',
|
||||
];
|
||||
/**
|
||||
* An array of configuration keys to override with database values
|
||||
* if they exist.
|
||||
*/
|
||||
protected array $keys = [
|
||||
'app:name',
|
||||
'app:locale',
|
||||
'captcha:driver',
|
||||
'captcha:hcaptcha:site_key',
|
||||
'captcha:hcaptcha:secret_key',
|
||||
'captcha:mcaptcha:site_key',
|
||||
'captcha:mcaptcha:secret_key',
|
||||
'captcha:mcaptcha:endpoint',
|
||||
'captcha:turnstile:site_key',
|
||||
'captcha:turnstile:secret_key',
|
||||
'captcha:friendly:site_key',
|
||||
'captcha:friendly:secret_key',
|
||||
// existing mail keys, etc...
|
||||
'pterodactyl:guzzle:timeout',
|
||||
'pterodactyl:guzzle:connect_timeout',
|
||||
'pterodactyl:console:count',
|
||||
'pterodactyl:console:frequency',
|
||||
'pterodactyl:auth:2fa_required',
|
||||
'pterodactyl:client_features:allocations:enabled',
|
||||
'pterodactyl:client_features:allocations:range_start',
|
||||
'pterodactyl: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',
|
||||
];
|
||||
/**
|
||||
* 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',
|
||||
];
|
||||
|
||||
/**
|
||||
* Boot the service provider.
|
||||
*/
|
||||
public function boot(ConfigRepository $config, Encrypter $encrypter, Log $log, SettingsRepositoryInterface $settings): void
|
||||
{
|
||||
// Only set the email driver settings from the database if we
|
||||
// are configured using SMTP as the driver.
|
||||
if ($config->get('mail.default') === 'smtp') {
|
||||
$this->keys = array_merge($this->keys, $this->emailKeys);
|
||||
}
|
||||
/**
|
||||
* 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(ConfigRepository $config, Encrypter $encrypter, Log $log, SettingsRepositoryInterface $settings): void
|
||||
{
|
||||
// Only set the email driver settings from the database if we
|
||||
// are configured using SMTP as the driver.
|
||||
if ($config->get('mail.default') === 'smtp') {
|
||||
$this->keys = array_merge($this->keys, $this->emailKeys);
|
||||
}
|
||||
|
||||
try {
|
||||
$values = $settings->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->get(str_replace(':', '.', $key)));
|
||||
if (in_array($key, self::$encrypted)) {
|
||||
try {
|
||||
$values = $settings->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;
|
||||
$value = $encrypter->decrypt($value);
|
||||
} catch (DecryptException $exception) {
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->keys as $key) {
|
||||
$value = array_get($values, 'settings::' . $key, $config->get(str_replace(':', '.', $key)));
|
||||
if (in_array($key, self::$encrypted)) {
|
||||
try {
|
||||
$value = $encrypter->decrypt($value);
|
||||
} catch (DecryptException $exception) {
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
$config->set(str_replace(':', '.', $key), $value);
|
||||
}
|
||||
}
|
||||
|
||||
public static function getEncryptedKeys(): array
|
||||
{
|
||||
return self::$encrypted;
|
||||
}
|
||||
public static function getEncryptedKeys(): array
|
||||
{
|
||||
return self::$encrypted;
|
||||
}
|
||||
}
|
||||
|
||||
666
composer.lock
generated
666
composer.lock
generated
File diff suppressed because it is too large
Load Diff
438
config/app.php
438
config/app.php
@@ -3,231 +3,231 @@
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Version
|
||||
|--------------------------------------------------------------------------
|
||||
| This value is set when creating a Pterodactyl release. You should not
|
||||
| change this value if you are not maintaining your own internal versions.
|
||||
*/
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Version
|
||||
|--------------------------------------------------------------------------
|
||||
| This value is set when creating a Pterodactyl release. You should not
|
||||
| change this value if you are not maintaining your own internal versions.
|
||||
*/
|
||||
|
||||
'version' => '3.0.0',
|
||||
'version' => '3.0.0',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value is the name of your application, which will be used when the
|
||||
| framework needs to place the application's name in a notification or
|
||||
| other UI elements where an application name needs to be displayed.
|
||||
|
|
||||
*/
|
||||
|
||||
'name' => env('APP_NAME', 'Pyrodactyl'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Environment
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the "environment" your application is currently
|
||||
| running in. This may determine how you prefer to configure various
|
||||
| services the application utilizes. Set this in your ".env" file.
|
||||
|
|
||||
*/
|
||||
|
||||
'env' => env('APP_ENV', 'production'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Debug Mode
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When your application is in debug mode, detailed error messages with
|
||||
| stack traces will be shown on every error that occurs within your
|
||||
| application. If disabled, a simple generic error page is shown.
|
||||
|
|
||||
*/
|
||||
|
||||
'debug' => (bool) env('APP_DEBUG', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application URL
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This URL is used by the console to properly generate URLs when using
|
||||
| the Artisan command line tool. You should set this to the root of
|
||||
| the application so that it's available within Artisan commands.
|
||||
|
|
||||
*/
|
||||
|
||||
'url' => env('APP_URL', 'http://localhost'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Timezone
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default timezone for your application, which
|
||||
| will be used by the PHP date and date-time functions. The timezone
|
||||
| is set to "UTC" by default as it is suitable for most use cases.
|
||||
|
|
||||
*/
|
||||
|
||||
'timezone' => env('APP_TIMEZONE', 'UTC'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Locale Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The application locale determines the default locale that will be used
|
||||
| by Laravel's translation / localization methods. This option can be
|
||||
| set to any locale for which you plan to have translation strings.
|
||||
|
|
||||
*/
|
||||
|
||||
'locale' => env('APP_LOCALE', 'en'),
|
||||
|
||||
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
|
||||
|
||||
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Encryption Key
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This key is utilized by Laravel's encryption services and should be set
|
||||
| to a random, 32 character string to ensure that all encrypted values
|
||||
| are secure. You should do this prior to deploying the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'cipher' => 'AES-256-CBC',
|
||||
|
||||
'key' => env('APP_KEY'),
|
||||
|
||||
'previous_keys' => [
|
||||
...array_filter(
|
||||
explode(',', env('APP_PREVIOUS_KEYS', ''))
|
||||
),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Maintenance Mode Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These configuration options determine the driver used to determine and
|
||||
| manage Laravel's "maintenance mode" status. The "cache" driver will
|
||||
| allow maintenance mode to be controlled across multiple machines.
|
||||
|
|
||||
| Supported drivers: "file", "cache"
|
||||
|
|
||||
*/
|
||||
|
||||
'maintenance' => [
|
||||
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
||||
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Exception Reporter Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| If you're encountering weird behavior with the Panel and no exceptions
|
||||
| are being logged try changing the environment variable below to be true.
|
||||
| This will override the default "don't report" behavior of the Panel and log
|
||||
| all exceptions. This will be quite noisy.
|
||||
|
|
||||
*/
|
||||
|
||||
'exceptions' => [
|
||||
'report_all' => env('APP_REPORT_ALL_EXCEPTIONS', false),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Autoloaded Service Providers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The service providers listed here will be automatically loaded on the
|
||||
| request to your application. Feel free to add your own services to
|
||||
| this array to grant expanded functionality to your applications.
|
||||
|
|
||||
*/
|
||||
|
||||
'providers' => [
|
||||
/*
|
||||
* Laravel Framework Service Providers...
|
||||
*/
|
||||
Illuminate\Auth\AuthServiceProvider::class,
|
||||
Illuminate\Broadcasting\BroadcastServiceProvider::class,
|
||||
Illuminate\Bus\BusServiceProvider::class,
|
||||
Illuminate\Cache\CacheServiceProvider::class,
|
||||
Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
|
||||
Illuminate\Cookie\CookieServiceProvider::class,
|
||||
Illuminate\Database\DatabaseServiceProvider::class,
|
||||
Illuminate\Encryption\EncryptionServiceProvider::class,
|
||||
Illuminate\Filesystem\FilesystemServiceProvider::class,
|
||||
Illuminate\Foundation\Providers\FoundationServiceProvider::class,
|
||||
Illuminate\Hashing\HashServiceProvider::class,
|
||||
Illuminate\Mail\MailServiceProvider::class,
|
||||
Illuminate\Notifications\NotificationServiceProvider::class,
|
||||
Illuminate\Pagination\PaginationServiceProvider::class,
|
||||
Illuminate\Pipeline\PipelineServiceProvider::class,
|
||||
Illuminate\Queue\QueueServiceProvider::class,
|
||||
Illuminate\Redis\RedisServiceProvider::class,
|
||||
Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
|
||||
Illuminate\Session\SessionServiceProvider::class,
|
||||
Illuminate\Translation\TranslationServiceProvider::class,
|
||||
Illuminate\Validation\ValidationServiceProvider::class,
|
||||
Illuminate\View\ViewServiceProvider::class,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value is the name of your application, which will be used when the
|
||||
| framework needs to place the application's name in a notification or
|
||||
| other UI elements where an application name needs to be displayed.
|
||||
|
|
||||
*/
|
||||
|
||||
'name' => env('APP_NAME', 'Pyrodactyl'),
|
||||
* Application Service Providers...
|
||||
*/
|
||||
Pterodactyl\Providers\ActivityLogServiceProvider::class,
|
||||
Pterodactyl\Providers\AppServiceProvider::class,
|
||||
Pterodactyl\Providers\AuthServiceProvider::class,
|
||||
Pterodactyl\Providers\BackupsServiceProvider::class,
|
||||
Pterodactyl\Providers\BladeServiceProvider::class,
|
||||
Pterodactyl\Providers\EventServiceProvider::class,
|
||||
Pterodactyl\Providers\HashidsServiceProvider::class,
|
||||
Pterodactyl\Providers\RouteServiceProvider::class,
|
||||
Pterodactyl\Providers\RepositoryServiceProvider::class,
|
||||
Pterodactyl\Providers\ViewComposerServiceProvider::class,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Environment
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the "environment" your application is currently
|
||||
| running in. This may determine how you prefer to configure various
|
||||
| services the application utilizes. Set this in your ".env" file.
|
||||
|
|
||||
*/
|
||||
* Additional Dependencies
|
||||
*/
|
||||
Prologue\Alerts\AlertsServiceProvider::class,
|
||||
],
|
||||
|
||||
'env' => env('APP_ENV', 'production'),
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Class Aliases
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This array of class aliases will be registered when this application
|
||||
| is started. However, feel free to register as many as you wish as
|
||||
| the aliases are "lazy" loaded, so they don't hinder performance.
|
||||
|
|
||||
*/
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Debug Mode
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When your application is in debug mode, detailed error messages with
|
||||
| stack traces will be shown on every error that occurs within your
|
||||
| application. If disabled, a simple generic error page is shown.
|
||||
|
|
||||
*/
|
||||
'aliases' => Facade::defaultAliases()->merge([
|
||||
'Alert' => Prologue\Alerts\Facades\Alert::class,
|
||||
'Carbon' => Carbon\Carbon::class,
|
||||
'JavaScript' => Laracasts\Utilities\JavaScript\JavaScriptFacade::class,
|
||||
'Theme' => Pterodactyl\Extensions\Facades\Theme::class,
|
||||
|
||||
'debug' => (bool) env('APP_DEBUG', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application URL
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This URL is used by the console to properly generate URLs when using
|
||||
| the Artisan command line tool. You should set this to the root of
|
||||
| the application so that it's available within Artisan commands.
|
||||
|
|
||||
*/
|
||||
|
||||
'url' => env('APP_URL', 'http://localhost'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Timezone
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default timezone for your application, which
|
||||
| will be used by the PHP date and date-time functions. The timezone
|
||||
| is set to "UTC" by default as it is suitable for most use cases.
|
||||
|
|
||||
*/
|
||||
|
||||
'timezone' => env('APP_TIMEZONE', 'UTC'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Locale Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The application locale determines the default locale that will be used
|
||||
| by Laravel's translation / localization methods. This option can be
|
||||
| set to any locale for which you plan to have translation strings.
|
||||
|
|
||||
*/
|
||||
|
||||
'locale' => env('APP_LOCALE', 'en'),
|
||||
|
||||
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
|
||||
|
||||
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Encryption Key
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This key is utilized by Laravel's encryption services and should be set
|
||||
| to a random, 32 character string to ensure that all encrypted values
|
||||
| are secure. You should do this prior to deploying the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'cipher' => 'AES-256-CBC',
|
||||
|
||||
'key' => env('APP_KEY'),
|
||||
|
||||
'previous_keys' => [
|
||||
...array_filter(
|
||||
explode(',', env('APP_PREVIOUS_KEYS', ''))
|
||||
),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Maintenance Mode Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These configuration options determine the driver used to determine and
|
||||
| manage Laravel's "maintenance mode" status. The "cache" driver will
|
||||
| allow maintenance mode to be controlled across multiple machines.
|
||||
|
|
||||
| Supported drivers: "file", "cache"
|
||||
|
|
||||
*/
|
||||
|
||||
'maintenance' => [
|
||||
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
||||
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Exception Reporter Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| If you're encountering weird behavior with the Panel and no exceptions
|
||||
| are being logged try changing the environment variable below to be true.
|
||||
| This will override the default "don't report" behavior of the Panel and log
|
||||
| all exceptions. This will be quite noisy.
|
||||
|
|
||||
*/
|
||||
|
||||
'exceptions' => [
|
||||
'report_all' => env('APP_REPORT_ALL_EXCEPTIONS', false),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Autoloaded Service Providers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The service providers listed here will be automatically loaded on the
|
||||
| request to your application. Feel free to add your own services to
|
||||
| this array to grant expanded functionality to your applications.
|
||||
|
|
||||
*/
|
||||
|
||||
'providers' => [
|
||||
/*
|
||||
* Laravel Framework Service Providers...
|
||||
*/
|
||||
Illuminate\Auth\AuthServiceProvider::class,
|
||||
Illuminate\Broadcasting\BroadcastServiceProvider::class,
|
||||
Illuminate\Bus\BusServiceProvider::class,
|
||||
Illuminate\Cache\CacheServiceProvider::class,
|
||||
Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
|
||||
Illuminate\Cookie\CookieServiceProvider::class,
|
||||
Illuminate\Database\DatabaseServiceProvider::class,
|
||||
Illuminate\Encryption\EncryptionServiceProvider::class,
|
||||
Illuminate\Filesystem\FilesystemServiceProvider::class,
|
||||
Illuminate\Foundation\Providers\FoundationServiceProvider::class,
|
||||
Illuminate\Hashing\HashServiceProvider::class,
|
||||
Illuminate\Mail\MailServiceProvider::class,
|
||||
Illuminate\Notifications\NotificationServiceProvider::class,
|
||||
Illuminate\Pagination\PaginationServiceProvider::class,
|
||||
Illuminate\Pipeline\PipelineServiceProvider::class,
|
||||
Illuminate\Queue\QueueServiceProvider::class,
|
||||
Illuminate\Redis\RedisServiceProvider::class,
|
||||
Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
|
||||
Illuminate\Session\SessionServiceProvider::class,
|
||||
Illuminate\Translation\TranslationServiceProvider::class,
|
||||
Illuminate\Validation\ValidationServiceProvider::class,
|
||||
Illuminate\View\ViewServiceProvider::class,
|
||||
|
||||
/*
|
||||
* Application Service Providers...
|
||||
*/
|
||||
Pterodactyl\Providers\ActivityLogServiceProvider::class,
|
||||
Pterodactyl\Providers\AppServiceProvider::class,
|
||||
Pterodactyl\Providers\AuthServiceProvider::class,
|
||||
Pterodactyl\Providers\BackupsServiceProvider::class,
|
||||
Pterodactyl\Providers\BladeServiceProvider::class,
|
||||
Pterodactyl\Providers\EventServiceProvider::class,
|
||||
Pterodactyl\Providers\HashidsServiceProvider::class,
|
||||
Pterodactyl\Providers\RouteServiceProvider::class,
|
||||
Pterodactyl\Providers\RepositoryServiceProvider::class,
|
||||
Pterodactyl\Providers\ViewComposerServiceProvider::class,
|
||||
|
||||
/*
|
||||
* Additional Dependencies
|
||||
*/
|
||||
Prologue\Alerts\AlertsServiceProvider::class,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Class Aliases
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This array of class aliases will be registered when this application
|
||||
| is started. However, feel free to register as many as you wish as
|
||||
| the aliases are "lazy" loaded, so they don't hinder performance.
|
||||
|
|
||||
*/
|
||||
|
||||
'aliases' => Facade::defaultAliases()->merge([
|
||||
'Alert' => Prologue\Alerts\Facades\Alert::class,
|
||||
'Carbon' => Carbon\Carbon::class,
|
||||
'JavaScript' => Laracasts\Utilities\JavaScript\JavaScriptFacade::class,
|
||||
'Theme' => Pterodactyl\Extensions\Facades\Theme::class,
|
||||
|
||||
// Custom Facades
|
||||
'Activity' => Pterodactyl\Facades\Activity::class,
|
||||
'LogBatch' => Pterodactyl\Facades\LogBatch::class,
|
||||
'LogTarget' => Pterodactyl\Facades\LogTarget::class,
|
||||
])->toArray(),
|
||||
// Custom Facades
|
||||
'Activity' => Pterodactyl\Facades\Activity::class,
|
||||
'LogBatch' => Pterodactyl\Facades\LogBatch::class,
|
||||
'LogTarget' => Pterodactyl\Facades\LogTarget::class,
|
||||
])->toArray(),
|
||||
];
|
||||
|
||||
53
config/captcha.php
Normal file
53
config/captcha.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'driver' => env('CAPTCHA_DRIVER', 'none'),
|
||||
|
||||
'providers' => [
|
||||
'hcaptcha' => [
|
||||
'enabled' => env('HCAPTCHA_ENABLED', false),
|
||||
'site_key' => env('HCAPTCHA_SITE_KEY'),
|
||||
'secret_key' => env('HCAPTCHA_SECRET_KEY'),
|
||||
'endpoint' => 'https://hcaptcha.com/siteverify',
|
||||
],
|
||||
|
||||
'mcaptcha' => [
|
||||
'enabled' => env('MCAPTCHA_ENABLED', false),
|
||||
'site_key' => env('MCAPTCHA_SITE_KEY'),
|
||||
'secret_key' => env('MCAPTCHA_SECRET_KEY'),
|
||||
'endpoint' => env('MCAPTCHA_ENDPOINT', 'https://mcaptcha.your-instance.com/api/v1/pow/verify'),
|
||||
],
|
||||
|
||||
'turnstile' => [
|
||||
'enabled' => env('TURNSTILE_ENABLED', false),
|
||||
'site_key' => env('TURNSTILE_SITE_KEY'),
|
||||
'secret_key' => env('TURNSTILE_SECRET_KEY'),
|
||||
'endpoint' => 'https://challenges.cloudflare.com/turnstile/v0/siteverify',
|
||||
],
|
||||
|
||||
'proton' => [
|
||||
'enabled' => env('PROTON_CAPTCHA_ENABLED', false),
|
||||
'site_key' => env('PROTON_CAPTCHA_SITE_KEY'),
|
||||
'secret_key' => env('PROTON_CAPTCHA_SECRET_KEY'),
|
||||
'endpoint' => 'https://api.proton.me/captcha/v3/verify',
|
||||
],
|
||||
|
||||
'friendly' => [
|
||||
'enabled' => env('FRIENDLY_CAPTCHA_ENABLED', false),
|
||||
'site_key' => env('FRIENDLY_CAPTCHA_SITE_KEY'),
|
||||
'secret_key' => env('FRIENDLY_CAPTCHA_SECRET_KEY'),
|
||||
'endpoint' => 'https://api.friendlycaptcha.com/api/v1/siteverify',
|
||||
],
|
||||
|
||||
'recaptcha' => [
|
||||
'enabled' => env('RECAPTCHA_ENABLED', false),
|
||||
'site_key' => env('RECAPTCHA_SITE_KEY'),
|
||||
'secret_key' => env('RECAPTCHA_SECRET_KEY'),
|
||||
'endpoint' => 'https://www.google.com/recaptcha/api/siteverify',
|
||||
],
|
||||
],
|
||||
|
||||
// Global settings
|
||||
'verify_domain' => false,
|
||||
'timeout' => 5, // seconds
|
||||
];
|
||||
@@ -1,57 +1,57 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cross-Origin Resource Sharing (CORS) Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure your settings for cross-origin resource sharing
|
||||
| or "CORS". This determines what cross-origin operations may execute
|
||||
| in web browsers. You are free to adjust these settings as needed.
|
||||
|
|
||||
| To learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
|
||||
|
|
||||
*/
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cross-Origin Resource Sharing (CORS) Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure your settings for cross-origin resource sharing
|
||||
| or "CORS". This determines what cross-origin operations may execute
|
||||
| in web browsers. You are free to adjust these settings as needed.
|
||||
|
|
||||
| To learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
|
||||
|
|
||||
*/
|
||||
|
||||
/*
|
||||
* You can enable CORS for 1 or multiple paths.
|
||||
* Example: ['api/*']
|
||||
*/
|
||||
'paths' => ['/api/client', '/api/application', '/api/client/*', '/api/application/*'],
|
||||
/*
|
||||
* You can enable CORS for 1 or multiple paths.
|
||||
* Example: ['api/*']
|
||||
*/
|
||||
'paths' => ['/api/client', '/api/application', '/api/client/*', '/api/application/*'],
|
||||
|
||||
/*
|
||||
* Matches the request method. `['*']` allows all methods.
|
||||
*/
|
||||
'allowed_methods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'],
|
||||
/*
|
||||
* Matches the request method. `['*']` allows all methods.
|
||||
*/
|
||||
'allowed_methods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'],
|
||||
|
||||
/*
|
||||
* Matches the request origin. `['*']` allows all origins. Wildcards can be used, eg `*.mydomain.com`
|
||||
*/
|
||||
'allowed_origins' => explode(',', env('APP_CORS_ALLOWED_ORIGINS') ?? ''),
|
||||
/*
|
||||
* Matches the request origin. `['*']` allows all origins. Wildcards can be used, eg `*.mydomain.com`
|
||||
*/
|
||||
'allowed_origins' => explode(',', env('APP_CORS_ALLOWED_ORIGINS') ?? ''),
|
||||
|
||||
/*
|
||||
* Patterns that can be used with `preg_match` to match the origin.
|
||||
*/
|
||||
'allowed_origins_patterns' => [],
|
||||
/*
|
||||
* Patterns that can be used with `preg_match` to match the origin.
|
||||
*/
|
||||
'allowed_origins_patterns' => [],
|
||||
|
||||
/*
|
||||
* Sets the Access-Control-Allow-Headers response header. `['*']` allows all headers.
|
||||
*/
|
||||
'allowed_headers' => ['*'],
|
||||
/*
|
||||
* Sets the Access-Control-Allow-Headers response header. `['*']` allows all headers.
|
||||
*/
|
||||
'allowed_headers' => ['*'],
|
||||
|
||||
/*
|
||||
* Sets the Access-Control-Expose-Headers response header with these headers.
|
||||
*/
|
||||
'exposed_headers' => [],
|
||||
/*
|
||||
* Sets the Access-Control-Expose-Headers response header with these headers.
|
||||
*/
|
||||
'exposed_headers' => [],
|
||||
|
||||
/*
|
||||
* Sets the Access-Control-Max-Age response header when > 0.
|
||||
*/
|
||||
'max_age' => 0,
|
||||
/*
|
||||
* Sets the Access-Control-Max-Age response header when > 0.
|
||||
*/
|
||||
'max_age' => 0,
|
||||
|
||||
/*
|
||||
* Sets the Access-Control-Allow-Credentials header.
|
||||
*/
|
||||
'supports_credentials' => true,
|
||||
/*
|
||||
* Sets the Access-Control-Allow-Credentials header.
|
||||
*/
|
||||
'supports_credentials' => true,
|
||||
];
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
/*
|
||||
* Enable or disable captchas
|
||||
*/
|
||||
'enabled' => env('RECAPTCHA_ENABLED', true),
|
||||
|
||||
/*
|
||||
* API endpoint for recaptcha checks. You should not edit this.
|
||||
*/
|
||||
'domain' => env('RECAPTCHA_DOMAIN', 'https://www.google.com/recaptcha/api/siteverify'),
|
||||
|
||||
/*
|
||||
* Use a custom secret key, we use our public one by default
|
||||
*/
|
||||
'secret_key' => env('RECAPTCHA_SECRET_KEY', '6LcJcjwUAAAAALOcDJqAEYKTDhwELCkzUkNDQ0J5'),
|
||||
'_shipped_secret_key' => '6LcJcjwUAAAAALOcDJqAEYKTDhwELCkzUkNDQ0J5',
|
||||
|
||||
/*
|
||||
* Use a custom website key, we use our public one by default
|
||||
*/
|
||||
'website_key' => env('RECAPTCHA_WEBSITE_KEY', '6LcJcjwUAAAAAO_Xqjrtj9wWufUpYRnK6BW8lnfn'),
|
||||
'_shipped_website_key' => '6LcJcjwUAAAAAO_Xqjrtj9wWufUpYRnK6BW8lnfn',
|
||||
|
||||
/*
|
||||
* Domain verification is enabled by default and compares the domain used when solving the captcha
|
||||
* as public keys can't have domain verification on google's side enabled (obviously).
|
||||
*/
|
||||
'verify_domain' => true,
|
||||
];
|
||||
@@ -22,8 +22,12 @@
|
||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@friendlycaptcha/sdk": "^0.1.22",
|
||||
"@hcaptcha/react-hcaptcha": "^1.12.0",
|
||||
"@headlessui/react": "^2.2.2",
|
||||
"@lezer/highlight": "^1.2.1",
|
||||
"@marsidev/react-turnstile": "^1.1.0",
|
||||
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
|
||||
"@preact/signals-react": "^3.0.1",
|
||||
"@radix-ui/react-checkbox": "^1.3.1",
|
||||
"@radix-ui/react-context-menu": "^2.2.14",
|
||||
@@ -53,7 +57,9 @@
|
||||
"formik": "^2.4.6",
|
||||
"framer-motion": "^12.10.5",
|
||||
"globals": "^16.1.0",
|
||||
"install": "^0.13.0",
|
||||
"laravel-vite-plugin": "^1.2.0",
|
||||
"lucide-react": "^0.511.0",
|
||||
"million": "^3.1.11",
|
||||
"pathe": "^2.0.3",
|
||||
"qrcode.react": "^4.2.0",
|
||||
@@ -61,6 +67,7 @@
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-fast-compare": "^3.2.2",
|
||||
"react-google-recaptcha-v3": "^1.11.0",
|
||||
"react-router-dom": "^7.6.0",
|
||||
"reaptcha": "^1.12.1",
|
||||
"sockette": "^2.0.6",
|
||||
@@ -126,5 +133,5 @@
|
||||
"firefox esr",
|
||||
"not dead"
|
||||
],
|
||||
"packageManager": "pnpm@10.10.0"
|
||||
"packageManager": "pnpm@10.11.0"
|
||||
}
|
||||
|
||||
94
pnpm-lock.yaml
generated
94
pnpm-lock.yaml
generated
@@ -53,12 +53,24 @@ importers:
|
||||
'@fortawesome/react-fontawesome':
|
||||
specifier: ^0.2.2
|
||||
version: 0.2.2(@fortawesome/fontawesome-svg-core@6.7.2)(react@19.1.0)
|
||||
'@friendlycaptcha/sdk':
|
||||
specifier: ^0.1.22
|
||||
version: 0.1.22
|
||||
'@hcaptcha/react-hcaptcha':
|
||||
specifier: ^1.12.0
|
||||
version: 1.12.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@headlessui/react':
|
||||
specifier: ^2.2.2
|
||||
version: 2.2.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@lezer/highlight':
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1
|
||||
'@marsidev/react-turnstile':
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@mcaptcha/vanilla-glue':
|
||||
specifier: 0.1.0-alpha-3
|
||||
version: 0.1.0-alpha-3
|
||||
'@preact/signals-react':
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1(react@19.1.0)
|
||||
@@ -146,9 +158,15 @@ importers:
|
||||
globals:
|
||||
specifier: ^16.1.0
|
||||
version: 16.1.0
|
||||
install:
|
||||
specifier: ^0.13.0
|
||||
version: 0.13.0
|
||||
laravel-vite-plugin:
|
||||
specifier: ^1.2.0
|
||||
version: 1.2.0(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(lightningcss@1.29.2))
|
||||
lucide-react:
|
||||
specifier: ^0.511.0
|
||||
version: 0.511.0(react@19.1.0)
|
||||
million:
|
||||
specifier: ^3.1.11
|
||||
version: 3.1.11(rollup@4.40.2)
|
||||
@@ -170,6 +188,9 @@ importers:
|
||||
react-fast-compare:
|
||||
specifier: ^3.2.2
|
||||
version: 3.2.2
|
||||
react-google-recaptcha-v3:
|
||||
specifier: ^1.11.0
|
||||
version: 1.11.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
react-router-dom:
|
||||
specifier: ^7.6.0
|
||||
version: 7.6.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
@@ -713,6 +734,18 @@ packages:
|
||||
'@fortawesome/fontawesome-svg-core': ~1 || ~6
|
||||
react: '>=16.3'
|
||||
|
||||
'@friendlycaptcha/sdk@0.1.22':
|
||||
resolution: {integrity: sha512-VK/dx5JWd11Y3fDgf9eXFWtZ9zLYrTgRUmHWibfJ1mL18/8OLQzgPXFyT2YSe74iHDG5pllJm4EFF7LW4wpvig==}
|
||||
|
||||
'@hcaptcha/loader@2.0.0':
|
||||
resolution: {integrity: sha512-fFQH6ApU/zCCl6Y1bnbsxsp1Er/lKX+qlgljrpWDeFcenpEtoP68hExlKSXECospzKLeSWcr06cbTjlR/x3IJA==}
|
||||
|
||||
'@hcaptcha/react-hcaptcha@1.12.0':
|
||||
resolution: {integrity: sha512-QiHnQQ52k8SJJSHkc3cq4TlYzag7oPd4f5ZqnjVSe4fJDSlZaOQFtu5F5AYisVslwaitdDELPVLRsRJxiiI0Aw==}
|
||||
peerDependencies:
|
||||
react: '>= 16.3.0'
|
||||
react-dom: '>= 16.3.0'
|
||||
|
||||
'@headlessui/react@2.2.2':
|
||||
resolution: {integrity: sha512-zbniWOYBQ8GHSUIOPY7BbdIn6PzUOq0z41RFrF30HbjsxG6Rrfk+6QulR8Kgf2Vwj2a/rE6i62q5vo+2gI5dJA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -819,6 +852,18 @@ packages:
|
||||
'@marijn/find-cluster-break@1.0.2':
|
||||
resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
|
||||
|
||||
'@marsidev/react-turnstile@1.1.0':
|
||||
resolution: {integrity: sha512-X7bP9ZYutDd+E+klPYF+/BJHqEyyVkN4KKmZcNRr84zs3DcMoftlMAuoKqNSnqg0HE7NQ1844+TLFSJoztCdSA==}
|
||||
peerDependencies:
|
||||
react: ^17.0.2 || ^18.0.0 || ^19.0
|
||||
react-dom: ^17.0.2 || ^18.0.0 || ^19.0
|
||||
|
||||
'@mcaptcha/core-glue@0.1.0-alpha-5':
|
||||
resolution: {integrity: sha512-16qWm5O5X0Y9LXULULaAks8Vf9FNlUUBcR5KDt49aWhFhG5++JzxNmCwQM9EJSHNU7y0U+FdyAWcGmjfKlkRLA==}
|
||||
|
||||
'@mcaptcha/vanilla-glue@0.1.0-alpha-3':
|
||||
resolution: {integrity: sha512-GT6TJBgmViGXcXiT5VOr+h/6iOnThSlZuCoOWncubyTZU9R3cgU5vWPkF7G6Ob6ee2CBe3yqBxxk24CFVGTVXw==}
|
||||
|
||||
'@modelcontextprotocol/sdk@1.11.1':
|
||||
resolution: {integrity: sha512-9LfmxKTb1v+vUS1/emSk1f5ePmTLkb9Le9AxOB5T0XM59EUumwcS45z05h7aiZx3GI0Bl7mjb3FMEglYj+acuQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -2480,6 +2525,10 @@ packages:
|
||||
inherits@2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
|
||||
install@0.13.0:
|
||||
resolution: {integrity: sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
internal-slot@1.1.0:
|
||||
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2743,6 +2792,11 @@ packages:
|
||||
lru-cache@5.1.1:
|
||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||
|
||||
lucide-react@0.511.0:
|
||||
resolution: {integrity: sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w==}
|
||||
peerDependencies:
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
magic-string@0.30.17:
|
||||
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
|
||||
|
||||
@@ -3111,6 +3165,12 @@ packages:
|
||||
react-fast-compare@3.2.2:
|
||||
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
|
||||
|
||||
react-google-recaptcha-v3@1.11.0:
|
||||
resolution: {integrity: sha512-kLQqpz/77m8+trpBwzqcxNtvWZYoZ/YO6Vm2cVTHW8hs80BWUfDpC7RDwuAvpswwtSYApWfaSpIDFWAIBNIYxQ==}
|
||||
peerDependencies:
|
||||
react: ^16.3 || ^17.0 || ^18.0 || ^19.0
|
||||
react-dom: ^17.0 || ^18.0 || ^19.0
|
||||
|
||||
react-is@16.13.1:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
|
||||
@@ -4223,6 +4283,17 @@ snapshots:
|
||||
prop-types: 15.8.1
|
||||
react: 19.1.0
|
||||
|
||||
'@friendlycaptcha/sdk@0.1.22': {}
|
||||
|
||||
'@hcaptcha/loader@2.0.0': {}
|
||||
|
||||
'@hcaptcha/react-hcaptcha@1.12.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.1
|
||||
'@hcaptcha/loader': 2.0.0
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
'@headlessui/react@2.2.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@floating-ui/react': 0.26.28(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
@@ -4364,6 +4435,17 @@ snapshots:
|
||||
|
||||
'@marijn/find-cluster-break@1.0.2': {}
|
||||
|
||||
'@marsidev/react-turnstile@1.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
'@mcaptcha/core-glue@0.1.0-alpha-5': {}
|
||||
|
||||
'@mcaptcha/vanilla-glue@0.1.0-alpha-3':
|
||||
dependencies:
|
||||
'@mcaptcha/core-glue': 0.1.0-alpha-5
|
||||
|
||||
'@modelcontextprotocol/sdk@1.11.1':
|
||||
dependencies:
|
||||
content-type: 1.0.5
|
||||
@@ -6164,6 +6246,8 @@ snapshots:
|
||||
|
||||
inherits@2.0.4: {}
|
||||
|
||||
install@0.13.0: {}
|
||||
|
||||
internal-slot@1.1.0:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
@@ -6405,6 +6489,10 @@ snapshots:
|
||||
dependencies:
|
||||
yallist: 3.1.1
|
||||
|
||||
lucide-react@0.511.0(react@19.1.0):
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
|
||||
magic-string@0.30.17:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
@@ -6689,6 +6777,12 @@ snapshots:
|
||||
|
||||
react-fast-compare@3.2.2: {}
|
||||
|
||||
react-google-recaptcha-v3@1.11.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
hoist-non-react-statics: 3.3.2
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
react-is@16.13.1: {}
|
||||
|
||||
react-remove-scroll-bar@2.3.8(@types/react@19.1.3)(react@19.1.0):
|
||||
|
||||
@@ -1,38 +1,69 @@
|
||||
import http from '@/api/http';
|
||||
|
||||
export interface LoginResponse {
|
||||
interface LoginData {
|
||||
user: string;
|
||||
password: string;
|
||||
'cf-turnstile-response'?: string;
|
||||
'h-captcha-response'?: string;
|
||||
'frc-captcha-response'?: string;
|
||||
captchaData?: string;
|
||||
}
|
||||
|
||||
interface LoginResponse {
|
||||
complete: boolean;
|
||||
intended?: string;
|
||||
confirmationToken?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface LoginData {
|
||||
username: string;
|
||||
password: string;
|
||||
recaptchaData?: string | null;
|
||||
}
|
||||
export default async (data: LoginData): Promise<LoginResponse> => {
|
||||
try {
|
||||
await http.get('/sanctum/csrf-cookie');
|
||||
|
||||
export default ({ username, password, recaptchaData }: LoginData): Promise<LoginResponse> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get('/sanctum/csrf-cookie')
|
||||
.then(() =>
|
||||
http.post('/auth/login', {
|
||||
user: username,
|
||||
password,
|
||||
'g-recaptcha-response': recaptchaData,
|
||||
}),
|
||||
)
|
||||
.then((response) => {
|
||||
if (!(response.data instanceof Object)) {
|
||||
return reject(new Error('An error occurred while processing the login request.'));
|
||||
}
|
||||
const payload: Record<string, string> = {
|
||||
user: data.user,
|
||||
password: data.password,
|
||||
};
|
||||
|
||||
return resolve({
|
||||
complete: response.data.data.complete,
|
||||
intended: response.data.data.intended || undefined,
|
||||
confirmationToken: response.data.data.confirmation_token || undefined,
|
||||
});
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
if (data['cf-turnstile-response']) {
|
||||
payload['cf-turnstile-response'] = data['cf-turnstile-response'];
|
||||
} else if (data['h-captcha-response']) {
|
||||
payload['h-captcha-response'] = data['h-captcha-response'];
|
||||
} else if (data['frc-captcha-response']) {
|
||||
payload['frc-captcha-response'] = data['frc-captcha-response'];
|
||||
} else if (data['g-captcha-response']) {
|
||||
payload['g-captcha-response'] = data['g-captcha-response'];
|
||||
} else if (data.captchaData) {
|
||||
payload.captchaData = data.captchaData;
|
||||
}
|
||||
|
||||
const response = await http.post('/auth/login', payload);
|
||||
|
||||
if (!response.data || typeof response.data !== 'object') {
|
||||
throw new Error('Invalid server response format');
|
||||
}
|
||||
|
||||
return {
|
||||
complete: response.data.complete ?? response.data.data?.complete ?? false,
|
||||
intended: response.data.intended ?? response.data.data?.intended,
|
||||
confirmationToken:
|
||||
response.data.confirmationToken ??
|
||||
response.data.data?.confirmation_token ??
|
||||
response.data.data?.confirmationToken,
|
||||
error: response.data.error ?? response.data.message,
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error('Login API Error:', {
|
||||
status: error.response?.status,
|
||||
data: error.response?.data,
|
||||
message: error.message,
|
||||
});
|
||||
|
||||
throw new Error(
|
||||
error.response?.data?.error ??
|
||||
error.response?.data?.message ??
|
||||
error.message ??
|
||||
'Login failed. Please try again.',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
57
resources/scripts/components/Captcha.tsx
Normal file
57
resources/scripts/components/Captcha.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
// components/Captcha.tsx
|
||||
import {
|
||||
CreateWidgetOptions,
|
||||
FRCWidgetCompleteEvent,
|
||||
FRCWidgetErrorEventData,
|
||||
FriendlyCaptchaSDK,
|
||||
WidgetErrorData,
|
||||
} from '@friendlycaptcha/sdk';
|
||||
import HCaptcha from '@hcaptcha/react-hcaptcha';
|
||||
import { Turnstile } from '@marsidev/react-turnstile';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface CaptchaProps {
|
||||
sitekey?: string;
|
||||
endpoint?: string;
|
||||
driver: 'none' | 'hcaptcha' | 'mcaptcha' | 'turnstile' | 'friendly' | 'recaptcha';
|
||||
onVerify: (token: string) => void;
|
||||
onError: () => void;
|
||||
onExpire: () => void;
|
||||
}
|
||||
|
||||
export default ({ driver, sitekey, endpoint, onVerify, onError, onExpire }: CaptchaProps) => {
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (driver !== 'none' && !loaded) {
|
||||
// Load any required external scripts here if needed
|
||||
setLoaded(true);
|
||||
}
|
||||
}, [driver]);
|
||||
|
||||
if (driver === 'hcaptcha') {
|
||||
return <HCaptcha sitekey={sitekey || ''} onVerify={onVerify} onError={onError} onExpire={onExpire} />;
|
||||
}
|
||||
|
||||
if (driver === 'turnstile') {
|
||||
return <Turnstile siteKey={sitekey || ''} onSuccess={onVerify} onError={onError} onExpire={onExpire} />;
|
||||
}
|
||||
|
||||
if (driver === 'recaptcha') {
|
||||
// TODO: Maybe make this work one day
|
||||
return <Turnstile siteKey={sitekey || ''} onSuccess={onVerify} onError={onError} onExpire={onExpire} />;
|
||||
}
|
||||
|
||||
if (driver === 'friendly') {
|
||||
return (
|
||||
<FriendlyCaptcha
|
||||
sitekey={sitekey || ''}
|
||||
endpoint={endpoint}
|
||||
onSuccess={(e: any) => onVerify(e.detail.token)}
|
||||
onError={onError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
48
resources/scripts/components/FriendlyCaptcha.tsx
Normal file
48
resources/scripts/components/FriendlyCaptcha.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React, { forwardRef, useEffect, useImperativeHandle } from 'react';
|
||||
|
||||
interface FriendlyCaptchaProps {
|
||||
sitekey: string;
|
||||
onComplete: (response: string) => void;
|
||||
onError: () => void;
|
||||
onExpire: () => void;
|
||||
}
|
||||
|
||||
const FriendlyCaptcha = forwardRef(({ sitekey, onComplete, onError, onExpire }: FriendlyCaptchaProps, ref) => {
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
const widgetRef = React.useRef<any>(null);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
reset: () => {
|
||||
if (widgetRef.current) {
|
||||
widgetRef.current.reset();
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (!window.friendlyChallenge) return;
|
||||
|
||||
if (containerRef.current) {
|
||||
widgetRef.current = new window.friendlyChallenge.WidgetInstance(
|
||||
containerRef.current,
|
||||
{
|
||||
startMode: 'auto',
|
||||
doneCallback: onComplete,
|
||||
errorCallback: onError,
|
||||
expiredCallback: onExpire,
|
||||
},
|
||||
{ sitekey },
|
||||
);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (widgetRef.current) {
|
||||
widgetRef.current.destroy();
|
||||
}
|
||||
};
|
||||
}, [sitekey]);
|
||||
|
||||
return <div ref={containerRef} className='frc-captcha dark' data-sitekey={sitekey} />;
|
||||
});
|
||||
|
||||
export default FriendlyCaptcha;
|
||||
@@ -1,119 +1,160 @@
|
||||
import HCaptcha from '@hcaptcha/react-hcaptcha';
|
||||
import { Turnstile } from '@marsidev/react-turnstile';
|
||||
import { useStoreState } from 'easy-peasy';
|
||||
import type { FormikHelpers } from 'formik';
|
||||
import { Formik } from 'formik';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import Reaptcha from 'reaptcha';
|
||||
import { object, string } from 'yup';
|
||||
|
||||
import FriendlyCaptcha from '@/components/FriendlyCaptcha';
|
||||
import LoginFormContainer from '@/components/auth/LoginFormContainer';
|
||||
import Button from '@/components/elements/Button';
|
||||
import Field from '@/components/elements/Field';
|
||||
import Logo from '@/components/elements/PyroLogo';
|
||||
|
||||
import login from '@/api/auth/login';
|
||||
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
|
||||
import Logo from '../elements/PyroLogo';
|
||||
|
||||
interface Values {
|
||||
username: string;
|
||||
user: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
function LoginContainer() {
|
||||
const ref = useRef<Reaptcha>(null);
|
||||
const [token, setToken] = useState('');
|
||||
const [friendlyLoaded, setFriendlyLoaded] = useState(false);
|
||||
const turnstileRef = useRef(null);
|
||||
const friendlyCaptchaRef = useRef<{ reset: () => void }>(null);
|
||||
const hCaptchaRef = useRef<HCaptcha>(null);
|
||||
const mCaptchaRef = useRef<{ reset: () => void }>(null);
|
||||
|
||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||
const { enabled: recaptchaEnabled, siteKey } = useStoreState((state) => state.settings.data!.recaptcha);
|
||||
const { captcha } = useStoreState((state) => state.settings.data!);
|
||||
const isTurnstileEnabled = captcha.driver === 'turnstile' && captcha.turnstile?.siteKey;
|
||||
const isFriendlyEnabled = captcha.driver === 'friendly' && captcha.friendly?.siteKey;
|
||||
const isHCaptchaEnabled = captcha.driver === 'hcaptcha' && captcha.hcaptcha?.siteKey;
|
||||
const isMCaptchaEnabled = captcha.driver === 'mcaptcha' && captcha.mcaptcha?.siteKey;
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
clearFlashes();
|
||||
|
||||
// Load FriendlyCaptcha script if needed
|
||||
if (isFriendlyEnabled && !window.friendlyChallenge) {
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://unpkg.com/friendly-challenge@0.9.12/widget.module.min.js';
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
script.onload = () => setFriendlyLoaded(true);
|
||||
document.body.appendChild(script);
|
||||
} else if (isFriendlyEnabled) {
|
||||
setFriendlyLoaded(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const resetCaptcha = () => {
|
||||
setToken('');
|
||||
if (isTurnstileEnabled && turnstileRef.current) {
|
||||
// @ts-ignore - The type doesn't expose the reset method directly
|
||||
turnstileRef.current.reset();
|
||||
}
|
||||
if (isFriendlyEnabled && friendlyCaptchaRef.current) {
|
||||
friendlyCaptchaRef.current.reset();
|
||||
}
|
||||
if (isHCaptchaEnabled && hCaptchaRef.current) {
|
||||
hCaptchaRef.current.resetCaptcha();
|
||||
}
|
||||
};
|
||||
|
||||
const handleCaptchaComplete = (response: string) => {
|
||||
setToken(response);
|
||||
};
|
||||
|
||||
const handleCaptchaError = (provider: string) => {
|
||||
setToken('');
|
||||
clearAndAddHttpError({ error: new Error(`${provider} challenge failed.`) });
|
||||
};
|
||||
|
||||
const handleCaptchaExpire = () => {
|
||||
setToken('');
|
||||
};
|
||||
|
||||
const onSubmit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
||||
clearFlashes();
|
||||
|
||||
// If there is no token in the state yet, request the token and then abort this submit request
|
||||
// since it will be re-submitted when the recaptcha data is returned by the component.
|
||||
if (recaptchaEnabled && !token) {
|
||||
ref.current!.execute().catch((error) => {
|
||||
console.error(error);
|
||||
|
||||
setSubmitting(false);
|
||||
clearAndAddHttpError({ error });
|
||||
});
|
||||
|
||||
if ((isTurnstileEnabled || isFriendlyEnabled || isHCaptchaEnabled) && !token) {
|
||||
setSubmitting(false);
|
||||
clearAndAddHttpError({ error: new Error('Please complete the CAPTCHA challenge.') });
|
||||
return;
|
||||
}
|
||||
|
||||
login({ ...values, recaptchaData: token })
|
||||
const requestData: Record<string, string> = {
|
||||
user: values.user,
|
||||
password: values.password,
|
||||
};
|
||||
|
||||
if (isTurnstileEnabled) {
|
||||
requestData['cf-turnstile-response'] = token;
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
requestData['cf-turnstile-remoteip'] = 'localhost';
|
||||
}
|
||||
} else if (isHCaptchaEnabled) {
|
||||
requestData['h-captcha-response'] = token;
|
||||
} else if (isFriendlyEnabled) {
|
||||
requestData['frc-captcha-response'] = token;
|
||||
}
|
||||
|
||||
login(requestData)
|
||||
.then((response) => {
|
||||
if (response.complete) {
|
||||
// @ts-expect-error this is valid
|
||||
window.location = response.intended || '/';
|
||||
window.location.href = response.intended || '/';
|
||||
return;
|
||||
}
|
||||
|
||||
navigate('/auth/login/checkpoint', { state: { token: response.confirmationToken } });
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
|
||||
setToken('');
|
||||
// https://github.com/jsardev/reaptcha/issues/218
|
||||
if (ref.current) {
|
||||
setTimeout(() => {
|
||||
if (ref.current) {
|
||||
ref.current.reset();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
console.error('Login error details:', {
|
||||
message: error.message,
|
||||
response: error.response?.data,
|
||||
config: error.config,
|
||||
});
|
||||
resetCaptcha();
|
||||
setSubmitting(false);
|
||||
clearAndAddHttpError({ error });
|
||||
|
||||
// Check if the error is specifically about invalid credentials
|
||||
if (error.response?.data?.errors?.some((e: any) => e.code === 'InvalidCredentials')) {
|
||||
clearAndAddHttpError({ error: new Error('Invalid username or password. Please try again.') });
|
||||
}
|
||||
if (error.response?.data?.errors?.some((e: any) => e.code === 'DisplayException')) {
|
||||
clearAndAddHttpError({ error: new Error('Invalid username or password. Please try again.') });
|
||||
} else {
|
||||
// Fall back to the server's error message or a generic CAPTCHA message
|
||||
const errorMsg = error.response?.data?.message || 'An Unknown Error Occured.';
|
||||
clearAndAddHttpError({ error: new Error(errorMsg) });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik
|
||||
onSubmit={onSubmit}
|
||||
initialValues={{ username: '', password: '' }}
|
||||
initialValues={{ user: '', password: '' }}
|
||||
validationSchema={object().shape({
|
||||
username: string().required('A username or email must be provided.'),
|
||||
user: string().required('A username or email must be provided.'),
|
||||
password: string().required('Please enter your account password.'),
|
||||
})}
|
||||
>
|
||||
{({ isSubmitting, setSubmitting, submitForm }) => (
|
||||
{({ isSubmitting }) => (
|
||||
<LoginFormContainer className={`w-full flex`}>
|
||||
<div className='flex h-12 mb-4 items-center w-full'>
|
||||
{/* temp src */}
|
||||
{/* <img
|
||||
className='w-full max-w-full h-full'
|
||||
loading='lazy'
|
||||
decoding='async'
|
||||
alt=''
|
||||
aria-hidden
|
||||
style={{
|
||||
color: 'transparent',
|
||||
}}
|
||||
src='https://i.imgur.com/Hbum4fc.png'
|
||||
/> */}
|
||||
{/* <NavLink to={'/'} className='flex shrink-0 h-full w-fit'> */}
|
||||
<Logo />
|
||||
</div>
|
||||
<div aria-hidden className='my-8 bg-[#ffffff33] min-h-[1px]'></div>
|
||||
<h2 className='text-xl font-extrabold mb-2'>Login</h2>
|
||||
<Field
|
||||
id='usernameOrEmail'
|
||||
type={'text'}
|
||||
label={'Username or Email'}
|
||||
name={'username'}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<Field id='user' type={'text'} label={'Username or Email'} name={'user'} disabled={isSubmitting} />
|
||||
<div className={`relative mt-6`}>
|
||||
<Field
|
||||
id='password'
|
||||
@@ -129,9 +170,58 @@ function LoginContainer() {
|
||||
Forgot Password?
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* CAPTCHA Providers */}
|
||||
{isTurnstileEnabled && (
|
||||
<div className='mt-6'>
|
||||
<Turnstile
|
||||
ref={turnstileRef}
|
||||
siteKey={captcha.turnstile.siteKey}
|
||||
onSuccess={handleCaptchaComplete}
|
||||
onError={() => handleCaptchaError('Turnstile')}
|
||||
onExpire={handleCaptchaExpire}
|
||||
options={{
|
||||
theme: 'dark',
|
||||
size: 'flexible',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isFriendlyEnabled && friendlyLoaded && (
|
||||
<div className='mt-6 w-full'>
|
||||
<FriendlyCaptcha
|
||||
ref={friendlyCaptchaRef}
|
||||
sitekey={captcha.friendly.siteKey}
|
||||
onComplete={handleCaptchaComplete}
|
||||
onError={() => handleCaptchaError('FriendlyCaptcha')}
|
||||
onExpire={handleCaptchaExpire}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isHCaptchaEnabled && (
|
||||
<div className='mt-6'>
|
||||
<HCaptcha
|
||||
ref={hCaptchaRef}
|
||||
sitekey={captcha.hcaptcha.siteKey}
|
||||
onVerify={handleCaptchaComplete}
|
||||
onError={() => handleCaptchaError('hCaptcha')}
|
||||
onExpire={handleCaptchaExpire}
|
||||
theme='dark'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isMCaptchaEnabled && (
|
||||
<div className='mt-6'>
|
||||
<p className='text-red-500'>mCaptcha implementation needed</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`mt-6`}>
|
||||
<Button
|
||||
className={`relative mt-4 w-full rounded-full bg-brand border-0 ring-0 outline-hidden capitalize font-bold text-sm py-2`}
|
||||
className={`relative mt-4 w-full rounded-full bg-brand border-0 ring-0 outline-hidden capitalize font-bold text-sm py-2 hover:cursor-pointer`}
|
||||
type={'submit'}
|
||||
size={'xlarge'}
|
||||
isLoading={isSubmitting}
|
||||
@@ -140,22 +230,6 @@ function LoginContainer() {
|
||||
Login
|
||||
</Button>
|
||||
</div>
|
||||
{recaptchaEnabled && (
|
||||
<Reaptcha
|
||||
ref={ref}
|
||||
size={'invisible'}
|
||||
sitekey={siteKey || '_invalid_key'}
|
||||
onVerify={(response) => {
|
||||
setToken(response);
|
||||
// Ensure submitForm is called after token is updated
|
||||
setTimeout(submitForm, 100);
|
||||
}}
|
||||
onExpire={() => {
|
||||
setSubmitting(false);
|
||||
setToken('');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</LoginFormContainer>
|
||||
)}
|
||||
</Formik>
|
||||
|
||||
@@ -1,12 +1,30 @@
|
||||
import { Action, action } from 'easy-peasy';
|
||||
|
||||
// Define captcha configuration type
|
||||
interface CaptchaConfig {
|
||||
driver: 'none' | 'hcaptcha' | 'mcaptcha' | 'turnstile' | 'friendly';
|
||||
hcaptcha: {
|
||||
siteKey: string;
|
||||
};
|
||||
mcaptcha: {
|
||||
siteKey: string;
|
||||
endpoint: string;
|
||||
};
|
||||
turnstile: {
|
||||
siteKey: string;
|
||||
};
|
||||
friendly: {
|
||||
siteKey: string;
|
||||
};
|
||||
recaptcha: {
|
||||
siteKey: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SiteSettings {
|
||||
name: string;
|
||||
locale: string;
|
||||
recaptcha: {
|
||||
enabled: boolean;
|
||||
siteKey: string;
|
||||
};
|
||||
captcha: CaptchaConfig;
|
||||
}
|
||||
|
||||
export interface SettingsStore {
|
||||
@@ -16,7 +34,6 @@ export interface SettingsStore {
|
||||
|
||||
const settings: SettingsStore = {
|
||||
data: undefined,
|
||||
|
||||
setSettings: action((state, payload) => {
|
||||
state.data = payload;
|
||||
}),
|
||||
|
||||
@@ -23,45 +23,45 @@
|
||||
<div class="box-body">
|
||||
You are running Pyrodactyl panel version <code>{{ config('app.version') }}</code>.
|
||||
</div>
|
||||
<div aria-hidden="true"
|
||||
<!-- <div aria-hidden="true"
|
||||
style="background-color: #ffffff33; position: absolute; height: 1px; width: 100%; margin-top: 20px;"></div>
|
||||
<div class="row" style="margin-top: 20px;">
|
||||
<div class="col-md-3 col-sm-6 col-xs-12">
|
||||
<div class="small-box bg-[#000000]">
|
||||
<div class="inner">
|
||||
<h3 id="cpu-load">--</h3>
|
||||
<p>CPU Usage</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="small-box bg-[#000000]">
|
||||
<div class="inner">
|
||||
<h3 id="cpu-load">--</h3>
|
||||
<p>CPU Usage</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="small-box bg-[#000000]">
|
||||
<div class="inner">
|
||||
<h3 id="ram-usage">--</h3>
|
||||
<p>Memory Usage</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="small-box bg-[#000000]">
|
||||
<div class="inner">
|
||||
<h3 id="ram-usage">--</h3>
|
||||
<p>Memory Usage</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
|
||||
<div class="small-box bg-[#000000]">
|
||||
<div class="inner items-center">
|
||||
<h3 id="disk-usage">--</h3>
|
||||
<p>Storage</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="small-box bg-[#000000]">
|
||||
<div class="inner items-center">
|
||||
<h3 id="disk-usage">--</h3>
|
||||
<p>Storage</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="small-box bg-[#000000]">
|
||||
<div class="inner">
|
||||
<h3 id="uptime">--</h3>
|
||||
<p>System Uptime</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="small-box bg-[#000000]">
|
||||
<div class="inner">
|
||||
<h3 id="uptime">--</h3>
|
||||
<p>System Uptime</p>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
<!-- </div> -->
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,266 +1,385 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('title')
|
||||
{{ $node->name }}: Settings
|
||||
{{ $node->name }}: Settings
|
||||
@endsection
|
||||
|
||||
@section('content-header')
|
||||
<h1>{{ $node->name }}<small>Configure your node settings.</small></h1>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{{ route('admin.index') }}">Admin</a></li>
|
||||
<li><a href="{{ route('admin.nodes') }}">Nodes</a></li>
|
||||
<li><a href="{{ route('admin.nodes.view', $node->id) }}">{{ $node->name }}</a></li>
|
||||
<li class="active">Settings</li>
|
||||
</ol>
|
||||
<h1>{{ $node->name }}<small>Configure your node settings.</small></h1>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{{ route('admin.index') }}">Admin</a></li>
|
||||
<li><a href="{{ route('admin.nodes') }}">Nodes</a></li>
|
||||
<li><a href="{{ route('admin.nodes.view', $node->id) }}">{{ $node->name }}</a></li>
|
||||
<li class="active">Settings</li>
|
||||
</ol>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="nav-tabs-custom nav-tabs-floating">
|
||||
<ul class="nav nav-tabs">
|
||||
<li><a href="{{ route('admin.nodes.view', $node->id) }}">About</a></li>
|
||||
<li class="active"><a href="{{ route('admin.nodes.view.settings', $node->id) }}">Settings</a></li>
|
||||
<li><a href="{{ route('admin.nodes.view.configuration', $node->id) }}">Configuration</a></li>
|
||||
<li><a href="{{ route('admin.nodes.view.allocation', $node->id) }}">Allocation</a></li>
|
||||
<li><a href="{{ route('admin.nodes.view.servers', $node->id) }}">Servers</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="nav-tabs-custom nav-tabs-floating">
|
||||
<ul class="nav nav-tabs">
|
||||
<li><a href="{{ route('admin.nodes.view', $node->id) }}">About</a></li>
|
||||
<li class="active"><a href="{{ route('admin.nodes.view.settings', $node->id) }}">Settings</a></li>
|
||||
<li><a href="{{ route('admin.nodes.view.configuration', $node->id) }}">Configuration</a></li>
|
||||
<li><a href="{{ route('admin.nodes.view.allocation', $node->id) }}">Allocation</a></li>
|
||||
<li><a href="{{ route('admin.nodes.view.servers', $node->id) }}">Servers</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<form action="{{ route('admin.nodes.view.settings', $node->id) }}" method="POST">
|
||||
</div>
|
||||
</div>
|
||||
<form action="{{ route('admin.nodes.view.settings', $node->id) }}" method="POST">
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="box">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Settings</h3>
|
||||
</div>
|
||||
<div class="box-body row">
|
||||
<div class="form-group col-xs-12">
|
||||
<label for="name" class="control-label">Node Name</label>
|
||||
<div>
|
||||
<input type="text" autocomplete="off" name="name" class="form-control" value="{{ old('name', $node->name) }}" />
|
||||
<p class="text-muted"><small>Character limits: <code>a-zA-Z0-9_.-</code> and <code>[Space]</code> (min 1, max 100 characters).</small></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-xs-12">
|
||||
<label for="description" class="control-label">Description</label>
|
||||
<div>
|
||||
<textarea name="description" id="description" rows="4" class="form-control">{{ $node->description }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-xs-12">
|
||||
<label for="name" class="control-label">Location</label>
|
||||
<div>
|
||||
<select name="location_id" class="form-control">
|
||||
@foreach($locations as $location)
|
||||
<option value="{{ $location->id }}" {{ (old('location_id', $node->location_id) === $location->id) ? 'selected' : '' }}>{{ $location->long }} ({{ $location->short }})</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-xs-12">
|
||||
<label for="public" class="control-label">Allow Automatic Allocation <sup><a data-toggle="tooltip" data-placement="top" title="Allow automatic allocation to this Node?">?</a></sup></label>
|
||||
<div>
|
||||
<input type="radio" name="public" value="1" {{ (old('public', $node->public)) ? 'checked' : '' }} id="public_1" checked> <label for="public_1" style="padding-left:5px;">Yes</label><br />
|
||||
<input type="radio" name="public" value="0" {{ (old('public', $node->public)) ? '' : 'checked' }} id="public_0"> <label for="public_0" style="padding-left:5px;">No</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-xs-12">
|
||||
<label for="fqdn" class="control-label">Public Fully Qualified Domain Name</label>
|
||||
<div>
|
||||
<input type="text" autocomplete="off" name="fqdn" class="form-control"
|
||||
value="{{ old('fqdn', $node->fqdn) }}" />
|
||||
</div>
|
||||
<p class="text-muted">
|
||||
<small>
|
||||
Domain name that browsers will use to connect to Wings (e.g <code>wings.example.com</code>).
|
||||
An IP address may be used <em>only</em> if you are not using SSL for this node.
|
||||
<a tabindex="0" data-toggle="popover" data-trigger="focus" title="Why do I need a FQDN?"
|
||||
data-content="In order to secure communications between your server and this node we use SSL. We cannot generate a SSL certificate for IP Addresses, and as such you will need to provide a FQDN.">Why?</a>
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group col-xs-12">
|
||||
<label for="internal_fqdn" class="control-label">
|
||||
Internal FQDN
|
||||
<strong>(Optional)</strong>
|
||||
</label>
|
||||
<div>
|
||||
<input type="text" autocomplete="off" name="internal_fqdn" class="form-control"
|
||||
value="{{ old('internal_fqdn', $node->internal_fqdn) }}" />
|
||||
</div>
|
||||
<p class="text-muted">
|
||||
<small>
|
||||
<strong>Optional:</strong>
|
||||
Leave blank to use the Public FQDN for panel-to-Wings communication.
|
||||
If specified, this internal domain name will be used for panel-to-Wings communication instead
|
||||
(e.g <code>wings-internal.example.com</code> or <code>10.0.0.5</code>).
|
||||
Useful for internal networks where the panel needs to communicate with Wings using a
|
||||
different address than what browsers use.
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group col-xs-12">
|
||||
<label class="form-label"><span class="label label-warning"><i class="fa fa-power-off"></i></span> Communicate Over SSL</label>
|
||||
<div>
|
||||
<div class="radio radio-success radio-inline">
|
||||
<input type="radio" id="pSSLTrue" value="https" name="scheme" {{ (old('scheme', $node->scheme) === 'https') ? 'checked' : '' }}>
|
||||
<label for="pSSLTrue"> Use SSL Connection</label>
|
||||
</div>
|
||||
<div class="radio radio-danger radio-inline">
|
||||
<input type="radio" id="pSSLFalse" value="http" name="scheme" {{ (old('scheme', $node->scheme) !== 'https') ? 'checked' : '' }}>
|
||||
<label for="pSSLFalse"> Use HTTP Connection</label>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted small">In most cases you should select to use a SSL connection. If using an IP Address or you do not wish to use SSL at all, select a HTTP connection.</p>
|
||||
</div>
|
||||
<div class="form-group col-xs-12">
|
||||
<label class="form-label"><span class="label label-warning"><i class="fa fa-power-off"></i></span> Behind Proxy</label>
|
||||
<div>
|
||||
<div class="radio radio-success radio-inline">
|
||||
<input type="radio" id="pProxyFalse" value="0" name="behind_proxy" {{ (old('behind_proxy', $node->behind_proxy) == false) ? 'checked' : '' }}>
|
||||
<label for="pProxyFalse"> Not Behind Proxy </label>
|
||||
</div>
|
||||
<div class="radio radio-info radio-inline">
|
||||
<input type="radio" id="pProxyTrue" value="1" name="behind_proxy" {{ (old('behind_proxy', $node->behind_proxy) == true) ? 'checked' : '' }}>
|
||||
<label for="pProxyTrue"> Behind Proxy </label>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted small">If you are running the daemon behind a proxy such as Cloudflare, select this to have the daemon skip looking for certificates on boot.</p>
|
||||
</div>
|
||||
<div class="form-group col-xs-12">
|
||||
<label class="form-label"><span class="label label-warning"><i class="fa fa-wrench"></i></span> Maintenance Mode</label>
|
||||
<div>
|
||||
<div class="radio radio-success radio-inline">
|
||||
<input type="radio" id="pMaintenanceFalse" value="0" name="maintenance_mode" {{ (old('maintenance_mode', $node->maintenance_mode) == false) ? 'checked' : '' }}>
|
||||
<label for="pMaintenanceFalse"> Disabled</label>
|
||||
</div>
|
||||
<div class="radio radio-warning radio-inline">
|
||||
<input type="radio" id="pMaintenanceTrue" value="1" name="maintenance_mode" {{ (old('maintenance_mode', $node->maintenance_mode) == true) ? 'checked' : '' }}>
|
||||
<label for="pMaintenanceTrue"> Enabled</label>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted small">If the node is marked as 'Under Maintenance' users won't be able to access servers that are on this node.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="box">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Settings</h3>
|
||||
</div>
|
||||
<div class="box-body row">
|
||||
<div class="form-group col-xs-12">
|
||||
<label for="name" class="control-label">Node Name</label>
|
||||
<div>
|
||||
<input type="text" autocomplete="off" name="name" class="form-control"
|
||||
value="{{ old('name', $node->name) }}" />
|
||||
<p class="text-muted"><small>Character limits: <code>a-zA-Z0-9_.-</code> and <code>[Space]</code> (min 1,
|
||||
max 100 characters).</small></p>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="box">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Allocation Limits</h3>
|
||||
</div>
|
||||
<div class="box-body row">
|
||||
<div class="col-xs-12">
|
||||
<div class="row">
|
||||
<div class="form-group col-xs-6">
|
||||
<label for="memory" class="control-label">Total Memory</label>
|
||||
<div class="input-group">
|
||||
<input type="text" name="memory" class="form-control" data-multiplicator="true" value="{{ old('memory', $node->memory) }}"/>
|
||||
<span class="input-group-addon">MiB</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-xs-6">
|
||||
<label for="memory_overallocate" class="control-label">Overallocate</label>
|
||||
<div class="input-group">
|
||||
<input type="text" name="memory_overallocate" class="form-control" value="{{ old('memory_overallocate', $node->memory_overallocate) }}"/>
|
||||
<span class="input-group-addon">%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted small">Enter the total amount of memory available on this node for allocation to servers. You may also provide a percentage that can allow allocation of more than the defined memory.</p>
|
||||
</div>
|
||||
<div class="col-xs-12">
|
||||
<div class="row">
|
||||
<div class="form-group col-xs-6">
|
||||
<label for="disk" class="control-label">Disk Space</label>
|
||||
<div class="input-group">
|
||||
<input type="text" name="disk" class="form-control" data-multiplicator="true" value="{{ old('disk', $node->disk) }}"/>
|
||||
<span class="input-group-addon">MiB</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-xs-6">
|
||||
<label for="disk_overallocate" class="control-label">Overallocate</label>
|
||||
<div class="input-group">
|
||||
<input type="text" name="disk_overallocate" class="form-control" value="{{ old('disk_overallocate', $node->disk_overallocate) }}"/>
|
||||
<span class="input-group-addon">%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted small">Enter the total amount of disk space available on this node for server allocation. You may also provide a percentage that will determine the amount of disk space over the set limit to allow.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="box">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">General Configuration</h3>
|
||||
</div>
|
||||
<div class="box-body row">
|
||||
<div class="form-group col-xs-12">
|
||||
<label for="disk_overallocate" class="control-label">Maximum Web Upload Filesize</label>
|
||||
<div class="input-group">
|
||||
<input type="text" name="upload_size" class="form-control" value="{{ old('upload_size', $node->upload_size) }}"/>
|
||||
<span class="input-group-addon">MiB</span>
|
||||
</div>
|
||||
<p class="text-muted"><small>Enter the maximum size of files that can be uploaded through the web-based file manager.</small></p>
|
||||
</div>
|
||||
<div class="col-xs-12">
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="daemonListen" class="control-label"><span class="label label-warning"><i class="fa fa-power-off"></i></span> Daemon Port</label>
|
||||
<div>
|
||||
<input type="text" name="daemonListen" class="form-control" value="{{ old('daemonListen', $node->daemonListen) }}"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="daemonSFTP" class="control-label"><span class="label label-warning"><i class="fa fa-power-off"></i></span> Daemon SFTP Port</label>
|
||||
<div>
|
||||
<input type="text" name="daemonSFTP" class="form-control" value="{{ old('daemonSFTP', $node->daemonSFTP) }}"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p class="text-muted"><small>The daemon runs its own SFTP management container and does not use the SSHd process on the main physical server. <Strong>Do not use the same port that you have assigned for your physical server's SSH process.</strong></small></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-xs-12">
|
||||
<label for="description" class="control-label">Description</label>
|
||||
<div>
|
||||
<textarea name="description" id="description" rows="4"
|
||||
class="form-control">{{ $node->description }}</textarea>
|
||||
</div>
|
||||
<div class="col-xs-12">
|
||||
<div class="box box-primary">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Save Settings</h3>
|
||||
</div>
|
||||
<div class="box-body row">
|
||||
<div class="form-group col-sm-6">
|
||||
<div>
|
||||
<input type="checkbox" name="reset_secret" id="reset_secret" /> <label for="reset_secret" class="control-label">Reset Daemon Master Key</label>
|
||||
</div>
|
||||
<p class="text-muted"><small>Resetting the daemon master key will void any request coming from the old key. This key is used for all sensitive operations on the daemon including server creation and deletion. We suggest changing this key regularly for security.</small></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
{!! method_field('PATCH') !!}
|
||||
{!! csrf_field() !!}
|
||||
<button type="submit" class="btn btn-primary pull-right">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-xs-12">
|
||||
<label for="name" class="control-label">Location</label>
|
||||
<div>
|
||||
<select name="location_id" class="form-control">
|
||||
@foreach($locations as $location)
|
||||
<option value="{{ $location->id }}" {{ (old('location_id', $node->location_id) === $location->id) ? 'selected' : '' }}>{{ $location->long }} ({{ $location->short }})</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-xs-12">
|
||||
<label for="public" class="control-label">Allow Automatic Allocation <sup><a data-toggle="tooltip"
|
||||
data-placement="top" title="Allow automatic allocation to this Node?">?</a></sup></label>
|
||||
<div>
|
||||
<input type="radio" name="public" value="1" {{ (old('public', $node->public)) ? 'checked' : '' }}
|
||||
id="public_1" checked> <label for="public_1" style="padding-left:5px;">Yes</label><br />
|
||||
<input type="radio" name="public" value="0" {{ (old('public', $node->public)) ? '' : 'checked' }}
|
||||
id="public_0"> <label for="public_0" style="padding-left:5px;">No</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-xs-12">
|
||||
<label for="fqdn" class="control-label">Public Fully Qualified Domain Name</label>
|
||||
<div>
|
||||
<input type="text" autocomplete="off" name="fqdn" class="form-control"
|
||||
value="{{ old('fqdn', $node->fqdn) }}" />
|
||||
</div>
|
||||
<p class="text-muted">
|
||||
<small>
|
||||
Domain name that browsers will use to connect to Wings (e.g <code>wings.example.com</code>).
|
||||
An IP address may be used <em>only</em> if you are not using SSL for this node.
|
||||
<a tabindex="0" data-toggle="popover" data-trigger="focus" title="Why do I need a FQDN?"
|
||||
data-content="In order to secure communications between your server and this node we use SSL. We cannot generate a SSL certificate for IP Addresses, and as such you will need to provide a FQDN.">Why?</a>
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group col-xs-12">
|
||||
<label for="internal_fqdn" class="control-label">
|
||||
Internal FQDN
|
||||
<strong>(Optional)</strong>
|
||||
</label>
|
||||
<div>
|
||||
<input type="text" autocomplete="off" name="internal_fqdn" class="form-control"
|
||||
value="{{ old('internal_fqdn', $node->internal_fqdn) }}" />
|
||||
</div>
|
||||
<p class="text-muted">
|
||||
<small>
|
||||
<strong>Optional:</strong>
|
||||
Leave blank to use the Public FQDN for panel-to-Wings communication.
|
||||
If specified, this internal domain name will be used for panel-to-Wings communication instead
|
||||
(e.g <code>wings-internal.example.com</code> or <code>10.0.0.5</code>).
|
||||
Useful for internal networks where the panel needs to communicate with Wings using a
|
||||
different address than what browsers use.
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group col-xs-12">
|
||||
<label class="form-label"><span class="label label-warning"><i class="fa fa-power-off"></i></span>
|
||||
Communicate Over SSL</label>
|
||||
<div>
|
||||
<div class="radio radio-success radio-inline">
|
||||
<input type="radio" id="pSSLTrue" value="https" name="scheme" {{ (old('scheme', $node->scheme) === 'https') ? 'checked' : '' }}>
|
||||
<label for="pSSLTrue"> Use SSL Connection</label>
|
||||
</div>
|
||||
<div class="radio radio-danger radio-inline">
|
||||
<input type="radio" id="pSSLFalse" value="http" name="scheme" {{ (old('scheme', $node->scheme) !== 'https') ? 'checked' : '' }}>
|
||||
<label for="pSSLFalse"> Use HTTP Connection</label>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted small">In most cases you should select to use a SSL connection. If using an IP Address
|
||||
or you do not wish to use SSL at all, select a HTTP connection.</p>
|
||||
</div>
|
||||
<div class="form-group col-xs-12">
|
||||
<label class="form-label"><span class="label label-warning"><i class="fa fa-power-off"></i></span> Behind
|
||||
Proxy</label>
|
||||
<div>
|
||||
<div class="radio radio-success radio-inline">
|
||||
<input type="radio" id="pProxyFalse" value="0" name="behind_proxy" {{ (old('behind_proxy', $node->behind_proxy) == false) ? 'checked' : '' }}>
|
||||
<label for="pProxyFalse"> Not Behind Proxy </label>
|
||||
</div>
|
||||
<div class="radio radio-info radio-inline">
|
||||
<input type="radio" id="pProxyTrue" value="1" name="behind_proxy" {{ (old('behind_proxy', $node->behind_proxy) == true) ? 'checked' : '' }}>
|
||||
<label for="pProxyTrue"> Behind Proxy </label>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted small">If you are running the daemon behind a proxy such as Cloudflare, select this to
|
||||
have the daemon skip looking for certificates on boot.</p>
|
||||
</div>
|
||||
<div class="form-group col-xs-12">
|
||||
<label class="form-label"><span class="label label-warning"><i class="fa fa-wrench"></i></span> Maintenance
|
||||
Mode</label>
|
||||
<div>
|
||||
<div class="radio radio-success radio-inline">
|
||||
<input type="radio" id="pMaintenanceFalse" value="0" name="maintenance_mode" {{ (old('maintenance_mode', $node->maintenance_mode) == false) ? 'checked' : '' }}>
|
||||
<label for="pMaintenanceFalse"> Disabled</label>
|
||||
</div>
|
||||
<div class="radio radio-warning radio-inline">
|
||||
<input type="radio" id="pMaintenanceTrue" value="1" name="maintenance_mode" {{ (old('maintenance_mode', $node->maintenance_mode) == true) ? 'checked' : '' }}>
|
||||
<label for="pMaintenanceTrue"> Enabled</label>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted small">If the node is marked as 'Under Maintenance' users won't be able to access
|
||||
servers that are on this node.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="form-group col-xs-12">
|
||||
<label for="description" class="control-label">Description</label>
|
||||
<div>
|
||||
<textarea name="description" id="description" rows="4" class="form-control">{{ $node->description }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-xs-12">
|
||||
<label for="name" class="control-label">Location</label>
|
||||
<div>
|
||||
<select name="location_id" class="form-control">
|
||||
@foreach($locations as $location)
|
||||
<option value="{{ $location->id }}" {{ (old('location_id', $node->location_id) === $location->id) ? 'selected' : '' }}>{{ $location->long }} ({{ $location->short }})</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-xs-12">
|
||||
<label for="public" class="control-label">Allow Automatic Allocation <sup><a data-toggle="tooltip"
|
||||
data-placement="top" title="Allow automatic allocation to this Node?">?</a></sup></label>
|
||||
<div>
|
||||
<input type="radio" name="public" value="1" {{ (old('public', $node->public)) ? 'checked' : '' }} id="public_1"
|
||||
checked> <label for="public_1" style="padding-left:5px;">Yes</label><br />
|
||||
<input type="radio" name="public" value="0" {{ (old('public', $node->public)) ? '' : 'checked' }} id="public_0">
|
||||
<label for="public_0" style="padding-left:5px;">No</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-xs-12">
|
||||
<label for="fqdn" class="control-label">Fully Qualified Domain Name</label>
|
||||
<div>
|
||||
<input type="text" autocomplete="off" name="fqdn" class="form-control" value="{{ old('fqdn', $node->fqdn) }}" />
|
||||
</div>
|
||||
<p class="text-muted"><small>Please enter domain name (e.g <code>node.example.com</code>) to be used for
|
||||
connecting to the daemon. An IP address may only be used if you are not using SSL for this node.
|
||||
<a tabindex="0" data-toggle="popover" data-trigger="focus" title="Why do I need a FQDN?"
|
||||
data-content="In order to secure communications between your server and this node we use SSL. We cannot generate a SSL certificate for IP Addresses, and as such you will need to provide a FQDN.">Why?</a>
|
||||
</small></p>
|
||||
</div>
|
||||
<div class="form-group col-xs-12">
|
||||
<label class="form-label"><span class="label label-warning"><i class="fa fa-power-off"></i></span>
|
||||
Communicate Over SSL</label>
|
||||
<div>
|
||||
<div class="radio radio-success radio-inline">
|
||||
<input type="radio" id="pSSLTrue" value="https" name="scheme" {{ (old('scheme', $node->scheme) === 'https') ? 'checked' : '' }}>
|
||||
<label for="pSSLTrue"> Use SSL Connection</label>
|
||||
</div>
|
||||
<div class="radio radio-danger radio-inline">
|
||||
<input type="radio" id="pSSLFalse" value="http" name="scheme" {{ (old('scheme', $node->scheme) !== 'https') ? 'checked' : '' }}>
|
||||
<label for="pSSLFalse"> Use HTTP Connection</label>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted small">In most cases you should select to use a SSL connection. If using an IP Address
|
||||
or you do not wish to use SSL at all, select a HTTP connection.</p>
|
||||
</div>
|
||||
<div class="form-group col-xs-12">
|
||||
<label class="form-label"><span class="label label-warning"><i class="fa fa-power-off"></i></span> Behind
|
||||
Proxy</label>
|
||||
<div>
|
||||
<div class="radio radio-success radio-inline">
|
||||
<input type="radio" id="pProxyFalse" value="0" name="behind_proxy" {{ (old('behind_proxy', $node->behind_proxy) == false) ? 'checked' : '' }}>
|
||||
<label for="pProxyFalse"> Not Behind Proxy </label>
|
||||
</div>
|
||||
<div class="radio radio-info radio-inline">
|
||||
<input type="radio" id="pProxyTrue" value="1" name="behind_proxy" {{ (old('behind_proxy', $node->behind_proxy) == true) ? 'checked' : '' }}>
|
||||
<label for="pProxyTrue"> Behind Proxy </label>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted small">If you are running the daemon behind a proxy such as Cloudflare, select this to
|
||||
have the daemon skip looking for certificates on boot.</p>
|
||||
</div>
|
||||
<div class="form-group col-xs-12">
|
||||
<label class="form-label"><span class="label label-warning"><i class="fa fa-wrench"></i></span> Maintenance
|
||||
Mode</label>
|
||||
<div>
|
||||
<div class="radio radio-success radio-inline">
|
||||
<input type="radio" id="pMaintenanceFalse" value="0" name="maintenance_mode" {{ (old('maintenance_mode', $node->maintenance_mode) == false) ? 'checked' : '' }}>
|
||||
<label for="pMaintenanceFalse"> Disabled</label>
|
||||
</div>
|
||||
<div class="radio radio-warning radio-inline">
|
||||
<input type="radio" id="pMaintenanceTrue" value="1" name="maintenance_mode" {{ (old('maintenance_mode', $node->maintenance_mode) == true) ? 'checked' : '' }}>
|
||||
<label for="pMaintenanceTrue"> Enabled</label>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted small">If the node is marked as 'Under Maintenance' users won't be able to access
|
||||
servers that are on this node.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="box">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Allocation Limits</h3>
|
||||
</div>
|
||||
<div class="box-body row">
|
||||
<div class="col-xs-12">
|
||||
<div class="row">
|
||||
<div class="form-group col-xs-6">
|
||||
<label for="memory" class="control-label">Total Memory</label>
|
||||
<div class="input-group">
|
||||
<input type="text" name="memory" class="form-control" data-multiplicator="true"
|
||||
value="{{ old('memory', $node->memory) }}" />
|
||||
<span class="input-group-addon">MiB</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-xs-6">
|
||||
<label for="memory_overallocate" class="control-label">Overallocate</label>
|
||||
<div class="input-group">
|
||||
<input type="text" name="memory_overallocate" class="form-control"
|
||||
value="{{ old('memory_overallocate', $node->memory_overallocate) }}" />
|
||||
<span class="input-group-addon">%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted small">Enter the total amount of memory available on this node for allocation to
|
||||
servers. You may also provide a percentage that can allow allocation of more than the defined memory.</p>
|
||||
</div>
|
||||
<div class="col-xs-12">
|
||||
<div class="row">
|
||||
<div class="form-group col-xs-6">
|
||||
<label for="disk" class="control-label">Disk Space</label>
|
||||
<div class="input-group">
|
||||
<input type="text" name="disk" class="form-control" data-multiplicator="true"
|
||||
value="{{ old('disk', $node->disk) }}" />
|
||||
<span class="input-group-addon">MiB</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-xs-6">
|
||||
<label for="disk_overallocate" class="control-label">Overallocate</label>
|
||||
<div class="input-group">
|
||||
<input type="text" name="disk_overallocate" class="form-control"
|
||||
value="{{ old('disk_overallocate', $node->disk_overallocate) }}" />
|
||||
<span class="input-group-addon">%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted small">Enter the total amount of disk space available on this node for server
|
||||
allocation. You may also provide a percentage that will determine the amount of disk space over the set
|
||||
limit to allow.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="box">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">General Configuration</h3>
|
||||
</div>
|
||||
<div class="box-body row">
|
||||
<div class="form-group col-xs-12">
|
||||
<label for="disk_overallocate" class="control-label">Maximum Web Upload Filesize</label>
|
||||
<div class="input-group">
|
||||
<input type="text" name="upload_size" class="form-control"
|
||||
value="{{ old('upload_size', $node->upload_size) }}" />
|
||||
<span class="input-group-addon">MiB</span>
|
||||
</div>
|
||||
<p class="text-muted"><small>Enter the maximum size of files that can be uploaded through the web-based file
|
||||
manager.</small></p>
|
||||
</div>
|
||||
<div class="col-xs-12">
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="daemonListen" class="control-label"><span class="label label-warning"><i
|
||||
class="fa fa-power-off"></i></span> Daemon Port</label>
|
||||
<div>
|
||||
<input type="text" name="daemonListen" class="form-control"
|
||||
value="{{ old('daemonListen', $node->daemonListen) }}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="daemonSFTP" class="control-label"><span class="label label-warning"><i
|
||||
class="fa fa-power-off"></i></span> Daemon SFTP Port</label>
|
||||
<div>
|
||||
<input type="text" name="daemonSFTP" class="form-control"
|
||||
value="{{ old('daemonSFTP', $node->daemonSFTP) }}" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p class="text-muted"><small>The daemon runs its own SFTP management container and does not use the SSHd
|
||||
process on the main physical server. <Strong>Do not use the same port that you have assigned for
|
||||
your physical server's SSH process.</strong></small></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12">
|
||||
<div class="box box-primary">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Save Settings</h3>
|
||||
</div>
|
||||
<div class="box-body row">
|
||||
<div class="form-group col-sm-6">
|
||||
<div>
|
||||
<input type="checkbox" name="reset_secret" id="reset_secret" /> <label for="reset_secret"
|
||||
class="control-label">Reset Daemon Master Key</label>
|
||||
</div>
|
||||
<p class="text-muted"><small>Resetting the daemon master key will void any request coming from the old key.
|
||||
This key is used for all sensitive operations on the daemon including server creation and deletion. We
|
||||
suggest changing this key regularly for security.</small></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
{!! method_field('PATCH') !!}
|
||||
{!! csrf_field() !!}
|
||||
<button type="submit" class="btn btn-primary pull-right">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@endsection
|
||||
|
||||
@section('footer-scripts')
|
||||
@parent
|
||||
<script>
|
||||
@parent
|
||||
<script>
|
||||
$('[data-toggle="popover"]').popover({
|
||||
placement: 'auto'
|
||||
placement: 'auto'
|
||||
});
|
||||
$('select[name="location_id"]').select2();
|
||||
</script>
|
||||
@endsection
|
||||
</script>
|
||||
@endsection
|
||||
@@ -18,54 +18,6 @@
|
||||
<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>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
@endsection
|
||||
|
||||
@section('content-header')
|
||||
<h1>Captcha Settings<small>Configure captcha settings for Pyrodactyl.</small></h1>
|
||||
<h1>Captcha Settings<small>Configure captcha settings for your panel.</small></h1>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{{ route('admin.index') }}">Admin</a></li>
|
||||
<li class="active">Settings</li>
|
||||
@@ -18,125 +18,245 @@
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<form action="" method="POST">
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
<div class="box">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">reCAPTCHA</h3>
|
||||
<h3 class="box-title">CAPTCHA Provider</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<div class="form-group col-md-4">
|
||||
<label class="control-label">Status</label>
|
||||
<label class="control-label">Provider</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 class="form-control" name="driver" id="captcha_provider">
|
||||
<option value="none" @if(isset($current) && $current['driver'] === 'none') selected @endif>Disabled
|
||||
</option>
|
||||
<option value="hcaptcha" @if(isset($current) && $current['driver'] === 'hcaptcha') selected @endif>
|
||||
hCaptcha</option>
|
||||
<option value="mcaptcha" @if(isset($current) && $current['driver'] === 'mcaptcha') selected @endif>
|
||||
mCaptcha</option>
|
||||
<option value="turnstile" @if(isset($current) && $current['driver'] === 'turnstile') selected @endif>
|
||||
Cloudflare
|
||||
Turnstile</option>
|
||||
<!-- <option value="proton" @if(isset($current) && $current['driver'] === 'proton') selected @endif>Proton Captcha -->
|
||||
<!-- </option> -->
|
||||
<option value="friendly" @if(isset($current) && $current['driver'] === 'friendly') selected @endif>
|
||||
Friendly Captcha
|
||||
</option>
|
||||
<!-- <option value="recaptcha" @if(isset($current) && $current['driver'] === 'recaptcha') selected @endif>
|
||||
ReCaptcha
|
||||
</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>
|
||||
<p class="text-muted small">Select which CAPTCHA provider you want to use.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- hCaptcha Settings -->
|
||||
<div class="box provider-settings" id="hcaptcha_settings"
|
||||
style="@if(isset($current) && $current['driver'] !== 'hcaptcha') display: none; @endif">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">hCaptcha Settings</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label class="control-label">Site Key</label>
|
||||
<div>
|
||||
<input type="text" class="form-control" name="hcaptcha[site_key]"
|
||||
value="{{ old('hcaptcha.site_key', isset($current) ? $current['hcaptcha']['site_key'] : '') }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label class="control-label">Secret Key</label>
|
||||
<div>
|
||||
<input type="text" class="form-control" name="hcaptcha[secret_key]"
|
||||
value="{{ old('hcaptcha.secret_key', isset($current) ? $current['hcaptcha']['secret_key'] : '') }}">
|
||||
<p class="text-muted small">Get your keys from <a href="https://dashboard.hcaptcha.com/"
|
||||
target="_blank">hCaptcha dashboard</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- mCaptcha Settings -->
|
||||
<div class="box provider-settings" id="mcaptcha_settings"
|
||||
style="@if(isset($current) && $current['driver'] !== 'mcaptcha') display: none; @endif">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">mCaptcha Settings</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<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')) }}">
|
||||
<input type="text" class="form-control" name="mcaptcha[site_key]"
|
||||
value="{{ old('mcaptcha.site_key', isset($current) ? $current['mcaptcha']['site_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>
|
||||
<input type="text" class="form-control" name="mcaptcha[secret_key]"
|
||||
value="{{ old('mcaptcha.secret_key', isset($current) ? $current['mcaptcha']['secret_key'] : '') }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label class="control-label">Endpoint</label>
|
||||
<div>
|
||||
<input type="text" class="form-control" name="mcaptcha[endpoint]"
|
||||
value="{{ old('mcaptcha.endpoint', isset($current) ? $current['mcaptcha']['endpoint'] : '') }}">
|
||||
<p class="text-muted small">URL to your mCaptcha instance API endpoint.</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 target="_blank" 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">
|
||||
|
||||
<!-- Cloudflare Turnstile Settings -->
|
||||
<div class="box provider-settings" id="turnstile_settings"
|
||||
style="@if(isset($current) && $current['driver'] !== 'turnstile') display: none; @endif">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">HTTP Connections</h3>
|
||||
<h3 class="box-title">Cloudflare Turnstile Settings</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label class="control-label">Connection Timeout</label>
|
||||
<label class="control-label">Site Key</label>
|
||||
<div>
|
||||
<input type="number" required class="form-control" name="pterodactyl:guzzle:connect_timeout"
|
||||
value="{{ old('pterodactyl:guzzle:connect_timeout', config('pterodactyl.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>
|
||||
<input type="text" class="form-control" name="turnstile[site_key]"
|
||||
value="{{ old('turnstile.site_key', isset($current) ? $current['turnstile']['site_key'] : '') }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label class="control-label">Request Timeout</label>
|
||||
<label class="control-label">Secret Key</label>
|
||||
<div>
|
||||
<input type="number" required class="form-control" name="pterodactyl:guzzle:timeout"
|
||||
value="{{ old('pterodactyl:guzzle:timeout', config('pterodactyl.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>
|
||||
<input type="text" class="form-control" name="turnstile[secret_key]"
|
||||
value="{{ old('turnstile.secret_key', isset($current) ? $current['turnstile']['secret_key'] : '') }}">
|
||||
<p class="text-muted small">Get your keys from <a
|
||||
href="https://dash.cloudflare.com/?to=/:account/turnstile" target="_blank">Cloudflare dashboard</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
|
||||
<!-- Proton Captcha Settings -->
|
||||
<div class="box provider-settings" id="proton_settings"
|
||||
style="@if(isset($current) && $current['driver'] !== 'proton') display: none; @endif">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Automatic Allocation Creation</h3>
|
||||
<h3 class="box-title">Proton Captcha Settings</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<div class="form-group col-md-4">
|
||||
<label class="control-label">Status</label>
|
||||
<div class="form-group col-md-6">
|
||||
<label class="control-label">Site Key</label>
|
||||
<div>
|
||||
<select class="form-control" name="pterodactyl:client_features:allocations:enabled">
|
||||
<option value="false">Disabled</option>
|
||||
<option value="true" @if(old('pterodactyl:client_features:allocations:enabled', config('pterodactyl.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>
|
||||
<input type="text" class="form-control" name="proton[site_key]"
|
||||
value="{{ old('proton.site_key', isset($current) && isset($current['proton']) ? $current['proton']['site_key'] : '') }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label class="control-label">Starting Port</label>
|
||||
<div class="form-group col-md-6">
|
||||
<label class="control-label">Secret Key</label>
|
||||
<div>
|
||||
<input type="number" class="form-control" name="pterodactyl:client_features:allocations:range_start"
|
||||
value="{{ old('pterodactyl:client_features:allocations:range_start', config('pterodactyl.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="pterodactyl:client_features:allocations:range_end"
|
||||
value="{{ old('pterodactyl:client_features:allocations:range_end', config('pterodactyl.client_features.allocations.range_end')) }}">
|
||||
<p class="text-muted small">The ending port in the range that can be automatically allocated.</p>
|
||||
<input type="text" class="form-control" name="proton[secret_key]"
|
||||
value="{{ old('proton.secret_key', isset($current) && isset($current['proton']) ? $current['proton']['secret_key'] : '') }}">
|
||||
<p class="text-muted small">Get your keys from <a href="https://account.proton.me/signup"
|
||||
target="_blank">Proton account</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Friendly Captcha Settings -->
|
||||
<div class="box provider-settings" id="friendly_settings"
|
||||
style="@if(isset($current) && $current['driver'] !== 'friendly') display: none; @endif">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Friendly Captcha Settings</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label class="control-label">Site Key</label>
|
||||
<div>
|
||||
<input type="text" class="form-control" name="friendly[site_key]"
|
||||
value="{{ old('friendly.site_key', isset($current) ? $current['friendly']['site_key'] : '') }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label class="control-label">Secret Key</label>
|
||||
<div>
|
||||
<input type="text" class="form-control" name="friendly[secret_key]"
|
||||
value="{{ old('friendly.secret_key', isset($current) ? $current['friendly']['secret_key'] : '') }}">
|
||||
<p class="text-muted small">Get your keys from <a href="https://friendlycaptcha.com/"
|
||||
target="_blank">Friendly Captcha</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Recaptcha Settings -->
|
||||
<div class="box provider-settings" id="recaptcha_settings"
|
||||
style="@if(isset($current) && $current['driver'] !== 'recaptcha') display: none; @endif">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Recaptcha Settings</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label class="control-label">Site Key</label>
|
||||
<div>
|
||||
<input type="text" class="form-control" name="recaptcha[site_key]"
|
||||
value="{{ old('recaptcha.site_key', isset($current) ? $current['recaptcha']['site_key'] : '') }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label class="control-label">Secret Key</label>
|
||||
<div>
|
||||
<input type="text" class="form-control" name="recaptcha[secret_key]"
|
||||
value="{{ old('recaptcha.secret_key', isset($current) ? $current['recaptcha']['secret_key'] : '') }}">
|
||||
<p class="text-muted small">Get your keys from <a href="https://www.google.com/recaptcha/admin/create"
|
||||
target="_blank">Google api Dashboard</a>.
|
||||
</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>
|
||||
<button type="submit" class="btn btn-sm btn-primary pull-right">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('footer-scripts')
|
||||
@parent
|
||||
<script>
|
||||
document.getElementById('captcha_provider').addEventListener('change', function () {
|
||||
|
||||
document.querySelectorAll('.provider-settings').forEach(el => {
|
||||
el.style.display = 'none';
|
||||
});
|
||||
|
||||
|
||||
const selectedProvider = this.value;
|
||||
if (selectedProvider !== 'none') {
|
||||
document.getElementById(selectedProvider + '_settings').style.display = 'block';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
@@ -38,8 +38,8 @@
|
||||
<div>
|
||||
<div class="btn-group" data-toggle="buttons">
|
||||
@php
|
||||
$level = old('pterodactyl:auth:2fa_required', config('pterodactyl.auth.2fa_required'));
|
||||
@endphp
|
||||
$level = old('pterodactyl:auth:2fa_required', config('pterodactyl.auth.2fa_required'));
|
||||
@endphp
|
||||
<label class="btn btn-primary @if ($level == 0) active @endif">
|
||||
<input type="radio" name="pterodactyl:auth:2fa_required" autocomplete="off" value="0" @if ($level == 0) checked @endif> Not Required
|
||||
</label>
|
||||
@@ -60,7 +60,7 @@
|
||||
<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
|
||||
@endforeach
|
||||
</select>
|
||||
<p class="text-muted"><small>The default language to use when rendering UI components.</small></p>
|
||||
</div>
|
||||
|
||||
@@ -2,201 +2,210 @@
|
||||
@include('partials/admin.settings.nav', ['activeTab' => 'mail'])
|
||||
|
||||
@section('title')
|
||||
Mail Settings
|
||||
Mail Settings
|
||||
@endsection
|
||||
|
||||
@section('content-header')
|
||||
<h1>Mail Settings<small>Configure how Pterodactyl should handle sending emails.</small></h1>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{{ route('admin.index') }}">Admin</a></li>
|
||||
<li class="active">Settings</li>
|
||||
</ol>
|
||||
<h1>Mail Settings<small>Configure how Pterodactyl should handle sending emails.</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>
|
||||
@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
|
||||
@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');
|
||||
});
|
||||
}
|
||||
<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 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 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 + ' ';
|
||||
}
|
||||
});
|
||||
}
|
||||
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'
|
||||
});
|
||||
}
|
||||
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
|
||||
$(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
|
||||
@@ -158,21 +158,21 @@
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
@if (count($errors) > 0)
|
||||
<div class="alert alert-danger">
|
||||
There was an error validating the data provided.<br><br>
|
||||
<ul>
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
<div class="alert alert-danger">
|
||||
There was an error validating the data provided.<br><br>
|
||||
<ul>
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
@foreach (Alert::getMessages() as $type => $messages)
|
||||
@foreach ($messages as $message)
|
||||
<div class="alert alert-{{ $type }} alert-dismissable" role="alert">
|
||||
{!! $message !!}
|
||||
</div>
|
||||
@endforeach
|
||||
@foreach ($messages as $message)
|
||||
<div class="alert alert-{{ $type }} alert-dismissable" role="alert">
|
||||
{!! $message !!}
|
||||
</div>
|
||||
@endforeach
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<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>
|
||||
<li @if($activeTab === 'captcha')class="active"@endif><a href="{{ route('admin.settings.captcha') }}">Captcha</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -69,11 +69,15 @@ Route::group(['prefix' => 'settings'], function () {
|
||||
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::get('/captcha', [Admin\Settings\CaptchaController::class, 'index'])->name('admin.settings.captcha');
|
||||
|
||||
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']);
|
||||
Route::patch('/captcha', [Admin\Settings\CaptchaController::class, 'update'])
|
||||
->name('admin.settings.captcha.update');
|
||||
});
|
||||
|
||||
/*
|
||||
|
||||
@@ -19,19 +19,19 @@ Route::get('/password', [Auth\LoginController::class, 'index'])->name('auth.forg
|
||||
Route::get('/password/reset/{token}', [Auth\LoginController::class, 'index'])->name('auth.reset');
|
||||
|
||||
// Apply a throttle to authentication action endpoints, in addition to the
|
||||
// recaptcha endpoints to slow down manual attack spammers even more. 🤷
|
||||
// captcha endpoints to slow down manual attack spammers even more. 🤷
|
||||
//
|
||||
// @see \Pterodactyl\Providers\RouteServiceProvider
|
||||
Route::middleware(['throttle:authentication'])->group(function () {
|
||||
// Login endpoints.
|
||||
Route::post('/login', [Auth\LoginController::class, 'login'])->middleware('recaptcha');
|
||||
Route::post('/login', [Auth\LoginController::class, 'login'])->middleware('captcha');
|
||||
Route::post('/login/checkpoint', Auth\LoginCheckpointController::class)->name('auth.login-checkpoint');
|
||||
|
||||
// Forgot password route. A post to this endpoint will trigger an
|
||||
// email to be sent containing a reset token.
|
||||
Route::post('/password', [Auth\ForgotPasswordController::class, 'sendResetLinkEmail'])
|
||||
->name('auth.post.forgot-password')
|
||||
->middleware('recaptcha');
|
||||
->middleware('captcha');
|
||||
});
|
||||
|
||||
// Password reset routes. This endpoint is hit after going through
|
||||
@@ -47,4 +47,4 @@ Route::post('/logout', [Auth\LoginController::class, 'logout'])
|
||||
->name('auth.logout');
|
||||
|
||||
// Catch any other combinations of routes and pass them off to the React component.
|
||||
Route::fallback([Auth\LoginController::class, 'index']);
|
||||
Route::fallback([Auth\LoginController::class, 'index']);
|
||||
Reference in New Issue
Block a user