Compare commits

..

1 Commits

Author SHA1 Message Date
github-actions[bot]
9b5fd477d5 ci(release): bump version 2024-08-04 17:28:48 +00:00
575 changed files with 24026 additions and 6289 deletions

View File

@@ -1,10 +0,0 @@
.git
node_modules
vendor
database/database.sqlite
storage/debugbar/*.json
storage/logs/*.log
storage/framework/cache/data/*
storage/framework/sessions/*
storage/framework/testing
storage/framework/views/*.php

View File

@@ -1,7 +1,33 @@
APP_ENV=production
APP_DEBUG=false
APP_KEY=
APP_URL=http://panel.test
APP_INSTALLED=false
APP_TIMEZONE=UTC
APP_URL=http://panel.test
APP_LOCALE=en
APP_INSTALLED=false
LOG_CHANNEL=daily
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=sqlite
CACHE_STORE=file
QUEUE_CONNECTION=database
SESSION_DRIVER=file
MAIL_MAILER=log
MAIL_HOST=smtp.example.com
MAIL_PORT=25
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=no-reply@example.com
MAIL_FROM_NAME="Pelican Admin"
# Set this to your domain to prevent it defaulting to 'localhost', causing mail servers such as Gmail to reject your mail
# MAIL_EHLO_DOMAIN=panel.example.com
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null

View File

@@ -33,6 +33,7 @@ body:
attributes:
label: Panel Version
description: Version number of your Panel (latest is not a version)
placeholder: 1.4.0
validations:
required: true
@@ -41,6 +42,7 @@ body:
attributes:
label: Wings Version
description: Version number of your Wings (latest is not a version)
placeholder: 1.4.2
validations:
required: true
@@ -66,7 +68,7 @@ body:
Run the following command to collect logs on your system.
Wings: `sudo wings diagnostics`
Panel: `tail -n 150 /var/www/pelican/storage/logs/laravel-$(date +%F).log | curl -X POST -F 'c=@-' paste.pelistuff.com`
Panel: `tail -n 150 /var/www/pelican/storage/logs/laravel-$(date +%F).log | nc pelipaste.com 99`
placeholder: "https://pelipaste.com/a1h6z"
render: bash
validations:

View File

@@ -1,65 +1,81 @@
#!/bin/ash -e
cd /app
#mkdir -p /var/log/supervisord/ /var/log/php8/ \
mkdir -p /var/log/panel/logs/ /var/log/supervisord/ /var/log/nginx/ /var/log/php8/ \
&& chmod 777 /var/log/panel/logs/ \
&& ln -s /app/storage/logs/ /var/log/panel/
## check for .env file and generate app keys if missing
if [ -f /pelican-data/.env ]; then
if [ -f /app/var/.env ]; then
echo "external vars exist."
rm -rf /var/www/html/.env
rm -rf /app/.env
ln -s /app/var/.env /app/
else
echo "external vars don't exist."
rm -rf /var/www/html/.env
touch /pelican-data/.env
rm -rf /app/.env
touch /app/var/.env
## manually generate a key because key generate --force fails
if [ -z $APP_KEY ]; then
echo -e "Generating key."
APP_KEY=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
echo -e "Generated app key: $APP_KEY"
echo -e "APP_KEY=$APP_KEY" > /pelican-data/.env
echo -e "APP_KEY=$APP_KEY" > /app/var/.env
else
echo -e "APP_KEY exists in environment, using that."
echo -e "APP_KEY=$APP_KEY" > /pelican-data/.env
echo -e "APP_KEY=$APP_KEY" > /app/var/.env
fi
## enable installer
echo -e "APP_INSTALLED=false" >> /pelican-data/.env
ln -s /app/var/.env /app/
fi
mkdir /pelican-data/database
ln -s /pelican-data/.env /var/www/html/
chown -h www-data:www-data /var/www/html/.env
ln -s /pelican-data/database/database.sqlite /var/www/html/database/
if ! grep -q "APP_KEY=" .env || grep -q "APP_KEY=$" .env; then
echo "Generating APP_KEY..."
php artisan key:generate --force
echo "Checking if https is required."
if [ -f /etc/nginx/http.d/panel.conf ]; then
echo "Using nginx config already in place."
if [ $LE_EMAIL ]; then
echo "Checking for cert update"
certbot certonly -d $(echo $APP_URL | sed 's~http[s]*://~~g') --standalone -m $LE_EMAIL --agree-tos -n
else
echo "No letsencrypt email is set"
fi
else
echo "APP_KEY is already set."
echo "Checking if letsencrypt email is set."
if [ -z $LE_EMAIL ]; then
echo "No letsencrypt email is set using http config."
cp .github/docker/default.conf /etc/nginx/http.d/panel.conf
else
echo "writing ssl config"
cp .github/docker/default_ssl.conf /etc/nginx/http.d/panel.conf
echo "updating ssl config for domain"
sed -i "s|<domain>|$(echo $APP_URL | sed 's~http[s]*://~~g')|g" /etc/nginx/http.d/panel.conf
echo "generating certs"
certbot certonly -d $(echo $APP_URL | sed 's~http[s]*://~~g') --standalone -m $LE_EMAIL --agree-tos -n
fi
echo "Removing the default nginx config"
rm -rf /etc/nginx/http.d/default.conf
fi
if [[ -z $DB_PORT ]]; then
echo -e "DB_PORT not specified, defaulting to 3306"
DB_PORT=3306
fi
## check for DB up before starting the panel
echo "Checking database status."
until nc -z -v -w30 $DB_HOST $DB_PORT
do
echo "Waiting for database connection..."
# wait for 1 seconds before check again
sleep 1
done
## make sure the db is set up
echo -e "Migrating Database"
php artisan migrate --force
echo -e "Optimizing Filament"
php artisan filament:optimize
echo -e "Migrating and Seeding D.B"
php artisan migrate --seed --force
## start cronjobs for the queue
echo -e "Starting cron jobs."
crond -L /var/log/crond -l 5
export SUPERVISORD_CADDY=false
## disable caddy if SKIP_CADDY is set
if [[ "${SKIP_CADDY:-}" == "true" ]]; then
echo "Starting PHP-FPM only"
else
echo "Starting PHP-FPM and Caddy"
export SUPERVISORD_CADDY=true
fi
chown -R www-data:www-data /pelican-data/.env /pelican-data/database
echo "Starting Supervisord"
echo -e "Starting supervisord."
exec "$@"

View File

@@ -1,7 +1,5 @@
[unix_http_server]
file=/tmp/supervisor.sock ; path to your socket file
username=dummy
password=dummy
[supervisord]
logfile=/var/log/supervisord/supervisord.log ; supervisord log file
@@ -20,8 +18,6 @@ supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[supervisorctl]
serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL for a unix socket
username=dummy
password=dummy
[program:php-fpm]
command=/usr/local/sbin/php-fpm -F
@@ -29,15 +25,15 @@ autostart=true
autorestart=true
[program:queue-worker]
command=/usr/local/bin/php /var/www/html/artisan queue:work --tries=3
user=www-data
command=/usr/local/bin/php /app/artisan queue:work --queue=high,standard,low --sleep=3 --tries=3
user=nginx
autostart=true
autorestart=true
[program:caddy]
command=caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
autostart=%(ENV_SUPERVISORD_CADDY)s
autorestart=%(ENV_SUPERVISORD_CADDY)s
[program:nginx]
command=/usr/sbin/nginx -g 'daemon off;'
autostart=true
autorestart=true
priority=10
stdout_events_enabled=true
stderr_events_enabled=true
stderr_events_enabled=true

View File

@@ -3,8 +3,10 @@ name: Tests
on:
push:
branches:
- main
- '**'
pull_request:
branches:
- '**'
jobs:
mysql:
@@ -87,7 +89,7 @@ jobs:
fail-fast: false
matrix:
php: [8.2, 8.3]
database: ["mariadb:10.6", "mariadb:10.11", "mariadb:11.4"]
database: ["mariadb:10.3", "mariadb:10.11", "mariadb:11.4"]
services:
database:
image: ${{ matrix.database }}

View File

@@ -1,86 +0,0 @@
name: Docker
on:
push:
branches:
- main
release:
types:
- published
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
name: Build and Push
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
# Always run against a tag, even if the commit into the tag has [docker skip] within the commit message.
if: "!contains(github.ref, 'main') || (!contains(github.event.head_commit.message, 'skip docker') && !contains(github.event.head_commit.message, 'docker skip'))"
steps:
- name: Code checkout
uses: actions/checkout@v4
- name: Docker metadata
id: docker_meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: |
latest=false
tags: |
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.action == 'published' && github.event.release.prerelease == false }}
type=ref,event=tag
type=ref,event=branch
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get Build Information
id: build_info
run: |
echo "version_tag=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_OUTPUT
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Build and Push (tag)
uses: docker/build-push-action@v5
if: "github.event_name == 'release' && github.event.action == 'published'"
with:
context: .
file: ./Dockerfile
push: true
platforms: linux/amd64,linux/arm64
build-args: |
VERSION=${{ steps.build_info.outputs.version_tag }}
labels: ${{ steps.docker_meta.outputs.labels }}
tags: ${{ steps.docker_meta.outputs.tags }}
- name: Build and Push (main)
uses: docker/build-push-action@v5
if: "github.event_name == 'push' && contains(github.ref, 'main')"
with:
context: .
file: ./Dockerfile
push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64,linux/arm64
build-args: |
VERSION=dev-${{ steps.build_info.outputs.short_sha }}
labels: ${{ steps.docker_meta.outputs.labels }}
tags: ${{ steps.docker_meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -1,12 +0,0 @@
{
admin off
email {$ADMIN_EMAIL}
}
{$APP_URL} {
root * /var/www/html/public
encode gzip
php_fastcgi 127.0.0.1:9000
file_server
}

View File

@@ -1,59 +1,41 @@
# Pelican Production Dockerfile
FROM node:20-alpine AS yarn
#FROM --platform=$TARGETOS/$TARGETARCH node:20-alpine AS yarn
WORKDIR /build
# Stage 0:
# Build the assets that are needed for the frontend. This build stage is then discarded
# since we won't need NodeJS anymore in the future. This Docker image ships a final production
# level distribution
FROM --platform=$TARGETOS/$TARGETARCH node:20-alpine
WORKDIR /app
COPY . ./
RUN yarn config set network-timeout 300000 \
&& yarn install --frozen-lockfile \
RUN yarn install --frozen-lockfile \
&& yarn run build:production
FROM php:8.3-fpm-alpine
# FROM --platform=$TARGETOS/$TARGETARCH php:8.3-fpm-alpine
# Stage 1:
# Build the actual container with all of the needed PHP dependencies that will run the application.
FROM --platform=$TARGETOS/$TARGETARCH php:8.3-fpm-alpine
WORKDIR /app
COPY . ./
COPY --from=0 /app/public/assets ./public/assets
RUN apk add --no-cache --update ca-certificates dcron curl git supervisor tar unzip nginx libpng-dev libxml2-dev libzip-dev icu-dev certbot certbot-nginx \
&& docker-php-ext-configure zip \
&& docker-php-ext-install bcmath gd intl pdo_mysql zip \
&& curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \
&& cp .env.example .env \
&& mkdir -p bootstrap/cache/ storage/logs storage/framework/sessions storage/framework/views storage/framework/cache \
&& chmod 777 -R bootstrap storage \
&& composer install --no-dev --optimize-autoloader \
&& rm -rf .env bootstrap/cache/*.php \
&& mkdir -p /app/storage/logs/ \
&& chown -R nginx:nginx .
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
RUN rm /usr/local/etc/php-fpm.conf \
&& echo "* * * * * /usr/local/bin/php /app/artisan schedule:run >> /dev/null 2>&1" >> /var/spool/cron/crontabs/root \
&& echo "0 23 * * * certbot renew --nginx --quiet" >> /var/spool/cron/crontabs/root \
&& sed -i s/ssl_session_cache/#ssl_session_cache/g /etc/nginx/nginx.conf \
&& mkdir -p /var/run/php /var/run/nginx
WORKDIR /var/www/html
# Install dependencies
RUN apk update && apk add --no-cache \
libpng-dev libjpeg-turbo-dev freetype-dev libzip-dev icu-dev \
zip unzip curl \
caddy ca-certificates supervisor \
&& docker-php-ext-install bcmath gd intl zip opcache pcntl posix pdo_mysql
# Copy the Caddyfile to the container
COPY Caddyfile /etc/caddy/Caddyfile
# Copy the application code to the container
COPY . .
COPY --from=yarn /build/public/assets ./public/assets
RUN touch .env
RUN composer install --no-dev --optimize-autoloader
# Set file permissions
RUN chmod -R 755 storage bootstrap/cache \
&& chown -R www-data:www-data ./
# Add scheduler to cron
RUN echo "* * * * * php /var/www/html/artisan schedule:run >> /dev/null 2>&1" | crontab -u www-data -
## supervisord config and log dir
RUN cp .github/docker/supervisord.conf /etc/supervisord.conf && \
mkdir /var/log/supervisord/
HEALTHCHECK --interval=5m --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost/up || exit 1
COPY .github/docker/default.conf /etc/nginx/http.d/default.conf
COPY .github/docker/www.conf /usr/local/etc/php-fpm.conf
COPY .github/docker/supervisord.conf /etc/supervisord.conf
EXPOSE 80 443
VOLUME /pelican-data
ENTRYPOINT [ "/bin/ash", ".github/docker/entrypoint.sh" ]
CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ]

View File

@@ -1,43 +0,0 @@
<?php
namespace App\Console\Commands\Egg;
use App\Models\Egg;
use App\Services\Eggs\Sharing\EggExporterService;
use Exception;
use Illuminate\Console\Command;
class CheckEggUpdatesCommand extends Command
{
protected $signature = 'p:egg:check-updates';
public function handle(EggExporterService $exporterService): void
{
$eggs = Egg::all();
foreach ($eggs as $egg) {
try {
if (is_null($egg->update_url)) {
$this->comment("{$egg->name}: Skipping (no update url set)");
continue;
}
$currentJson = json_decode($exporterService->handle($egg->id));
unset($currentJson->exported_at);
$updatedJson = json_decode(file_get_contents($egg->update_url));
unset($updatedJson->exported_at);
if (md5(json_encode($currentJson)) === md5(json_encode($updatedJson))) {
$this->info("{$egg->name}: Up-to-date");
cache()->put("eggs.{$egg->uuid}.update", false, now()->addHour());
} else {
$this->warn("{$egg->name}: Found update");
cache()->put("eggs.{$egg->uuid}.update", true, now()->addHour());
}
} catch (Exception $exception) {
$this->error("{$egg->name}: Error ({$exception->getMessage()})");
}
}
}
}

View File

@@ -3,13 +3,19 @@
namespace App\Console\Commands\Environment;
use Illuminate\Console\Command;
use App\Traits\Commands\EnvironmentWriterTrait;
use Illuminate\Support\Facades\Artisan;
class AppSettingsCommand extends Command
{
use EnvironmentWriterTrait;
protected $description = 'Configure basic environment settings for the Panel.';
protected $signature = 'p:environment:setup';
protected $signature = 'p:environment:setup
{--url= : The URL that this Panel is running on.}';
protected array $variables = [];
public function handle(): void
{
@@ -24,6 +30,21 @@ class AppSettingsCommand extends Command
Artisan::call('key:generate');
}
Artisan::call('filament:optimize');
$this->variables['APP_TIMEZONE'] = 'UTC';
$this->variables['APP_URL'] = $this->option('url') ?? $this->ask(
'Application URL',
config('app.url', 'https://example.com')
);
// Make sure session cookies are set as "secure" when using HTTPS
if (str_starts_with($this->variables['APP_URL'], 'https://')) {
$this->variables['SESSION_SECURE_COOKIE'] = 'true';
}
$this->comment('Writing variables to .env file');
$this->writeToEnvironment($this->variables);
$this->info("Setup complete. Vist {$this->variables['APP_URL']}/installer to complete the installation");
}
}

View File

@@ -2,10 +2,10 @@
namespace App\Console\Commands\Environment;
use App\Traits\Commands\RequestRedisSettingsTrait;
use App\Traits\EnvironmentWriterTrait;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Kernel;
use App\Traits\Commands\EnvironmentWriterTrait;
use App\Traits\Commands\RequestRedisSettingsTrait;
class CacheSettingsCommand extends Command
{
@@ -23,7 +23,6 @@ class CacheSettingsCommand extends Command
protected $signature = 'p:environment:cache
{--driver= : The cache driver backend to use.}
{--redis-host= : Redis host to use for connections.}
{--redis-user= : User used to connect to redis.}
{--redis-pass= : Password used to connect to redis.}
{--redis-port= : Port to connect to redis over.}';
@@ -54,6 +53,7 @@ class CacheSettingsCommand extends Command
if (config('queue.default') !== 'sync') {
$this->call('p:environment:queue-service', [
'--use-redis' => true,
'--overwrite' => true,
]);
}

View File

@@ -2,10 +2,10 @@
namespace App\Console\Commands\Environment;
use App\Traits\EnvironmentWriterTrait;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Database\DatabaseManager;
use App\Traits\Commands\EnvironmentWriterTrait;
class DatabaseSettingsCommand extends Command
{
@@ -42,13 +42,6 @@ class DatabaseSettingsCommand extends Command
*/
public function handle(): int
{
$this->error('Changing the database driver will NOT move any database data!');
$this->error('Please make sure you made a database backup first!');
$this->error('After changing the driver you will have to manually move the old data to the new database.');
if (!$this->confirm('Do you want to continue?')) {
return 1;
}
$selected = config('database.default', 'sqlite');
$this->variables['DB_CONNECTION'] = $this->option('driver') ?? $this->choice(
'Database Driver',

View File

@@ -2,8 +2,8 @@
namespace App\Console\Commands\Environment;
use App\Traits\EnvironmentWriterTrait;
use Illuminate\Console\Command;
use App\Traits\Commands\EnvironmentWriterTrait;
class EmailSettingsCommand extends Command
{
@@ -70,7 +70,7 @@ class EmailSettingsCommand extends Command
/**
* Handle variables for SMTP driver.
*/
private function setupSmtpDriverVariables(): void
private function setupSmtpDriverVariables()
{
$this->variables['MAIL_HOST'] = $this->option('host') ?? $this->ask(
trans('command/messages.environment.mail.ask_smtp_host'),
@@ -101,7 +101,7 @@ class EmailSettingsCommand extends Command
/**
* Handle variables for mailgun driver.
*/
private function setupMailgunDriverVariables(): void
private function setupMailgunDriverVariables()
{
$this->variables['MAILGUN_DOMAIN'] = $this->option('host') ?? $this->ask(
trans('command/messages.environment.mail.ask_mailgun_domain'),
@@ -122,7 +122,7 @@ class EmailSettingsCommand extends Command
/**
* Handle variables for mandrill driver.
*/
private function setupMandrillDriverVariables(): void
private function setupMandrillDriverVariables()
{
$this->variables['MANDRILL_SECRET'] = $this->option('password') ?? $this->ask(
trans('command/messages.environment.mail.ask_mandrill_secret'),
@@ -133,7 +133,7 @@ class EmailSettingsCommand extends Command
/**
* Handle variables for postmark driver.
*/
private function setupPostmarkDriverVariables(): void
private function setupPostmarkDriverVariables()
{
$this->variables['MAIL_DRIVER'] = 'smtp';
$this->variables['MAIL_HOST'] = 'smtp.postmarkapp.com';

View File

@@ -2,10 +2,10 @@
namespace App\Console\Commands\Environment;
use App\Traits\Commands\RequestRedisSettingsTrait;
use App\Traits\EnvironmentWriterTrait;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Kernel;
use App\Traits\Commands\EnvironmentWriterTrait;
use App\Traits\Commands\RequestRedisSettingsTrait;
class QueueSettingsCommand extends Command
{
@@ -23,7 +23,6 @@ class QueueSettingsCommand extends Command
protected $signature = 'p:environment:queue
{--driver= : The queue driver backend to use.}
{--redis-host= : Redis host to use for connections.}
{--redis-user= : User used to connect to redis.}
{--redis-pass= : Password used to connect to redis.}
{--redis-port= : Port to connect to redis over.}';
@@ -53,6 +52,7 @@ class QueueSettingsCommand extends Command
$this->requestRedisSettings();
$this->call('p:environment:queue-service', [
'--use-redis' => true,
'--overwrite' => true,
]);
}

View File

@@ -1,54 +0,0 @@
<?php
namespace App\Console\Commands\Environment;
use App\Traits\Commands\RequestRedisSettingsTrait;
use App\Traits\EnvironmentWriterTrait;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Kernel;
class RedisSetupCommand extends Command
{
use EnvironmentWriterTrait;
use RequestRedisSettingsTrait;
protected $description = 'Configure the Panel to use Redis as cache, queue and session driver.';
protected $signature = 'p:redis:setup
{--redis-host= : Redis host to use for connections.}
{--redis-user= : User used to connect to redis.}
{--redis-pass= : Password used to connect to redis.}
{--redis-port= : Port to connect to redis over.}';
protected array $variables = [];
/**
* RedisSetupCommand constructor.
*/
public function __construct(private Kernel $console)
{
parent::__construct();
}
/**
* Handle command execution.
*/
public function handle(): int
{
$this->variables['CACHE_STORE'] = 'redis';
$this->variables['QUEUE_CONNECTION'] = 'redis';
$this->variables['SESSION_DRIVERS'] = 'redis';
$this->requestRedisSettings();
$this->call('p:environment:queue-service', [
'--overwrite' => true,
]);
$this->writeToEnvironment($this->variables);
$this->info($this->console->output());
return 0;
}
}

View File

@@ -2,10 +2,10 @@
namespace App\Console\Commands\Environment;
use App\Traits\Commands\RequestRedisSettingsTrait;
use App\Traits\EnvironmentWriterTrait;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Kernel;
use App\Traits\Commands\EnvironmentWriterTrait;
use App\Traits\Commands\RequestRedisSettingsTrait;
class SessionSettingsCommand extends Command
{
@@ -24,7 +24,6 @@ class SessionSettingsCommand extends Command
protected $signature = 'p:environment:session
{--driver= : The session driver backend to use.}
{--redis-host= : Redis host to use for connections.}
{--redis-user= : User used to connect to redis.}
{--redis-pass= : Password used to connect to redis.}
{--redis-port= : Port to connect to redis over.}';
@@ -55,6 +54,7 @@ class SessionSettingsCommand extends Command
if (config('queue.default') !== 'sync') {
$this->call('p:environment:queue-service', [
'--use-redis' => true,
'--overwrite' => true,
]);
}

View File

@@ -26,8 +26,8 @@ class InfoCommand extends Command
{
$this->output->title('Version Information');
$this->table([], [
['Panel Version', $this->versionService->currentPanelVersion()],
['Latest Version', $this->versionService->latestPanelVersion()],
['Panel Version', $this->versionService->versionData()['version']],
['Latest Version', $this->versionService->getPanel()],
['Up-to-Date', $this->versionService->isLatestPanel() ? 'Yes' : $this->formatText('No', 'bg=red')],
], 'compact');

View File

@@ -2,7 +2,7 @@
namespace App\Console\Commands\Overrides;
use App\Traits\Commands\RequiresDatabaseMigrations;
use App\Console\RequiresDatabaseMigrations;
use Illuminate\Database\Console\Seeds\SeedCommand as BaseSeedCommand;
class SeedCommand extends BaseSeedCommand

View File

@@ -2,7 +2,7 @@
namespace App\Console\Commands\Overrides;
use App\Traits\Commands\RequiresDatabaseMigrations;
use App\Console\RequiresDatabaseMigrations;
use Illuminate\Foundation\Console\UpCommand as BaseUpCommand;
class UpCommand extends BaseUpCommand

View File

@@ -6,6 +6,7 @@ use Illuminate\Console\Command;
use App\Models\Schedule;
use Illuminate\Database\Eloquent\Builder;
use App\Services\Schedules\ProcessScheduleService;
use Carbon\Carbon;
class ProcessRunnableCommand extends Command
{
@@ -23,7 +24,7 @@ class ProcessRunnableCommand extends Command
->whereRelation('server', fn (Builder $builder) => $builder->whereNull('status'))
->where('is_active', true)
->where('is_processing', false)
->where('next_run_at', '<=', now('UTC')->toDateTimeString())
->where('next_run_at', '<=', Carbon::now()->toDateTimeString())
->get();
if ($schedules->count() < 1) {
@@ -50,7 +51,7 @@ class ProcessRunnableCommand extends Command
* never throw an exception out, otherwise you'll end up killing the entire run group causing
* any other schedules to not process correctly.
*/
protected function processSchedule(Schedule $schedule): void
protected function processSchedule(Schedule $schedule)
{
if ($schedule->tasks->isEmpty()) {
return;

View File

@@ -178,7 +178,7 @@ class UpgradeCommand extends Command
$this->info(__('commands.upgrade.success'));
}
protected function withProgress(ProgressBar $bar, \Closure $callback): void
protected function withProgress(ProgressBar $bar, \Closure $callback)
{
$bar->clear();
$callback();

View File

@@ -52,7 +52,7 @@ class MakeUserCommand extends Command
['UUID', $user->uuid],
['Email', $user->email],
['Username', $user->username],
['Admin', $user->isRootAdmin() ? 'Yes' : 'No'],
['Admin', $user->root_admin ? 'Yes' : 'No'],
]);
return 0;

View File

@@ -2,17 +2,15 @@
namespace App\Console;
use App\Console\Commands\Egg\CheckEggUpdatesCommand;
use App\Console\Commands\Maintenance\CleanServiceBackupFilesCommand;
use App\Console\Commands\Maintenance\PruneImagesCommand;
use App\Console\Commands\Maintenance\PruneOrphanedBackupsCommand;
use App\Console\Commands\Schedule\ProcessRunnableCommand;
use App\Jobs\NodeStatistics;
use App\Models\ActivityLog;
use App\Models\Webhook;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Database\Console\PruneCommand;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use App\Console\Commands\Schedule\ProcessRunnableCommand;
use App\Console\Commands\Maintenance\PruneOrphanedBackupsCommand;
use App\Console\Commands\Maintenance\CleanServiceBackupFilesCommand;
use App\Console\Commands\Maintenance\PruneImagesCommand;
class Kernel extends ConsoleKernel
{
@@ -37,7 +35,6 @@ class Kernel extends ConsoleKernel
$schedule->command(CleanServiceBackupFilesCommand::class)->daily();
$schedule->command(PruneImagesCommand::class)->daily();
$schedule->command(CheckEggUpdatesCommand::class)->hourly();
$schedule->job(new NodeStatistics())->everyFiveSeconds()->withoutOverlapping();
@@ -49,9 +46,5 @@ class Kernel extends ConsoleKernel
if (config('activity.prune_days')) {
$schedule->command(PruneCommand::class, ['--model' => [ActivityLog::class]])->daily();
}
if (config('panel.webhook.prune_days')) {
$schedule->command(PruneCommand::class, ['--model' => [Webhook::class]])->daily();
}
}
}

View File

@@ -1,16 +1,32 @@
<?php
namespace App\Traits\Commands;
use App\Traits\CheckMigrationsTrait;
use Illuminate\Console\Command;
namespace App\Console;
/**
* @mixin Command
* @mixin \Illuminate\Console\Command
*/
trait RequiresDatabaseMigrations
{
use CheckMigrationsTrait;
/**
* Checks if the migrations have finished running by comparing the last migration file.
*/
protected function hasCompletedMigrations(): bool
{
/** @var \Illuminate\Database\Migrations\Migrator $migrator */
$migrator = $this->getLaravel()->make('migrator');
$files = $migrator->getMigrationFiles(database_path('migrations'));
if (!$migrator->repositoryExists()) {
return false;
}
if (array_diff(array_keys($files), $migrator->getRepository()->getRan())) {
return false;
}
return true;
}
/**
* Throw a massive error into the console to hopefully catch the users attention and get

View File

@@ -1,16 +0,0 @@
<?php
namespace App\Enums;
enum RolePermissionModels: string
{
case ApiKey = 'apiKey';
case DatabaseHost = 'databaseHost';
case Database = 'database';
case Egg = 'egg';
case Mount = 'mount';
case Node = 'node';
case Role = 'role';
case Server = 'server';
case User = 'user';
}

View File

@@ -1,12 +0,0 @@
<?php
namespace App\Enums;
enum RolePermissionPrefixes: string
{
case ViewAny = 'viewList';
case View = 'view';
case Create = 'create';
case Update = 'update';
case Delete = 'delete';
}

View File

@@ -12,7 +12,7 @@ class FailedCaptcha extends Event
/**
* Create a new event instance.
*/
public function __construct(public string $ip, public ?string $message)
public function __construct(public string $ip, public string $domain)
{
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Events\Server;
use App\Events\Event;
use App\Models\Server;
use Illuminate\Queue\SerializesModels;
class Created extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public Server $server)
{
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Events\Server;
use App\Events\Event;
use App\Models\Server;
use Illuminate\Queue\SerializesModels;
class Creating extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public Server $server)
{
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Events\Server;
use App\Events\Event;
use App\Models\Server;
use Illuminate\Queue\SerializesModels;
class Deleted extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public Server $server)
{
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Events\Server;
use App\Events\Event;
use App\Models\Server;
use Illuminate\Queue\SerializesModels;
class Deleting extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public Server $server)
{
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Events\Server;
use App\Events\Event;
use App\Models\Server;
use Illuminate\Queue\SerializesModels;
class Saved extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public Server $server)
{
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Events\Server;
use App\Events\Event;
use App\Models\Server;
use Illuminate\Queue\SerializesModels;
class Saving extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public Server $server)
{
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Events\Server;
use App\Events\Event;
use App\Models\Server;
use Illuminate\Queue\SerializesModels;
class Updated extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public Server $server)
{
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Events\Server;
use App\Events\Event;
use App\Models\Server;
use Illuminate\Queue\SerializesModels;
class Updating extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public Server $server)
{
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Events\Subuser;
use App\Events\Event;
use App\Models\Subuser;
use Illuminate\Queue\SerializesModels;
class Created extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public Subuser $subuser)
{
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Events\Subuser;
use App\Events\Event;
use App\Models\Subuser;
use Illuminate\Queue\SerializesModels;
class Creating extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public Subuser $subuser)
{
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Events\Subuser;
use App\Events\Event;
use App\Models\Subuser;
use Illuminate\Queue\SerializesModels;
class Deleted extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public Subuser $subuser)
{
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Events\Subuser;
use App\Events\Event;
use App\Models\Subuser;
use Illuminate\Queue\SerializesModels;
class Deleting extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public Subuser $subuser)
{
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Events\User;
use App\Models\User;
use App\Events\Event;
use Illuminate\Queue\SerializesModels;
class Created extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public User $user)
{
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Events\User;
use App\Models\User;
use App\Events\Event;
use Illuminate\Queue\SerializesModels;
class Creating extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public User $user)
{
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Events\User;
use App\Models\User;
use App\Events\Event;
use Illuminate\Queue\SerializesModels;
class Deleted extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public User $user)
{
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Events\User;
use App\Models\User;
use App\Events\Event;
use Illuminate\Queue\SerializesModels;
class Deleting extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public User $user)
{
}
}

View File

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

View File

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

View File

@@ -4,22 +4,18 @@ namespace App\Exceptions;
use Exception;
use Filament\Notifications\Notification;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Psr\Log\LoggerInterface;
use Illuminate\Http\Response;
use Illuminate\Container\Container;
use Prologue\Alerts\AlertsMessageBag;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
class DisplayException extends PanelException implements HttpExceptionInterface
{
public const LEVEL_DEBUG = 'debug';
public const LEVEL_INFO = 'info';
public const LEVEL_WARNING = 'warning';
public const LEVEL_ERROR = 'error';
/**
@@ -50,7 +46,7 @@ class DisplayException extends PanelException implements HttpExceptionInterface
* and then redirecting them back to the page that they came from. If the
* request originated from an API hit, return the error in JSONAPI spec format.
*/
public function render(Request $request): bool|RedirectResponse|JsonResponse
public function render(Request $request)
{
if ($request->is('livewire/update')) {
Notification::make()
@@ -59,13 +55,15 @@ class DisplayException extends PanelException implements HttpExceptionInterface
->danger()
->send();
return false;
return;
}
if ($request->expectsJson()) {
return response()->json(Handler::toArray($this), $this->getStatusCode(), $this->getHeaders());
}
app(AlertsMessageBag::class)->danger($this->getMessage())->flash();
return redirect()->back()->withInput();
}
@@ -75,10 +73,10 @@ class DisplayException extends PanelException implements HttpExceptionInterface
*
* @throws \Throwable
*/
public function report(): void
public function report()
{
if (!$this->getPrevious() instanceof \Exception || !Handler::isReportable($this->getPrevious())) {
return;
return null;
}
try {
@@ -87,6 +85,6 @@ class DisplayException extends PanelException implements HttpExceptionInterface
throw $this->getPrevious();
}
$logger->{$this->getErrorLevel()}($this->getPrevious());
return $logger->{$this->getErrorLevel()}($this->getPrevious());
}
}

View File

@@ -114,7 +114,7 @@ class Handler extends ExceptionHandler
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Http\Request $request
*
* @throws \Throwable
*/
@@ -140,7 +140,7 @@ class Handler extends ExceptionHandler
* Transform a validation exception into a consistent format to be returned for
* calls to the API.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Http\Request $request
*/
public function invalidJson($request, ValidationException $exception): JsonResponse
{
@@ -236,7 +236,7 @@ class Handler extends ExceptionHandler
/**
* Convert an authentication exception into an unauthenticated response.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Http\Request $request
*/
protected function unauthenticated($request, AuthenticationException $exception): JsonResponse|RedirectResponse
{
@@ -273,7 +273,6 @@ class Handler extends ExceptionHandler
*/
public static function toArray(\Throwable $e): array
{
// @phpstan-ignore-next-line
return (new self(app()))->convertExceptionToArray($e);
}
}

View File

@@ -7,6 +7,9 @@ use GuzzleHttp\Exception\GuzzleException;
use App\Exceptions\DisplayException;
use Illuminate\Support\Facades\Context;
/**
* @method \GuzzleHttp\Exception\GuzzleException getPrevious()
*/
class DaemonConnectionException extends DisplayException
{
private int $statusCode = Response::HTTP_GATEWAY_TIMEOUT;

View File

@@ -10,7 +10,7 @@ class HttpForbiddenException extends HttpException
/**
* 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);
}

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Exceptions\Http\Server;
use App\Exceptions\DisplayException;
class FileTypeNotEditableException extends DisplayException
{
}

View File

@@ -12,7 +12,7 @@ class ServerStateConflictException extends ConflictHttpException
* Exception thrown when the server is in an unsupported state for API access or
* 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.';
if ($server->isSuspended()) {

View File

@@ -11,7 +11,7 @@ class TwoFactorAuthRequiredException extends HttpException implements HttpExcept
/**
* TwoFactorAuthRequiredException constructor.
*/
public function __construct(?\Throwable $previous = null)
public function __construct(\Throwable $previous = null)
{
parent::__construct(Response::HTTP_BAD_REQUEST, 'Two-factor authentication is required on this account in order to access this endpoint.', $previous);
}

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Exceptions\Repository\Daemon;
use App\Exceptions\Repository\RepositoryException;
class InvalidPowerSignalException extends RepositoryException
{
}

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Exceptions\Repository;
use App\Exceptions\PanelException;
class RepositoryException extends PanelException
{
}

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Exceptions\Service\Allocation;
use App\Exceptions\PanelException;
class AllocationDoesNotBelongToServerException extends PanelException
{
}

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Exceptions\Service\Egg;
use App\Exceptions\DisplayException;
class BadJsonFormatException extends DisplayException
{
}

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Exceptions\Service\Egg;
use App\Exceptions\DisplayException;
class NoParentConfigurationFoundException extends DisplayException
{
}

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Exceptions\Service\Schedule\Task;
use App\Exceptions\DisplayException;
class TaskIntervalTooLongException extends DisplayException
{
}

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Exceptions\Service\Server;
use App\Exceptions\PanelException;
class RequiredVariableMissingException extends PanelException
{
}

View File

@@ -10,7 +10,7 @@ class ServiceLimitExceededException extends DisplayException
* Exception thrown when something goes over a defined limit, such as allocated
* 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);
}

View File

@@ -7,7 +7,6 @@ use App\Exceptions\DisplayException;
class TwoFactorAuthenticationTokenInvalid extends DisplayException
{
public string $title = 'Invalid 2FA Code';
public string $icon = 'tabler-2fa';
public function __construct()

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Exceptions\Transformer;
use App\Exceptions\PanelException;
class InvalidTransformerLevelException extends PanelException
{
}

View File

@@ -34,7 +34,7 @@ class BackupManager
/**
* Returns a backup adapter instance.
*/
public function adapter(?string $name = null): FilesystemAdapter
public function adapter(string $name = null): FilesystemAdapter
{
return $this->get($name ?: $this->getDefaultAdapter());
}
@@ -145,7 +145,7 @@ class BackupManager
/**
* Unset the given adapter instances.
*
* @param string|string[] $adapter
* @param string|string[] $adapter
*/
public function forget(array|string $adapter): self
{

View File

@@ -7,9 +7,7 @@ use App\Models\DatabaseHost;
class DynamicDatabaseConnection
{
public const DB_CHARSET = 'utf8';
public const DB_COLLATION = 'utf8_unicode_ci';
public const DB_DRIVER = 'mysql';
/**

View File

@@ -0,0 +1,13 @@
<?php
namespace App\Extensions\Facades;
use Illuminate\Support\Facades\Facade;
class Theme extends Facade
{
protected static function getFacadeAccessor(): string
{
return 'extensions.themes';
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Extensions\Themes;
class Theme
{
public function js($path): string
{
return sprintf('<script src="%s"></script>' . PHP_EOL, $this->getUrl($path));
}
public function css($path): string
{
return sprintf('<link media="all" type="text/css" rel="stylesheet" href="%s"/>' . PHP_EOL, $this->getUrl($path));
}
protected function getUrl($path): string
{
return '/themes/panel/' . ltrim($path, '/');
}
}

14
app/Facades/LogBatch.php Normal file
View File

@@ -0,0 +1,14 @@
<?php
namespace App\Facades;
use Illuminate\Support\Facades\Facade;
use App\Services\Activity\ActivityLogBatchService;
class LogBatch extends Facade
{
protected static function getFacadeAccessor(): string
{
return ActivityLogBatchService::class;
}
}

View File

@@ -1,36 +0,0 @@
<?php
namespace App\Filament\Pages\Auth;
use Coderflex\FilamentTurnstile\Forms\Components\Turnstile;
use Filament\Pages\Auth\Login as BaseLogin;
class Login extends BaseLogin
{
protected function getForms(): array
{
return [
'form' => $this->form(
$this->makeForm()
->schema([
$this->getEmailFormComponent(),
$this->getPasswordFormComponent(),
$this->getRememberFormComponent(),
Turnstile::make('captcha')
->hidden(!config('turnstile.turnstile_enabled'))
->validationMessages([
'required' => config('turnstile.error_messages.turnstile_check_message'),
]),
])
->statePath('data'),
),
];
}
protected function throwFailureValidationException(): never
{
$this->dispatch('reset-captcha');
parent::throwFailureValidationException();
}
}

View File

@@ -2,7 +2,6 @@
namespace App\Filament\Pages;
use App\Filament\Resources\NodeResource\Pages\CreateNode;
use App\Filament\Resources\NodeResource\Pages\ListNodes;
use App\Models\Egg;
use App\Models\Node;
@@ -29,20 +28,16 @@ class Dashboard extends Page
public string $activeTab = 'nodes';
private SoftwareVersionService $softwareVersionService;
public function mount(SoftwareVersionService $softwareVersionService): void
{
$this->softwareVersionService = $softwareVersionService;
}
public function getViewData(): array
{
/** @var SoftwareVersionService $softwareVersionService */
$softwareVersionService = app(SoftwareVersionService::class);
return [
'inDevelopment' => config('app.version') === 'canary',
'version' => $this->softwareVersionService->currentPanelVersion(),
'latestVersion' => $this->softwareVersionService->latestPanelVersion(),
'isLatest' => $this->softwareVersionService->isLatestPanel(),
'version' => $softwareVersionService->versionData()['version'],
'latestVersion' => $softwareVersionService->getPanel(),
'isLatest' => $softwareVersionService->isLatestPanel(),
'eggsCount' => Egg::query()->count(),
'nodesList' => ListNodes::getUrl(),
'nodesCount' => Node::query()->count(),
@@ -66,13 +61,13 @@ class Dashboard extends Page
CreateAction::make()
->label(trans('dashboard/index.sections.intro-first-node.button_label'))
->icon('tabler-server-2')
->url(CreateNode::getUrl()),
->url(route('filament.admin.resources.nodes.create')),
],
'supportActions' => [
CreateAction::make()
->label(trans('dashboard/index.sections.intro-support.button_donate'))
->icon('tabler-cash')
->url('https://pelican.dev/donate', true)
->url($softwareVersionService->getDonations(), true)
->color('success'),
],
'helpActions' => [

View File

@@ -2,29 +2,24 @@
namespace App\Filament\Pages\Installer;
use App\Filament\Pages\Dashboard;
use App\Filament\Pages\Installer\Steps\CacheStep;
use App\Filament\Pages\Installer\Steps\AdminUserStep;
use App\Filament\Pages\Installer\Steps\DatabaseStep;
use App\Filament\Pages\Installer\Steps\EnvironmentStep;
use App\Filament\Pages\Installer\Steps\QueueStep;
use App\Filament\Pages\Installer\Steps\RedisStep;
use App\Filament\Pages\Installer\Steps\RequirementsStep;
use App\Filament\Pages\Installer\Steps\SessionStep;
use App\Models\User;
use App\Services\Users\UserCreationService;
use App\Traits\CheckMigrationsTrait;
use App\Traits\EnvironmentWriterTrait;
use App\Traits\Commands\EnvironmentWriterTrait;
use Exception;
use Filament\Forms\Components\Actions\Action;
use Filament\Facades\Filament;
use Filament\Forms\Components\Wizard;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Notifications\Notification;
use Filament\Pages\Concerns\HasUnsavedDataChangesAlert;
use Filament\Pages\SimplePage;
use Filament\Support\Enums\MaxWidth;
use Filament\Support\Exceptions\Halt;
use Illuminate\Http\RedirectResponse;
use Illuminate\Routing\Redirector;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
@@ -34,11 +29,11 @@ use Illuminate\Support\HtmlString;
*/
class PanelInstaller extends SimplePage implements HasForms
{
use CheckMigrationsTrait;
use EnvironmentWriterTrait;
use HasUnsavedDataChangesAlert;
use InteractsWithForms;
public array $data = [];
public $data = [];
protected static string $view = 'filament.pages.installer';
@@ -47,40 +42,39 @@ class PanelInstaller extends SimplePage implements HasForms
return MaxWidth::SevenExtraLarge;
}
public static function isInstalled(): bool
public function mount()
{
// This defaults to true so existing panels count as "installed"
return env('APP_INSTALLED', true);
}
public function mount(): void
{
abort_if(self::isInstalled(), 404);
if (is_installed()) {
abort(404);
}
$this->form->fill();
}
public function dehydrate(): void
{
Artisan::call('config:clear');
Artisan::call('cache:clear');
}
protected function getFormSchema(): array
{
return [
Wizard::make([
RequirementsStep::make(),
EnvironmentStep::make($this),
DatabaseStep::make($this),
CacheStep::make($this),
QueueStep::make($this),
SessionStep::make(),
EnvironmentStep::make(),
DatabaseStep::make(),
RedisStep::make()
->hidden(fn (Get $get) => $get('env.SESSION_DRIVER') != 'redis' && $get('env.QUEUE_CONNECTION') != 'redis' && $get('env.CACHE_STORE') != 'redis'),
AdminUserStep::make(),
])
->persistStepInQueryString()
->nextAction(fn (Action $action) => $action->keyBindings('enter'))
->submitAction(new HtmlString(Blade::render(<<<'BLADE'
<x-filament::button
type="submit"
size="sm"
wire:loading.attr="disabled"
>
Finish
<span wire:loading><x-filament::loading-indicator class="h-4 w-4" /></span>
</x-filament::button>
BLADE))),
];
@@ -91,94 +85,48 @@ class PanelInstaller extends SimplePage implements HasForms
return 'data';
}
public function submit(UserCreationService $userCreationService): Redirector|RedirectResponse
protected function hasUnsavedDataChangesAlert(): bool
{
// Disable installer
$this->writeToEnvironment(['APP_INSTALLED' => 'true']);
// Create admin user & login
$user = $this->createAdminUser($userCreationService);
auth()->guard()->login($user, true);
// Write session data at the very end to avoid "page expired" errors
$this->writeToEnv('env_session');
// Redirect to admin panel
return redirect(Dashboard::getUrl());
return true;
}
public function writeToEnv(string $key): void
public function submit()
{
try {
$variables = array_get($this->data, $key);
$variables = array_filter($variables); // Filter array to remove NULL values
$inputs = $this->form->getState();
// Write variables to .env file
$variables = array_get($inputs, 'env');
$this->writeToEnvironment($variables);
} catch (Exception $exception) {
report($exception);
Notification::make()
->title('Could not write to .env file')
->body($exception->getMessage())
->danger()
->persistent()
->send();
throw new Halt('Error while writing .env file');
}
Artisan::call('config:clear');
}
public function runMigrations(string $driver): void
{
try {
// Run migrations
Artisan::call('migrate', [
'--force' => true,
'--seed' => true,
'--database' => $driver,
]);
} catch (Exception $exception) {
report($exception);
Notification::make()
->title('Migrations failed')
->body($exception->getMessage())
->danger()
->persistent()
->send();
throw new Halt('Error while running migrations');
}
if (!$this->hasCompletedMigrations()) {
Notification::make()
->title('Migrations failed')
->danger()
->persistent()
->send();
throw new Halt('Migrations failed');
}
}
public function createAdminUser(UserCreationService $userCreationService): User
{
try {
$userData = array_get($this->data, 'user');
// Create first admin user
$userData = array_get($inputs, 'user');
$userData['root_admin'] = true;
app(UserCreationService::class)->handle($userData);
return $userCreationService->handle($userData);
} catch (Exception $exception) {
report($exception);
// Install setup complete
$this->writeToEnvironment(['APP_INSTALLED' => true]);
$this->rememberData();
Notification::make()
->title('Could not create admin user')
->body($exception->getMessage())
->danger()
->persistent()
->title('Successfully Installed')
->success()
->send();
throw new Halt('Error while creating admin user');
redirect()->intended(Filament::getUrl());
} catch (Exception $exception) {
Notification::make()
->title('Installation Failed')
->body($exception->getMessage())
->danger()
->send();
}
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Filament\Pages\Installer\Steps;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step;
class AdminUserStep
{
public static function make(): Step
{
return Step::make('user')
->label('Admin User')
->schema([
TextInput::make('user.email')
->label('Admin E-Mail')
->required()
->email()
->default('admin@example.com'),
TextInput::make('user.username')
->label('Admin Username')
->required()
->default('admin'),
TextInput::make('user.password')
->label('Admin Password')
->required()
->password()
->revealable(),
]);
}
}

View File

@@ -1,123 +0,0 @@
<?php
namespace App\Filament\Pages\Installer\Steps;
use App\Filament\Pages\Installer\PanelInstaller;
use Exception;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Components\Wizard\Step;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Notifications\Notification;
use Filament\Support\Exceptions\Halt;
use Illuminate\Foundation\Application;
use Illuminate\Redis\RedisManager;
class CacheStep
{
public const CACHE_DRIVERS = [
'file' => 'Filesystem',
'redis' => 'Redis',
];
public static function make(PanelInstaller $installer): Step
{
return Step::make('cache')
->label('Cache')
->columns()
->schema([
ToggleButtons::make('env_cache.CACHE_STORE')
->label('Cache Driver')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The driver used for caching. We recommend "Filesystem".')
->required()
->inline()
->options(self::CACHE_DRIVERS)
->default(config('cache.default'))
->columnSpanFull()
->live()
->afterStateUpdated(function ($state, Set $set, Get $get) {
if ($state !== 'redis') {
$set('env_cache.REDIS_HOST', null);
$set('env_cache.REDIS_PORT', null);
$set('env_cache.REDIS_USERNAME', null);
$set('env_cache.REDIS_PASSWORD', null);
} else {
$set('env_cache.REDIS_HOST', $get('env_cache.REDIS_HOST') ?? '127.0.0.1');
$set('env_cache.REDIS_PORT', $get('env_cache.REDIS_PORT') ?? '6379');
$set('env_cache.REDIS_USERNAME', null);
}
}),
TextInput::make('env_cache.REDIS_HOST')
->label('Redis Host')
->placeholder('127.0.0.1')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The host of your redis server. Make sure it is reachable.')
->required(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis')
->default(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis' ? config('database.redis.default.host') : null)
->visible(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis'),
TextInput::make('env_cache.REDIS_PORT')
->label('Redis Port')
->placeholder('6379')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The port of your redis server.')
->required(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis')
->default(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis' ? config('database.redis.default.port') : null)
->visible(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis'),
TextInput::make('env_cache.REDIS_USERNAME')
->label('Redis Username')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The name of your redis user. Can be empty')
->default(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis' ? config('database.redis.default.username') : null)
->visible(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis'),
TextInput::make('env_cache.REDIS_PASSWORD')
->label('Redis Password')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The password for your redis user. Can be empty.')
->password()
->revealable()
->default(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis' ? config('database.redis.default.password') : null)
->visible(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis'),
])
->afterValidation(function (Get $get, Application $app) use ($installer) {
$driver = $get('env_cache.CACHE_STORE');
if (!self::testConnection($app, $driver, $get('env_cache.REDIS_HOST'), $get('env_cache.REDIS_PORT'), $get('env_cache.REDIS_USERNAME'), $get('env_cache.REDIS_PASSWORD'))) {
throw new Halt('Redis connection failed');
}
$installer->writeToEnv('env_cache');
});
}
private static function testConnection(Application $app, string $driver, ?string $host, null|string|int $port, ?string $username, ?string $password): bool
{
if ($driver !== 'redis') {
return true;
}
try {
$redis = new RedisManager($app, 'predis', [
'default' => [
'host' => $host,
'port' => $port,
'username' => $username,
'password' => $password,
],
]);
$redis->connection()->command('ping');
} catch (Exception $exception) {
Notification::make()
->title('Redis connection failed')
->body($exception->getMessage())
->danger()
->send();
return false;
}
return true;
}
}

View File

@@ -2,138 +2,94 @@
namespace App\Filament\Pages\Installer\Steps;
use App\Filament\Pages\Installer\PanelInstaller;
use Exception;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Components\Wizard\Step;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Notifications\Notification;
use Filament\Support\Exceptions\Halt;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\DatabaseManager;
use PDOException;
class DatabaseStep
{
public const DATABASE_DRIVERS = [
'sqlite' => 'SQLite',
'mariadb' => 'MariaDB',
'mysql' => 'MySQL',
];
public static function make(PanelInstaller $installer): Step
public static function make(): Step
{
return Step::make('database')
->label('Database')
->columns()
->schema([
ToggleButtons::make('env_database.DB_CONNECTION')
->label('Database Driver')
TextInput::make('env.DB_DATABASE')
->label(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite' ? 'Database Path' : 'Database Name')
->columnSpanFull()
->hintIcon('tabler-question-mark')
->hintIconTooltip('The driver used for the panel database. We recommend "SQLite".')
->hintIconTooltip(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite' ? 'The path of your .sqlite file relative to the database folder.' : 'The name of the panel database.')
->required()
->inline()
->options(self::DATABASE_DRIVERS)
->default(config('database.default'))
->live()
->afterStateUpdated(function ($state, Set $set, Get $get) {
$set('env_database.DB_DATABASE', $state === 'sqlite' ? 'database.sqlite' : 'panel');
if ($state === 'sqlite') {
$set('env_database.DB_HOST', null);
$set('env_database.DB_PORT', null);
$set('env_database.DB_USERNAME', null);
$set('env_database.DB_PASSWORD', null);
} else {
$set('env_database.DB_HOST', $get('env_database.DB_HOST') ?? '127.0.0.1');
$set('env_database.DB_PORT', $get('env_database.DB_PORT') ?? '3306');
$set('env_database.DB_USERNAME', $get('env_database.DB_USERNAME') ?? 'pelican');
}
}),
TextInput::make('env_database.DB_DATABASE')
->label(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite' ? 'Database Path' : 'Database Name')
->placeholder(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite' ? 'database.sqlite' : 'panel')
->hintIcon('tabler-question-mark')
->hintIconTooltip(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite' ? 'The path of your .sqlite file relative to the database folder.' : 'The name of the panel database.')
->required()
->default('database.sqlite'),
TextInput::make('env_database.DB_HOST')
->default(fn (Get $get) => env('DB_DATABASE', $get('env.DB_CONNECTION') === 'sqlite' ? 'database.sqlite' : 'panel')),
TextInput::make('env.DB_HOST')
->label('Database Host')
->placeholder('127.0.0.1')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The host of your database. Make sure it is reachable.')
->required(fn (Get $get) => $get('env_database.DB_CONNECTION') !== 'sqlite')
->hidden(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite'),
TextInput::make('env_database.DB_PORT')
->required()
->default(env('DB_HOST', '127.0.0.1'))
->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'),
TextInput::make('env.DB_PORT')
->label('Database Port')
->placeholder('3306')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The port of your database.')
->required()
->numeric()
->minValue(1)
->maxValue(65535)
->required(fn (Get $get) => $get('env_database.DB_CONNECTION') !== 'sqlite')
->hidden(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite'),
TextInput::make('env_database.DB_USERNAME')
->default(env('DB_PORT', 3306))
->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'),
TextInput::make('env.DB_USERNAME')
->label('Database Username')
->placeholder('pelican')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The name of your database user.')
->required(fn (Get $get) => $get('env_database.DB_CONNECTION') !== 'sqlite')
->hidden(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite'),
TextInput::make('env_database.DB_PASSWORD')
->required()
->default(env('DB_USERNAME', 'pelican'))
->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'),
TextInput::make('env.DB_PASSWORD')
->label('Database Password')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The password of your database user. Can be empty.')
->password()
->revealable()
->hidden(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite'),
->default(env('DB_PASSWORD'))
->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'),
])
->afterValidation(function (Get $get) use ($installer) {
$driver = $get('env_database.DB_CONNECTION');
->afterValidation(function (Get $get) {
$driver = $get('env.DB_CONNECTION');
if ($driver !== 'sqlite') {
/** @var DatabaseManager $database */
$database = app(DatabaseManager::class);
if (!self::testConnection($driver, $get('env_database.DB_HOST'), $get('env_database.DB_PORT'), $get('env_database.DB_DATABASE'), $get('env_database.DB_USERNAME'), $get('env_database.DB_PASSWORD'))) {
throw new Halt('Database connection failed');
try {
config()->set('database.connections._panel_install_test', [
'driver' => $driver,
'host' => $get('env.DB_HOST'),
'port' => $get('env.DB_PORT'),
'database' => $get('env.DB_DATABASE'),
'username' => $get('env.DB_USERNAME'),
'password' => $get('env.DB_PASSWORD'),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'strict' => true,
]);
$database->connection('_panel_install_test')->getPdo();
} catch (PDOException $exception) {
Notification::make()
->title('Database connection failed')
->body($exception->getMessage())
->danger()
->send();
$database->disconnect('_panel_install_test');
throw new Halt('Database connection failed');
}
}
$installer->writeToEnv('env_database');
$installer->runMigrations($driver);
});
}
private static function testConnection(string $driver, ?string $host, null|string|int $port, ?string $database, ?string $username, ?string $password): bool
{
if ($driver === 'sqlite') {
return true;
}
try {
config()->set('database.connections._panel_install_test', [
'driver' => $driver,
'host' => $host,
'port' => $port,
'database' => $database,
'username' => $username,
'password' => $password,
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'strict' => true,
]);
DB::connection('_panel_install_test')->getPdo();
} catch (Exception $exception) {
DB::disconnect('_panel_install_test');
Notification::make()
->title('Database connection failed')
->body($exception->getMessage())
->danger()
->send();
return false;
}
return true;
}
}

View File

@@ -2,51 +2,93 @@
namespace App\Filament\Pages\Installer\Steps;
use App\Filament\Pages\Installer\PanelInstaller;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Components\Wizard\Step;
use Filament\Forms\Set;
class EnvironmentStep
{
public static function make(PanelInstaller $installer): Step
public const CACHE_DRIVERS = [
'file' => 'Filesystem',
'redis' => 'Redis',
];
public const SESSION_DRIVERS = [
'file' => 'Filesystem',
'redis' => 'Redis',
'database' => 'Database',
'cookie' => 'Cookie',
];
public const QUEUE_DRIVERS = [
'database' => 'Database',
'redis' => 'Redis',
'sync' => 'Synchronous',
];
public const DATABASE_DRIVERS = [
'sqlite' => 'SQLite',
'mariadb' => 'MariaDB',
'mysql' => 'MySQL',
];
public static function make(): Step
{
return Step::make('environment')
->label('Environment')
->columns()
->schema([
TextInput::make('env_general.APP_NAME')
TextInput::make('env.APP_NAME')
->label('App Name')
->hintIcon('tabler-question-mark')
->hintIconTooltip('This will be the Name of your Panel.')
->required()
->default(config('app.name')),
TextInput::make('env_general.APP_URL')
TextInput::make('env.APP_URL')
->label('App URL')
->hintIcon('tabler-question-mark')
->hintIconTooltip('This will be the URL you access your Panel from.')
->required()
->default(url('')),
Fieldset::make('adminuser')
->label('Admin User')
->columns(3)
->schema([
TextInput::make('user.email')
->label('E-Mail')
->required()
->email()
->placeholder('admin@example.com'),
TextInput::make('user.username')
->label('Username')
->required()
->placeholder('admin'),
TextInput::make('user.password')
->label('Password')
->required()
->password()
->revealable(),
]),
])
->afterValidation(fn () => $installer->writeToEnv('env_general'));
->default(config('app.url'))
->live()
->afterStateUpdated(fn ($state, Set $set) => $set('env.SESSION_SECURE_COOKIE', str_starts_with($state, 'https://'))),
Toggle::make('env.SESSION_SECURE_COOKIE')
->hidden()
->default(env('SESSION_SECURE_COOKIE')),
ToggleButtons::make('env.CACHE_STORE')
->label('Cache Driver')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The driver used for caching. We recommend "Filesystem".')
->required()
->inline()
->options(self::CACHE_DRIVERS)
->default(config('cache.default', 'file')),
ToggleButtons::make('env.SESSION_DRIVER')
->label('Session Driver')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The driver used for storing sessions. We recommend "Filesystem" or "Database".')
->required()
->inline()
->options(self::SESSION_DRIVERS)
->default(config('session.driver', 'file')),
ToggleButtons::make('env.QUEUE_CONNECTION')
->label('Queue Driver')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The driver used for handling queues. We recommend "Database".')
->required()
->inline()
->options(self::QUEUE_DRIVERS)
->default(config('queue.default', 'database')),
ToggleButtons::make('env.DB_CONNECTION')
->label('Database Driver')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The driver used for the panel database. We recommend "SQLite".')
->required()
->inline()
->options(self::DATABASE_DRIVERS)
->default(config('database.default', 'sqlite')),
]);
}
}

View File

@@ -1,64 +0,0 @@
<?php
namespace App\Filament\Pages\Installer\Steps;
use App\Filament\Pages\Installer\PanelInstaller;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Components\Wizard\Step;
use Filament\Forms\Get;
use Illuminate\Support\HtmlString;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
class QueueStep
{
public const QUEUE_DRIVERS = [
'database' => 'Database',
'redis' => 'Redis',
'sync' => 'Sync',
];
public static function make(PanelInstaller $installer): Step
{
return Step::make('queue')
->label('Queue')
->columns()
->schema([
ToggleButtons::make('env_queue.QUEUE_CONNECTION')
->label('Queue Driver')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The driver used for handling queues. We recommend "Database".')
->required()
->inline()
->options(self::QUEUE_DRIVERS)
->disableOptionWhen(fn ($value, Get $get) => $value === 'redis' && $get('env_cache.CACHE_STORE') !== 'redis')
->default(config('queue.default')),
Toggle::make('done')
->label('I have done both steps below.')
->accepted(fn () => !file_exists('/.dockerenv'))
->inline(false)
->validationMessages([
'accepted' => 'You need to do both steps before continuing!',
])
->hidden(fn () => file_exists('/.dockerenv')),
TextInput::make('crontab')
->label(new HtmlString('Run the following command to set up your crontab. Note that <code>www-data</code> is your webserver user. On some systems this username might be different!'))
->disabled()
->hintAction(CopyAction::make())
->default('(crontab -l -u www-data 2>/dev/null; echo "* * * * * php ' . base_path() . '/artisan schedule:run >> /dev/null 2>&1") | crontab -u www-data -')
->hidden(fn () => file_exists('/.dockerenv'))
->columnSpanFull(),
TextInput::make('queueService')
->label(new HtmlString('To setup the queue worker service you simply have to run the following command.'))
->disabled()
->hintAction(CopyAction::make())
->default('sudo php ' . base_path() . '/artisan p:environment:queue-service')
->hidden(fn () => file_exists('/.dockerenv'))
->columnSpanFull(),
])
->afterValidation(function () use ($installer) {
$installer->writeToEnv('env_queue');
});
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Filament\Pages\Installer\Steps;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step;
class RedisStep
{
public static function make(): Step
{
return Step::make('redis')
->label('Redis')
->columns()
->schema([
TextInput::make('env.REDIS_HOST')
->label('Redis Host')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The host of your redis server. Make sure it is reachable.')
->required()
->default(config('database.redis.default.host')),
TextInput::make('env.REDIS_PORT')
->label('Redis Port')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The port of your redis server.')
->required()
->default(config('database.redis.default.port')),
TextInput::make('env.REDIS_USERNAME')
->label('Redis Username')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The name of your redis user. Can be empty')
->default(config('database.redis.default.username')),
TextInput::make('env.REDIS_PASSWORD')
->label('Redis Password')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The password for your redis user. Can be empty.')
->password()
->revealable()
->default(config('database.redis.default.password')),
]);
}
}

View File

@@ -10,20 +10,18 @@ use Filament\Support\Exceptions\Halt;
class RequirementsStep
{
public const MIN_PHP_VERSION = '8.2';
public static function make(): Step
{
$correctPhpVersion = version_compare(PHP_VERSION, self::MIN_PHP_VERSION) >= 0;
$correctPhpVersion = version_compare(PHP_VERSION, '8.2.0') >= 0;
$fields = [
Section::make('PHP Version')
->description(self::MIN_PHP_VERSION . ' or newer')
->description('8.2 or newer')
->icon($correctPhpVersion ? 'tabler-check' : 'tabler-x')
->iconColor($correctPhpVersion ? 'success' : 'danger')
->schema([
Placeholder::make('')
->content('Your PHP Version is ' . PHP_VERSION . '.'),
->content('Your PHP Version ' . ($correctPhpVersion ? 'is' : 'needs to be') .' 8.2 or newer.'),
]),
];
@@ -82,7 +80,7 @@ class RequirementsStep
->danger()
->send();
throw new Halt('Some requirements are missing');
throw new Halt();
}
});
}

View File

@@ -1,38 +0,0 @@
<?php
namespace App\Filament\Pages\Installer\Steps;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Components\Wizard\Step;
use Filament\Forms\Get;
class SessionStep
{
public const SESSION_DRIVERS = [
'file' => 'Filesystem',
'database' => 'Database',
'cookie' => 'Cookie',
'redis' => 'Redis',
];
public static function make(): Step
{
return Step::make('session')
->label('Session')
->schema([
ToggleButtons::make('env_session.SESSION_DRIVER')
->label('Session Driver')
->hintIcon('tabler-question-mark')
->hintIconTooltip('The driver used for storing sessions. We recommend "Filesystem" or "Database".')
->required()
->inline()
->options(self::SESSION_DRIVERS)
->disableOptionWhen(fn ($value, Get $get) => $value === 'redis' && $get('env_cache.CACHE_STORE') !== 'redis')
->default(config('session.driver')),
TextInput::make('env_session.SESSION_SECURE_COOKIE')
->hidden()
->default(request()->isSecure()),
]);
}
}

View File

@@ -4,11 +4,10 @@ namespace App\Filament\Pages;
use App\Models\Backup;
use App\Notifications\MailTested;
use App\Traits\EnvironmentWriterTrait;
use App\Traits\Commands\EnvironmentWriterTrait;
use Exception;
use Filament\Actions\Action;
use Filament\Forms\Components\Actions\Action as FormAction;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\Tabs\Tab;
@@ -25,11 +24,8 @@ use Filament\Notifications\Notification;
use Filament\Pages\Concerns\HasUnsavedDataChangesAlert;
use Filament\Pages\Concerns\InteractsWithHeaderActions;
use Filament\Pages\Page;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Notification as MailNotification;
use Illuminate\Support\HtmlString;
/**
* @property Form $form
@@ -42,7 +38,6 @@ class Settings extends Page implements HasForms
use InteractsWithHeaderActions;
protected static ?string $navigationIcon = 'tabler-settings';
protected static ?string $navigationGroup = 'Advanced';
protected static string $view = 'filament.pages.settings';
@@ -54,28 +49,21 @@ class Settings extends Page implements HasForms
$this->form->fill();
}
public static function canAccess(): bool
{
return auth()->user()->can('view settings');
}
protected function getFormSchema(): array
{
return [
Tabs::make('Tabs')
->columns()
->persistTabInQueryString()
->disabled(fn () => !auth()->user()->can('update settings'))
->tabs([
Tab::make('general')
->label('General')
->icon('tabler-home')
->schema($this->generalSettings()),
Tab::make('captcha')
->label('Captcha')
Tab::make('recaptcha')
->label('reCAPTCHA')
->icon('tabler-shield')
->schema($this->captchaSettings())
->columns(3),
->schema($this->recaptchaSettings()),
Tab::make('mail')
->label('Mail')
->icon('tabler-mail')
@@ -104,7 +92,7 @@ class Settings extends Page implements HasForms
->hintIcon('tabler-question-mark')
->hintIconTooltip('Favicons should be placed in the public folder, located in the root panel directory.')
->required()
->default(env('APP_FAVICON', '/pelican.ico')),
->default(env('APP_FAVICON', './pelican.ico')),
Toggle::make('APP_DEBUG')
->label('Enable Debug Mode?')
->inline(false)
@@ -151,84 +139,67 @@ class Settings extends Page implements HasForms
->separator()
->splitKeys(['Tab', ' '])
->placeholder('New IP or IP Range')
->default(env('TRUSTED_PROXIES', implode(',', config('trustedproxy.proxies'))))
->default(env('TRUSTED_PROXIES', config('trustedproxy.proxies')))
->hintActions([
FormAction::make('clear')
->label('Clear')
->color('danger')
->icon('tabler-trash')
->requiresConfirmation()
->authorize(fn () => auth()->user()->can('update settings'))
->action(fn (Set $set) => $set('TRUSTED_PROXIES', [])),
FormAction::make('cloudflare')
->label('Set to Cloudflare IPs')
->icon('tabler-brand-cloudflare')
->authorize(fn () => auth()->user()->can('update settings'))
->action(function (Client $client, Set $set) {
$ips = collect();
try {
$response = $client->request(
'GET',
'https://api.cloudflare.com/client/v4/ips',
config('panel.guzzle')
);
if ($response->getStatusCode() === 200) {
$result = json_decode($response->getBody(), true)['result'];
foreach (['ipv4_cidrs', 'ipv6_cidrs'] as $value) {
$ips->push(...data_get($result, $value));
}
$ips->unique();
}
} catch (GuzzleException $e) {
}
$set('TRUSTED_PROXIES', $ips->values()->all());
}),
->action(fn (Set $set) => $set('TRUSTED_PROXIES', [
'173.245.48.0/20',
'103.21.244.0/22',
'103.22.200.0/22',
'103.31.4.0/22',
'141.101.64.0/18',
'108.162.192.0/18',
'190.93.240.0/20',
'188.114.96.0/20',
'197.234.240.0/22',
'198.41.128.0/17',
'162.158.0.0/15',
'104.16.0.0/13',
'104.24.0.0/14',
'172.64.0.0/13',
'131.0.72.0/22',
])),
]),
];
}
private function captchaSettings(): array
private function recaptchaSettings(): array
{
return [
Toggle::make('TURNSTILE_ENABLED')
->label('Enable Turnstile Captcha?')
Toggle::make('RECAPTCHA_ENABLED')
->label('Enable reCAPTCHA?')
->inline(false)
->columnSpan(1)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->live()
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('TURNSTILE_ENABLED', (bool) $state))
->default(env('TURNSTILE_ENABLED', config('turnstile.turnstile_enabled'))),
Placeholder::make('info')
->columnSpan(2)
->content(new HtmlString('<p>You can generate the keys on your <u><a href="https://developers.cloudflare.com/turnstile/get-started/#get-a-sitekey-and-secret-key" target="_blank">Cloudflare Dashboard</a></u>. A Cloudflare account is required.</p>')),
TextInput::make('TURNSTILE_SITE_KEY')
->label('Site Key')
->afterStateUpdated(fn ($state, Set $set) => $set('RECAPTCHA_ENABLED', (bool) $state))
->default(env('RECAPTCHA_ENABLED', config('recaptcha.enabled'))),
TextInput::make('RECAPTCHA_DOMAIN')
->label('Domain')
->required()
->visible(fn (Get $get) => $get('TURNSTILE_ENABLED'))
->default(env('TURNSTILE_SITE_KEY', config('turnstile.turnstile_site_key')))
->placeholder('1x00000000000000000000AA'),
TextInput::make('TURNSTILE_SECRET_KEY')
->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED'))
->default(env('RECAPTCHA_DOMAIN', config('recaptcha.domain'))),
TextInput::make('RECAPTCHA_WEBSITE_KEY')
->label('Website Key')
->required()
->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED'))
->default(env('RECAPTCHA_WEBSITE_KEY', config('recaptcha.website_key'))),
TextInput::make('RECAPTCHA_SECRET_KEY')
->label('Secret Key')
->required()
->visible(fn (Get $get) => $get('TURNSTILE_ENABLED'))
->default(env('TURNSTILE_SECRET_KEY', config('turnstile.secret_key')))
->placeholder('1x0000000000000000000000000000000AA'),
Toggle::make('TURNSTILE_VERIFY_DOMAIN')
->label('Verify domain?')
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->visible(fn (Get $get) => $get('TURNSTILE_ENABLED'))
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('TURNSTILE_VERIFY_DOMAIN', (bool) $state))
->default(env('TURNSTILE_VERIFY_DOMAIN', config('turnstile.turnstile_verify_domain'))),
->visible(fn (Get $get) => $get('RECAPTCHA_ENABLED'))
->default(env('RECAPTCHA_SECRET_KEY', config('recaptcha.secret_key'))),
];
}
@@ -254,7 +225,6 @@ class Settings extends Page implements HasForms
->label('Send Test Mail')
->icon('tabler-send')
->hidden(fn (Get $get) => $get('MAIL_MAILER') === 'log')
->authorize(fn () => auth()->user()->can('update settings'))
->action(function () {
try {
MailNotification::route('mail', auth()->user()->email)
@@ -292,26 +262,28 @@ class Settings extends Page implements HasForms
->visible(fn (Get $get) => $get('MAIL_MAILER') === 'smtp')
->schema([
TextInput::make('MAIL_HOST')
->label('Host')
->label('SMTP Host')
->required()
->default(env('MAIL_HOST', config('mail.mailers.smtp.host'))),
TextInput::make('MAIL_PORT')
->label('Port')
->label('SMTP Port')
->required()
->numeric()
->minValue(1)
->maxValue(65535)
->default(env('MAIL_PORT', config('mail.mailers.smtp.port'))),
TextInput::make('MAIL_USERNAME')
->label('Username')
->label('SMTP Username')
->required()
->default(env('MAIL_USERNAME', config('mail.mailers.smtp.username'))),
TextInput::make('MAIL_PASSWORD')
->label('Password')
->label('SMTP Password')
->password()
->revealable()
->default(env('MAIL_PASSWORD')),
ToggleButtons::make('MAIL_ENCRYPTION')
->label('Encryption')
->label('SMTP encryption')
->required()
->inline()
->options(['tls' => 'TLS', 'ssl' => 'SSL', '' => 'None'])
->default(env('MAIL_ENCRYPTION', config('mail.mailers.smtp.encryption', 'tls'))),
@@ -321,15 +293,15 @@ class Settings extends Page implements HasForms
->visible(fn (Get $get) => $get('MAIL_MAILER') === 'mailgun')
->schema([
TextInput::make('MAILGUN_DOMAIN')
->label('Domain')
->label('Mailgun Domain')
->required()
->default(env('MAILGUN_DOMAIN', config('services.mailgun.domain'))),
TextInput::make('MAILGUN_SECRET')
->label('Secret')
->label('Mailgun Secret')
->required()
->default(env('MAILGUN_SECRET', config('services.mailgun.secret'))),
->default(env('MAIL_USERNAME', config('services.mailgun.secret'))),
TextInput::make('MAILGUN_ENDPOINT')
->label('Endpoint')
->label('Mailgun Endpoint')
->required()
->default(env('MAILGUN_ENDPOINT', config('services.mailgun.endpoint'))),
]),
@@ -542,39 +514,6 @@ class Settings extends Page implements HasForms
->suffix('Requests Per Minute')
->default(env('APP_API_APPLICATION_RATELIMIT', config('http.rate_limit.application'))),
]),
Section::make('Server')
->description('Settings for Servers.')
->columns()
->collapsible()
->collapsed()
->schema([
Toggle::make('PANEL_EDITABLE_SERVER_DESCRIPTIONS')
->label('Allow Users to edit Server Descriptions?')
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->live()
->columnSpanFull()
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_EDITABLE_SERVER_DESCRIPTIONS', (bool) $state))
->default(env('PANEL_EDITABLE_SERVER_DESCRIPTIONS', config('panel.editable_server_descriptions'))),
]),
Section::make('Webhook')
->description('Configure how often old webhook logs should be pruned.')
->columns()
->collapsible()
->collapsed()
->schema([
TextInput::make('APP_WEBHOOK_PRUNE_DAYS')
->label('Prune age')
->required()
->numeric()
->minValue(1)
->maxValue(365)
->suffix('Days')
->default(env('APP_WEBHOOK_PRUNE_DAYS', config('panel.webhook.prune_days'))),
]),
];
}
@@ -623,9 +562,12 @@ class Settings extends Page implements HasForms
return [
Action::make('save')
->action('save')
->authorize(fn () => auth()->user()->can('update settings'))
->keyBindings(['mod+s']),
];
}
protected function getFormActions(): array
{
return [];
}
}

View File

@@ -5,28 +5,31 @@ namespace App\Filament\Resources;
use App\Filament\Resources\ApiKeyResource\Pages;
use App\Models\ApiKey;
use Filament\Resources\Resource;
use Illuminate\Database\Eloquent\Model;
class ApiKeyResource extends Resource
{
protected static ?string $model = ApiKey::class;
protected static ?string $label = 'API Key';
protected static ?string $navigationIcon = 'tabler-key';
protected static ?string $navigationGroup = 'Advanced';
public static function getNavigationBadge(): ?string
{
return static::getModel()::where('key_type', ApiKey::TYPE_APPLICATION)->count() ?: null;
return static::getModel()::where('key_type', '2')->count() ?: null;
}
public static function canEdit(Model $record): bool
public static function canEdit($record): bool
{
return false;
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [

View File

@@ -11,7 +11,6 @@ use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Form;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Database\Eloquent\Model;
class CreateApiKey extends CreateRecord
{
@@ -42,7 +41,7 @@ class CreateApiKey extends CreateRecord
'md' => 2,
])
->schema(
collect(ApiKey::getPermissionList())->map(fn ($resource) => ToggleButtons::make('permissions_' . $resource)
collect(ApiKey::RESOURCES)->map(fn ($resource) => ToggleButtons::make("r_$resource")
->label(str($resource)->replace('_', ' ')->title())->inline()
->options([
0 => 'None',
@@ -88,20 +87,4 @@ class CreateApiKey extends CreateRecord
->columnSpanFull(),
]);
}
protected function handleRecordCreation(array $data): Model
{
$permissions = [];
foreach (ApiKey::getPermissionList() as $permission) {
if (isset($data['permissions_' . $permission])) {
$permissions[$permission] = intval($data['permissions_' . $permission]);
unset($data['permissions_' . $permission]);
}
}
$data['permissions'] = $permissions;
return parent::handleRecordCreation($data);
}
}

View File

@@ -6,7 +6,6 @@ use App\Filament\Resources\ApiKeyResource;
use App\Models\ApiKey;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\CreateAction;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
@@ -52,23 +51,13 @@ class ListApiKeys extends ListRecords
])
->actions([
DeleteAction::make(),
])
->emptyStateIcon('tabler-key')
->emptyStateDescription('')
->emptyStateHeading('No API Keys')
->emptyStateActions([
CreateAction::make('create')
->label('Create API Key')
->button(),
]);
}
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make()
->label('Create API Key')
->hidden(fn () => ApiKey::where('key_type', ApiKey::TYPE_APPLICATION)->count() <= 0),
Actions\CreateAction::make(),
];
}
}

View File

@@ -10,10 +10,9 @@ class DatabaseHostResource extends Resource
{
protected static ?string $model = DatabaseHost::class;
protected static ?string $label = 'Database Host';
protected static ?string $label = 'Databases';
protected static ?string $navigationIcon = 'tabler-database';
protected static ?string $navigationGroup = 'Advanced';
public static function getNavigationBadge(): ?string
@@ -21,6 +20,13 @@ class DatabaseHostResource extends Resource
return static::getModel()::count() ?: null;
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [

View File

@@ -4,22 +4,18 @@ namespace App\Filament\Resources\DatabaseHostResource\Pages;
use App\Filament\Resources\DatabaseHostResource;
use App\Services\Databases\Hosts\HostCreationService;
use Closure;
use Exception;
use Filament\Forms;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Resources\Pages\CreateRecord;
use Filament\Forms;
use Filament\Forms\Components\Section;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Database\Eloquent\Model;
use PDOException;
class CreateDatabaseHost extends CreateRecord
{
private HostCreationService $service;
protected static string $resource = DatabaseHostResource::class;
protected ?string $heading = 'Database Hosts';
@@ -28,11 +24,6 @@ class CreateDatabaseHost extends CreateRecord
protected ?string $subheading = '(database servers that can have individual databases)';
public function boot(HostCreationService $service): void
{
$this->service = $service;
}
public function form(Form $form): Form
{
return $form
@@ -103,10 +94,10 @@ class CreateDatabaseHost extends CreateRecord
protected function handleRecordCreation(array $data): Model
{
return $this->service->handle($data);
return resolve(HostCreationService::class)->handle($data);
}
public function exception(Exception $e, Closure $stopPropagation): void
public function exception($e, $stopPropagation): void
{
if ($e instanceof PDOException) {
Notification::make()

View File

@@ -6,16 +6,14 @@ use App\Filament\Resources\DatabaseHostResource;
use App\Filament\Resources\DatabaseHostResource\RelationManagers\DatabasesRelationManager;
use App\Models\DatabaseHost;
use App\Services\Databases\Hosts\HostUpdateService;
use Closure;
use Exception;
use Filament\Actions;
use Filament\Forms;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Resources\Pages\EditRecord;
use Filament\Forms;
use Filament\Forms\Components\Section;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
use Illuminate\Database\Eloquent\Model;
use PDOException;
@@ -23,13 +21,6 @@ class EditDatabaseHost extends EditRecord
{
protected static string $resource = DatabaseHostResource::class;
private HostUpdateService $hostUpdateService;
public function boot(HostUpdateService $hostUpdateService): void
{
$this->hostUpdateService = $hostUpdateService;
}
public function form(Form $form): Form
{
return $form
@@ -106,16 +97,12 @@ class EditDatabaseHost extends EditRecord
];
}
protected function handleRecordUpdate(Model $record, array $data): Model
protected function handleRecordUpdate($record, array $data): Model
{
if (!$record instanceof DatabaseHost) {
return $record;
}
return $this->hostUpdateService->handle($record, $data);
return resolve(HostUpdateService::class)->handle($record->id, $data);
}
public function exception(Exception $e, Closure $stopPropagation): void
public function exception($e, $stopPropagation): void
{
if ($e instanceof PDOException) {
Notification::make()

View File

@@ -3,11 +3,9 @@
namespace App\Filament\Resources\DatabaseHostResource\Pages;
use App\Filament\Resources\DatabaseHostResource;
use App\Models\DatabaseHost;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\BulkActionGroup;
use Filament\Tables\Actions\CreateAction;
use Filament\Tables\Actions\DeleteBulkAction;
use Filament\Tables\Actions\EditAction;
use Filament\Tables\Columns\TextColumn;
@@ -32,41 +30,27 @@ class ListDatabaseHosts extends ListRecords
->sortable(),
TextColumn::make('username')
->searchable(),
TextColumn::make('databases_count')
->counts('databases')
->icon('tabler-database')
->label('Databases'),
TextColumn::make('max_databases')
->numeric()
->sortable(),
TextColumn::make('node.name')
->icon('tabler-server-2')
->placeholder('No Nodes')
->numeric()
->sortable(),
])
->checkIfRecordIsSelectableUsing(fn (DatabaseHost $databaseHost) => !$databaseHost->databases_count)
->actions([
EditAction::make(),
])
->bulkActions([
BulkActionGroup::make([
DeleteBulkAction::make()
->authorize(fn () => auth()->user()->can('delete databasehost')),
DeleteBulkAction::make(),
]),
])
->emptyStateIcon('tabler-database')
->emptyStateDescription('')
->emptyStateHeading('No Database Hosts')
->emptyStateActions([
CreateAction::make('create')
->label('Create Database Host')
->button(),
]);
}
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make('create')
->label('Create Database Host')
->hidden(fn () => DatabaseHost::count() <= 0),
Actions\CreateAction::make('create')->label('New Database Host'),
];
}
}

View File

@@ -8,7 +8,6 @@ use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Actions\ViewAction;
@@ -41,7 +40,6 @@ class DatabasesRelationManager extends RelationManager
->formatStateUsing(fn (Get $get, Database $database) => 'jdbc:mysql://' . $get('username') . ':' . urlencode($database->password) . '@' . $database->host->host . ':' . $database->host->port . '/' . $get('database')),
]);
}
public function table(Table $table): Table
{
return $table
@@ -62,7 +60,7 @@ class DatabasesRelationManager extends RelationManager
]);
}
protected function rotatePassword(DatabasePasswordService $service, Database $database, Set $set, Get $get): void
protected function rotatePassword(DatabasePasswordService $service, Database $database, $set, $get): void
{
$newPassword = $service->handle($database);
$jdbcString = 'jdbc:mysql://' . $get('username') . ':' . urlencode($newPassword) . '@' . $database->host->host . ':' . $database->host->port . '/' . $get('database');

View File

@@ -13,7 +13,6 @@ class DatabaseResource extends Resource
protected static ?string $navigationIcon = 'tabler-database';
protected static bool $shouldRegisterNavigation = false;
protected static ?string $navigationGroup = 'Advanced';
public static function getNavigationBadge(): ?string
@@ -21,6 +20,13 @@ class DatabaseResource extends Resource
return static::getModel()::count() ?: null;
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [

View File

@@ -21,12 +21,9 @@ class CreateDatabase extends CreateRecord
->searchable()
->preload()
->required(),
Select::make('database_host_id')
->relationship('host', 'name')
->searchable()
->selectablePlaceholder(false)
->preload()
->required(),
TextInput::make('database_host_id')
->required()
->numeric(),
TextInput::make('database')
->required()
->maxLength(255),

View File

@@ -4,10 +4,10 @@ namespace App\Filament\Resources\DatabaseResource\Pages;
use App\Filament\Resources\DatabaseResource;
use Filament\Actions;
use Filament\Tables\Actions\EditAction;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\BulkActionGroup;
use Filament\Tables\Actions\DeleteBulkAction;
use Filament\Tables\Actions\EditAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
@@ -48,8 +48,7 @@ class ListDatabases extends ListRecords
])
->bulkActions([
BulkActionGroup::make([
DeleteBulkAction::make()
->authorize(fn () => auth()->user()->can('delete database')),
DeleteBulkAction::make(),
]),
]);
}

View File

@@ -21,6 +21,13 @@ class EggResource extends Resource
return static::getModel()::count() ?: null;
}
public static function getRelations(): array
{
return [
//
];
}
public static function getGloballySearchableAttributes(): array
{
return ['name', 'tags', 'uuid', 'id'];

View File

@@ -2,7 +2,6 @@
namespace App\Filament\Resources\EggResource\Pages;
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
use App\Filament\Resources\EggResource;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Fieldset;
@@ -16,9 +15,10 @@ use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Form;
use Filament\Forms\Set;
use Filament\Resources\Pages\CreateRecord;
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
use Filament\Forms;
use Filament\Forms\Form;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
@@ -27,7 +27,6 @@ class CreateEgg extends CreateRecord
protected static string $resource = EggResource::class;
protected static bool $canCreateAnother = false;
public function form(Form $form): Form
{
return $form
@@ -135,7 +134,7 @@ class CreateEgg extends CreateRecord
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
$data['default_value'] ??= '';
$data['description'] ??= '';
$data['rules'] ??= [];
$data['rules'] ??= '';
$data['user_viewable'] ??= '';
$data['user_editable'] ??= '';
@@ -144,7 +143,7 @@ class CreateEgg extends CreateRecord
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
$data['default_value'] ??= '';
$data['description'] ??= '';
$data['rules'] ??= [];
$data['rules'] ??= '';
$data['user_viewable'] ??= '';
$data['user_editable'] ??= '';
@@ -156,7 +155,7 @@ class CreateEgg extends CreateRecord
->debounce(750)
->maxLength(255)
->columnSpanFull()
->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString())
->afterStateUpdated(fn (Forms\Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString())
)
->required(),
Textarea::make('description')->columnSpanFull(),
@@ -174,30 +173,7 @@ class CreateEgg extends CreateRecord
Checkbox::make('user_viewable')->label('Viewable'),
Checkbox::make('user_editable')->label('Editable'),
]),
TagsInput::make('rules')
->columnSpanFull()
->placeholder('Add Rule')
->reorderable()
->suggestions([
'required',
'nullable',
'string',
'integer',
'numeric',
'boolean',
'alpha',
'alpha_dash',
'alpha_num',
'url',
'email',
'regex:',
'min:',
'max:',
'between:',
'between:1024,65535',
'in:',
'in:true,false',
]),
Textarea::make('rules')->columnSpanFull(),
]),
]),
Tab::make('Install Script')
@@ -211,7 +187,7 @@ class CreateEgg extends CreateRecord
TextInput::make('script_container')
->required()
->maxLength(255)
->default('ghcr.io/pelican-eggs/installers:debian'),
->default('alpine:3.4'),
Select::make('script_entry')
->selectablePlaceholder(false)

View File

@@ -2,11 +2,9 @@
namespace App\Filament\Resources\EggResource\Pages;
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
use App\Filament\Resources\EggResource;
use App\Filament\Resources\EggResource\RelationManagers\ServersRelationManager;
use App\Models\Egg;
use App\Services\Eggs\Sharing\EggExporterService;
use App\Services\Eggs\Sharing\EggImporterService;
use Exception;
use Filament\Actions;
@@ -24,10 +22,12 @@ use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Form;
use Filament\Forms\Set;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
use App\Services\Eggs\Sharing\EggExporterService;
use Filament\Forms;
use Filament\Forms\Form;
class EditEgg extends EditRecord
{
@@ -40,7 +40,6 @@ class EditEgg extends EditRecord
Tabs::make()->tabs([
Tab::make('Configuration')
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
->icon('tabler-egg')
->schema([
TextInput::make('name')
->required()
@@ -81,7 +80,6 @@ class EditEgg extends EditRecord
->helperText('')
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
Toggle::make('force_outgoing_ip')
->inline(false)
->hintIcon('tabler-question-mark')
->hintIconTooltip("Forces all outgoing network traffic to have its Source IP NATed to the IP of the server's primary allocation IP.
Required for certain games to work properly when the Node has multiple public IP addresses.
@@ -107,9 +105,9 @@ class EditEgg extends EditRecord
->valueLabel('Image URI')
->helperText('The docker images available to servers using this egg.'),
]),
Tab::make('Process Management')
->columns()
->icon('tabler-server-cog')
->schema([
Select::make('config_from')
->label('Copy Settings From')
@@ -132,7 +130,6 @@ class EditEgg extends EditRecord
]),
Tab::make('Egg Variables')
->columnSpanFull()
->icon('tabler-variable')
->schema([
Repeater::make('variables')
->label('')
@@ -147,7 +144,7 @@ class EditEgg extends EditRecord
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
$data['default_value'] ??= '';
$data['description'] ??= '';
$data['rules'] ??= [];
$data['rules'] ??= '';
$data['user_viewable'] ??= '';
$data['user_editable'] ??= '';
@@ -156,7 +153,7 @@ class EditEgg extends EditRecord
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
$data['default_value'] ??= '';
$data['description'] ??= '';
$data['rules'] ??= [];
$data['rules'] ??= '';
$data['user_viewable'] ??= '';
$data['user_editable'] ??= '';
@@ -168,7 +165,7 @@ class EditEgg extends EditRecord
->debounce(750)
->maxLength(255)
->columnSpanFull()
->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString())
->afterStateUpdated(fn (Forms\Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString())
)
->required(),
Textarea::make('description')->columnSpanFull(),
@@ -186,47 +183,27 @@ class EditEgg extends EditRecord
Checkbox::make('user_viewable')->label('Viewable'),
Checkbox::make('user_editable')->label('Editable'),
]),
TagsInput::make('rules')
->columnSpanFull()
->placeholder('Add Rule')
->reorderable()
->suggestions([
'required',
'nullable',
'string',
'integer',
'numeric',
'boolean',
'alpha',
'alpha_dash',
'alpha_num',
'url',
'email',
'regex:',
'min:',
'max:',
'between:',
'between:1024,65535',
'in:',
'in:true,false',
]),
TextInput::make('rules')->columnSpanFull(),
]),
]),
Tab::make('Install Script')
->columns(3)
->icon('tabler-file-download')
->schema([
Select::make('copy_script_from')
->placeholder('None')
->relationship('scriptFrom', 'name', ignoreRecord: true),
TextInput::make('script_container')
->required()
->maxLength(255)
->default('alpine:3.4'),
TextInput::make('script_entry')
->required()
->maxLength(255)
->default('ash'),
MonacoEditor::make('script_install')
->label('Install Script')
->columnSpanFull()
@@ -234,6 +211,7 @@ class EditEgg extends EditRecord
->language('shell')
->view('filament.plugins.monaco-editor'),
]),
])->columnSpanFull()->persistTabInQueryString(),
]);
}
@@ -244,13 +222,14 @@ class EditEgg extends EditRecord
Actions\DeleteAction::make('deleteEgg')
->disabled(fn (Egg $egg): bool => $egg->servers()->count() > 0)
->label(fn (Egg $egg): string => $egg->servers()->count() <= 0 ? 'Delete' : 'In Use'),
Actions\Action::make('exportEgg')
->label('Export')
->color('primary')
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
echo $service->handle($egg->id);
}, 'egg-' . $egg->getKebabName() . '.json'))
->authorize(fn () => auth()->user()->can('export egg')),
}, 'egg-' . $egg->getKebabName() . '.json')),
Actions\Action::make('importEgg')
->label('Import')
->form([
@@ -280,7 +259,10 @@ class EditEgg extends EditRecord
->contained(false),
])
->action(function (array $data, Egg $egg, EggImporterService $eggImportService): void {
->action(function (array $data, Egg $egg): void {
/** @var EggImporterService $eggImportService */
$eggImportService = resolve(EggImporterService::class);
if (!empty($data['egg'])) {
try {
$eggImportService->fromFile($data['egg'], $egg);
@@ -288,14 +270,16 @@ class EditEgg extends EditRecord
Notification::make()
->title('Import Failed')
->body($exception->getMessage())
->danger() // Will Robinson
->danger()
->send();
report($exception);
return;
}
} elseif (!empty($data['url'])) {
}
if (!empty($data['url'])) {
try {
$eggImportService->fromUrl($data['url'], $egg);
} catch (Exception $exception) {
@@ -316,8 +300,8 @@ class EditEgg extends EditRecord
->title('Import Success')
->success()
->send();
})
->authorize(fn () => auth()->user()->can('import egg')),
}),
$this->getSaveFormAction()->formId('form'),
];
}

View File

@@ -14,13 +14,13 @@ use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\Action;
use Filament\Tables\Actions\BulkActionGroup;
use Filament\Tables\Actions\DeleteBulkAction;
use Filament\Tables\Actions\EditAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
use Filament\Tables;
class ListEggs extends ListRecords
{
@@ -49,56 +49,20 @@ class ListEggs extends ListRecords
])
->actions([
EditAction::make(),
Action::make('export')
Tables\Actions\Action::make('export')
->icon('tabler-download')
->label('Export')
->color('primary')
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
echo $service->handle($egg->id);
}, 'egg-' . $egg->getKebabName() . '.json'))
->authorize(fn () => auth()->user()->can('export egg')),
Action::make('update')
->icon('tabler-cloud-download')
->label('Update')
->color('success')
->requiresConfirmation()
->modalHeading('Are you sure you want to update this egg?')
->modalDescription('If you made any changes to the egg they will be overwritten!')
->modalIconColor('danger')
->modalSubmitAction(fn (Actions\StaticAction $action) => $action->color('danger'))
->action(function (Egg $egg, EggImporterService $eggImporterService) {
try {
$eggImporterService->fromUrl($egg->update_url, $egg);
cache()->forget("eggs.{$egg->uuid}.update");
} catch (Exception $exception) {
Notification::make()
->title('Egg Update failed')
->body($exception->getMessage())
->danger()
->send();
report($exception);
return;
}
Notification::make()
->title('Egg updated')
->success()
->send();
})
->authorize(fn () => auth()->user()->can('import egg'))
->visible(fn (Egg $egg) => cache()->get("eggs.{$egg->uuid}.update", false)),
}, 'egg-' . $egg->getKebabName() . '.json')),
])
->bulkActions([
BulkActionGroup::make([
DeleteBulkAction::make()
->authorize(fn () => auth()->user()->can('delete egg')),
DeleteBulkAction::make(),
]),
]);
}
protected function getHeaderActions(): array
{
return [
@@ -131,7 +95,10 @@ class ListEggs extends ListRecords
->contained(false),
])
->action(function (array $data, EggImporterService $eggImportService): void {
->action(function (array $data): void {
/** @var EggImporterService $eggImportService */
$eggImportService = resolve(EggImporterService::class);
if (!empty($data['egg'])) {
/** @var TemporaryUploadedFile[] $eggFile */
$eggFile = $data['egg'];
@@ -142,7 +109,6 @@ class ListEggs extends ListRecords
} catch (Exception $exception) {
Notification::make()
->title('Import Failed')
->body($exception->getMessage())
->danger()
->send();
@@ -159,7 +125,6 @@ class ListEggs extends ListRecords
} catch (Exception $exception) {
Notification::make()
->title('Import Failed')
->body($exception->getMessage())
->danger()
->send();
@@ -173,8 +138,7 @@ class ListEggs extends ListRecords
->title('Import Success')
->success()
->send();
})
->authorize(fn () => auth()->user()->can('import egg')),
}),
];
}
}

View File

@@ -16,7 +16,7 @@ class ServersRelationManager extends RelationManager
{
return $table
->recordTitleAttribute('servers')
->emptyStateDescription('No Servers')->emptyStateHeading('No servers are assigned to this Egg.')
->emptyStateDescription('No Servers')->emptyStateHeading('No servers are assigned this egg.')
->searchable(false)
->columns([
TextColumn::make('user.username')

View File

@@ -11,7 +11,6 @@ class MountResource extends Resource
protected static ?string $model = Mount::class;
protected static ?string $navigationIcon = 'tabler-layers-linked';
protected static ?string $navigationGroup = 'Advanced';
public static function getNavigationBadge(): ?string
@@ -19,6 +18,13 @@ class MountResource extends Resource
return static::getModel()::count() ?: null;
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [

View File

@@ -4,14 +4,14 @@ namespace App\Filament\Resources\MountResource\Pages;
use App\Filament\Resources\MountResource;
use Filament\Actions;
use Filament\Forms\Components\Group;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Form;
use Filament\Resources\Pages\EditRecord;
use Filament\Forms\Components\Group;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Form;
class EditMount extends EditRecord
{
@@ -96,7 +96,6 @@ class EditMount extends EditRecord
'lg' => 2,
]);
}
protected function getHeaderActions(): array
{
return [

View File

@@ -17,7 +17,6 @@ use Filament\Tables\Table;
class ListMounts extends ListRecords
{
protected static string $resource = MountResource::class;
public function table(Table $table): Table
{
return $table
@@ -44,8 +43,7 @@ class ListMounts extends ListRecords
])
->bulkActions([
BulkActionGroup::make([
DeleteBulkAction::make()
->authorize(fn () => auth()->user()->can('delete mount')),
DeleteBulkAction::make(),
]),
])
->emptyStateIcon('tabler-layers-linked')
@@ -57,7 +55,6 @@ class ListMounts extends ListRecords
->button(),
]);
}
protected function getHeaderActions(): array
{
return [

View File

@@ -3,8 +3,8 @@
namespace App\Filament\Resources\NodeResource\Pages;
use App\Filament\Resources\NodeResource;
use Filament\Forms;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\TextInput;
@@ -153,6 +153,7 @@ class CreateNode extends CreateRecord
'lg' => 2,
])
->required()
->regex('/[a-zA-Z0-9_\.\- ]+/')
->helperText('This name is for display only and can be changed later.')
->maxLength(100),
@@ -219,7 +220,7 @@ class CreateNode extends CreateRecord
ToggleButtons::make('public')
->default(true)
->columnSpan(1)
->label('Use Node for deployment?')->inline()
->label('Automatic Allocation')->inline()
->options([
true => 'Yes',
false => 'No',
@@ -229,7 +230,11 @@ class CreateNode extends CreateRecord
false => 'danger',
]),
TagsInput::make('tags')
->placeholder('Add Tags')
->label('Tags')
->disabled()
->placeholder('Not Implemented')
->hintIcon('tabler-question-mark')
->hintIconTooltip('Not Implemented')
->columnSpan(2),
TextInput::make('upload_size')
->label('Upload Limit')
@@ -398,7 +403,7 @@ class CreateNode extends CreateRecord
protected function getRedirectUrlParameters(): array
{
return [
'tab' => '-configuration-file-tab',
'tab' => '-configuration-tab',
];
}

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