Compare commits

..

No commits in common. "main" and "v1.0.0-beta21" have entirely different histories.

1550 changed files with 18929 additions and 52001 deletions

View File

@ -64,9 +64,10 @@ body:
label: Error Logs label: Error Logs
description: | description: |
Run the following command to collect logs on your system. Run the following command to collect logs on your system.
Wings: `sudo wings diagnostics --hastebin-url=https://logs.pelican.dev`
Panel: `tail -n 300 /var/www/pelican/storage/logs/laravel-$(date +%F).log | curl --data-binary @- https://logs.pelican.dev` Wings: `sudo wings diagnostics`
placeholder: "https://logs.pelican.dev/c17f750e" Panel: `tail -n 150 /var/www/pelican/storage/logs/laravel-$(date +%F).log | curl -X POST -F 'c=@-' paste.pelistuff.com`
placeholder: "https://pelipaste.com/a1h6z"
render: bash render: bash
validations: validations:
required: false required: false

View File

@ -11,9 +11,9 @@ jobs:
name: UI name: UI
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: true fail-fast: false
matrix: matrix:
node-version: [20, 22] node-version: [18, 20]
steps: steps:
- name: Code Checkout - name: Code Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4

View File

@ -6,75 +6,12 @@ on:
- main - main
pull_request: pull_request:
env:
APP_ENV: testing
APP_DEBUG: "false"
APP_KEY: ThisIsARandomStringForTests12345
APP_TIMEZONE: UTC
APP_URL: http://localhost/
CACHE_DRIVER: array
MAIL_MAILER: array
SESSION_DRIVER: array
QUEUE_CONNECTION: sync
GUZZLE_TIMEOUT: 60
GUZZLE_CONNECT_TIMEOUT: 60
jobs: jobs:
sqlite:
name: SQLite
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
php: [8.2, 8.3, 8.4]
env:
DB_CONNECTION: sqlite
DB_DATABASE: testing.sqlite
steps:
- name: Code Checkout
uses: actions/checkout@v4
- name: Get cache directory
id: composer-cache
run: |
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-${{ matrix.php }}-
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2
coverage: none
- name: Install dependencies
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
- name: Create SQLite file
run: touch database/testing.sqlite
- name: Unit tests
run: vendor/bin/pest tests/Unit
env:
DB_HOST: UNIT_NO_DB
SKIP_MIGRATIONS: true
- name: Integration tests
run: vendor/bin/pest tests/Integration
mysql: mysql:
name: MySQL name: MySQL
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: true fail-fast: false
matrix: matrix:
php: [8.2, 8.3, 8.4] php: [8.2, 8.3, 8.4]
database: ["mysql:8"] database: ["mysql:8"]
@ -88,10 +25,21 @@ jobs:
- 3306 - 3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
env: env:
APP_ENV: testing
APP_DEBUG: "false"
APP_KEY: ThisIsARandomStringForTests12345
APP_TIMEZONE: UTC
APP_URL: http://localhost/
CACHE_DRIVER: array
MAIL_MAILER: array
SESSION_DRIVER: array
QUEUE_CONNECTION: sync
DB_CONNECTION: mysql DB_CONNECTION: mysql
DB_HOST: 127.0.0.1 DB_HOST: 127.0.0.1
DB_DATABASE: testing DB_DATABASE: testing
DB_USERNAME: root DB_USERNAME: root
GUZZLE_TIMEOUT: 60
GUZZLE_CONNECT_TIMEOUT: 60
steps: steps:
- name: Code Checkout - name: Code Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -136,7 +84,7 @@ jobs:
name: MariaDB name: MariaDB
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: true fail-fast: false
matrix: matrix:
php: [8.2, 8.3, 8.4] php: [8.2, 8.3, 8.4]
database: ["mariadb:10.6", "mariadb:10.11", "mariadb:11.4"] database: ["mariadb:10.6", "mariadb:10.11", "mariadb:11.4"]
@ -150,10 +98,21 @@ jobs:
- 3306 - 3306
options: --health-cmd="mariadb-admin ping || mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 options: --health-cmd="mariadb-admin ping || mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
env: env:
APP_ENV: testing
APP_DEBUG: "false"
APP_KEY: ThisIsARandomStringForTests12345
APP_TIMEZONE: UTC
APP_URL: http://localhost/
CACHE_DRIVER: array
MAIL_MAILER: array
SESSION_DRIVER: array
QUEUE_CONNECTION: sync
DB_CONNECTION: mariadb DB_CONNECTION: mariadb
DB_HOST: 127.0.0.1 DB_HOST: 127.0.0.1
DB_DATABASE: testing DB_DATABASE: testing
DB_USERNAME: root DB_USERNAME: root
GUZZLE_TIMEOUT: 60
GUZZLE_CONNECT_TIMEOUT: 60
steps: steps:
- name: Code Checkout - name: Code Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -194,11 +153,72 @@ jobs:
DB_PORT: ${{ job.services.database.ports[3306] }} DB_PORT: ${{ job.services.database.ports[3306] }}
DB_USERNAME: root DB_USERNAME: root
sqlite:
name: SQLite
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: [8.2, 8.3, 8.4]
env:
APP_ENV: testing
APP_DEBUG: "false"
APP_KEY: ThisIsARandomStringForTests12345
APP_TIMEZONE: UTC
APP_URL: http://localhost/
CACHE_DRIVER: array
MAIL_MAILER: array
SESSION_DRIVER: array
QUEUE_CONNECTION: sync
DB_CONNECTION: sqlite
DB_DATABASE: testing.sqlite
GUZZLE_TIMEOUT: 60
GUZZLE_CONNECT_TIMEOUT: 60
steps:
- name: Code Checkout
uses: actions/checkout@v4
- name: Get cache directory
id: composer-cache
run: |
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-${{ matrix.php }}-
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2
coverage: none
- name: Install dependencies
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
- name: Create SQLite file
run: touch database/testing.sqlite
- name: Unit tests
run: vendor/bin/pest tests/Unit
env:
DB_HOST: UNIT_NO_DB
SKIP_MIGRATIONS: true
- name: Integration tests
run: vendor/bin/pest tests/Integration
postgresql: postgresql:
name: PostgreSQL name: PostgreSQL
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: true fail-fast: false
matrix: matrix:
php: [8.2, 8.3, 8.4] php: [8.2, 8.3, 8.4]
database: ["postgres:14"] database: ["postgres:14"]
@ -218,11 +238,22 @@ jobs:
--health-timeout 5s --health-timeout 5s
--health-retries 5 --health-retries 5
env: env:
APP_ENV: testing
APP_DEBUG: "false"
APP_KEY: ThisIsARandomStringForTests12345
APP_TIMEZONE: UTC
APP_URL: http://localhost/
CACHE_DRIVER: array
MAIL_MAILER: array
SESSION_DRIVER: array
QUEUE_CONNECTION: sync
DB_CONNECTION: pgsql DB_CONNECTION: pgsql
DB_HOST: 127.0.0.1 DB_HOST: 127.0.0.1
DB_DATABASE: testing DB_DATABASE: testing
DB_USERNAME: postgres DB_USERNAME: postgres
DB_PASSWORD: postgres DB_PASSWORD: postgres
GUZZLE_TIMEOUT: 60
GUZZLE_CONNECT_TIMEOUT: 60
steps: steps:
- name: Code Checkout - name: Code Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4

View File

@ -66,6 +66,8 @@ jobs:
permissions: permissions:
contents: read contents: read
packages: write packages: write
strategy:
fail-fast: false
# Start a temp local registry because workflow can not pull from localy loaded images # Start a temp local registry because workflow can not pull from localy loaded images
services: services:
registry: registry:
@ -132,11 +134,6 @@ jobs:
docker push localhost:5000/base-php:arm64 docker push localhost:5000/base-php:arm64
rm base-php-arm64.tar base-php-amd64.tar rm base-php-arm64.tar base-php-amd64.tar
- name: Update version in config/app.php (tag)
if: "github.event_name == 'release' && github.event.action == 'published'"
run: |
sed -i "s/'version' => 'canary',/'version' => '${{ steps.build_info.outputs.version_tag }}',/" config/app.php
- name: Build and Push (tag) - name: Build and Push (tag)
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
if: "github.event_name == 'release' && github.event.action == 'published'" if: "github.event_name == 'release' && github.event.action == 'published'"

View File

@ -33,7 +33,7 @@ jobs:
name: PHPStan name: PHPStan
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: true fail-fast: false
matrix: matrix:
php: [ 8.2, 8.3, 8.4 ] php: [ 8.2, 8.3, 8.4 ]
steps: steps:

2
.gitignore vendored
View File

@ -21,9 +21,9 @@ yarn-error.log
/.idea /.idea
/.nova /.nova
/.vscode /.vscode
/.ddev
public/assets/manifest.json public/assets/manifest.json
/database/*.sqlite* /database/*.sqlite*
filament-monaco-editor/
_ide_helper* _ide_helper*
/.phpstorm.meta.php /.phpstorm.meta.php

View File

@ -2,7 +2,7 @@
# Pelican Production Dockerfile # Pelican Production Dockerfile
## ##
# If you want to build this locally you want to run `docker build -f Dockerfile.dev .` # If you want to build this locally you want to run `docker build -f Dockerfile.dev`
## ##
# ================================ # ================================
@ -63,8 +63,8 @@ FROM --platform=$TARGETOS/$TARGETARCH localhost:5000/base-php:$TARGETARCH AS fin
WORKDIR /var/www/html WORKDIR /var/www/html
# Install additional required libraries # Install additional required libraries
RUN apk add --no-cache \ RUN apk update && apk add --no-cache \
caddy ca-certificates supervisor supercronic fcgi caddy ca-certificates supervisor supercronic
COPY --chown=root:www-data --chmod=640 --from=composerbuild /build . COPY --chown=root:www-data --chmod=640 --from=composerbuild /build .
COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public
@ -85,8 +85,7 @@ RUN chown root:www-data ./ \
&& ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \ && ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \
# Allow www-data write permissions where necessary # Allow www-data write permissions where necessary
&& chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \ && chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \ && chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord
&& chown -R www-data: /usr/local/etc/php/
# Configure Supervisor # Configure Supervisor
COPY docker/supervisord.conf /etc/supervisord.conf COPY docker/supervisord.conf /etc/supervisord.conf
@ -94,11 +93,10 @@ COPY docker/Caddyfile /etc/caddy/Caddyfile
# Add Laravel scheduler to crontab # Add Laravel scheduler to crontab
COPY docker/crontab /etc/supercronic/crontab COPY docker/crontab /etc/supercronic/crontab
COPY docker/entrypoint.sh /entrypoint.sh COPY docker/entrypoint.sh ./docker/entrypoint.sh
COPY docker/healthcheck.sh /healthcheck.sh
HEALTHCHECK --interval=5m --timeout=10s --start-period=5s --retries=3 \ HEALTHCHECK --interval=5m --timeout=10s --start-period=5s --retries=3 \
CMD /bin/ash /healthcheck.sh CMD curl -f http://localhost/up || exit 1
EXPOSE 80 443 EXPOSE 80 443
@ -106,5 +104,5 @@ VOLUME /pelican-data
USER www-data USER www-data
ENTRYPOINT [ "/bin/ash", "/entrypoint.sh" ] ENTRYPOINT [ "/bin/ash", "docker/entrypoint.sh" ]
CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ] CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ]

View File

@ -67,8 +67,8 @@ FROM --platform=$TARGETOS/$TARGETARCH base AS final
WORKDIR /var/www/html WORKDIR /var/www/html
# Install additional required libraries # Install additional required libraries
RUN apk add --no-cache \ RUN apk update && apk add --no-cache \
caddy ca-certificates supervisor supercronic fcgi coreutils caddy ca-certificates supervisor supercronic
COPY --chown=root:www-data --chmod=640 --from=composerbuild /build . COPY --chown=root:www-data --chmod=640 --from=composerbuild /build .
COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public
@ -89,8 +89,7 @@ RUN chown root:www-data ./ \
&& ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \ && ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \
# Allow www-data write permissions where necessary # Allow www-data write permissions where necessary
&& chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \ && chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \ && chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord
&& chown -R www-data: /usr/local/etc/php/
# Configure Supervisor # Configure Supervisor
COPY docker/supervisord.conf /etc/supervisord.conf COPY docker/supervisord.conf /etc/supervisord.conf
@ -98,11 +97,10 @@ COPY docker/Caddyfile /etc/caddy/Caddyfile
# Add Laravel scheduler to crontab # Add Laravel scheduler to crontab
COPY docker/crontab /etc/supercronic/crontab COPY docker/crontab /etc/supercronic/crontab
COPY docker/entrypoint.sh /entrypoint.sh COPY docker/entrypoint.sh ./docker/entrypoint.sh
COPY docker/healthcheck.sh /healthcheck.sh
HEALTHCHECK --interval=5m --timeout=10s --start-period=5s --retries=3 \ HEALTHCHECK --interval=5m --timeout=10s --start-period=5s --retries=3 \
CMD /bin/ash /healthcheck.sh CMD curl -f http://localhost/up || exit 1
EXPOSE 80 443 EXPOSE 80 443
@ -110,5 +108,5 @@ VOLUME /pelican-data
USER www-data USER www-data
ENTRYPOINT [ "/bin/ash", "/entrypoint.sh" ] ENTRYPOINT [ "/bin/ash", "docker/entrypoint.sh" ]
CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ] CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ]

View File

@ -2,14 +2,10 @@
namespace App\Console\Commands\Egg; namespace App\Console\Commands\Egg;
use App\Enums\EggFormat;
use App\Models\Egg; use App\Models\Egg;
use App\Services\Eggs\Sharing\EggExporterService; use App\Services\Eggs\Sharing\EggExporterService;
use Exception; use Exception;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
use JsonException;
use Symfony\Component\Yaml\Yaml;
class CheckEggUpdatesCommand extends Command class CheckEggUpdatesCommand extends Command
{ {
@ -27,9 +23,6 @@ class CheckEggUpdatesCommand extends Command
} }
} }
/**
* @throws JsonException
*/
private function check(Egg $egg, EggExporterService $exporterService): void private function check(Egg $egg, EggExporterService $exporterService): void
{ {
if (is_null($egg->update_url)) { if (is_null($egg->update_url)) {
@ -38,24 +31,22 @@ class CheckEggUpdatesCommand extends Command
return; return;
} }
$ext = strtolower(pathinfo(parse_url($egg->update_url, PHP_URL_PATH), PATHINFO_EXTENSION)); $currentJson = json_decode($exporterService->handle($egg->id));
$isYaml = in_array($ext, ['yaml', 'yml']); unset($currentJson->exported_at);
$local = $isYaml $updatedEgg = file_get_contents($egg->update_url);
? Yaml::parse($exporterService->handle($egg->id, EggFormat::YAML)) assert($updatedEgg !== false);
: json_decode($exporterService->handle($egg->id, EggFormat::JSON), true); $updatedJson = json_decode($updatedEgg);
unset($updatedJson->exported_at);
$remote = Http::timeout(5)->connectTimeout(1)->get($egg->update_url)->throw()->body(); if (md5(json_encode($currentJson, JSON_THROW_ON_ERROR)) === md5(json_encode($updatedJson, JSON_THROW_ON_ERROR))) {
$remote = $isYaml ? Yaml::parse($remote) : json_decode($remote, true); $this->info("$egg->name: Up-to-date");
cache()->put("eggs.$egg->uuid.update", false, now()->addHour());
unset($local['exported_at'], $remote['exported_at']); return;
}
$localHash = md5(json_encode($local, JSON_THROW_ON_ERROR)); $this->warn("$egg->name: Found update");
$remoteHash = md5(json_encode($remote, JSON_THROW_ON_ERROR)); cache()->put("eggs.$egg->uuid.update", true, now()->addHour());
$status = $localHash === $remoteHash ? 'Up-to-date' : 'Found update';
$this->{($localHash === $remoteHash) ? 'info' : 'warn'}("$egg->name: $status");
cache()->put("eggs.$egg->uuid.update", $localHash !== $remoteHash, now()->addHour());
} }
} }

View File

@ -1,46 +0,0 @@
<?php
namespace App\Console\Commands\Egg;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
class UpdateEggIndexCommand extends Command
{
protected $signature = 'p:egg:update-index';
public function handle(): int
{
try {
$data = Http::timeout(5)->connectTimeout(1)->get('https://raw.githubusercontent.com/pelican-eggs/pelican-eggs.github.io/refs/heads/main/content/pelican.json')->throw()->json();
} catch (Exception $exception) {
$this->error($exception->getMessage());
return 1;
}
$index = [];
foreach ($data['nests'] as $nest) {
$nestName = $nest['nest_type'];
$this->info("Nest: $nestName");
$nestEggs = [];
foreach ($nest['Eggs'] as $egg) {
$eggName = $egg['egg']['name'];
$this->comment("Egg: $eggName");
$nestEggs[$egg['download_url']] = $eggName;
}
$index[$nestName] = $nestEggs;
$this->info('');
}
cache()->forever('eggs.index', $index);
return 0;
}
}

View File

@ -6,7 +6,6 @@ use App\Traits\EnvironmentWriterTrait;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Kernel; use Illuminate\Contracts\Console\Kernel;
use Illuminate\Database\DatabaseManager; use Illuminate\Database\DatabaseManager;
use PDOException;
class DatabaseSettingsCommand extends Command class DatabaseSettingsCommand extends Command
{ {
@ -106,7 +105,7 @@ class DatabaseSettingsCommand extends Command
]); ]);
$this->database->connection('_panel_command_test')->getPdo(); $this->database->connection('_panel_command_test')->getPdo();
} catch (PDOException $exception) { } catch (\PDOException $exception) {
$this->output->error(sprintf('Unable to connect to the MySQL server using the provided credentials. The error returned was "%s".', $exception->getMessage())); $this->output->error(sprintf('Unable to connect to the MySQL server using the provided credentials. The error returned was "%s".', $exception->getMessage()));
$this->output->error(trans('commands.database_settings.DB_error_2')); $this->output->error(trans('commands.database_settings.DB_error_2'));
@ -166,7 +165,7 @@ class DatabaseSettingsCommand extends Command
]); ]);
$this->database->connection('_panel_command_test')->getPdo(); $this->database->connection('_panel_command_test')->getPdo();
} catch (PDOException $exception) { } catch (\PDOException $exception) {
$this->output->error(sprintf('Unable to connect to the MariaDB server using the provided credentials. The error returned was "%s".', $exception->getMessage())); $this->output->error(sprintf('Unable to connect to the MariaDB server using the provided credentials. The error returned was "%s".', $exception->getMessage()));
$this->output->error(trans('commands.database_settings.DB_error_2')); $this->output->error(trans('commands.database_settings.DB_error_2'));

View File

@ -2,7 +2,6 @@
namespace App\Console\Commands\Environment; namespace App\Console\Commands\Environment;
use App\Exceptions\PanelException;
use App\Traits\EnvironmentWriterTrait; use App\Traits\EnvironmentWriterTrait;
use Illuminate\Console\Command; use Illuminate\Console\Command;
@ -29,7 +28,7 @@ class EmailSettingsCommand extends Command
/** /**
* Handle command execution. * Handle command execution.
* *
* @throws PanelException * @throws \App\Exceptions\PanelException
*/ */
public function handle(): void public function handle(): void
{ {

View File

@ -18,17 +18,6 @@ class QueueWorkerServiceCommand extends Command
public function handle(): void public function handle(): void
{ {
if (@file_exists('/.dockerenv')) {
$result = Process::run('supervisorctl restart queue-worker');
if ($result->failed()) {
$this->error('Error restarting service: ' . $result->errorOutput());
return;
}
$this->line('Queue worker service file updated successfully.');
return;
}
$serviceName = $this->option('service-name') ?? $this->ask('Queue worker service name', 'pelican-queue'); $serviceName = $this->option('service-name') ?? $this->ask('Queue worker service name', 'pelican-queue');
$path = '/etc/systemd/system/' . $serviceName . '.service'; $path = '/etc/systemd/system/' . $serviceName . '.service';

View File

@ -4,8 +4,8 @@ namespace App\Console\Commands\Maintenance;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory;
use Illuminate\Contracts\Filesystem\Filesystem; use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory;
use SplFileInfo; use SplFileInfo;
class CleanServiceBackupFilesCommand extends Command class CleanServiceBackupFilesCommand extends Command

View File

@ -5,7 +5,6 @@ namespace App\Console\Commands\Maintenance;
use App\Models\Backup; use App\Models\Backup;
use Carbon\CarbonImmutable; use Carbon\CarbonImmutable;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use InvalidArgumentException;
class PruneOrphanedBackupsCommand extends Command class PruneOrphanedBackupsCommand extends Command
{ {
@ -17,7 +16,7 @@ class PruneOrphanedBackupsCommand extends Command
{ {
$since = $this->option('prune-age') ?? config('backups.prune_age', 360); $since = $this->option('prune-age') ?? config('backups.prune_age', 360);
if (!$since || !is_digit($since)) { if (!$since || !is_digit($since)) {
throw new InvalidArgumentException('The "--prune-age" argument must be a value greater than 0.'); throw new \InvalidArgumentException('The "--prune-age" argument must be a value greater than 0.');
} }
$query = Backup::query() $query = Backup::query()

View File

@ -2,7 +2,6 @@
namespace App\Console\Commands\Node; namespace App\Console\Commands\Node;
use App\Exceptions\Model\DataValidationException;
use App\Models\Node; use App\Models\Node;
use Illuminate\Console\Command; use Illuminate\Console\Command;
@ -25,7 +24,6 @@ class MakeNodeCommand extends Command
{--overallocateCpu= : Enter the amount of cpu to overallocate (% or -1 to overallocate the maximum).} {--overallocateCpu= : Enter the amount of cpu to overallocate (% or -1 to overallocate the maximum).}
{--uploadSize= : Enter the maximum upload filesize.} {--uploadSize= : Enter the maximum upload filesize.}
{--daemonListeningPort= : Enter the daemon listening port.} {--daemonListeningPort= : Enter the daemon listening port.}
{--daemonConnectingPort= : Enter the daemon connecting port.}
{--daemonSFTPPort= : Enter the daemon SFTP listening port.} {--daemonSFTPPort= : Enter the daemon SFTP listening port.}
{--daemonSFTPAlias= : Enter the daemon SFTP alias.} {--daemonSFTPAlias= : Enter the daemon SFTP alias.}
{--daemonBase= : Enter the base folder.}'; {--daemonBase= : Enter the base folder.}';
@ -35,7 +33,7 @@ class MakeNodeCommand extends Command
/** /**
* Handle the command execution process. * Handle the command execution process.
* *
* @throws DataValidationException * @throws \App\Exceptions\Model\DataValidationException
*/ */
public function handle(): void public function handle(): void
{ {
@ -59,7 +57,6 @@ class MakeNodeCommand extends Command
$data['cpu_overallocate'] = $this->option('overallocateCpu') ?? $this->ask(trans('commands.make_node.cpu_overallocate'), '-1'); $data['cpu_overallocate'] = $this->option('overallocateCpu') ?? $this->ask(trans('commands.make_node.cpu_overallocate'), '-1');
$data['upload_size'] = $this->option('uploadSize') ?? $this->ask(trans('commands.make_node.upload_size'), '256'); $data['upload_size'] = $this->option('uploadSize') ?? $this->ask(trans('commands.make_node.upload_size'), '256');
$data['daemon_listen'] = $this->option('daemonListeningPort') ?? $this->ask(trans('commands.make_node.daemonListen'), '8080'); $data['daemon_listen'] = $this->option('daemonListeningPort') ?? $this->ask(trans('commands.make_node.daemonListen'), '8080');
$data['daemon_connect'] = $this->option('daemonConnectingPort') ?? $this->ask(trans('commands.make_node.daemonConnect'), '8080');
$data['daemon_sftp'] = $this->option('daemonSFTPPort') ?? $this->ask(trans('commands.make_node.daemonSFTP'), '2022'); $data['daemon_sftp'] = $this->option('daemonSFTPPort') ?? $this->ask(trans('commands.make_node.daemonSFTP'), '2022');
$data['daemon_sftp_alias'] = $this->option('daemonSFTPAlias') ?? $this->ask(trans('commands.make_node.daemonSFTPAlias'), ''); $data['daemon_sftp_alias'] = $this->option('daemonSFTPAlias') ?? $this->ask(trans('commands.make_node.daemonSFTPAlias'), '');
$data['daemon_base'] = $this->option('daemonBase') ?? $this->ask(trans('commands.make_node.daemonBase'), '/var/lib/pelican/volumes'); $data['daemon_base'] = $this->option('daemonBase') ?? $this->ask(trans('commands.make_node.daemonBase'), '/var/lib/pelican/volumes');

View File

@ -17,7 +17,7 @@ class NodeConfigurationCommand extends Command
{ {
$column = ctype_digit((string) $this->argument('node')) ? 'id' : 'uuid'; $column = ctype_digit((string) $this->argument('node')) ? 'id' : 'uuid';
/** @var Node $node */ /** @var \App\Models\Node $node */
$node = Node::query()->where($column, $this->argument('node'))->firstOr(function () { $node = Node::query()->where($column, $this->argument('node'))->firstOr(function () {
$this->error(trans('commands.node_config.error_not_exist')); $this->error(trans('commands.node_config.error_not_exist'));

View File

@ -2,10 +2,10 @@
namespace App\Console\Commands\Schedule; namespace App\Console\Commands\Schedule;
use App\Models\Schedule;
use App\Services\Schedules\ProcessScheduleService;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use App\Models\Schedule;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use App\Services\Schedules\ProcessScheduleService;
use Throwable; use Throwable;
class ProcessRunnableCommand extends Command class ProcessRunnableCommand extends Command
@ -64,7 +64,7 @@ class ProcessRunnableCommand extends Command
} catch (Throwable $exception) { } catch (Throwable $exception) {
logger()->error($exception, ['schedule_id' => $schedule->id]); logger()->error($exception, ['schedule_id' => $schedule->id]);
$this->error(trans('commands.schedule.process.error_message', ['schedules' => " #$schedule->id: " . $exception->getMessage()])); $this->error(trans('commands.schedule.process.no_tasks') . " #$schedule->id: " . $exception->getMessage());
} }
} }
} }

View File

@ -3,12 +3,12 @@
namespace App\Console\Commands\Server; namespace App\Console\Commands\Server;
use App\Models\Server; use App\Models\Server;
use App\Repositories\Daemon\DaemonServerRepository;
use Exception;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Validation\Factory as ValidatorFactory;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
use Illuminate\Validation\Factory as ValidatorFactory;
use App\Repositories\Daemon\DaemonPowerRepository;
use Exception;
class BulkPowerActionCommand extends Command class BulkPowerActionCommand extends Command
{ {
@ -19,7 +19,7 @@ class BulkPowerActionCommand extends Command
protected $description = 'Perform bulk power management on large groupings of servers or nodes at once.'; protected $description = 'Perform bulk power management on large groupings of servers or nodes at once.';
public function handle(DaemonServerRepository $serverRepository, ValidatorFactory $validator): void public function handle(DaemonPowerRepository $powerRepository, ValidatorFactory $validator): void
{ {
$action = $this->argument('action'); $action = $this->argument('action');
$nodes = empty($this->option('nodes')) ? [] : explode(',', $this->option('nodes')); $nodes = empty($this->option('nodes')) ? [] : explode(',', $this->option('nodes'));
@ -52,7 +52,7 @@ class BulkPowerActionCommand extends Command
$bar = $this->output->createProgressBar($count); $bar = $this->output->createProgressBar($count);
$this->getQueryBuilder($servers, $nodes)->get()->each(function ($server, int $index) use ($action, $serverRepository, &$bar): mixed { $this->getQueryBuilder($servers, $nodes)->get()->each(function ($server, int $index) use ($action, $powerRepository, &$bar): mixed {
$bar->clear(); $bar->clear();
if (!$server instanceof Server) { if (!$server instanceof Server) {
@ -60,7 +60,7 @@ class BulkPowerActionCommand extends Command
} }
try { try {
$serverRepository->setServer($server)->power($action); $powerRepository->setServer($server)->send($action);
} catch (Exception $exception) { } catch (Exception $exception) {
$this->output->error(trans('command/messages.server.power.action_failed', [ $this->output->error(trans('command/messages.server.power.action_failed', [
'name' => $server->name, 'name' => $server->name,

View File

@ -0,0 +1,193 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Console\Kernel;
use Symfony\Component\Process\Process;
use Symfony\Component\Console\Helper\ProgressBar;
class UpgradeCommand extends Command
{
protected const DEFAULT_URL = 'https://github.com/pelican-dev/panel/releases/%s/panel.tar.gz';
protected $signature = 'p:upgrade
{--user= : The user that PHP runs under. All files will be owned by this user.}
{--group= : The group that PHP runs under. All files will be owned by this group.}
{--url= : The specific archive to download.}
{--release= : A specific version to download from GitHub. Leave blank to use latest.}
{--skip-download : If set no archive will be downloaded.}';
protected $description = 'Downloads a new archive from GitHub and then executes the normal upgrade commands.';
/**
* Executes an upgrade command which will run through all of our standard
* Panel commands and enable users to basically just download
* the archive and execute this and be done.
*
* This places the application in maintenance mode as well while the commands
* are being executed.
*
* @throws \Exception
*/
public function handle(): void
{
$skipDownload = $this->option('skip-download');
if (!$skipDownload) {
$this->output->warning(trans('commands.upgrade.integrity'));
$this->output->comment(trans('commands.upgrade.source_url'));
$this->line($this->getUrl());
}
$user = 'www-data';
$group = 'www-data';
if ($this->input->isInteractive()) {
if (!$skipDownload) {
$skipDownload = !$this->confirm(trans('commands.upgrade.skipDownload'), true);
}
if (is_null($this->option('user'))) {
$userDetails = function_exists('posix_getpwuid') ? posix_getpwuid(fileowner('public')) : [];
$user = $userDetails['name'] ?? 'www-data';
$message = trans('commands.upgrade.webserver_user', ['user' => $user]);
if (!$this->confirm($message, true)) {
$user = $this->anticipate(
trans('commands.upgrade.name_webserver'),
[
'www-data',
'nginx',
'apache',
]
);
}
}
if (is_null($this->option('group'))) {
$groupDetails = function_exists('posix_getgrgid') ? posix_getgrgid(filegroup('public')) : [];
$group = $groupDetails['name'] ?? 'www-data';
$message = trans('commands.upgrade.group_webserver', ['group' => $user]);
if (!$this->confirm($message, true)) {
$group = $this->anticipate(
trans('commands.upgrade.group_webserver_question'),
[
'www-data',
'nginx',
'apache',
]
);
}
}
if (!$this->confirm(trans('commands.upgrade.are_your_sure'))) {
$this->warn(trans('commands.upgrade.terminated'));
return;
}
}
ini_set('output_buffering', '0');
$bar = $this->output->createProgressBar($skipDownload ? 9 : 10);
$bar->start();
if (!$skipDownload) {
$this->withProgress($bar, function () {
$this->line("\$upgrader> curl -L \"{$this->getUrl()}\" | tar -xzv");
$process = Process::fromShellCommandline("curl -L \"{$this->getUrl()}\" | tar -xzv");
$process->run(function ($type, $buffer) {
$this->{$type === Process::ERR ? 'error' : 'line'}($buffer);
});
});
}
$this->withProgress($bar, function () {
$this->line('$upgrader> php artisan down');
$this->call('down');
});
$this->withProgress($bar, function () {
$this->line('$upgrader> chmod -R 755 storage bootstrap/cache');
$process = new Process(['chmod', '-R', '755', 'storage', 'bootstrap/cache']);
$process->run(function ($type, $buffer) {
$this->{$type === Process::ERR ? 'error' : 'line'}($buffer);
});
});
$this->withProgress($bar, function () {
$command = ['composer', 'install', '--no-ansi'];
if (config('app.env') === 'production' && !config('app.debug')) {
$command[] = '--optimize-autoloader';
$command[] = '--no-dev';
}
$this->line('$upgrader> ' . implode(' ', $command));
$process = new Process($command);
$process->setTimeout(10 * 60);
$process->run(function ($type, $buffer) {
$this->line($buffer);
});
});
/** @var \Illuminate\Foundation\Application $app */
$app = require __DIR__ . '/../../../bootstrap/app.php';
/** @var \App\Console\Kernel $kernel */
$kernel = $app->make(Kernel::class);
$kernel->bootstrap();
$this->setLaravel($app);
$this->withProgress($bar, function () {
$this->line('$upgrader> php artisan view:clear');
$this->call('view:clear');
});
$this->withProgress($bar, function () {
$this->line('$upgrader> php artisan config:clear');
$this->call('config:clear');
});
$this->withProgress($bar, function () {
$this->line('$upgrader> php artisan migrate --force --seed');
$this->call('migrate', ['--force' => true, '--seed' => true]);
});
$this->withProgress($bar, function () use ($user, $group) {
$this->line("\$upgrader> chown -R {$user}:{$group} *");
$process = Process::fromShellCommandline("chown -R {$user}:{$group} *", $this->getLaravel()->basePath());
$process->setTimeout(10 * 60);
$process->run(function ($type, $buffer) {
$this->{$type === Process::ERR ? 'error' : 'line'}($buffer);
});
});
$this->withProgress($bar, function () {
$this->line('$upgrader> php artisan queue:restart');
$this->call('queue:restart');
});
$this->withProgress($bar, function () {
$this->line('$upgrader> php artisan up');
$this->call('up');
});
$this->newLine(2);
$this->info(trans('commands.upgrade.success'));
}
protected function withProgress(ProgressBar $bar, \Closure $callback): void
{
$bar->clear();
$callback();
$bar->advance();
$bar->display();
}
protected function getUrl(): string
{
if ($this->option('url')) {
return $this->option('url');
}
return sprintf(self::DEFAULT_URL, $this->option('release') ? 'download/v' . $this->option('release') : 'latest/download');
}
}

View File

@ -3,8 +3,8 @@
namespace App\Console\Commands\User; namespace App\Console\Commands\User;
use App\Models\User; use App\Models\User;
use Illuminate\Console\Command;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert;
use Illuminate\Console\Command;
class DeleteUserCommand extends Command class DeleteUserCommand extends Command
{ {
@ -35,7 +35,7 @@ class DeleteUserCommand extends Command
if ($this->input->isInteractive()) { if ($this->input->isInteractive()) {
$tableValues = []; $tableValues = [];
foreach ($results as $user) { foreach ($results as $user) {
$tableValues[] = [$user->id, $user->email, $user->username]; $tableValues[] = [$user->id, $user->email, $user->name];
} }
$this->table(['User ID', 'Email', 'Name'], $tableValues); $this->table(['User ID', 'Email', 'Name'], $tableValues);

View File

@ -2,7 +2,6 @@
namespace App\Console\Commands\User; namespace App\Console\Commands\User;
use App\Exceptions\Model\DataValidationException;
use App\Models\User; use App\Models\User;
use Illuminate\Console\Command; use Illuminate\Console\Command;
@ -15,22 +14,20 @@ class DisableTwoFactorCommand extends Command
/** /**
* Handle command execution process. * Handle command execution process.
* *
* @throws DataValidationException * @throws \App\Exceptions\Model\DataValidationException
*/ */
public function handle(): void public function handle(): void
{ {
if ($this->input->isInteractive()) { if ($this->input->isInteractive()) {
$this->output->warning(trans('command/messages.user.2fa_help_text')); $this->output->warning(trans('command/messages.user.2fa_help_text.0') . trans('command/messages.user.2fa_help_text.1'));
} }
$email = $this->option('email') ?? $this->ask(trans('command/messages.user.ask_email')); $email = $this->option('email') ?? $this->ask(trans('command/messages.user.ask_email'));
$user = User::where('email', $email)->firstOrFail(); $user = User::query()->where('email', $email)->firstOrFail();
$user->update([ $user->use_totp = false;
'mfa_app_secret' => null, $user->totp_secret = null;
'mfa_app_recovery_codes' => null, $user->save();
'mfa_email_enabled' => false,
]);
$this->info(trans('command/messages.user.2fa_disabled', ['email' => $user->email])); $this->info(trans('command/messages.user.2fa_disabled', ['email' => $user->email]));
} }

View File

@ -2,10 +2,9 @@
namespace App\Console\Commands\User; namespace App\Console\Commands\User;
use App\Exceptions\Model\DataValidationException;
use App\Services\Users\UserCreationService;
use Exception; use Exception;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use App\Services\Users\UserCreationService;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
class MakeUserCommand extends Command class MakeUserCommand extends Command
@ -26,7 +25,7 @@ class MakeUserCommand extends Command
* Handle command request to create a new user. * Handle command request to create a new user.
* *
* @throws Exception * @throws Exception
* @throws DataValidationException * @throws \App\Exceptions\Model\DataValidationException
*/ */
public function handle(): int public function handle(): int
{ {

View File

@ -3,11 +3,11 @@
namespace App\Console; namespace App\Console;
use App\Console\Commands\Egg\CheckEggUpdatesCommand; use App\Console\Commands\Egg\CheckEggUpdatesCommand;
use App\Console\Commands\Egg\UpdateEggIndexCommand;
use App\Console\Commands\Maintenance\CleanServiceBackupFilesCommand; use App\Console\Commands\Maintenance\CleanServiceBackupFilesCommand;
use App\Console\Commands\Maintenance\PruneImagesCommand; use App\Console\Commands\Maintenance\PruneImagesCommand;
use App\Console\Commands\Maintenance\PruneOrphanedBackupsCommand; use App\Console\Commands\Maintenance\PruneOrphanedBackupsCommand;
use App\Console\Commands\Schedule\ProcessRunnableCommand; use App\Console\Commands\Schedule\ProcessRunnableCommand;
use App\Jobs\NodeStatistics;
use App\Models\ActivityLog; use App\Models\ActivityLog;
use App\Models\Webhook; use App\Models\Webhook;
use Illuminate\Console\Scheduling\Schedule; use Illuminate\Console\Scheduling\Schedule;
@ -42,9 +42,9 @@ class Kernel extends ConsoleKernel
$schedule->command(CleanServiceBackupFilesCommand::class)->daily(); $schedule->command(CleanServiceBackupFilesCommand::class)->daily();
$schedule->command(PruneImagesCommand::class)->daily(); $schedule->command(PruneImagesCommand::class)->daily();
$schedule->command(CheckEggUpdatesCommand::class)->hourly();
$schedule->command(CheckEggUpdatesCommand::class)->daily(); $schedule->job(new NodeStatistics())->everyFiveSeconds()->withoutOverlapping();
$schedule->command(UpdateEggIndexCommand::class)->daily();
if (config('backups.prune_age')) { if (config('backups.prune_age')) {
// Every 30 minutes, run the backup pruning command so that any abandoned backups can be deleted. // Every 30 minutes, run the backup pruning command so that any abandoned backups can be deleted.

View File

@ -2,13 +2,12 @@
namespace App\Contracts\Http; namespace App\Contracts\Http;
use App\Enums\SubuserPermission;
interface ClientPermissionsRequest interface ClientPermissionsRequest
{ {
/** /**
* Returns the permission used to validate that the authenticated user may perform * Returns the permissions string indicating which permission should be used to
* this action against the given resource (server). * validate that the authenticated user has permission to perform this action against
* the given resource (server).
*/ */
public function permission(): SubuserPermission|string; public function permission(): string;
} }

View File

@ -32,6 +32,6 @@ enum BackupStatus: string implements HasColor, HasIcon, HasLabel
public function getLabel(): string public function getLabel(): string
{ {
return trans('server/backup.backup_status.' . $this->value); return str($this->value)->headline();
} }
} }

View File

@ -68,7 +68,7 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
public function getLabel(): string public function getLabel(): string
{ {
return trans('server/console.status.' . $this->value); return str($this->value)->title();
} }
public function isOffline(): bool public function isOffline(): bool
@ -88,7 +88,7 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
public function isStartable(): bool public function isStartable(): bool
{ {
return !in_array($this, [ContainerStatus::Running, ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting, ContainerStatus::Missing]); return !in_array($this, [ContainerStatus::Running, ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting]);
} }
public function isRestartable(): bool public function isRestartable(): bool
@ -97,16 +97,18 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
return true; return true;
} }
return !in_array($this, [ContainerStatus::Offline, ContainerStatus::Missing]); return !in_array($this, [ContainerStatus::Offline]);
} }
public function isStoppable(): bool public function isStoppable(): bool
{ {
return !in_array($this, [ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting, ContainerStatus::Exited, ContainerStatus::Offline, ContainerStatus::Missing]); return !in_array($this, [ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting, ContainerStatus::Exited, ContainerStatus::Offline]);
} }
public function isKillable(): bool public function isKillable(): bool
{ {
return !in_array($this, [ContainerStatus::Offline, ContainerStatus::Running, ContainerStatus::Exited, ContainerStatus::Missing]); // [ContainerStatus::Restarting, ContainerStatus::Removing, ContainerStatus::Dead, ContainerStatus::Created]
return !in_array($this, [ContainerStatus::Offline, ContainerStatus::Running, ContainerStatus::Exited]);
} }
} }

View File

@ -1,9 +0,0 @@
<?php
namespace App\Enums;
enum CustomRenderHooks: string
{
case FooterStart = 'pelican::footer.start';
case FooterEnd = 'pelican::footer.end';
}

View File

@ -1,37 +0,0 @@
<?php
namespace App\Enums;
enum CustomizationKey: string
{
case ConsoleRows = 'console_rows';
case ConsoleFont = 'console_font';
case ConsoleFontSize = 'console_font_size';
case ConsoleGraphPeriod = 'console_graph_period';
case TopNavigation = 'top_navigation';
case DashboardLayout = 'dashboard_layout';
public function getDefaultValue(): string|int|bool
{
return match ($this) {
self::ConsoleRows => 30,
self::ConsoleFont => 'monospace',
self::ConsoleFontSize => 14,
self::ConsoleGraphPeriod => 30,
self::TopNavigation => config('panel.filament.default-navigation', 'sidebar'),
self::DashboardLayout => 'grid',
};
}
/** @return array<string, string|int|bool> */
public static function getDefaultCustomization(): array
{
$default = [];
foreach (self::cases() as $key) {
$default[$key->value] = $key->getDefaultValue();
}
return $default;
}
}

View File

@ -0,0 +1,141 @@
<?php
namespace App\Enums;
use Filament\Support\Contracts\HasLabel;
enum EditorLanguages: string implements HasLabel
{
case plaintext = 'plaintext';
case abap = 'abap';
case apex = 'apex';
case azcali = 'azcali';
case bat = 'bat';
case bicep = 'bicep';
case cameligo = 'cameligo';
case coljure = 'coljure';
case coffeescript = 'coffeescript';
case c = 'c';
case cpp = 'cpp';
case csharp = 'csharp';
case csp = 'csp';
case css = 'css';
case cypher = 'cypher';
case dart = 'dart';
case dockerfile = 'dockerfile';
case ecl = 'ecl';
case elixir = 'elixir';
case flow9 = 'flow9';
case fsharp = 'fsharp';
case go = 'go';
case graphql = 'graphql';
case handlebars = 'handlebars';
case hcl = 'hcl';
case html = 'html';
case ini = 'ini';
case java = 'java';
case javascript = 'javascript';
case julia = 'julia';
case json = 'json';
case kotlin = 'kotlin';
case less = 'less';
case lexon = 'lexon';
case lua = 'lua';
case liquid = 'liquid';
case m3 = 'm3';
case markdown = 'markdown';
case mdx = 'mdx';
case mips = 'mips';
case msdax = 'msdax';
case mysql = 'mysql';
case objectivec = 'objective-c';
case pascal = 'pascal';
case pascaligo = 'pascaligo';
case perl = 'perl';
case pgsql = 'pgsql';
case php = 'php';
case pla = 'pla';
case postiats = 'postiats';
case powerquery = 'powerquery';
case powershell = 'powershell';
case proto = 'proto';
case pug = 'pug';
case python = 'python';
case qsharp = 'qsharp';
case r = 'r';
case razor = 'razor';
case redis = 'redis';
case redshift = 'redshift';
case restructuredtext = 'restructuredtext';
case ruby = 'ruby';
case rust = 'rust';
case sb = 'sb';
case scala = 'scala';
case scheme = 'scheme';
case scss = 'scss';
case shell = 'shell';
case sol = 'sol';
case aes = 'aes';
case sparql = 'sparql';
case sql = 'sql';
case st = 'st';
case swift = 'swift';
case systemverilog = 'systemverilog';
case verilog = 'verilog';
case tcl = 'tcl';
case twig = 'twig';
case typescript = 'typescript';
case typespec = 'typespec';
case vb = 'vb';
case wgsl = 'wgsl';
case xml = 'xml';
case yaml = 'yaml';
public static function fromWithAlias(string $match): self
{
return match ($match) {
'h' => self::c,
'cc', 'hpp' => self::cpp,
'cs' => self::csharp,
'class' => self::java,
'htm' => self::html,
'js', 'mjs', 'cjs' => self::javascript,
'kt', 'kts' => self::kotlin,
'md' => self::markdown,
'm' => self::objectivec,
'pl', 'pm' => self::perl,
'php3', 'php4', 'php5', 'phtml' => self::php,
'py', 'pyc', 'pyo', 'pyi' => self::python,
'rdata', 'rds' => self::r,
'rb', 'erb' => self::ruby,
'sc' => self::scala,
'sh', 'zsh' => self::shell,
'ts', 'tsx' => self::typescript,
'yml' => self::yaml,
default => self::tryFrom($match) ?? self::plaintext,
};
}
public function getLabel(): string
{
return $this->name;
}
}

View File

@ -1,9 +0,0 @@
<?php
namespace App\Enums;
enum EggFormat: string
{
case YAML = 'yaml';
case JSON = 'json';
}

View File

@ -1,9 +0,0 @@
<?php
namespace App\Enums;
enum HeaderActionPosition: string
{
case Before = 'before';
case After = 'after';
}

View File

@ -1,9 +0,0 @@
<?php
namespace App\Enums;
enum HeaderWidgetPosition: string
{
case Before = 'before';
case After = 'after';
}

View File

@ -1,27 +0,0 @@
<?php
namespace App\Enums;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasLabel;
enum ScheduleStatus: string implements HasColor, HasLabel
{
case Inactive = 'inactive';
case Processing = 'processing';
case Active = 'active';
public function getColor(): string
{
return match ($this) {
self::Inactive => 'danger',
self::Processing => 'warning',
self::Active => 'success',
};
}
public function getLabel(): string
{
return trans('server/schedule.schedule_status.' . $this->value);
}
}

View File

@ -2,50 +2,9 @@
namespace App\Enums; namespace App\Enums;
use App\Models\Server; enum ServerResourceType
enum ServerResourceType: string
{ {
case Uptime = 'uptime'; case Unit;
case CPU = 'cpu_absolute'; case Percentage;
case Memory = 'memory_bytes'; case Time;
case Disk = 'disk_bytes';
case CPULimit = 'cpu';
case MemoryLimit = 'memory';
case DiskLimit = 'disk';
/**
* @return int resource amount in bytes
*/
public function getResourceAmount(Server $server): int
{
if ($this->isLimit()) {
$resourceAmount = $server->{$this->value} ?? 0;
if (!$this->isPercentage()) {
// Our limits are entered as MiB/ MB so we need to convert them to bytes
$resourceAmount *= config('panel.use_binary_prefix') ? 1024 * 1024 : 1000 * 1000;
}
return $resourceAmount;
}
return $server->retrieveResources()[$this->value] ?? 0;
}
public function isLimit(): bool
{
return $this === ServerResourceType::CPULimit || $this === ServerResourceType::MemoryLimit || $this === ServerResourceType::DiskLimit;
}
public function isTime(): bool
{
return $this === ServerResourceType::Uptime;
}
public function isPercentage(): bool
{
return $this === ServerResourceType::CPU || $this === ServerResourceType::CPULimit;
}
} }

View File

@ -8,6 +8,7 @@ use Filament\Support\Contracts\HasLabel;
enum ServerState: string implements HasColor, HasIcon, HasLabel enum ServerState: string implements HasColor, HasIcon, HasLabel
{ {
case Normal = 'normal';
case Installing = 'installing'; case Installing = 'installing';
case InstallFailed = 'install_failed'; case InstallFailed = 'install_failed';
case ReinstallFailed = 'reinstall_failed'; case ReinstallFailed = 'reinstall_failed';
@ -17,6 +18,7 @@ enum ServerState: string implements HasColor, HasIcon, HasLabel
public function getIcon(): string public function getIcon(): string
{ {
return match ($this) { return match ($this) {
self::Normal => 'tabler-heart',
self::Installing => 'tabler-heart-bolt', self::Installing => 'tabler-heart-bolt',
self::InstallFailed => 'tabler-heart-x', self::InstallFailed => 'tabler-heart-x',
self::ReinstallFailed => 'tabler-heart-x', self::ReinstallFailed => 'tabler-heart-x',
@ -25,17 +27,10 @@ enum ServerState: string implements HasColor, HasIcon, HasLabel
}; };
} }
public function getColor(bool $hex = false): string public function getColor(): string
{ {
if ($hex) {
return match ($this) {
self::Installing, self::RestoringBackup => '#2563EB',
self::Suspended => '#D97706',
self::InstallFailed, self::ReinstallFailed => '#EF4444',
};
}
return match ($this) { return match ($this) {
self::Normal => 'primary',
self::Installing => 'primary', self::Installing => 'primary',
self::InstallFailed => 'danger', self::InstallFailed => 'danger',
self::ReinstallFailed => 'danger', self::ReinstallFailed => 'danger',

View File

@ -1,11 +0,0 @@
<?php
namespace App\Enums;
enum StartupVariableType: string
{
case Text = 'text';
case Number = 'number';
case Select = 'select';
case Toggle = 'toggle';
}

View File

@ -1,88 +0,0 @@
<?php
namespace App\Enums;
enum SubuserPermission: string
{
case WebsocketConnect = 'websocket.connect';
case ControlConsole = 'control.console';
case ControlStart = 'control.start';
case ControlStop = 'control.stop';
case ControlRestart = 'control.restart';
case FileRead = 'file.read';
case FileReadContent = 'file.read-content';
case FileCreate = 'file.create';
case FileUpdate = 'file.update';
case FileDelete = 'file.delete';
case FileArchive = 'file.archive';
case FileSftp = 'file.sftp';
case BackupRead = 'backup.read';
case BackupCreate = 'backup.create';
case BackupDelete = 'backup.delete';
case BackupDownload = 'backup.download';
case BackupRestore = 'backup.restore';
case ScheduleRead = 'schedule.read';
case ScheduleCreate = 'schedule.create';
case ScheduleUpdate = 'schedule.update';
case ScheduleDelete = 'schedule.delete';
case UserRead = 'user.read';
case UserCreate = 'user.create';
case UserUpdate = 'user.update';
case UserDelete = 'user.delete';
case DatabaseRead = 'database.read';
case DatabaseCreate = 'database.create';
case DatabaseUpdate = 'database.update';
case DatabaseDelete = 'database.delete';
case DatabaseViewPassword = 'database.view-password';
case AllocationRead = 'allocation.read';
case AllocationCreate = 'allocation.create';
case AllocationUpdate = 'allocation.update';
case AllocationDelete = 'allocation.delete';
case ActivityRead = 'activity.read';
case StartupRead = 'startup.read';
case StartupUpdate = 'startup.update';
case StartupDockerImage = 'startup.docker-image';
case SettingsRename = 'settings.rename';
case SettingsDescription = 'settings.description';
case SettingsReinstall = 'settings.reinstall';
/** @return string[] */
public function split(): array
{
return explode('.', $this->value, 2);
}
public function isHidden(): bool
{
return $this === self::WebsocketConnect;
}
public function getIcon(): ?string
{
[$group, $permission] = $this->split();
return match ($group) {
'control' => 'tabler-terminal-2',
'user' => 'tabler-users',
'file' => 'tabler-files',
'backup' => 'tabler-file-zip',
'allocation' => 'tabler-network',
'startup' => 'tabler-player-play',
'database' => 'tabler-database',
'schedule' => 'tabler-clock',
'settings' => 'tabler-settings',
'activity' => 'tabler-stack',
default => null,
};
}
}

View File

@ -1,34 +0,0 @@
<?php
namespace App\Enums;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
enum WebhookType: string implements HasColor, HasIcon, HasLabel
{
case Regular = 'regular';
case Discord = 'discord';
public function getLabel(): string
{
return trans('admin/webhook.' . $this->value);
}
public function getColor(): ?string
{
return match ($this) {
self::Regular => null,
self::Discord => 'blurple',
};
}
public function getIcon(): string
{
return match ($this) {
self::Regular => 'tabler-world-www',
self::Discord => 'tabler-brand-discord',
};
}
}

View File

@ -2,9 +2,9 @@
namespace App\Events; namespace App\Events;
use Illuminate\Support\Str;
use App\Models\ActivityLog; use App\Models\ActivityLog;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
class ActivityLogged extends Event class ActivityLogged extends Event
{ {

View File

@ -0,0 +1,11 @@
<?php
namespace App\Events\Auth;
use App\Models\User;
use App\Events\Event;
class DirectLogin extends Event
{
public function __construct(public User $user, public bool $remember) {}
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Events\Auth;
use App\Events\Event;
use Illuminate\Queue\SerializesModels;
class FailedPasswordReset extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public string $ip, public string $email) {}
}

View File

@ -2,8 +2,8 @@
namespace App\Events\Auth; namespace App\Events\Auth;
use App\Events\Event;
use App\Models\User; use App\Models\User;
use App\Events\Event;
class ProvidedAuthenticationToken extends Event class ProvidedAuthenticationToken extends Event
{ {

View File

@ -4,14 +4,13 @@ namespace App\Exceptions;
use Exception; use Exception;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Illuminate\Container\Container;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Illuminate\Http\Response;
use Illuminate\Container\Container;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Throwable;
/** /**
* @deprecated * @deprecated
@ -29,7 +28,7 @@ class DisplayException extends PanelException implements HttpExceptionInterface
/** /**
* DisplayException constructor. * DisplayException constructor.
*/ */
public function __construct(string $message, ?Throwable $previous = null, protected string $level = self::LEVEL_ERROR, int $code = 0) public function __construct(string $message, ?\Throwable $previous = null, protected string $level = self::LEVEL_ERROR, int $code = 0)
{ {
parent::__construct($message, $code, $previous); parent::__construct($message, $code, $previous);
} }
@ -80,11 +79,11 @@ class DisplayException extends PanelException implements HttpExceptionInterface
* Log the exception to the logs using the defined error level only if the previous * Log the exception to the logs using the defined error level only if the previous
* exception is set. * exception is set.
* *
* @throws Throwable * @throws \Throwable
*/ */
public function report(): void public function report(): void
{ {
if (!$this->getPrevious() instanceof Exception || !Handler::isReportable($this->getPrevious())) { if (!$this->getPrevious() instanceof \Exception || !Handler::isReportable($this->getPrevious())) {
return; return;
} }

View File

@ -2,27 +2,24 @@
namespace App\Exceptions; namespace App\Exceptions;
use Exception; use Illuminate\Support\Arr;
use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Support\Str;
use Illuminate\Auth\AuthenticationException; use Illuminate\Http\JsonResponse;
use Illuminate\Support\Collection;
use Illuminate\Container\Container; use Illuminate\Container\Container;
use Illuminate\Database\Connection; use Illuminate\Database\Connection;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Foundation\Application;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Session\TokenMismatchException; use Illuminate\Session\TokenMismatchException;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
use PDOException;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\Mailer\Exception\TransportException; use Symfony\Component\Mailer\Exception\TransportException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Throwable; use Throwable;
class Handler extends ExceptionHandler class Handler extends ExceptionHandler
@ -82,7 +79,7 @@ class Handler extends ExceptionHandler
$this->dontReport = []; $this->dontReport = [];
} }
$this->reportable(function (PDOException $ex) { $this->reportable(function (\PDOException $ex) {
$ex = $this->generateCleanedExceptionStack($ex); $ex = $this->generateCleanedExceptionStack($ex);
}); });
@ -91,7 +88,7 @@ class Handler extends ExceptionHandler
}); });
} }
private function generateCleanedExceptionStack(Throwable $exception): string private function generateCleanedExceptionStack(\Throwable $exception): string
{ {
$cleanedStack = ''; $cleanedStack = '';
foreach ($exception->getTrace() as $index => $item) { foreach ($exception->getTrace() as $index => $item) {
@ -120,11 +117,11 @@ class Handler extends ExceptionHandler
/** /**
* Render an exception into an HTTP response. * Render an exception into an HTTP response.
* *
* @param Request $request * @param \Illuminate\Http\Request $request
* *
* @throws Throwable * @throws \Throwable
*/ */
public function render($request, Throwable $e): Response public function render($request, \Throwable $e): Response
{ {
$connections = $this->container->make(Connection::class); $connections = $this->container->make(Connection::class);
@ -146,7 +143,7 @@ class Handler extends ExceptionHandler
* Transform a validation exception into a consistent format to be returned for * Transform a validation exception into a consistent format to be returned for
* calls to the API. * calls to the API.
* *
* @param Request $request * @param \Illuminate\Http\Request $request
*/ */
public function invalidJson($request, ValidationException $exception): JsonResponse public function invalidJson($request, ValidationException $exception): JsonResponse
{ {
@ -252,7 +249,7 @@ class Handler extends ExceptionHandler
/** /**
* Return an array of exceptions that should not be reported. * Return an array of exceptions that should not be reported.
*/ */
public static function isReportable(Exception $exception): bool public static function isReportable(\Exception $exception): bool
{ {
return (new self(Container::getInstance()))->shouldReport($exception); return (new self(Container::getInstance()))->shouldReport($exception);
} }
@ -260,7 +257,7 @@ class Handler extends ExceptionHandler
/** /**
* Convert an authentication exception into an unauthenticated response. * Convert an authentication exception into an unauthenticated response.
* *
* @param Request $request * @param \Illuminate\Http\Request $request
*/ */
protected function unauthenticated($request, AuthenticationException $exception): JsonResponse|RedirectResponse protected function unauthenticated($request, AuthenticationException $exception): JsonResponse|RedirectResponse
{ {
@ -294,7 +291,7 @@ class Handler extends ExceptionHandler
* *
* @return array<mixed> * @return array<mixed>
*/ */
public static function toArray(Throwable $e): array public static function toArray(\Throwable $e): array
{ {
return self::exceptionToArray($e); return self::exceptionToArray($e);
} }

View File

@ -4,14 +4,13 @@ namespace App\Exceptions\Http;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\HttpException;
use Throwable;
class HttpForbiddenException extends HttpException class HttpForbiddenException extends HttpException
{ {
/** /**
* HttpForbiddenException constructor. * HttpForbiddenException constructor.
*/ */
public function __construct(?string $message = null, ?Throwable $previous = null) public function __construct(?string $message = null, ?\Throwable $previous = null)
{ {
parent::__construct(Response::HTTP_FORBIDDEN, $message, $previous); parent::__construct(Response::HTTP_FORBIDDEN, $message, $previous);
} }

View File

@ -5,7 +5,6 @@ namespace App\Exceptions\Http\Server;
use App\Enums\ServerState; use App\Enums\ServerState;
use App\Models\Server; use App\Models\Server;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException; use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
use Throwable;
class ServerStateConflictException extends ConflictHttpException class ServerStateConflictException extends ConflictHttpException
{ {
@ -13,7 +12,7 @@ class ServerStateConflictException extends ConflictHttpException
* Exception thrown when the server is in an unsupported state for API access or * Exception thrown when the server is in an unsupported state for API access or
* certain operations within the codebase. * certain operations within the codebase.
*/ */
public function __construct(Server $server, ?Throwable $previous = null) public function __construct(Server $server, ?\Throwable $previous = null)
{ {
$message = 'This server is currently in an unsupported state, please try again later.'; $message = 'This server is currently in an unsupported state, please try again later.';
if ($server->isSuspended()) { if ($server->isSuspended()) {

View File

@ -2,15 +2,13 @@
namespace App\Exceptions; namespace App\Exceptions;
use App\Exceptions\Solutions\ManifestDoesNotExistSolution;
use Exception;
use Spatie\Ignition\Contracts\ProvidesSolution;
use Spatie\Ignition\Contracts\Solution; use Spatie\Ignition\Contracts\Solution;
use Spatie\Ignition\Contracts\ProvidesSolution;
class ManifestDoesNotExistException extends Exception implements ProvidesSolution class ManifestDoesNotExistException extends \Exception implements ProvidesSolution
{ {
public function getSolution(): Solution public function getSolution(): Solution
{ {
return new ManifestDoesNotExistSolution(); return new Solutions\ManifestDoesNotExistSolution();
} }
} }

View File

@ -2,11 +2,11 @@
namespace App\Exceptions\Model; namespace App\Exceptions\Model;
use Illuminate\Support\MessageBag;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Contracts\Validation\Validator;
use App\Exceptions\PanelException; use App\Exceptions\PanelException;
use Illuminate\Contracts\Support\MessageProvider; use Illuminate\Contracts\Support\MessageProvider;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\MessageBag;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
class DataValidationException extends PanelException implements HttpExceptionInterface, MessageProvider class DataValidationException extends PanelException implements HttpExceptionInterface, MessageProvider

View File

@ -2,6 +2,4 @@
namespace App\Exceptions; namespace App\Exceptions;
use Exception; class PanelException extends \Exception {}
class PanelException extends Exception {}

View File

@ -1,7 +0,0 @@
<?php
namespace App\Exceptions\Repository;
use Exception;
class FileExistsException extends Exception {}

View File

@ -1,7 +0,0 @@
<?php
namespace App\Exceptions\Service\Deployment;
use App\Exceptions\DisplayException;
class NoViableNodeException extends DisplayException {}

View File

@ -2,8 +2,8 @@
namespace App\Exceptions\Service; namespace App\Exceptions\Service;
use App\Exceptions\DisplayException;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use App\Exceptions\DisplayException;
class HasActiveServersException extends DisplayException class HasActiveServersException extends DisplayException
{ {

View File

@ -3,7 +3,6 @@
namespace App\Exceptions\Service; namespace App\Exceptions\Service;
use App\Exceptions\DisplayException; use App\Exceptions\DisplayException;
use Throwable;
class ServiceLimitExceededException extends DisplayException class ServiceLimitExceededException extends DisplayException
{ {
@ -11,7 +10,7 @@ class ServiceLimitExceededException extends DisplayException
* Exception thrown when something goes over a defined limit, such as allocated * Exception thrown when something goes over a defined limit, such as allocated
* ports, tasks, databases, etc. * ports, tasks, databases, etc.
*/ */
public function __construct(string $message, ?Throwable $previous = null) public function __construct(string $message, ?\Throwable $previous = null)
{ {
parent::__construct($message, $previous, self::LEVEL_WARNING); parent::__construct($message, $previous, self::LEVEL_WARNING);
} }

View File

@ -0,0 +1,17 @@
<?php
namespace App\Exceptions\Service\User;
use App\Exceptions\DisplayException;
class TwoFactorAuthenticationTokenInvalid extends DisplayException
{
public string $title = 'Invalid 2FA Code';
public string $icon = 'tabler-2fa';
public function __construct()
{
parent::__construct('The provided two-factor authentication token was not valid.');
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Extensions\Avatar;
use App\Models\User;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
abstract class AvatarProvider
{
/**
* @var array<string, static>
*/
protected static array $providers = [];
public static function getProvider(string $id): ?self
{
return Arr::get(static::$providers, $id);
}
/**
* @return array<string, static>
*/
public static function getAll(): array
{
return static::$providers;
}
public function __construct()
{
static::$providers[$this->getId()] = $this;
}
abstract public function getId(): string;
abstract public function get(User $user): ?string;
public function getName(): string
{
return Str::title($this->getId());
}
}

View File

@ -1,14 +0,0 @@
<?php
namespace App\Extensions\Avatar;
use App\Models\User;
interface AvatarSchemaInterface
{
public function getId(): string;
public function getName(): string;
public function get(User $user): ?string;
}

View File

@ -1,55 +0,0 @@
<?php
namespace App\Extensions\Avatar;
use App\Models\User;
use Illuminate\Support\Facades\Storage;
class AvatarService
{
/** @var AvatarSchemaInterface[] */
private array $schemas = [];
public function __construct(
private readonly bool $allowUploadedAvatars,
private readonly string $activeSchema,
) {}
public function get(string $id): ?AvatarSchemaInterface
{
return array_get($this->schemas, $id);
}
public function getActiveSchema(): ?AvatarSchemaInterface
{
return $this->get($this->activeSchema);
}
public function getAvatarUrl(User $user): ?string
{
if ($this->allowUploadedAvatars) {
$path = "avatars/$user->id.png";
if (Storage::disk('public')->exists($path)) {
return Storage::url($path);
}
}
return $this->getActiveSchema()?->get($user);
}
public function register(AvatarSchemaInterface $schema): void
{
if (array_key_exists($schema->getId(), $this->schemas)) {
return;
}
$this->schemas[$schema->getId()] = $schema;
}
/** @return array<string, string> */
public function getMappings(): array
{
return collect($this->schemas)->mapWithKeys(fn ($schema) => [$schema->getId() => $schema->getName()])->all();
}
}

View File

@ -1,24 +1,24 @@
<?php <?php
namespace App\Extensions\Avatar\Schemas; namespace App\Extensions\Avatar\Providers;
use App\Extensions\Avatar\AvatarSchemaInterface; use App\Extensions\Avatar\AvatarProvider;
use App\Models\User; use App\Models\User;
class GravatarSchema implements AvatarSchemaInterface class GravatarProvider extends AvatarProvider
{ {
public function getId(): string public function getId(): string
{ {
return 'gravatar'; return 'gravatar';
} }
public function getName(): string
{
return 'Gravatar';
}
public function get(User $user): string public function get(User $user): string
{ {
return 'https://gravatar.com/avatar/' . md5($user->email); return 'https://gravatar.com/avatar/' . md5($user->email);
} }
public static function register(): self
{
return new self();
}
} }

View File

@ -1,11 +1,11 @@
<?php <?php
namespace App\Extensions\Avatar\Schemas; namespace App\Extensions\Avatar\Providers;
use App\Extensions\Avatar\AvatarSchemaInterface; use App\Extensions\Avatar\AvatarProvider;
use App\Models\User; use App\Models\User;
class UiAvatarsSchema implements AvatarSchemaInterface class UiAvatarsProvider extends AvatarProvider
{ {
public function getId(): string public function getId(): string
{ {
@ -22,4 +22,9 @@ class UiAvatarsSchema implements AvatarSchemaInterface
// UI Avatars is the default of filament so just return null here // UI Avatars is the default of filament so just return null here
return null; return null;
} }
public static function register(): self
{
return new self();
}
} }

View File

@ -2,16 +2,15 @@
namespace App\Extensions\Backups; namespace App\Extensions\Backups;
use App\Extensions\Filesystem\S3Filesystem;
use Aws\S3\S3Client;
use Closure; use Closure;
use Illuminate\Foundation\Application; use Aws\S3\S3Client;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use InvalidArgumentException;
use League\Flysystem\FilesystemAdapter;
use League\Flysystem\InMemory\InMemoryFilesystemAdapter;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert;
use Illuminate\Foundation\Application;
use League\Flysystem\FilesystemAdapter;
use App\Extensions\Filesystem\S3Filesystem;
use League\Flysystem\InMemory\InMemoryFilesystemAdapter;
class BackupManager class BackupManager
{ {
@ -65,7 +64,7 @@ class BackupManager
$config = $this->getConfig($name); $config = $this->getConfig($name);
if (empty($config['adapter'])) { if (empty($config['adapter'])) {
throw new InvalidArgumentException("Backup disk [$name] does not have a configured adapter."); throw new \InvalidArgumentException("Backup disk [$name] does not have a configured adapter.");
} }
$adapter = $config['adapter']; $adapter = $config['adapter'];
@ -83,7 +82,7 @@ class BackupManager
return $instance; return $instance;
} }
throw new InvalidArgumentException("Adapter [$adapter] is not supported."); throw new \InvalidArgumentException("Adapter [$adapter] is not supported.");
} }
/** /**

View File

@ -1,48 +0,0 @@
<?php
namespace App\Extensions\Captcha;
use App\Extensions\Captcha\Schemas\CaptchaSchemaInterface;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
class CaptchaService
{
/** @var array<string, CaptchaSchemaInterface> */
private array $schemas = [];
/**
* @return CaptchaSchemaInterface[]
*/
public function getAll(): array
{
return $this->schemas;
}
public function get(string $id): ?CaptchaSchemaInterface
{
return array_get($this->schemas, $id);
}
public function register(CaptchaSchemaInterface $schema): void
{
if (array_key_exists($schema->getId(), $this->schemas)) {
return;
}
config()->set('captcha.' . Str::lower($schema->getId()), $schema->getConfig());
$this->schemas[$schema->getId()] = $schema;
}
/** @return Collection<CaptchaSchemaInterface> */
public function getActiveSchemas(): Collection
{
return collect($this->schemas)
->filter(fn (CaptchaSchemaInterface $schema) => $schema->isEnabled());
}
public function getActiveSchema(): ?CaptchaSchemaInterface
{
return $this->getActiveSchemas()->first();
}
}

View File

@ -0,0 +1,118 @@
<?php
namespace App\Extensions\Captcha\Providers;
use Filament\Forms\Components\Component;
use Filament\Forms\Components\TextInput;
use Illuminate\Foundation\Application;
use Illuminate\Support\Str;
abstract class CaptchaProvider
{
/**
* @var array<string, static>
*/
protected static array $providers = [];
/**
* @return self|static[]
*/
public static function get(?string $id = null): array|self
{
return $id ? static::$providers[$id] : static::$providers;
}
protected function __construct(protected Application $app)
{
if (array_key_exists($this->getId(), static::$providers)) {
if (!$this->app->runningUnitTests()) {
logger()->warning("Tried to create duplicate Captcha provider with id '{$this->getId()}'");
}
return;
}
config()->set('captcha.' . Str::lower($this->getId()), $this->getConfig());
static::$providers[$this->getId()] = $this;
}
abstract public function getId(): string;
abstract public function getComponent(): Component;
/**
* @return array<string, string|string[]|bool|null>
*/
public function getConfig(): array
{
$id = Str::upper($this->getId());
return [
'site_key' => env("CAPTCHA_{$id}_SITE_KEY"),
'secret_key' => env("CAPTCHA_{$id}_SECRET_KEY"),
];
}
/**
* @return Component[]
*/
public function getSettingsForm(): array
{
$id = Str::upper($this->getId());
return [
TextInput::make("CAPTCHA_{$id}_SITE_KEY")
->label('Site Key')
->placeholder('Site Key')
->columnSpan(2)
->required()
->password()
->revealable()
->autocomplete(false)
->default(env("CAPTCHA_{$id}_SITE_KEY")),
TextInput::make("CAPTCHA_{$id}_SECRET_KEY")
->label('Secret Key')
->placeholder('Secret Key')
->columnSpan(2)
->required()
->password()
->revealable()
->autocomplete(false)
->default(env("CAPTCHA_{$id}_SECRET_KEY")),
];
}
public function getName(): string
{
return Str::title($this->getId());
}
public function getIcon(): ?string
{
return null;
}
public function isEnabled(): bool
{
$id = Str::upper($this->getId());
return env("CAPTCHA_{$id}_ENABLED", false);
}
/**
* @return array<string, string|bool>
*/
public function validateResponse(?string $captchaResponse = null): array
{
return [
'success' => false,
'message' => 'validateResponse not defined',
];
}
public function verifyDomain(string $hostname, ?string $requestUrl = null): bool
{
return true;
}
}

View File

@ -0,0 +1,106 @@
<?php
namespace App\Extensions\Captcha\Providers;
use App\Filament\Components\Forms\Fields\TurnstileCaptcha;
use Exception;
use Filament\Forms\Components\Component;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Toggle;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\HtmlString;
class TurnstileProvider extends CaptchaProvider
{
public function getId(): string
{
return 'turnstile';
}
public function getComponent(): Component
{
return TurnstileCaptcha::make('turnstile');
}
/**
* @return array<string, string|string[]|bool|null>
*/
public function getConfig(): array
{
return array_merge(parent::getConfig(), [
'verify_domain' => env('CAPTCHA_TURNSTILE_VERIFY_DOMAIN'),
]);
}
/**
* @return Component[]
*/
public function getSettingsForm(): array
{
return array_merge(parent::getSettingsForm(), [
Toggle::make('CAPTCHA_TURNSTILE_VERIFY_DOMAIN')
->label(trans('admin/setting.captcha.verify'))
->columnSpan(2)
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->default(env('CAPTCHA_TURNSTILE_VERIFY_DOMAIN', true)),
Placeholder::make('info')
->label(trans('admin/setting.captcha.info_label'))
->columnSpan(2)
->content(new HtmlString(trans('admin/setting.captcha.info'))),
]);
}
public function getIcon(): string
{
return 'tabler-brand-cloudflare';
}
public static function register(Application $app): self
{
return new self($app);
}
/**
* @return array<string, string|bool>
*/
public function validateResponse(?string $captchaResponse = null): array
{
$captchaResponse ??= request()->get('cf-turnstile-response');
if (!$secret = env('CAPTCHA_TURNSTILE_SECRET_KEY')) {
throw new Exception('Turnstile secret key is not defined.');
}
$response = Http::asJson()
->timeout(15)
->connectTimeout(5)
->throw()
->post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [
'secret' => $secret,
'response' => $captchaResponse,
]);
return count($response->json()) ? $response->json() : [
'success' => false,
'message' => 'Unknown error occurred, please try again',
];
}
public function verifyDomain(string $hostname, ?string $requestUrl = null): bool
{
if (!env('CAPTCHA_TURNSTILE_VERIFY_DOMAIN', true)) {
return true;
}
$requestUrl ??= request()->url;
$requestUrl = parse_url($requestUrl);
return $hostname === array_get($requestUrl, 'host');
}
}

View File

@ -1,59 +0,0 @@
<?php
namespace App\Extensions\Captcha\Schemas;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Component;
use Illuminate\Support\Str;
abstract class BaseSchema
{
abstract public function getId(): string;
public function getName(): string
{
return Str::upper($this->getId());
}
/**
* @return array<string, string|string[]|bool|null>
*/
public function getConfig(): array
{
$id = Str::upper($this->getId());
return [
'site_key' => env("CAPTCHA_{$id}_SITE_KEY"),
'secret_key' => env("CAPTCHA_{$id}_SECRET_KEY"),
];
}
/**
* @return Component[]
*/
public function getSettingsForm(): array
{
$id = Str::upper($this->getId());
return [
TextInput::make("CAPTCHA_{$id}_SITE_KEY")
->label('Site Key')
->placeholder('Site Key')
->columnSpan(2)
->required()
->password()
->revealable()
->autocomplete(false)
->default(env("CAPTCHA_{$id}_SITE_KEY")),
TextInput::make("CAPTCHA_{$id}_SECRET_KEY")
->label('Secret Key')
->placeholder('Secret Key')
->columnSpan(2)
->required()
->password()
->revealable()
->autocomplete(false)
->default(env("CAPTCHA_{$id}_SECRET_KEY")),
];
}
}

View File

@ -1,30 +0,0 @@
<?php
namespace App\Extensions\Captcha\Schemas;
use Filament\Schemas\Components\Component;
interface CaptchaSchemaInterface
{
public function getId(): string;
public function getName(): string;
/**
* @return array<string, string|string[]|bool|null>
*/
public function getConfig(): array;
public function isEnabled(): bool;
public function getFormComponent(): Component;
/**
* @return Component[]
*/
public function getSettingsForm(): array;
public function getIcon(): ?string;
public function validateResponse(?string $captchaResponse = null): void;
}

View File

@ -1,23 +0,0 @@
<?php
namespace App\Extensions\Captcha\Schemas\Turnstile;
use App\Extensions\Captcha\CaptchaService;
use Closure;
use Exception;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\App;
class Rule implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
try {
App::call(fn (CaptchaService $service) => $service->get('turnstile')->validateResponse($value));
} catch (Exception $exception) {
report($exception);
$fail('Captcha validation failed: ' . $exception->getMessage());
}
}
}

View File

@ -1,117 +0,0 @@
<?php
namespace App\Extensions\Captcha\Schemas\Turnstile;
use App\Extensions\Captcha\Schemas\BaseSchema;
use App\Extensions\Captcha\Schemas\CaptchaSchemaInterface;
use Exception;
use Filament\Forms\Components\Toggle;
use Filament\Infolists\Components\TextEntry;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\HtmlString;
class TurnstileSchema extends BaseSchema implements CaptchaSchemaInterface
{
public function getId(): string
{
return 'turnstile';
}
public function isEnabled(): bool
{
return env('CAPTCHA_TURNSTILE_ENABLED', false);
}
public function getFormComponent(): Component
{
return Component::make('turnstile');
}
/**
* @return array<string, string|string[]|bool|null>
*/
public function getConfig(): array
{
return array_merge(parent::getConfig(), [
'verify_domain' => env('CAPTCHA_TURNSTILE_VERIFY_DOMAIN'),
]);
}
/**
* @return \Filament\Support\Components\Component[]
*
* @throws Exception
*/
public function getSettingsForm(): array
{
return array_merge(parent::getSettingsForm(), [
Toggle::make('CAPTCHA_TURNSTILE_VERIFY_DOMAIN')
->label(trans('admin/setting.captcha.verify'))
->columnSpan(2)
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->default(env('CAPTCHA_TURNSTILE_VERIFY_DOMAIN', true)),
TextEntry::make('info')
->label(trans('admin/setting.captcha.info_label'))
->columnSpan(2)
->state(new HtmlString(trans('admin/setting.captcha.info'))),
]);
}
public function getIcon(): ?string
{
return 'tabler-brand-cloudflare';
}
/**
* @throws Exception
*/
public function validateResponse(?string $captchaResponse = null): void
{
$captchaResponse ??= request()->get('cf-turnstile-response');
if (!$secret = env('CAPTCHA_TURNSTILE_SECRET_KEY')) {
throw new Exception('Turnstile secret key is not defined.');
}
$response = Http::asJson()
->timeout(15)
->connectTimeout(5)
->throw()
->post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [
'secret' => $secret,
'response' => $captchaResponse,
])
->json();
if (!$response['success']) {
match ($response['error-codes'][0] ?? null) {
'missing-input-secret' => throw new Exception('The secret parameter was not passed.'),
'invalid-input-secret' => throw new Exception('The secret parameter was invalid, did not exist, or is a testing secret key with a non-testing response.'),
'missing-input-response' => throw new Exception('The response parameter (token) was not passed.'),
'invalid-input-response' => throw new Exception('The response parameter (token) is invalid or has expired.'),
'bad-request' => throw new Exception('The request was rejected because it was malformed.'),
'timeout-or-duplicate' => throw new Exception('The response parameter (token) has already been validated before.'),
default => throw new Exception('An internal error happened while validating the response.'),
};
}
if (!$this->verifyDomain($response['hostname'] ?? '')) {
throw new Exception('Domain verification failed.');
}
}
private function verifyDomain(string $hostname): bool
{
if (!env('CAPTCHA_TURNSTILE_VERIFY_DOMAIN', true)) {
return true;
}
$requestUrl = parse_url(request()->url());
return $hostname === array_get($requestUrl, 'host');
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Extensions\Features;
use Filament\Actions\Action;
use Illuminate\Foundation\Application;
abstract class FeatureProvider
{
/**
* @var array<string, static>
*/
protected static array $providers = [];
/**
* @param string[] $id
* @return self|static[]
*/
public static function getProviders(string|array|null $id = null): array|self
{
if (is_array($id)) {
return array_intersect_key(static::$providers, array_flip($id));
}
return $id ? static::$providers[$id] : static::$providers;
}
protected function __construct(protected Application $app)
{
if (array_key_exists($this->getId(), static::$providers)) {
if (!$this->app->runningUnitTests()) {
logger()->warning("Tried to create duplicate Feature provider with id '{$this->getId()}'");
}
return;
}
static::$providers[$this->getId()] = $this;
}
abstract public function getId(): string;
/**
* A matching subset string (case-insensitive) from the console output
*
* @return array<string>
*/
abstract public function getListeners(): array;
abstract public function getAction(): Action;
}

View File

@ -1,15 +0,0 @@
<?php
namespace App\Extensions\Features;
use Filament\Actions\Action;
interface FeatureSchemaInterface
{
/** @return string[] */
public function getListeners(): array;
public function getId(): string;
public function getAction(): Action;
}

View File

@ -1,52 +0,0 @@
<?php
namespace App\Extensions\Features;
class FeatureService
{
/** @var FeatureSchemaInterface[] */
private array $schemas = [];
/**
* @return FeatureSchemaInterface[]
*/
public function getAll(): array
{
return $this->schemas;
}
public function get(string $id): ?FeatureSchemaInterface
{
return array_get($this->schemas, $id);
}
/**
* @param ?string[] $features
* @return FeatureSchemaInterface[]
*/
public function getActiveSchemas(?array $features = []): array
{
return collect($this->schemas)->only($features)->all();
}
public function register(FeatureSchemaInterface $schema): void
{
if (array_key_exists($schema->getId(), $this->schemas)) {
return;
}
$this->schemas[$schema->getId()] = $schema;
}
/**
* @param ?string[] $features
* @return array<string, array<string>>
*/
public function getMappings(?array $features = []): array
{
return collect($this->getActiveSchemas($features))
->mapWithKeys(fn (FeatureSchemaInterface $schema) => [
$schema->getId() => $schema->getListeners(),
])->all();
}
}

View File

@ -1,27 +1,32 @@
<?php <?php
namespace App\Extensions\Features\Schemas; namespace App\Extensions\Features;
use App\Enums\SubuserPermission;
use App\Extensions\Features\FeatureSchemaInterface;
use App\Facades\Activity; use App\Facades\Activity;
use App\Models\Permission;
use App\Models\Server; use App\Models\Server;
use App\Models\ServerVariable; use App\Models\ServerVariable;
use App\Repositories\Daemon\DaemonServerRepository; use App\Repositories\Daemon\DaemonPowerRepository;
use Closure; use Closure;
use Exception; use Exception;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Facades\Filament; use Filament\Facades\Filament;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Illuminate\Support\HtmlString; use Illuminate\Support\HtmlString;
class GSLTokenSchema implements FeatureSchemaInterface class GSLToken extends FeatureProvider
{ {
public function __construct(protected Application $app)
{
parent::__construct($app);
}
/** @return array<string> */ /** @return array<string> */
public function getListeners(): array public function getListeners(): array
{ {
@ -36,9 +41,6 @@ class GSLTokenSchema implements FeatureSchemaInterface
return 'gsl_token'; return 'gsl_token';
} }
/**
* @throws Exception
*/
public function getAction(): Action public function getAction(): Action
{ {
/** @var Server $server */ /** @var Server $server */
@ -54,9 +56,9 @@ class GSLTokenSchema implements FeatureSchemaInterface
->modalHeading('Invalid GSL token') ->modalHeading('Invalid GSL token')
->modalDescription('It seems like your Gameserver Login Token (GSL token) is invalid or has expired.') ->modalDescription('It seems like your Gameserver Login Token (GSL token) is invalid or has expired.')
->modalSubmitActionLabel('Update GSL Token') ->modalSubmitActionLabel('Update GSL Token')
->disabledSchema(fn () => !user()?->can(SubuserPermission::StartupUpdate, $server)) ->disabledForm(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_UPDATE, $server))
->schema([ ->form([
TextEntry::make('info') Placeholder::make('info')
->label(new HtmlString(Blade::render('You can either <x-filament::link href="https://steamcommunity.com/dev/managegameservers" target="_blank">generate a new one</x-filament::link> and enter it below or leave the field blank to remove it completely.'))), ->label(new HtmlString(Blade::render('You can either <x-filament::link href="https://steamcommunity.com/dev/managegameservers" target="_blank">generate a new one</x-filament::link> and enter it below or leave the field blank to remove it completely.'))),
TextInput::make('gsltoken') TextInput::make('gsltoken')
->label('GSL Token') ->label('GSL Token')
@ -73,12 +75,13 @@ class GSLTokenSchema implements FeatureSchemaInterface
} }
}, },
]) ])
->hintIcon('tabler-code', fn () => implode('|', $serverVariable->variable->rules)) ->hintIcon('tabler-code')
->label(fn () => $serverVariable->variable->name) ->label(fn () => $serverVariable->variable->name)
->hintIconTooltip(fn () => implode('|', $serverVariable->variable->rules))
->prefix(fn () => '{{' . $serverVariable->variable->env_variable . '}}') ->prefix(fn () => '{{' . $serverVariable->variable->env_variable . '}}')
->helperText(fn () => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description), ->helperText(fn () => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description),
]) ])
->action(function (array $data, DaemonServerRepository $serverRepository) use ($server, $serverVariable) { ->action(function (array $data, DaemonPowerRepository $powerRepository) use ($server, $serverVariable) {
/** @var Server $server */ /** @var Server $server */
$server = Filament::getTenant(); $server = Filament::getTenant();
try { try {
@ -100,7 +103,7 @@ class GSLTokenSchema implements FeatureSchemaInterface
->log(); ->log();
} }
$serverRepository->setServer($server)->power('restart'); $powerRepository->setServer($server)->send('restart');
Notification::make() Notification::make()
->title('GSL Token updated') ->title('GSL Token updated')
@ -116,4 +119,9 @@ class GSLTokenSchema implements FeatureSchemaInterface
} }
}); });
} }
public static function register(Application $app): self
{
return new self($app);
}
} }

View File

@ -1,21 +1,26 @@
<?php <?php
namespace App\Extensions\Features\Schemas; namespace App\Extensions\Features;
use App\Enums\SubuserPermission;
use App\Extensions\Features\FeatureSchemaInterface;
use App\Facades\Activity; use App\Facades\Activity;
use App\Models\Permission;
use App\Models\Server; use App\Models\Server;
use App\Repositories\Daemon\DaemonServerRepository; use App\Repositories\Daemon\DaemonPowerRepository;
use Exception; use Exception;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Facades\Filament; use Filament\Facades\Filament;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Infolists\Components\TextEntry;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Illuminate\Foundation\Application;
class JavaVersionSchema implements FeatureSchemaInterface class JavaVersion extends FeatureProvider
{ {
public function __construct(protected Application $app)
{
parent::__construct($app);
}
/** @return array<string> */ /** @return array<string> */
public function getListeners(): array public function getListeners(): array
{ {
@ -44,9 +49,9 @@ class JavaVersionSchema implements FeatureSchemaInterface
->modalHeading('Unsupported Java Version') ->modalHeading('Unsupported Java Version')
->modalDescription('This server is currently running an unsupported version of Java and cannot be started.') ->modalDescription('This server is currently running an unsupported version of Java and cannot be started.')
->modalSubmitActionLabel('Update Docker Image') ->modalSubmitActionLabel('Update Docker Image')
->disabledSchema(fn () => !user()?->can(SubuserPermission::StartupDockerImage, $server)) ->disabledForm(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_DOCKER_IMAGE, $server))
->schema([ ->form([
TextEntry::make('java') Placeholder::make('java')
->label('Please select a supported version from the list below to continue starting the server.'), ->label('Please select a supported version from the list below to continue starting the server.'),
Select::make('image') Select::make('image')
->label('Docker Image') ->label('Docker Image')
@ -56,9 +61,10 @@ class JavaVersionSchema implements FeatureSchemaInterface
->default(fn () => $server->image) ->default(fn () => $server->image)
->notIn(fn () => $server->image) ->notIn(fn () => $server->image)
->required() ->required()
->preload(), ->preload()
->native(false),
]) ])
->action(function (array $data, DaemonServerRepository $serverRepository) use ($server) { ->action(function (array $data, DaemonPowerRepository $powerRepository) use ($server) {
try { try {
$new = $data['image']; $new = $data['image'];
$original = $server->image; $original = $server->image;
@ -70,7 +76,7 @@ class JavaVersionSchema implements FeatureSchemaInterface
->log(); ->log();
} }
$serverRepository->setServer($server)->power('restart'); $powerRepository->setServer($server)->send('restart');
Notification::make() Notification::make()
->title('Docker image updated') ->title('Docker image updated')
@ -86,4 +92,9 @@ class JavaVersionSchema implements FeatureSchemaInterface
} }
}); });
} }
public static function register(Application $app): self
{
return new self($app);
}
} }

View File

@ -1,20 +1,25 @@
<?php <?php
namespace App\Extensions\Features\Schemas; namespace App\Extensions\Features;
use App\Extensions\Features\FeatureSchemaInterface;
use App\Models\Server; use App\Models\Server;
use App\Repositories\Daemon\DaemonFileRepository; use App\Repositories\Daemon\DaemonFileRepository;
use App\Repositories\Daemon\DaemonServerRepository; use App\Repositories\Daemon\DaemonPowerRepository;
use Exception; use Exception;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Facades\Filament; use Filament\Facades\Filament;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString; use Illuminate\Support\HtmlString;
class MinecraftEulaSchema implements FeatureSchemaInterface class MinecraftEula extends FeatureProvider
{ {
public function __construct(protected Application $app)
{
parent::__construct($app);
}
/** @return array<string> */ /** @return array<string> */
public function getListeners(): array public function getListeners(): array
{ {
@ -35,14 +40,14 @@ class MinecraftEulaSchema implements FeatureSchemaInterface
->modalHeading('Minecraft EULA') ->modalHeading('Minecraft EULA')
->modalDescription(new HtmlString(Blade::render('By pressing "I Accept" below you are indicating your agreement to the <x-filament::link href="https://minecraft.net/eula" target="_blank">Minecraft EULA </x-filament::link>.'))) ->modalDescription(new HtmlString(Blade::render('By pressing "I Accept" below you are indicating your agreement to the <x-filament::link href="https://minecraft.net/eula" target="_blank">Minecraft EULA </x-filament::link>.')))
->modalSubmitActionLabel('I Accept') ->modalSubmitActionLabel('I Accept')
->action(function (DaemonFileRepository $fileRepository, DaemonServerRepository $serverRepository) { ->action(function (DaemonFileRepository $fileRepository, DaemonPowerRepository $powerRepository) {
try { try {
/** @var Server $server */ /** @var Server $server */
$server = Filament::getTenant(); $server = Filament::getTenant();
$fileRepository->setServer($server)->putContent('eula.txt', 'eula=true'); $fileRepository->setServer($server)->putContent('eula.txt', 'eula=true');
$serverRepository->setServer($server)->power('restart'); $powerRepository->setServer($server)->send('restart');
Notification::make() Notification::make()
->title('Minecraft EULA accepted') ->title('Minecraft EULA accepted')
@ -58,4 +63,9 @@ class MinecraftEulaSchema implements FeatureSchemaInterface
} }
}); });
} }
public static function register(Application $app): self
{
return new self($app);
}
} }

View File

@ -1,14 +1,19 @@
<?php <?php
namespace App\Extensions\Features\Schemas; namespace App\Extensions\Features;
use App\Extensions\Features\FeatureSchemaInterface;
use Filament\Actions\Action; use Filament\Actions\Action;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString; use Illuminate\Support\HtmlString;
class PIDLimitSchema implements FeatureSchemaInterface class PIDLimit extends FeatureProvider
{ {
public function __construct(protected Application $app)
{
parent::__construct($app);
}
/** @return array<string> */ /** @return array<string> */
public function getListeners(): array public function getListeners(): array
{ {
@ -32,9 +37,9 @@ class PIDLimitSchema implements FeatureSchemaInterface
return Action::make($this->getId()) return Action::make($this->getId())
->requiresConfirmation() ->requiresConfirmation()
->icon('tabler-alert-triangle') ->icon('tabler-alert-triangle')
->modalHeading(fn () => user()?->isAdmin() ? 'Memory or process limit reached...' : 'Possible resource limit reached...') ->modalHeading(fn () => auth()->user()->isAdmin() ? 'Memory or process limit reached...' : 'Possible resource limit reached...')
->modalDescription(new HtmlString(Blade::render( ->modalDescription(new HtmlString(Blade::render(
user()?->isAdmin() ? <<<'HTML' auth()->user()->isAdmin() ? <<<'HTML'
<p> <p>
This server has reached the maximum process or memory limit. This server has reached the maximum process or memory limit.
</p> </p>
@ -63,4 +68,9 @@ class PIDLimitSchema implements FeatureSchemaInterface
->modalCancelActionLabel('Close') ->modalCancelActionLabel('Close')
->action(fn () => null); ->action(fn () => null);
} }
public static function register(Application $app): self
{
return new self($app);
}
} }

View File

@ -1,14 +1,19 @@
<?php <?php
namespace App\Extensions\Features\Schemas; namespace App\Extensions\Features;
use App\Extensions\Features\FeatureSchemaInterface;
use Filament\Actions\Action; use Filament\Actions\Action;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString; use Illuminate\Support\HtmlString;
class SteamDiskSpaceSchema implements FeatureSchemaInterface class SteamDiskSpace extends FeatureProvider
{ {
public function __construct(protected Application $app)
{
parent::__construct($app);
}
/** @return array<string> */ /** @return array<string> */
public function getListeners(): array public function getListeners(): array
{ {
@ -29,7 +34,7 @@ class SteamDiskSpaceSchema implements FeatureSchemaInterface
->requiresConfirmation() ->requiresConfirmation()
->modalHeading('Out of available disk space...') ->modalHeading('Out of available disk space...')
->modalDescription(new HtmlString(Blade::render( ->modalDescription(new HtmlString(Blade::render(
user()?->isAdmin() ? <<<'HTML' auth()->user()->isAdmin() ? <<<'HTML'
<p> <p>
This server has run out of available disk space and cannot complete the install or update This server has run out of available disk space and cannot complete the install or update
process. process.
@ -51,4 +56,9 @@ class SteamDiskSpaceSchema implements FeatureSchemaInterface
->modalCancelActionLabel('Close') ->modalCancelActionLabel('Close')
->action(fn () => null); ->action(fn () => null);
} }
public static function register(Application $app): self
{
return new self($app);
}
} }

View File

@ -6,7 +6,7 @@ use App\Models\ApiKey;
use Laravel\Sanctum\NewAccessToken as SanctumAccessToken; use Laravel\Sanctum\NewAccessToken as SanctumAccessToken;
/** /**
* @property ApiKey $accessToken * @property \App\Models\ApiKey $accessToken
*/ */
class NewAccessToken extends SanctumAccessToken class NewAccessToken extends SanctumAccessToken
{ {

View File

@ -2,7 +2,6 @@
namespace App\Extensions\Lcobucci\JWT\Encoding; namespace App\Extensions\Lcobucci\JWT\Encoding;
use DateTimeImmutable;
use Lcobucci\JWT\ClaimsFormatter; use Lcobucci\JWT\ClaimsFormatter;
use Lcobucci\JWT\Token\RegisteredClaims; use Lcobucci\JWT\Token\RegisteredClaims;
@ -21,7 +20,7 @@ final class TimestampDates implements ClaimsFormatter
continue; continue;
} }
assert($claims[$claim] instanceof DateTimeImmutable); assert($claims[$claim] instanceof \DateTimeImmutable);
$claims[$claim] = $claims[$claim]->getTimestamp(); $claims[$claim] = $claims[$claim]->getTimestamp();
} }

View File

@ -1,39 +0,0 @@
<?php
namespace App\Extensions\OAuth;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Wizard\Step;
interface OAuthSchemaInterface
{
public function getId(): string;
public function getName(): string;
public function getConfigKey(): string;
/** @return ?class-string */
public function getSocialiteProvider(): ?string;
/**
* @return array<string, string|string[]|bool|null>
*/
public function getServiceConfig(): array;
/** @return Component[] */
public function getSettingsForm(): array;
/** @return Step[] */
public function getSetupSteps(): array;
public function getIcon(): ?string;
public function getHexColor(): ?string;
public function isEnabled(): bool;
public function shouldCreateMissingUsers(): bool;
public function shouldLinkMissingUsers(): bool;
}

View File

@ -1,71 +0,0 @@
<?php
namespace App\Extensions\OAuth;
use App\Models\User;
use Illuminate\Support\Facades\Event;
use Laravel\Socialite\Contracts\User as OAuthUser;
use SocialiteProviders\Manager\SocialiteWasCalled;
class OAuthService
{
/** @var OAuthSchemaInterface[] */
private array $schemas = [];
/** @return OAuthSchemaInterface[] */
public function getAll(): array
{
return $this->schemas;
}
public function get(string $id): ?OAuthSchemaInterface
{
return array_get($this->schemas, $id);
}
/** @return OAuthSchemaInterface[] */
public function getEnabled(): array
{
return collect($this->schemas)
->filter(fn (OAuthSchemaInterface $schema) => $schema->isEnabled())
->all();
}
public function register(OAuthSchemaInterface $schema): void
{
if (array_key_exists($schema->getId(), $this->schemas)) {
return;
}
config()->set('services.' . $schema->getId(), array_merge($schema->getServiceConfig(), ['redirect' => '/auth/oauth/callback/' . $schema->getId()]));
if ($schema->getSocialiteProvider()) {
Event::listen(fn (SocialiteWasCalled $event) => $event->extendSocialite($schema->getId(), $schema->getSocialiteProvider()));
}
$this->schemas[$schema->getId()] = $schema;
}
public function linkUser(User $user, OAuthSchemaInterface $schema, OAuthUser $oauthUser): User
{
$oauth = $user->oauth ?? [];
$oauth[$schema->getId()] = $oauthUser->getId();
$user->update(['oauth' => $oauth]);
return $user->refresh();
}
public function unlinkUser(User $user, OAuthSchemaInterface $schema): User
{
$oauth = $user->oauth ?? [];
if (!isset($oauth[$schema->getId()])) {
return $user;
}
unset($oauth[$schema->getId()]);
$user->update(['oauth' => $oauth]);
return $user->refresh();
}
}

View File

@ -1,50 +1,36 @@
<?php <?php
namespace App\Extensions\OAuth\Schemas; namespace App\Extensions\OAuth\Providers;
use Filament\Forms\Components\ColorPicker; use Filament\Forms\Components\ColorPicker;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry; use Illuminate\Foundation\Application;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
use SocialiteProviders\Authentik\Provider; use SocialiteProviders\Authentik\Provider;
final class AuthentikSchema extends OAuthSchema final class AuthentikProvider extends OAuthProvider
{ {
public function __construct(protected Application $app)
{
parent::__construct($app);
}
public function getId(): string public function getId(): string
{ {
return 'authentik'; return 'authentik';
} }
public function getSocialiteProvider(): string public function getProviderClass(): string
{ {
return Provider::class; return Provider::class;
} }
public function getServiceConfig(): array public function getServiceConfig(): array
{ {
return array_merge(parent::getServiceConfig(), [ return [
'base_url' => env('OAUTH_AUTHENTIK_BASE_URL'), 'base_url' => env('OAUTH_AUTHENTIK_BASE_URL'),
]); 'client_id' => env('OAUTH_AUTHENTIK_CLIENT_ID'),
} 'client_secret' => env('OAUTH_AUTHENTIK_CLIENT_SECRET'),
];
public function getSetupSteps(): array
{
return array_merge([
Step::make('Create Authentik Application')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p>On your Authentik dashboard select <b>Applications</b>, then select <b>Create with Provider</b>.</p><p>On the creation step select <b>OAuth2/OpenID Provider</b> and on the configure step set <b>Redirect URIs/Origins</b> to the value below.</p>'))),
TextInput::make('_noenv_callback')
->label('Callback URL')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('/auth/oauth/callback/authentik')),
]),
], parent::getSetupSteps());
} }
public function getSettingsForm(): array public function getSettingsForm(): array
@ -80,4 +66,9 @@ final class AuthentikSchema extends OAuthSchema
{ {
return env('OAUTH_AUTHENTIK_DISPLAY_COLOR', '#fd4b2d'); return env('OAUTH_AUTHENTIK_DISPLAY_COLOR', '#fd4b2d');
} }
public static function register(Application $app): self
{
return new self($app);
}
} }

View File

@ -0,0 +1,38 @@
<?php
namespace App\Extensions\OAuth\Providers;
use Illuminate\Foundation\Application;
final class CommonProvider extends OAuthProvider
{
protected function __construct(protected Application $app, private string $id, private ?string $providerClass, private ?string $icon, private ?string $hexColor)
{
parent::__construct($app);
}
public function getId(): string
{
return $this->id;
}
public function getProviderClass(): ?string
{
return $this->providerClass;
}
public function getIcon(): ?string
{
return $this->icon;
}
public function getHexColor(): ?string
{
return $this->hexColor;
}
public static function register(Application $app, string $id, ?string $providerClass = null, ?string $icon = null, ?string $hexColor = null): static
{
return new self($app, $id, $providerClass, $icon, $hexColor);
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace App\Extensions\OAuth\Providers;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
use SocialiteProviders\Discord\Provider;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
final class DiscordProvider extends OAuthProvider
{
public function __construct(protected Application $app)
{
parent::__construct($app);
}
public function getId(): string
{
return 'discord';
}
public function getProviderClass(): string
{
return Provider::class;
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new Discord OAuth App')
->schema([
Placeholder::make('')
->content(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://discord.com/developers/applications" target="_blank">Discord Developer Portal</x-filament::link> and click on <b>New Application</b>. Enter a <b>Name</b> (e.g. your panel name) and click on <b>Create</b>.</p><p>Copy the <b>Client ID</b> and the <b>Client Secret</b> from the OAuth2 tab, you will need them in the final step.</p>'))),
Placeholder::make('')
->content(new HtmlString('<p>Under <b>Redirects</b> add the below URL.</p>')),
TextInput::make('_noenv_callback')
->label('Redirect URL')
->dehydrated()
->disabled()
->hintAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
->formatStateUsing(fn () => url('/auth/oauth/callback/discord')),
]),
], parent::getSetupSteps());
}
public function getIcon(): string
{
return 'tabler-brand-discord-f';
}
public function getHexColor(): string
{
return '#5865F2';
}
public static function register(Application $app): self
{
return new self($app);
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace App\Extensions\OAuth\Providers;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
final class GithubProvider extends OAuthProvider
{
public function __construct(protected Application $app)
{
parent::__construct($app);
}
public function getId(): string
{
return 'github';
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new Github OAuth App')
->schema([
Placeholder::make('')
->content(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://github.com/settings/developers" target="_blank">Github Developer Dashboard</x-filament::link>, go to <b>OAuth Apps</b> and click on <b>New OAuth App</b>.</p><p>Enter an <b>Application name</b> (e.g. your panel name), set <b>Homepage URL</b> to your panel url and enter the below url as <b>Authorization callback URL</b>.</p>'))),
TextInput::make('_noenv_callback')
->label('Authorization callback URL')
->dehydrated()
->disabled()
->hintAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
->default(fn () => url('/auth/oauth/callback/github')),
Placeholder::make('')
->content(new HtmlString('<p>When you filled all fields click on <b>Register application</b>.</p>')),
]),
Step::make('Create Client Secret')
->schema([
Placeholder::make('')
->content(new HtmlString('<p>Once you registered your app, generate a new <b>Client Secret</b>.</p><p>You will also need the <b>Client ID</b>.</p>')),
]),
], parent::getSetupSteps());
}
public function getIcon(): string
{
return 'tabler-brand-github-f';
}
public function getHexColor(): string
{
return '#4078c0';
}
public static function register(Application $app): self
{
return new self($app);
}
}

View File

@ -1,15 +1,22 @@
<?php <?php
namespace App\Extensions\OAuth\Schemas; namespace App\Extensions\OAuth\Providers;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry; use Filament\Forms\Components\Wizard\Step;
use Filament\Schemas\Components\Wizard\Step; use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString; use Illuminate\Support\HtmlString;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
final class GitlabSchema extends OAuthSchema final class GitlabProvider extends OAuthProvider
{ {
public function __construct(protected Application $app)
{
parent::__construct($app);
}
public function getId(): string public function getId(): string
{ {
return 'gitlab'; return 'gitlab';
@ -40,14 +47,13 @@ final class GitlabSchema extends OAuthSchema
return array_merge([ return array_merge([
Step::make('Register new Gitlab OAuth App') Step::make('Register new Gitlab OAuth App')
->schema([ ->schema([
TextEntry::make('register_application') Placeholder::make('')
->hiddenLabel() ->content(new HtmlString(Blade::render('Check out the <x-filament::link href="https://docs.gitlab.com/integration/oauth_provider/" target="_blank">Gitlab docs</x-filament::link> on how to create the oauth app.'))),
->state(new HtmlString(Blade::render('Check out the <x-filament::link href="https://docs.gitlab.com/integration/oauth_provider/" target="_blank">Gitlab docs</x-filament::link> on how to create the oauth app.'))),
TextInput::make('_noenv_callback') TextInput::make('_noenv_callback')
->label('Redirect URI') ->label('Redirect URI')
->dehydrated() ->dehydrated()
->disabled() ->disabled()
->hintCopy() ->hintAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
->default(fn () => url('/auth/oauth/callback/gitlab')), ->default(fn () => url('/auth/oauth/callback/gitlab')),
]), ]),
], parent::getSetupSteps()); ], parent::getSetupSteps());
@ -62,4 +68,9 @@ final class GitlabSchema extends OAuthSchema
{ {
return '#fca326'; return '#fca326';
} }
public static function register(Application $app): self
{
return new self($app);
}
} }

View File

@ -0,0 +1,131 @@
<?php
namespace App\Extensions\OAuth\Providers;
use Filament\Forms\Components\Component;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Str;
use SocialiteProviders\Manager\SocialiteWasCalled;
abstract class OAuthProvider
{
/**
* @var array<string, static>
*/
protected static array $providers = [];
/**
* @return self|static[]
*/
public static function get(?string $id = null): array|self
{
return $id ? static::$providers[$id] : static::$providers;
}
protected function __construct(protected Application $app)
{
if (array_key_exists($this->getId(), static::$providers)) {
if (!$this->app->runningUnitTests()) {
logger()->warning("Tried to create duplicate OAuth provider with id '{$this->getId()}'");
}
return;
}
config()->set('services.' . $this->getId(), array_merge($this->getServiceConfig(), ['redirect' => '/auth/oauth/callback/' . $this->getId()]));
if ($this->getProviderClass()) {
Event::listen(function (SocialiteWasCalled $event) {
$event->extendSocialite($this->getId(), $this->getProviderClass());
});
}
static::$providers[$this->getId()] = $this;
}
abstract public function getId(): string;
public function getProviderClass(): ?string
{
return null;
}
/**
* @return array<string, string|string[]|bool|null>
*/
public function getServiceConfig(): array
{
$id = Str::upper($this->getId());
return [
'client_id' => env("OAUTH_{$id}_CLIENT_ID"),
'client_secret' => env("OAUTH_{$id}_CLIENT_SECRET"),
];
}
/**
* @return Component[]
*/
public function getSettingsForm(): array
{
$id = Str::upper($this->getId());
return [
TextInput::make("OAUTH_{$id}_CLIENT_ID")
->label('Client ID')
->placeholder('Client ID')
->columnSpan(2)
->required()
->password()
->revealable()
->autocomplete(false)
->default(env("OAUTH_{$id}_CLIENT_ID")),
TextInput::make("OAUTH_{$id}_CLIENT_SECRET")
->label('Client Secret')
->placeholder('Client Secret')
->columnSpan(2)
->required()
->password()
->revealable()
->autocomplete(false)
->default(env("OAUTH_{$id}_CLIENT_SECRET")),
];
}
/**
* @return Step[]
*/
public function getSetupSteps(): array
{
return [
Step::make('OAuth Config')
->columns(4)
->schema($this->getSettingsForm()),
];
}
public function getName(): string
{
return Str::title($this->getId());
}
public function getIcon(): ?string
{
return null;
}
public function getHexColor(): ?string
{
return null;
}
public function isEnabled(): bool
{
$id = Str::upper($this->getId());
return env("OAUTH_{$id}_ENABLED", false);
}
}

View File

@ -1,22 +1,28 @@
<?php <?php
namespace App\Extensions\OAuth\Schemas; namespace App\Extensions\OAuth\Providers;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry; use Filament\Forms\Components\Wizard\Step;
use Filament\Schemas\Components\Wizard\Step; use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString; use Illuminate\Support\HtmlString;
use SocialiteProviders\Steam\Provider; use SocialiteProviders\Steam\Provider;
final class SteamSchema extends OAuthSchema final class SteamProvider extends OAuthProvider
{ {
public function __construct(protected Application $app)
{
parent::__construct($app);
}
public function getId(): string public function getId(): string
{ {
return 'steam'; return 'steam';
} }
public function getSocialiteProvider(): string public function getProviderClass(): string
{ {
return Provider::class; return Provider::class;
} }
@ -52,9 +58,8 @@ final class SteamSchema extends OAuthSchema
return array_merge([ return array_merge([
Step::make('Create API Key') Step::make('Create API Key')
->schema([ ->schema([
TextEntry::make('create_api_key') Placeholder::make('')
->hiddenLabel() ->content(new HtmlString(Blade::render('Visit <x-filament::link href="https://steamcommunity.com/dev/apikey" target="_blank">https://steamcommunity.com/dev/apikey</x-filament::link> to generate an API key.'))),
->state(new HtmlString(Blade::render('Visit <x-filament::link href="https://steamcommunity.com/dev/apikey" target="_blank">https://steamcommunity.com/dev/apikey</x-filament::link> to generate an API key.'))),
]), ]),
], parent::getSetupSteps()); ], parent::getSetupSteps());
} }
@ -68,4 +73,9 @@ final class SteamSchema extends OAuthSchema
{ {
return '#00adee'; return '#00adee';
} }
public static function register(Application $app): self
{
return new self($app);
}
} }

View File

@ -1,45 +0,0 @@
<?php
namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
final class BitbucketSchema extends OAuthSchema
{
public function getId(): string
{
return 'bitbucket';
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new Bitbucket Consumer')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud" target="_blank">Bitbucket OAuth Documentation</x-filament::link> and follow the steps in <b>Create a consumer</b>.</p><p>For the <b>Callback URL</b> use the value below.</p>'))),
TextInput::make('_noenv_callback')
->label('Callback URL')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('/auth/oauth/callback/bitbucket')),
]),
], parent::getSetupSteps());
}
public function getIcon(): string
{
return 'tabler-brand-bitbucket-f';
}
public function getHexColor(): string
{
return '#205081';
}
}

View File

@ -1,39 +0,0 @@
<?php
namespace App\Extensions\OAuth\Schemas;
final class CommonSchema extends OAuthSchema
{
public function __construct(
private readonly string $id,
private readonly ?string $name = null,
private readonly ?string $configName = null,
private readonly ?string $icon = null,
private readonly ?string $hexColor = null,
) {}
public function getId(): string
{
return $this->id;
}
public function getName(): string
{
return $this->name ?? parent::getName();
}
public function getConfigKey(): string
{
return $this->configName ?? parent::getConfigKey();
}
public function getIcon(): ?string
{
return $this->icon;
}
public function getHexColor(): ?string
{
return $this->hexColor;
}
}

View File

@ -1,54 +0,0 @@
<?php
namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
use SocialiteProviders\Discord\Provider;
final class DiscordSchema extends OAuthSchema
{
public function getId(): string
{
return 'discord';
}
public function getSocialiteProvider(): string
{
return Provider::class;
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new Discord OAuth App')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://discord.com/developers/applications" target="_blank">Discord Developer Portal</x-filament::link> and click on <b>New Application</b>. Enter a <b>Name</b> (e.g. your panel name) and click on <b>Create</b>.</p><p>Copy the <b>Client ID</b> and the <b>Client Secret</b> from the OAuth2 tab, you will need them in the final step.</p>'))),
TextEntry::make('set_redirect')
->hiddenLabel()
->state(new HtmlString('<p>Under <b>Redirects</b> add the below URL.</p>')),
TextInput::make('_noenv_callback')
->label('Redirect URL')
->dehydrated()
->disabled()
->hintCopy()
->formatStateUsing(fn () => url('/auth/oauth/callback/discord')),
]),
], parent::getSetupSteps());
}
public function getIcon(): string
{
return 'tabler-brand-discord-f';
}
public function getHexColor(): string
{
return '#5865F2';
}
}

View File

@ -1,48 +0,0 @@
<?php
namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
final class FacebookSchema extends OAuthSchema
{
public function getId(): string
{
return 'facebook';
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new Facebook Application')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://developers.facebook.com/apps" target="_blank">Facebook Developer Dashboard</x-filament::link> and select or create a new app you will use for authentication. Make sure to have "Authenticate and request data from users with Facebook Login" as one of the Use Cases.</p><p>Once selected go to <b>Use Cases</b> and customize "Authenticate and request data from users with Facebook Login", from there go to <b>Settings</b> and add <b>Valid OAuth Redirect URIs</b> using the value below.</p>'))),
TextInput::make('_noenv_callback')
->label('Valid OAuth Redirect URIs')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('/auth/oauth/callback/facebook')),
TextEntry::make('get_app_info')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p>To obtain the OAuth values go to <b>App Settings > Basic</b>.</p>'))),
]),
], parent::getSetupSteps());
}
public function getIcon(): string
{
return 'tabler-brand-facebook-f';
}
public function getHexColor(): string
{
return '#1877f2';
}
}

View File

@ -1,54 +0,0 @@
<?php
namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
final class GithubSchema extends OAuthSchema
{
public function getId(): string
{
return 'github';
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new Github OAuth App')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://github.com/settings/developers" target="_blank">Github Developer Dashboard</x-filament::link>, go to <b>OAuth Apps</b> and click on <b>New OAuth App</b>.</p><p>Enter an <b>Application name</b> (e.g. your panel name), set <b>Homepage URL</b> to your panel url and enter the below url as <b>Authorization callback URL</b>.</p>'))),
TextInput::make('_noenv_callback')
->label('Authorization callback URL')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('/auth/oauth/callback/github')),
TextEntry::make('register_application')
->hiddenLabel()
->state(new HtmlString('<p>When you filled all fields click on <b>Register application</b>.</p>')),
]),
Step::make('Create Client Secret')
->schema([
TextEntry::make('create_client_secret')
->hiddenLabel()
->state(new HtmlString('<p>Once you registered your app, generate a new <b>Client Secret</b>.</p><p>You will also need the <b>Client ID</b>.</p>')),
]),
], parent::getSetupSteps());
}
public function getIcon(): string
{
return 'tabler-brand-github-f';
}
public function getHexColor(): string
{
return '#4078c0';
}
}

View File

@ -1,54 +0,0 @@
<?php
namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
final class GoogleSchema extends OAuthSchema
{
public function getId(): string
{
return 'google';
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new OAuth client')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://console.developers.google.com/" target="_blank">Google API Console</x-filament::link> and create or select the project you want to use.</p><p>Navigate or search <b>Credentials</b>, click on the <b>Create Credentials</b> button and select <b>OAuth client ID</b>. On the Application type select <b>Web Application</b>.</p><p>On <b>Authorized JavaScript origins</b> and <b>Authorized redirect URIs</b> add and use the values below.</p>'))),
TextInput::make('_noenv_origin')
->label('Authorized JavaScript origins')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('')),
TextInput::make('_noenv_callback')
->label('Authorized redirect URIs')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('/auth/oauth/callback/google')),
TextEntry::make('register_application')
->hiddenLabel()
->state(new HtmlString('<p>When you filled all fields click on <b>Create</b>.</p>')),
]),
], parent::getSetupSteps());
}
public function getIcon(): string
{
return 'tabler-brand-google-f';
}
public function getHexColor(): string
{
return '#4285f4';
}
}

View File

@ -1,45 +0,0 @@
<?php
namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
final class LinkedinSchema extends OAuthSchema
{
public function getId(): string
{
return 'linkedin';
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Obtain Linkedin App OAuth Config')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p><x-filament::link href="https://www.linkedin.com/developers/apps/new" target="_blank">Create</x-filament::link> or <x-filament::link href="https://www.linkedin.com/developers/apps" target="_blank">select</x-filament::link> the one you will be using for authentication.</p><p>Select the <b>Auth</b> tab and set <b>Authorized redirect URLs for your app</b> to the value below.</p>'))),
TextInput::make('_noenv_callback')
->label('Authorized redirect URL')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('/auth/oauth/callback/linkedin')),
]),
], parent::getSetupSteps());
}
public function getIcon(): string
{
return 'tabler-brand-linkedin-f';
}
public function getHexColor(): string
{
return '#0a66c2';
}
}

View File

@ -1,137 +0,0 @@
<?php
namespace App\Extensions\OAuth\Schemas;
use App\Extensions\OAuth\OAuthSchemaInterface;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Str;
abstract class OAuthSchema implements OAuthSchemaInterface
{
abstract public function getId(): string;
public function getSocialiteProvider(): ?string
{
return null;
}
public function getServiceConfig(): array
{
$id = Str::upper($this->getId());
return [
'client_id' => env("OAUTH_{$id}_CLIENT_ID"),
'client_secret' => env("OAUTH_{$id}_CLIENT_SECRET"),
];
}
/**
* @return Component[]
*/
public function getSettingsForm(): array
{
$id = Str::upper($this->getId());
return [
TextInput::make("OAUTH_{$id}_CLIENT_ID")
->label('Client ID')
->placeholder('Client ID')
->columnSpan(2)
->required()
->password()
->revealable()
->autocomplete(false)
->default(env("OAUTH_{$id}_CLIENT_ID")),
TextInput::make("OAUTH_{$id}_CLIENT_SECRET")
->label('Client Secret')
->placeholder('Client Secret')
->columnSpan(2)
->required()
->password()
->revealable()
->autocomplete(false)
->default(env("OAUTH_{$id}_CLIENT_SECRET")),
Toggle::make("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS")
->label(trans('admin/setting.oauth.create_missing_users'))
->columnSpan(2)
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->formatStateUsing(fn ($state) => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS", (bool) $state))
->default(env("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS")),
Toggle::make("OAUTH_{$id}_SHOULD_LINK_MISSING_USERS")
->label(trans('admin/setting.oauth.link_missing_users'))
->columnSpan(2)
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->formatStateUsing(fn ($state) => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set("OAUTH_{$id}_SHOULD_LINK_MISSING_USERS", (bool) $state))
->default(env("OAUTH_{$id}_SHOULD_LINK_MISSING_USERS")),
];
}
/**
* @return Step[]
*/
public function getSetupSteps(): array
{
return [
Step::make('OAuth Config')
->columns(4)
->schema($this->getSettingsForm()),
];
}
public function getName(): string
{
return Str::title($this->getId());
}
public function getConfigKey(): string
{
$id = Str::upper($this->getId());
return "OAUTH_{$id}_ENABLED";
}
public function getIcon(): ?string
{
return null;
}
public function getHexColor(): ?string
{
return null;
}
public function isEnabled(): bool
{
$id = Str::upper($this->getId());
return env("OAUTH_{$id}_ENABLED", false);
}
public function shouldCreateMissingUsers(): bool
{
$id = Str::upper($this->getId());
return env("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS", false);
}
public function shouldLinkMissingUsers(): bool
{
$id = Str::upper($this->getId());
return env("OAUTH_{$id}_SHOULD_LINK_MISSING_USERS", false);
}
}

View File

@ -1,45 +0,0 @@
<?php
namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
final class SlackSchema extends OAuthSchema
{
public function getId(): string
{
return 'slack';
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new Slack OAuth')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p><x-filament::link href="https://api.slack.com/apps?new_app=1" target="_blank">Create</x-filament::link> a slack app or <x-filament::link href="https://api.slack.com/apps" target="_blank">select</x-filament::link> the one you will be using for authentication.</p><p>Navigate to the <b>OAuth & Permissions</b> section and configure the <b>Redirect URL</b> using the value below.</p>'))),
TextInput::make('_noenv_callback')
->label('Redirect URL')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('/auth/oauth/callback/slack')),
]),
], parent::getSetupSteps());
}
public function getIcon(): string
{
return 'tabler-brand-slack';
}
public function getHexColor(): string
{
return '#6ecadc';
}
}

View File

@ -1,54 +0,0 @@
<?php
namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
final class XSchema extends OAuthSchema
{
public function getId(): string
{
return 'x';
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new X App')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://developer.x.com/en/portal/dashboard" target="_blank">X Developer Dashboard</x-filament::link> and create or select the project app you want to use.</p><p>Go to the app\'s settings and set up <b>User authentication</b> if not yet. Make sure to select <b>Web App</b> as the type of app.</p><p>For the <b>Callback URI / Redirect URL</b> and <b>Website URL</b> set it using the value below.</p>'))),
TextInput::make('_noenv_origin')
->label('Website URL')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('')),
TextInput::make('_noenv_callback')
->label('Callback URI / Redirect URL')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('/auth/oauth/callback/x')),
TextEntry::make('register_application')
->hiddenLabel()
->state(new HtmlString('<p>If you have already set this up go to your app\'s <b>Keys and tokens</b> and obtain the Client ID and Secret there.</p>')),
]),
], parent::getSetupSteps());
}
public function getIcon(): string
{
return 'tabler-brand-x';
}
public function getHexColor(): string
{
return '#1da1f2';
}
}

View File

@ -2,22 +2,20 @@
namespace App\Extensions\Spatie\Fractalistic; namespace App\Extensions\Spatie\Fractalistic;
use App\Extensions\League\Fractal\Serializers\PanelSerializer;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use League\Fractal\Scope; use League\Fractal\Scope;
use League\Fractal\TransformerAbstract; use League\Fractal\TransformerAbstract;
use Spatie\Fractal\Fractal as SpatieFractal; use Spatie\Fractal\Fractal as SpatieFractal;
use Spatie\Fractalistic\Exceptions\InvalidTransformation; use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Spatie\Fractalistic\Exceptions\NoTransformerSpecified; use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use App\Extensions\League\Fractal\Serializers\PanelSerializer;
class Fractal extends SpatieFractal class Fractal extends SpatieFractal
{ {
/** /**
* Create fractal data. * Create fractal data.
* *
* @throws InvalidTransformation * @throws \Spatie\Fractalistic\Exceptions\InvalidTransformation
* @throws NoTransformerSpecified * @throws \Spatie\Fractalistic\Exceptions\NoTransformerSpecified
*/ */
public function createData(): Scope public function createData(): Scope
{ {

View File

@ -1,32 +0,0 @@
<?php
namespace App\Extensions\Tasks\Schemas;
use App\Models\Schedule;
use App\Models\Task;
use App\Services\Backups\InitiateBackupService;
final class CreateBackupSchema extends TaskSchema
{
public function __construct(private InitiateBackupService $backupService) {}
public function getId(): string
{
return 'backup';
}
public function runTask(Task $task): void
{
$this->backupService->setIgnoredFiles(explode(PHP_EOL, $task->payload))->handle($task->server, null, true);
}
public function canCreate(Schedule $schedule): bool
{
return $schedule->server->backup_limit > 0;
}
public function getPayloadLabel(): string
{
return trans('server/schedule.tasks.actions.backup.files_to_ignore');
}
}

View File

@ -1,26 +0,0 @@
<?php
namespace App\Extensions\Tasks\Schemas;
use App\Models\Task;
use App\Services\Files\DeleteFilesService;
final class DeleteFilesSchema extends TaskSchema
{
public function __construct(private DeleteFilesService $deleteFilesService) {}
public function getId(): string
{
return 'delete_files';
}
public function runTask(Task $task): void
{
$this->deleteFilesService->handle($task->server, explode(PHP_EOL, $task->payload));
}
public function getPayloadLabel(): string
{
return trans('server/schedule.tasks.actions.delete_files.files_to_delete');
}
}

Some files were not shown because too many files have changed in this diff Show More