mirror of
https://github.com/pelican-dev/panel.git
synced 2026-05-04 18:00:48 +03:00
Compare commits
1 Commits
v1.0.0-bet
...
release/v1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f5938d230 |
@@ -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
|
||||
@@ -4,6 +4,7 @@ APP_KEY=
|
||||
APP_TIMEZONE=UTC
|
||||
APP_URL=http://panel.test
|
||||
APP_LOCALE=en
|
||||
APP_ENVIRONMENT_ONLY=true
|
||||
|
||||
LOG_CHANNEL=daily
|
||||
LOG_STACK=single
|
||||
@@ -26,7 +27,11 @@ 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
|
||||
|
||||
# Set this to true, and set start & end ports to auto create allocations.
|
||||
PANEL_CLIENT_ALLOCATIONS_ENABLED=false
|
||||
PANEL_CLIENT_ALLOCATIONS_RANGE_START=
|
||||
PANEL_CLIENT_ALLOCATIONS_RANGE_END=
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -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:
|
||||
|
||||
83
.github/docker/entrypoint.sh
vendored
83
.github/docker/entrypoint.sh
vendored
@@ -1,58 +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
|
||||
|
||||
ln -s /app/var/.env /app/
|
||||
fi
|
||||
|
||||
mkdir /pelican-data/database
|
||||
ln -s /pelican-data/.env /var/www/html/
|
||||
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 "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 [[ -z $SKIP_CADDY ]]; then
|
||||
echo "Starting PHP-FPM and Caddy"
|
||||
export SUPERVISORD_CADDY=true
|
||||
else
|
||||
echo "Starting PHP-FPM only"
|
||||
fi
|
||||
|
||||
chown -R www-data:www-data . /pelican-data/.env /pelican-data/database
|
||||
|
||||
echo "Starting Supervisord"
|
||||
echo -e "Starting supervisord."
|
||||
exec "$@"
|
||||
|
||||
12
.github/docker/supervisord.conf
vendored
12
.github/docker/supervisord.conf
vendored
@@ -25,15 +25,15 @@ autostart=true
|
||||
autorestart=true
|
||||
|
||||
[program:queue-worker]
|
||||
command=/usr/local/bin/php /var/www/html/artisan queue:work --queue=high,standard,low --sleep=3 --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
|
||||
9
.github/workflows/ci.yaml
vendored
9
.github/workflows/ci.yaml
vendored
@@ -32,6 +32,7 @@ jobs:
|
||||
APP_KEY: ThisIsARandomStringForTests12345
|
||||
APP_TIMEZONE: UTC
|
||||
APP_URL: http://localhost/
|
||||
APP_ENVIRONMENT_ONLY: "true"
|
||||
CACHE_DRIVER: array
|
||||
MAIL_MAILER: array
|
||||
SESSION_DRIVER: array
|
||||
@@ -40,8 +41,6 @@ jobs:
|
||||
DB_HOST: 127.0.0.1
|
||||
DB_DATABASE: testing
|
||||
DB_USERNAME: root
|
||||
GUZZLE_TIMEOUT: 60
|
||||
GUZZLE_CONNECT_TIMEOUT: 60
|
||||
steps:
|
||||
- name: Code Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -105,6 +104,7 @@ jobs:
|
||||
APP_KEY: ThisIsARandomStringForTests12345
|
||||
APP_TIMEZONE: UTC
|
||||
APP_URL: http://localhost/
|
||||
APP_ENVIRONMENT_ONLY: "true"
|
||||
CACHE_DRIVER: array
|
||||
MAIL_MAILER: array
|
||||
SESSION_DRIVER: array
|
||||
@@ -113,8 +113,6 @@ jobs:
|
||||
DB_HOST: 127.0.0.1
|
||||
DB_DATABASE: testing
|
||||
DB_USERNAME: root
|
||||
GUZZLE_TIMEOUT: 60
|
||||
GUZZLE_CONNECT_TIMEOUT: 60
|
||||
steps:
|
||||
- name: Code Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -168,14 +166,13 @@ jobs:
|
||||
APP_KEY: ThisIsARandomStringForTests12345
|
||||
APP_TIMEZONE: UTC
|
||||
APP_URL: http://localhost/
|
||||
APP_ENVIRONMENT_ONLY: "true"
|
||||
CACHE_DRIVER: array
|
||||
MAIL_MAILER: array
|
||||
SESSION_DRIVER: array
|
||||
QUEUE_CONNECTION: sync
|
||||
DB_CONNECTION: sqlite
|
||||
DB_DATABASE: testing.sqlite
|
||||
GUZZLE_TIMEOUT: 60
|
||||
GUZZLE_CONNECT_TIMEOUT: 60
|
||||
steps:
|
||||
- name: Code Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
11
Caddyfile
11
Caddyfile
@@ -1,11 +0,0 @@
|
||||
{
|
||||
email {$ADMIN_EMAIL}
|
||||
}
|
||||
|
||||
{$APP_URL} {
|
||||
root * /var/www/html/public
|
||||
encode gzip
|
||||
|
||||
php_fastcgi 127.0.0.1:9000
|
||||
file_server
|
||||
}
|
||||
85
Dockerfile
85
Dockerfile
@@ -1,58 +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 install --frozen-lockfile \
|
||||
&& yarn run build:production
|
||||
|
||||
RUN yarn install --frozen-lockfile && yarn run build:production
|
||||
# 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 .
|
||||
|
||||
FROM php:8.3-fpm-alpine
|
||||
# FROM --platform=$TARGETOS/$TARGETARCH php:8.3-fpm-alpine
|
||||
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
|
||||
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
|
||||
|
||||
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 /var/www/html/storage \
|
||||
&& chmod -R 755 /var/www/html/bootstrap/cache
|
||||
|
||||
# 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
|
||||
|
||||
EXPOSE 80:2019
|
||||
EXPOSE 443
|
||||
|
||||
VOLUME /pelican-data
|
||||
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
|
||||
ENTRYPOINT [ "/bin/ash", ".github/docker/entrypoint.sh" ]
|
||||
CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ]
|
||||
|
||||
@@ -2,49 +2,163 @@
|
||||
|
||||
namespace App\Console\Commands\Environment;
|
||||
|
||||
use App\Traits\EnvironmentWriterTrait;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
use App\Traits\Commands\EnvironmentWriterTrait;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
class AppSettingsCommand extends Command
|
||||
{
|
||||
use EnvironmentWriterTrait;
|
||||
|
||||
public const CACHE_DRIVERS = [
|
||||
'file' => 'Filesystem (recommended)',
|
||||
'redis' => 'Redis',
|
||||
];
|
||||
|
||||
public const SESSION_DRIVERS = [
|
||||
'file' => 'Filesystem (recommended)',
|
||||
'redis' => 'Redis',
|
||||
'database' => 'Database',
|
||||
'cookie' => 'Cookie',
|
||||
];
|
||||
|
||||
public const QUEUE_DRIVERS = [
|
||||
'database' => 'Database (recommended)',
|
||||
'redis' => 'Redis',
|
||||
'sync' => 'Synchronous',
|
||||
];
|
||||
|
||||
protected $description = 'Configure basic environment settings for the Panel.';
|
||||
|
||||
protected $signature = 'p:environment:setup
|
||||
{--url= : The URL that this Panel is running on.}';
|
||||
{--url= : The URL that this Panel is running on.}
|
||||
{--cache= : The cache driver backend to use.}
|
||||
{--session= : The session driver backend to use.}
|
||||
{--queue= : The queue driver backend to use.}
|
||||
{--redis-host= : Redis host to use for connections.}
|
||||
{--redis-pass= : Password used to connect to redis.}
|
||||
{--redis-port= : Port to connect to redis over.}
|
||||
{--settings-ui= : Enable or disable the settings UI.}';
|
||||
|
||||
protected array $variables = [];
|
||||
|
||||
public function handle(): void
|
||||
/**
|
||||
* AppSettingsCommand constructor.
|
||||
*/
|
||||
public function __construct(private Kernel $console)
|
||||
{
|
||||
$path = base_path('.env');
|
||||
if (!file_exists($path)) {
|
||||
$this->comment('Copying example .env file');
|
||||
copy($path . '.example', $path);
|
||||
}
|
||||
|
||||
if (!config('app.key')) {
|
||||
$this->comment('Generating app key');
|
||||
Artisan::call('key:generate');
|
||||
}
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle command execution.
|
||||
*
|
||||
* @throws \App\Exceptions\PanelException
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$this->variables['APP_TIMEZONE'] = 'UTC';
|
||||
|
||||
$this->output->comment(__('commands.appsettings.comment.url'));
|
||||
$this->variables['APP_URL'] = $this->option('url') ?? $this->ask(
|
||||
'Application URL',
|
||||
config('app.url', 'https://example.com')
|
||||
);
|
||||
|
||||
$selected = config('cache.default', 'file');
|
||||
$this->variables['CACHE_STORE'] = $this->option('cache') ?? $this->choice(
|
||||
'Cache Driver',
|
||||
self::CACHE_DRIVERS,
|
||||
array_key_exists($selected, self::CACHE_DRIVERS) ? $selected : null
|
||||
);
|
||||
|
||||
$selected = config('session.driver', 'file');
|
||||
$this->variables['SESSION_DRIVER'] = $this->option('session') ?? $this->choice(
|
||||
'Session Driver',
|
||||
self::SESSION_DRIVERS,
|
||||
array_key_exists($selected, self::SESSION_DRIVERS) ? $selected : null
|
||||
);
|
||||
|
||||
$selected = config('queue.default', 'database');
|
||||
$this->variables['QUEUE_CONNECTION'] = $this->option('queue') ?? $this->choice(
|
||||
'Queue Driver',
|
||||
self::QUEUE_DRIVERS,
|
||||
array_key_exists($selected, self::QUEUE_DRIVERS) ? $selected : null
|
||||
);
|
||||
|
||||
if (!is_null($this->option('settings-ui'))) {
|
||||
$this->variables['APP_ENVIRONMENT_ONLY'] = $this->option('settings-ui') == 'true' ? 'false' : 'true';
|
||||
} else {
|
||||
$this->variables['APP_ENVIRONMENT_ONLY'] = $this->confirm(__('commands.appsettings.comment.settings_ui'), true) ? 'false' : 'true';
|
||||
}
|
||||
|
||||
// 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');
|
||||
$redisUsed = count(collect($this->variables)->filter(function ($item) {
|
||||
return $item === 'redis';
|
||||
})) !== 0;
|
||||
|
||||
if ($redisUsed) {
|
||||
$this->requestRedisSettings();
|
||||
}
|
||||
|
||||
$path = base_path('.env');
|
||||
if (!file_exists($path)) {
|
||||
copy($path . '.example', $path);
|
||||
}
|
||||
|
||||
$this->writeToEnvironment($this->variables);
|
||||
|
||||
$this->info("Setup complete. Vist {$this->variables['APP_URL']}/installer to complete the installation");
|
||||
if (!config('app.key')) {
|
||||
Artisan::call('key:generate');
|
||||
}
|
||||
|
||||
if ($this->variables['QUEUE_CONNECTION'] !== 'sync') {
|
||||
$this->call('p:environment:queue-service', [
|
||||
'--use-redis' => $redisUsed,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->info($this->console->output());
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request redis connection details and verify them.
|
||||
*/
|
||||
private function requestRedisSettings(): void
|
||||
{
|
||||
$this->output->note(__('commands.appsettings.redis.note'));
|
||||
$this->variables['REDIS_HOST'] = $this->option('redis-host') ?? $this->ask(
|
||||
'Redis Host',
|
||||
config('database.redis.default.host')
|
||||
);
|
||||
|
||||
$askForRedisPassword = true;
|
||||
if (!empty(config('database.redis.default.password'))) {
|
||||
$this->variables['REDIS_PASSWORD'] = config('database.redis.default.password');
|
||||
$askForRedisPassword = $this->confirm('It seems a password is already defined for Redis, would you like to change it?');
|
||||
}
|
||||
|
||||
if ($askForRedisPassword) {
|
||||
$this->output->comment(__('commands.appsettings.redis.comment'));
|
||||
$this->variables['REDIS_PASSWORD'] = $this->option('redis-pass') ?? $this->output->askHidden(
|
||||
'Redis Password'
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($this->variables['REDIS_PASSWORD'])) {
|
||||
$this->variables['REDIS_PASSWORD'] = 'null';
|
||||
}
|
||||
|
||||
$this->variables['REDIS_PORT'] = $this->option('redis-port') ?? $this->ask(
|
||||
'Redis Port',
|
||||
config('database.redis.default.port')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,68 +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 CacheSettingsCommand extends Command
|
||||
{
|
||||
use EnvironmentWriterTrait;
|
||||
use RequestRedisSettingsTrait;
|
||||
|
||||
public const CACHE_DRIVERS = [
|
||||
'file' => 'Filesystem (default)',
|
||||
'database' => 'Database',
|
||||
'redis' => 'Redis',
|
||||
];
|
||||
|
||||
protected $description = 'Configure cache settings for the Panel.';
|
||||
|
||||
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.}';
|
||||
|
||||
protected array $variables = [];
|
||||
|
||||
/**
|
||||
* CacheSettingsCommand constructor.
|
||||
*/
|
||||
public function __construct(private Kernel $console)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle command execution.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$selected = config('cache.default', 'file');
|
||||
$this->variables['CACHE_STORE'] = $this->option('driver') ?? $this->choice(
|
||||
'Cache Driver',
|
||||
self::CACHE_DRIVERS,
|
||||
array_key_exists($selected, self::CACHE_DRIVERS) ? $selected : null
|
||||
);
|
||||
|
||||
if ($this->variables['CACHE_STORE'] === 'redis') {
|
||||
$this->requestRedisSettings();
|
||||
|
||||
if (config('queue.default') !== 'sync') {
|
||||
$this->call('p:environment:queue-service', [
|
||||
'--overwrite' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->writeToEnvironment($this->variables);
|
||||
|
||||
$this->info($this->console->output());
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
@@ -61,8 +61,6 @@ class EmailSettingsCommand extends Command
|
||||
|
||||
$this->writeToEnvironment($this->variables);
|
||||
|
||||
$this->call('queue:restart');
|
||||
|
||||
$this->line('Updating stored environment configuration file.');
|
||||
$this->line('');
|
||||
}
|
||||
|
||||
@@ -1,66 +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 QueueSettingsCommand extends Command
|
||||
{
|
||||
use EnvironmentWriterTrait;
|
||||
use RequestRedisSettingsTrait;
|
||||
|
||||
public const QUEUE_DRIVERS = [
|
||||
'database' => 'Database (default)',
|
||||
'redis' => 'Redis',
|
||||
'sync' => 'Synchronous',
|
||||
];
|
||||
|
||||
protected $description = 'Configure queue settings for the Panel.';
|
||||
|
||||
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.}';
|
||||
|
||||
protected array $variables = [];
|
||||
|
||||
/**
|
||||
* QueueSettingsCommand constructor.
|
||||
*/
|
||||
public function __construct(private Kernel $console)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle command execution.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$selected = config('queue.default', 'database');
|
||||
$this->variables['QUEUE_CONNECTION'] = $this->option('driver') ?? $this->choice(
|
||||
'Queue Driver',
|
||||
self::QUEUE_DRIVERS,
|
||||
array_key_exists($selected, self::QUEUE_DRIVERS) ? $selected : null
|
||||
);
|
||||
|
||||
if ($this->variables['QUEUE_CONNECTION'] === 'redis') {
|
||||
$this->requestRedisSettings();
|
||||
|
||||
$this->call('p:environment:queue-service', [
|
||||
'--overwrite' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->writeToEnvironment($this->variables);
|
||||
|
||||
$this->info($this->console->output());
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ class QueueWorkerServiceCommand extends Command
|
||||
{--service-name= : Name of the queue worker service.}
|
||||
{--user= : The user that PHP runs under.}
|
||||
{--group= : The group that PHP runs under.}
|
||||
{--use-redis : Whether redis is used.}
|
||||
{--overwrite : Force overwrite if the service file already exists.}';
|
||||
|
||||
public function handle(): void
|
||||
@@ -31,8 +32,7 @@ class QueueWorkerServiceCommand extends Command
|
||||
$user = $this->option('user') ?? $this->ask('Webserver User', 'www-data');
|
||||
$group = $this->option('group') ?? $this->ask('Webserver Group', 'www-data');
|
||||
|
||||
$redisUsed = config('queue.default') === 'redis' || config('session.driver') === 'redis' || config('cache.default') === 'redis';
|
||||
$afterRedis = $redisUsed ? '
|
||||
$afterRedis = $this->option('use-redis') ? '
|
||||
After=redis-server.service' : '';
|
||||
|
||||
$basePath = base_path();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,69 +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 SessionSettingsCommand extends Command
|
||||
{
|
||||
use EnvironmentWriterTrait;
|
||||
use RequestRedisSettingsTrait;
|
||||
|
||||
public const SESSION_DRIVERS = [
|
||||
'file' => 'Filesystem (default)',
|
||||
'redis' => 'Redis',
|
||||
'database' => 'Database',
|
||||
'cookie' => 'Cookie',
|
||||
];
|
||||
|
||||
protected $description = 'Configure session settings for the Panel.';
|
||||
|
||||
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.}';
|
||||
|
||||
protected array $variables = [];
|
||||
|
||||
/**
|
||||
* SessionSettingsCommand constructor.
|
||||
*/
|
||||
public function __construct(private Kernel $console)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle command execution.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$selected = config('session.driver', 'file');
|
||||
$this->variables['SESSION_DRIVER'] = $this->option('driver') ?? $this->choice(
|
||||
'Session Driver',
|
||||
self::SESSION_DRIVERS,
|
||||
array_key_exists($selected, self::SESSION_DRIVERS) ? $selected : null
|
||||
);
|
||||
|
||||
if ($this->variables['SESSION_DRIVER'] === 'redis') {
|
||||
$this->requestRedisSettings();
|
||||
|
||||
if (config('queue.default') !== 'sync') {
|
||||
$this->call('p:environment:queue-service', [
|
||||
'--overwrite' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->writeToEnvironment($this->variables);
|
||||
|
||||
$this->info($this->console->output());
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Maintenance;
|
||||
|
||||
use App\Models\Node;
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class PruneImagesCommand extends Command
|
||||
{
|
||||
protected $signature = 'p:maintenance:prune-images {node?}';
|
||||
|
||||
protected $description = 'Clean up all dangling docker images to clear up disk space.';
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$node = $this->argument('node');
|
||||
|
||||
if (empty($node)) {
|
||||
$nodes = Node::all();
|
||||
/** @var Node $node */
|
||||
foreach ($nodes as $node) {
|
||||
$this->cleanupImages($node);
|
||||
}
|
||||
} else {
|
||||
$this->cleanupImages((int) $node);
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanupImages(int|Node $node): void
|
||||
{
|
||||
if (!$node instanceof Node) {
|
||||
$node = Node::query()->findOrFail($node);
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::daemon($node)
|
||||
->connectTimeout(5)
|
||||
->timeout(30)
|
||||
->delete('/api/system/docker/image/prune')
|
||||
->json() ?? [];
|
||||
|
||||
if (empty($response) || $response['ImagesDeleted'] === null) {
|
||||
$this->warn("Node {$node->id}: No images to clean up.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$count = count($response['ImagesDeleted']);
|
||||
|
||||
$useBinaryPrefix = config('panel.use_binary_prefix');
|
||||
$space = round($useBinaryPrefix ? $response['SpaceReclaimed'] / 1024 / 1024 : $response['SpaceReclaimed'] / 1000 / 1000, 2) . ($useBinaryPrefix ? ' MiB' : ' MB');
|
||||
|
||||
$this->info("Node {$node->id}: Cleaned up {$count} dangling docker images. ({$space})");
|
||||
} catch (Exception $exception) {
|
||||
$this->error($exception->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -15,7 +15,7 @@ class DeleteUserCommand extends Command
|
||||
public function handle(): int
|
||||
{
|
||||
$search = $this->option('user') ?? $this->ask(trans('command/messages.user.search_users'));
|
||||
Assert::notEmpty($search, 'Search term should not be empty.');
|
||||
Assert::notEmpty($search, 'Search term should be an email address, got: %s.');
|
||||
|
||||
$results = User::query()
|
||||
->where('id', 'LIKE', "$search%")
|
||||
@@ -42,8 +42,6 @@ class DeleteUserCommand extends Command
|
||||
if (!$deleteUser = $this->ask(trans('command/messages.user.select_search_user'))) {
|
||||
return $this->handle();
|
||||
}
|
||||
|
||||
$deleteUser = User::query()->findOrFail($deleteUser);
|
||||
} else {
|
||||
if (count($results) > 1) {
|
||||
$this->error(trans('command/messages.user.multiple_found'));
|
||||
@@ -55,7 +53,8 @@ class DeleteUserCommand extends Command
|
||||
}
|
||||
|
||||
if ($this->confirm(trans('command/messages.user.confirm_delete')) || !$this->input->isInteractive()) {
|
||||
$deleteUser->delete();
|
||||
$user = User::query()->findOrFail($deleteUser);
|
||||
$user->delete();
|
||||
|
||||
$this->info(trans('command/messages.user.deleted'));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Console;
|
||||
|
||||
use App\Jobs\NodeStatistics;
|
||||
use App\Models\ActivityLog;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Database\Console\PruneCommand;
|
||||
@@ -10,7 +9,6 @@ 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
|
||||
{
|
||||
@@ -32,11 +30,7 @@ class Kernel extends ConsoleKernel
|
||||
|
||||
// Execute scheduled commands for servers every minute, as if there was a normal cron running.
|
||||
$schedule->command(ProcessRunnableCommand::class)->everyMinute()->withoutOverlapping();
|
||||
|
||||
$schedule->command(CleanServiceBackupFilesCommand::class)->daily();
|
||||
$schedule->command(PruneImagesCommand::class)->daily();
|
||||
|
||||
$schedule->job(new NodeStatistics())->everyFiveSeconds()->withoutOverlapping();
|
||||
|
||||
if (config('backups.prune_age')) {
|
||||
// Every 30 minutes, run the backup pruning command so that any abandoned backups can be deleted.
|
||||
|
||||
@@ -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
|
||||
@@ -6,14 +6,12 @@ enum ContainerStatus: string
|
||||
{
|
||||
// Docker Based
|
||||
case Created = 'created';
|
||||
case Starting = 'starting';
|
||||
case Running = 'running';
|
||||
case Restarting = 'restarting';
|
||||
case Exited = 'exited';
|
||||
case Paused = 'paused';
|
||||
case Dead = 'dead';
|
||||
case Removing = 'removing';
|
||||
case Stopping = 'stopping';
|
||||
case Offline = 'offline';
|
||||
|
||||
// HTTP Based
|
||||
@@ -22,17 +20,15 @@ enum ContainerStatus: string
|
||||
public function icon(): string
|
||||
{
|
||||
return match ($this) {
|
||||
|
||||
self::Created => 'tabler-heart-plus',
|
||||
self::Starting => 'tabler-heart-up',
|
||||
self::Running => 'tabler-heartbeat',
|
||||
self::Restarting => 'tabler-heart-bolt',
|
||||
self::Exited => 'tabler-heart-exclamation',
|
||||
self::Paused => 'tabler-heart-pause',
|
||||
self::Dead, self::Offline => 'tabler-heart-x',
|
||||
self::Dead => 'tabler-heart-x',
|
||||
self::Removing => 'tabler-heart-down',
|
||||
self::Missing => 'tabler-heart-search',
|
||||
self::Stopping => 'tabler-heart-minus',
|
||||
self::Missing => 'tabler-heart-question',
|
||||
self::Offline => 'tabler-heart-bolt',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -40,7 +36,6 @@ enum ContainerStatus: string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Created => 'primary',
|
||||
self::Starting => 'warning',
|
||||
self::Running => 'success',
|
||||
self::Restarting => 'info',
|
||||
self::Exited => 'danger',
|
||||
@@ -48,7 +43,6 @@ enum ContainerStatus: string
|
||||
self::Dead => 'danger',
|
||||
self::Removing => 'warning',
|
||||
self::Missing => 'danger',
|
||||
self::Stopping => 'warning',
|
||||
self::Offline => 'gray',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
10
app/Filament/Clusters/Settings.php
Normal file
10
app/Filament/Clusters/Settings.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Clusters;
|
||||
|
||||
use Filament\Clusters\Cluster;
|
||||
|
||||
class Settings extends Cluster
|
||||
{
|
||||
protected static ?string $navigationIcon = 'tabler-settings';
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer;
|
||||
|
||||
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\RedisStep;
|
||||
use App\Filament\Pages\Installer\Steps\RequirementsStep;
|
||||
use App\Models\User;
|
||||
use App\Services\Users\UserCreationService;
|
||||
use App\Traits\CheckMigrationsTrait;
|
||||
use App\Traits\EnvironmentWriterTrait;
|
||||
use Exception;
|
||||
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 Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
/**
|
||||
* @property Form $form
|
||||
*/
|
||||
class PanelInstaller extends SimplePage implements HasForms
|
||||
{
|
||||
use CheckMigrationsTrait;
|
||||
use EnvironmentWriterTrait;
|
||||
use HasUnsavedDataChangesAlert;
|
||||
use InteractsWithForms;
|
||||
|
||||
public $data = [];
|
||||
|
||||
protected static string $view = 'filament.pages.installer';
|
||||
|
||||
public function getMaxWidth(): MaxWidth|string
|
||||
{
|
||||
return MaxWidth::SevenExtraLarge;
|
||||
}
|
||||
|
||||
public static function show(): bool
|
||||
{
|
||||
if (User::count() <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (config('panel.client_features.installer.enabled')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function mount()
|
||||
{
|
||||
abort_unless(self::show(), 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(),
|
||||
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()
|
||||
->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))),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getFormStatePath(): ?string
|
||||
{
|
||||
return 'data';
|
||||
}
|
||||
|
||||
protected function hasUnsavedDataChangesAlert(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
$inputs = $this->form->getState();
|
||||
|
||||
// Write variables to .env file
|
||||
$variables = array_get($inputs, 'env');
|
||||
$this->writeToEnvironment($variables);
|
||||
|
||||
// Clear config cache
|
||||
Artisan::call('config:clear');
|
||||
|
||||
// Run migrations
|
||||
Artisan::call('migrate', [
|
||||
'--force' => true,
|
||||
'--seed' => true,
|
||||
'--database' => $variables['DB_CONNECTION'],
|
||||
]);
|
||||
|
||||
if (!$this->hasCompletedMigrations()) {
|
||||
throw new Exception('Migrations didn\'t run successfully. Double check your database configuration.');
|
||||
}
|
||||
|
||||
// Create first admin user
|
||||
$userData = array_get($inputs, 'user');
|
||||
$userData['root_admin'] = true;
|
||||
$user = app(UserCreationService::class)->handle($userData);
|
||||
|
||||
// Install setup complete
|
||||
$this->writeToEnvironment(['APP_INSTALLER' => 'false']);
|
||||
|
||||
$this->rememberData();
|
||||
|
||||
Notification::make()
|
||||
->title('Successfully Installed')
|
||||
->success()
|
||||
->send();
|
||||
|
||||
auth()->loginUsingId($user->id);
|
||||
|
||||
return redirect('/admin');
|
||||
} catch (Exception $exception) {
|
||||
report($exception);
|
||||
|
||||
Notification::make()
|
||||
->title('Installation Failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->persistent()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
<?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()
|
||||
->placeholder('admin@example.com'),
|
||||
TextInput::make('user.username')
|
||||
->label('Admin Username')
|
||||
->required()
|
||||
->placeholder('admin'),
|
||||
TextInput::make('user.password')
|
||||
->label('Admin Password')
|
||||
->required()
|
||||
->password()
|
||||
->revealable(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Exceptions\Halt;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use PDOException;
|
||||
|
||||
class DatabaseStep
|
||||
{
|
||||
public static function make(): Step
|
||||
{
|
||||
return Step::make('database')
|
||||
->label('Database')
|
||||
->columns()
|
||||
->schema([
|
||||
TextInput::make('env.DB_DATABASE')
|
||||
->label(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite' ? 'Database Path' : 'Database Name')
|
||||
->columnSpanFull()
|
||||
->hintIcon('tabler-question-mark')
|
||||
->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()
|
||||
->default(fn (Get $get) => env('DB_DATABASE', $get('env.DB_CONNECTION') === 'sqlite' ? 'database.sqlite' : 'panel')),
|
||||
TextInput::make('env.DB_HOST')
|
||||
->label('Database Host')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The host of your database. Make sure it is reachable.')
|
||||
->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')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The port of your database.')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(env('DB_PORT', 3306))
|
||||
->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'),
|
||||
TextInput::make('env.DB_USERNAME')
|
||||
->label('Database Username')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The name of your database user.')
|
||||
->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()
|
||||
->default(env('DB_PASSWORD'))
|
||||
->hidden(fn (Get $get) => $get('env.DB_CONNECTION') === 'sqlite'),
|
||||
])
|
||||
->afterValidation(function (Get $get) {
|
||||
$driver = $get('env.DB_CONNECTION');
|
||||
if ($driver !== 'sqlite') {
|
||||
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,
|
||||
]);
|
||||
|
||||
DB::connection('_panel_install_test')->getPdo();
|
||||
} catch (PDOException $exception) {
|
||||
Notification::make()
|
||||
->title('Database connection failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
DB::disconnect('_panel_install_test');
|
||||
|
||||
throw new Halt('Database connection failed');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
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 const CACHE_DRIVERS = [
|
||||
'file' => 'Filesystem',
|
||||
'redis' => 'Redis',
|
||||
];
|
||||
|
||||
public const SESSION_DRIVERS = [
|
||||
'file' => 'Filesystem',
|
||||
'redis' => 'Redis',
|
||||
'database' => 'Database',
|
||||
'cookie' => 'Cookie',
|
||||
];
|
||||
|
||||
public const QUEUE_DRIVERS = [
|
||||
'sync' => 'Sync',
|
||||
'database' => 'Database',
|
||||
'redis' => 'Redis',
|
||||
];
|
||||
|
||||
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.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.APP_URL')
|
||||
->label('App URL')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('This will be the URL you access your Panel from.')
|
||||
->required()
|
||||
->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 "Sync" or "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')),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use Exception;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Exceptions\Halt;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
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')),
|
||||
])
|
||||
->afterValidation(function (Get $get) {
|
||||
try {
|
||||
config()->set('database.redis._panel_install_test', [
|
||||
'host' => $get('env.REDIS_HOST'),
|
||||
'username' => $get('env.REDIS_USERNAME'),
|
||||
'password' => $get('env.REDIS_PASSWORD'),
|
||||
'port' => $get('env.REDIS_PORT'),
|
||||
]);
|
||||
|
||||
Redis::connection('_panel_install_test')->command('ping');
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Redis connection failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
throw new Halt('Redis connection failed');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Exceptions\Halt;
|
||||
|
||||
class RequirementsStep
|
||||
{
|
||||
public static function make(): Step
|
||||
{
|
||||
$correctPhpVersion = version_compare(PHP_VERSION, '8.2.0') >= 0;
|
||||
|
||||
$fields = [
|
||||
Section::make('PHP Version')
|
||||
->description('8.2 or newer')
|
||||
->icon($correctPhpVersion ? 'tabler-check' : 'tabler-x')
|
||||
->iconColor($correctPhpVersion ? 'success' : 'danger')
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->content('Your PHP Version ' . ($correctPhpVersion ? 'is' : 'needs to be') .' 8.2 or newer.'),
|
||||
]),
|
||||
];
|
||||
|
||||
$phpExtensions = [
|
||||
'BCMath' => extension_loaded('bcmath'),
|
||||
'cURL' => extension_loaded('curl'),
|
||||
'GD' => extension_loaded('gd'),
|
||||
'intl' => extension_loaded('intl'),
|
||||
'mbstring' => extension_loaded('mbstring'),
|
||||
'MySQL' => extension_loaded('pdo_mysql'),
|
||||
'SQLite3' => extension_loaded('pdo_sqlite'),
|
||||
'XML' => extension_loaded('xml'),
|
||||
'Zip' => extension_loaded('zip'),
|
||||
];
|
||||
$allExtensionsInstalled = !in_array(false, $phpExtensions);
|
||||
|
||||
$fields[] = Section::make('PHP Extensions')
|
||||
->description(implode(', ', array_keys($phpExtensions)))
|
||||
->icon($allExtensionsInstalled ? 'tabler-check' : 'tabler-x')
|
||||
->iconColor($allExtensionsInstalled ? 'success' : 'danger')
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->content('All needed PHP Extensions are installed.')
|
||||
->visible($allExtensionsInstalled),
|
||||
Placeholder::make('')
|
||||
->content('The following PHP Extensions are missing: ' . implode(', ', array_keys($phpExtensions, false)))
|
||||
->visible(!$allExtensionsInstalled),
|
||||
]);
|
||||
|
||||
$folderPermissions = [
|
||||
'Storage' => substr(sprintf('%o', fileperms(base_path('storage/'))), -4) >= 755,
|
||||
'Cache' => substr(sprintf('%o', fileperms(base_path('bootstrap/cache/'))), -4) >= 755,
|
||||
];
|
||||
$correctFolderPermissions = !in_array(false, $folderPermissions);
|
||||
|
||||
$fields[] = Section::make('Folder Permissions')
|
||||
->description(implode(', ', array_keys($folderPermissions)))
|
||||
->icon($correctFolderPermissions ? 'tabler-check' : 'tabler-x')
|
||||
->iconColor($correctFolderPermissions ? 'success' : 'danger')
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->content('All Folders have the correct permissions.')
|
||||
->visible($correctFolderPermissions),
|
||||
Placeholder::make('')
|
||||
->content('The following Folders have wrong permissions: ' . implode(', ', array_keys($folderPermissions, false)))
|
||||
->visible(!$correctFolderPermissions),
|
||||
]);
|
||||
|
||||
return Step::make('requirements')
|
||||
->label('Server Requirements')
|
||||
->schema($fields)
|
||||
->afterValidation(function () use ($correctPhpVersion, $allExtensionsInstalled, $correctFolderPermissions) {
|
||||
if (!$correctPhpVersion || !$allExtensionsInstalled || !$correctFolderPermissions) {
|
||||
Notification::make()
|
||||
->title('Some requirements are missing!')
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
throw new Halt();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,578 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use App\Models\Backup;
|
||||
use App\Notifications\MailTested;
|
||||
use App\Traits\EnvironmentWriterTrait;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\Actions\Action as FormAction;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Forms\Components\Tabs\Tab;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Concerns\HasUnsavedDataChangesAlert;
|
||||
use Filament\Pages\Concerns\InteractsWithHeaderActions;
|
||||
use Filament\Pages\Page;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Notification as MailNotification;
|
||||
|
||||
/**
|
||||
* @property Form $form
|
||||
*/
|
||||
class Settings extends Page implements HasForms
|
||||
{
|
||||
use EnvironmentWriterTrait;
|
||||
use HasUnsavedDataChangesAlert;
|
||||
use InteractsWithForms;
|
||||
use InteractsWithHeaderActions;
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-settings';
|
||||
protected static ?string $navigationGroup = 'Advanced';
|
||||
|
||||
protected static string $view = 'filament.pages.settings';
|
||||
|
||||
public ?array $data = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$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('recaptcha')
|
||||
->label('reCAPTCHA')
|
||||
->icon('tabler-shield')
|
||||
->schema($this->recaptchaSettings()),
|
||||
Tab::make('mail')
|
||||
->label('Mail')
|
||||
->icon('tabler-mail')
|
||||
->schema($this->mailSettings()),
|
||||
Tab::make('backup')
|
||||
->label('Backup')
|
||||
->icon('tabler-box')
|
||||
->schema($this->backupSettings()),
|
||||
Tab::make('misc')
|
||||
->label('Misc')
|
||||
->icon('tabler-tool')
|
||||
->schema($this->miscSettings()),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
private function generalSettings(): array
|
||||
{
|
||||
return [
|
||||
TextInput::make('APP_NAME')
|
||||
->label('App Name')
|
||||
->required()
|
||||
->alphaNum()
|
||||
->default(env('APP_NAME', 'Pelican')),
|
||||
TextInput::make('APP_FAVICON')
|
||||
->label('App Favicon')
|
||||
->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')),
|
||||
Toggle::make('APP_DEBUG')
|
||||
->label('Enable Debug Mode?')
|
||||
->inline(false)
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('APP_DEBUG', (bool) $state))
|
||||
->default(env('APP_DEBUG', config('app.debug'))),
|
||||
ToggleButtons::make('FILAMENT_TOP_NAVIGATION')
|
||||
->label('Navigation')
|
||||
->inline()
|
||||
->options([
|
||||
false => 'Sidebar',
|
||||
true => 'Topbar',
|
||||
])
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('FILAMENT_TOP_NAVIGATION', (bool) $state))
|
||||
->default(env('FILAMENT_TOP_NAVIGATION', config('panel.filament.top-navigation'))),
|
||||
ToggleButtons::make('PANEL_USE_BINARY_PREFIX')
|
||||
->label('Unit prefix')
|
||||
->inline()
|
||||
->options([
|
||||
false => 'Decimal Prefix (MB/ GB)',
|
||||
true => 'Binary Prefix (MiB/ GiB)',
|
||||
])
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_USE_BINARY_PREFIX', (bool) $state))
|
||||
->default(env('PANEL_USE_BINARY_PREFIX', config('panel.use_binary_prefix'))),
|
||||
ToggleButtons::make('APP_2FA_REQUIRED')
|
||||
->label('2FA Requirement')
|
||||
->inline()
|
||||
->options([
|
||||
0 => 'Not required',
|
||||
1 => 'Required for only Admins',
|
||||
2 => 'Required for all Users',
|
||||
])
|
||||
->formatStateUsing(fn ($state): int => (int) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('APP_2FA_REQUIRED', (int) $state))
|
||||
->default(env('APP_2FA_REQUIRED', config('panel.auth.2fa_required'))),
|
||||
TagsInput::make('TRUSTED_PROXIES')
|
||||
->label('Trusted Proxies')
|
||||
->separator()
|
||||
->splitKeys(['Tab', ' '])
|
||||
->placeholder('New IP or IP Range')
|
||||
->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(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 recaptchaSettings(): array
|
||||
{
|
||||
return [
|
||||
Toggle::make('RECAPTCHA_ENABLED')
|
||||
->label('Enable reCAPTCHA?')
|
||||
->inline(false)
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->live()
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->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('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('RECAPTCHA_ENABLED'))
|
||||
->default(env('RECAPTCHA_SECRET_KEY', config('recaptcha.secret_key'))),
|
||||
];
|
||||
}
|
||||
|
||||
private function mailSettings(): array
|
||||
{
|
||||
return [
|
||||
ToggleButtons::make('MAIL_MAILER')
|
||||
->label('Mail Driver')
|
||||
->columnSpanFull()
|
||||
->inline()
|
||||
->options([
|
||||
'log' => 'Print mails to Log',
|
||||
'smtp' => 'SMTP Server',
|
||||
'sendmail' => 'sendmail Binary',
|
||||
'mailgun' => 'Mailgun',
|
||||
'mandrill' => 'Mandrill',
|
||||
'postmark' => 'Postmark',
|
||||
])
|
||||
->live()
|
||||
->default(env('MAIL_MAILER', config('mail.default')))
|
||||
->hintAction(
|
||||
FormAction::make('test')
|
||||
->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)
|
||||
->notify(new MailTested(auth()->user()));
|
||||
|
||||
Notification::make()
|
||||
->title('Test Mail sent')
|
||||
->success()
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Test Mail failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
})
|
||||
),
|
||||
Section::make('"From" Settings')
|
||||
->description('Set the Address and Name used as "From" in mails.')
|
||||
->columns()
|
||||
->schema([
|
||||
TextInput::make('MAIL_FROM_ADDRESS')
|
||||
->label('From Address')
|
||||
->required()
|
||||
->email()
|
||||
->default(env('MAIL_FROM_ADDRESS', config('mail.from.address'))),
|
||||
TextInput::make('MAIL_FROM_NAME')
|
||||
->label('From Name')
|
||||
->required()
|
||||
->default(env('MAIL_FROM_NAME', config('mail.from.name'))),
|
||||
]),
|
||||
Section::make('SMTP Configuration')
|
||||
->columns()
|
||||
->visible(fn (Get $get) => $get('MAIL_MAILER') === 'smtp')
|
||||
->schema([
|
||||
TextInput::make('MAIL_HOST')
|
||||
->label('Host')
|
||||
->required()
|
||||
->default(env('MAIL_HOST', config('mail.mailers.smtp.host'))),
|
||||
TextInput::make('MAIL_PORT')
|
||||
->label('Port')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(env('MAIL_PORT', config('mail.mailers.smtp.port'))),
|
||||
TextInput::make('MAIL_USERNAME')
|
||||
->label('Username')
|
||||
->default(env('MAIL_USERNAME', config('mail.mailers.smtp.username'))),
|
||||
TextInput::make('MAIL_PASSWORD')
|
||||
->label('Password')
|
||||
->password()
|
||||
->revealable()
|
||||
->default(env('MAIL_PASSWORD')),
|
||||
ToggleButtons::make('MAIL_ENCRYPTION')
|
||||
->label('Encryption')
|
||||
->inline()
|
||||
->options(['tls' => 'TLS', 'ssl' => 'SSL', '' => 'None'])
|
||||
->default(env('MAIL_ENCRYPTION', config('mail.mailers.smtp.encryption', 'tls'))),
|
||||
]),
|
||||
Section::make('Mailgun Configuration')
|
||||
->columns()
|
||||
->visible(fn (Get $get) => $get('MAIL_MAILER') === 'mailgun')
|
||||
->schema([
|
||||
TextInput::make('MAILGUN_DOMAIN')
|
||||
->label('Domain')
|
||||
->required()
|
||||
->default(env('MAILGUN_DOMAIN', config('services.mailgun.domain'))),
|
||||
TextInput::make('MAILGUN_SECRET')
|
||||
->label('Secret')
|
||||
->required()
|
||||
->default(env('MAILGUN_SECRET', config('services.mailgun.secret'))),
|
||||
TextInput::make('MAILGUN_ENDPOINT')
|
||||
->label('Endpoint')
|
||||
->required()
|
||||
->default(env('MAILGUN_ENDPOINT', config('services.mailgun.endpoint'))),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
private function backupSettings(): array
|
||||
{
|
||||
return [
|
||||
ToggleButtons::make('APP_BACKUP_DRIVER')
|
||||
->label('Backup Driver')
|
||||
->columnSpanFull()
|
||||
->inline()
|
||||
->options([
|
||||
Backup::ADAPTER_DAEMON => 'Wings',
|
||||
Backup::ADAPTER_AWS_S3 => 'S3',
|
||||
])
|
||||
->live()
|
||||
->default(env('APP_BACKUP_DRIVER', config('backups.default'))),
|
||||
Section::make('Throttles')
|
||||
->description('Configure how many backups can be created in a period. Set period to 0 to disable this throttle.')
|
||||
->columns()
|
||||
->schema([
|
||||
TextInput::make('BACKUP_THROTTLE_LIMIT')
|
||||
->label('Limit')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->default(config('backups.throttles.limit')),
|
||||
TextInput::make('BACKUP_THROTTLE_PERIOD')
|
||||
->label('Period')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->suffix('Seconds')
|
||||
->default(config('backups.throttles.period')),
|
||||
]),
|
||||
Section::make('S3 Configuration')
|
||||
->columns()
|
||||
->visible(fn (Get $get) => $get('APP_BACKUP_DRIVER') === Backup::ADAPTER_AWS_S3)
|
||||
->schema([
|
||||
TextInput::make('AWS_DEFAULT_REGION')
|
||||
->label('Default Region')
|
||||
->required()
|
||||
->default(config('backups.disks.s3.region')),
|
||||
TextInput::make('AWS_ACCESS_KEY_ID')
|
||||
->label('Access Key ID')
|
||||
->required()
|
||||
->default(config('backups.disks.s3.key')),
|
||||
TextInput::make('AWS_SECRET_ACCESS_KEY')
|
||||
->label('Secret Access Key')
|
||||
->required()
|
||||
->default(config('backups.disks.s3.secret')),
|
||||
TextInput::make('AWS_BACKUPS_BUCKET')
|
||||
->label('Bucket')
|
||||
->required()
|
||||
->default(config('backups.disks.s3.bucket')),
|
||||
TextInput::make('AWS_ENDPOINT')
|
||||
->label('Endpoint')
|
||||
->required()
|
||||
->default(config('backups.disks.s3.endpoint')),
|
||||
Toggle::make('AWS_USE_PATH_STYLE_ENDPOINT')
|
||||
->label('Use path style endpoint?')
|
||||
->inline(false)
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->live()
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('AWS_USE_PATH_STYLE_ENDPOINT', (bool) $state))
|
||||
->default(env('AWS_USE_PATH_STYLE_ENDPOINT', config('backups.disks.s3.use_path_style_endpoint'))),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
private function miscSettings(): array
|
||||
{
|
||||
return [
|
||||
Section::make('Automatic Allocation Creation')
|
||||
->description('Toggle if Users can create allocations via the client area.')
|
||||
->columns()
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
Toggle::make('PANEL_CLIENT_ALLOCATIONS_ENABLED')
|
||||
->label('Allow Users to create allocations?')
|
||||
->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_CLIENT_ALLOCATIONS_ENABLED', (bool) $state))
|
||||
->default(env('PANEL_CLIENT_ALLOCATIONS_ENABLED', config('panel.client_features.allocations.enabled'))),
|
||||
TextInput::make('PANEL_CLIENT_ALLOCATIONS_RANGE_START')
|
||||
->label('Starting Port')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1024)
|
||||
->maxValue(65535)
|
||||
->visible(fn (Get $get) => $get('PANEL_CLIENT_ALLOCATIONS_ENABLED'))
|
||||
->default(env('PANEL_CLIENT_ALLOCATIONS_RANGE_START')),
|
||||
TextInput::make('PANEL_CLIENT_ALLOCATIONS_RANGE_END')
|
||||
->label('Ending Port')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1024)
|
||||
->maxValue(65535)
|
||||
->visible(fn (Get $get) => $get('PANEL_CLIENT_ALLOCATIONS_ENABLED'))
|
||||
->default(env('PANEL_CLIENT_ALLOCATIONS_RANGE_END')),
|
||||
]),
|
||||
Section::make('Mail Notifications')
|
||||
->description('Toggle which mail notifications should be sent to Users.')
|
||||
->columns()
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
Toggle::make('PANEL_SEND_INSTALL_NOTIFICATION')
|
||||
->label('Server Installed')
|
||||
->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_SEND_INSTALL_NOTIFICATION', (bool) $state))
|
||||
->default(env('PANEL_SEND_INSTALL_NOTIFICATION', config('panel.email.send_install_notification'))),
|
||||
Toggle::make('PANEL_SEND_REINSTALL_NOTIFICATION')
|
||||
->label('Server Reinstalled')
|
||||
->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_SEND_REINSTALL_NOTIFICATION', (bool) $state))
|
||||
->default(env('PANEL_SEND_REINSTALL_NOTIFICATION', config('panel.email.send_reinstall_notification'))),
|
||||
]),
|
||||
Section::make('Connections')
|
||||
->description('Timeouts used when making requests.')
|
||||
->columns()
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
TextInput::make('GUZZLE_TIMEOUT')
|
||||
->label('Request Timeout')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(15)
|
||||
->maxValue(60)
|
||||
->suffix('Seconds')
|
||||
->default(env('GUZZLE_TIMEOUT', config('panel.guzzle.timeout'))),
|
||||
TextInput::make('GUZZLE_CONNECT_TIMEOUT')
|
||||
->label('Connect Timeout')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(5)
|
||||
->maxValue(60)
|
||||
->suffix('Seconds')
|
||||
->default(env('GUZZLE_CONNECT_TIMEOUT', config('panel.guzzle.connect_timeout'))),
|
||||
]),
|
||||
Section::make('Activity Logs')
|
||||
->description('Configure how often old activity logs should be pruned and whether admin activities should be logged.')
|
||||
->columns()
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
TextInput::make('APP_ACTIVITY_PRUNE_DAYS')
|
||||
->label('Prune age')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(365)
|
||||
->suffix('Days')
|
||||
->default(env('APP_ACTIVITY_PRUNE_DAYS', config('activity.prune_days'))),
|
||||
Toggle::make('APP_ACTIVITY_HIDE_ADMIN')
|
||||
->label('Hide admin activities?')
|
||||
->inline(false)
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->live()
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('APP_ACTIVITY_HIDE_ADMIN', (bool) $state))
|
||||
->default(env('APP_ACTIVITY_HIDE_ADMIN', config('activity.hide_admin_activity'))),
|
||||
]),
|
||||
Section::make('API')
|
||||
->description('Defines the rate limit for the number of requests per minute that can be executed.')
|
||||
->columns()
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
TextInput::make('APP_API_CLIENT_RATELIMIT')
|
||||
->label('Client API Rate Limit')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->suffix('Requests Per Minute')
|
||||
->default(env('APP_API_CLIENT_RATELIMIT', config('http.rate_limit.client'))),
|
||||
TextInput::make('APP_API_APPLICATION_RATELIMIT')
|
||||
->label('Application API Rate Limit')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->suffix('Requests Per Minute')
|
||||
->default(env('APP_API_APPLICATION_RATELIMIT', config('http.rate_limit.application'))),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getFormStatePath(): ?string
|
||||
{
|
||||
return 'data';
|
||||
}
|
||||
|
||||
protected function hasUnsavedDataChangesAlert(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
try {
|
||||
$data = $this->form->getState();
|
||||
|
||||
// Convert bools to a string, so they are correctly written to the .env file
|
||||
$data = array_map(fn ($value) => is_bool($value) ? ($value ? 'true' : 'false') : $value, $data);
|
||||
|
||||
$this->writeToEnvironment($data);
|
||||
|
||||
Artisan::call('config:clear');
|
||||
Artisan::call('queue:restart');
|
||||
|
||||
$this->rememberData();
|
||||
|
||||
$this->redirect($this->getUrl());
|
||||
|
||||
Notification::make()
|
||||
->title('Settings saved')
|
||||
->success()
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Save failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('save')
|
||||
->action('save')
|
||||
->authorize(fn () => auth()->user()->can('update settings'))
|
||||
->keyBindings(['mod+s']),
|
||||
];
|
||||
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,8 @@ class EditDatabaseHost extends EditRecord
|
||||
->helperText('The password for the database user.')
|
||||
->password()
|
||||
->revealable()
|
||||
->maxLength(255),
|
||||
->maxLength(255)
|
||||
->required(),
|
||||
Select::make('node_id')
|
||||
->searchable()
|
||||
->preload()
|
||||
|
||||
@@ -42,8 +42,7 @@ class ListDatabaseHosts extends ListRecords
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete databasehost')),
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -74,10 +74,9 @@ class CreateEgg extends CreateRecord
|
||||
->helperText('')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
TextInput::make('update_url')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('URLs must point directly to the raw .json file.')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->url(),
|
||||
->disabled()
|
||||
->helperText('Not implemented.')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
KeyValue::make('docker_images')
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
@@ -134,7 +133,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'] ??= '';
|
||||
|
||||
@@ -143,7 +142,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'] ??= '';
|
||||
|
||||
@@ -173,30 +172,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')
|
||||
@@ -210,7 +186,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)
|
||||
|
||||
@@ -2,15 +2,12 @@
|
||||
|
||||
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;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
@@ -25,9 +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\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
|
||||
{
|
||||
@@ -91,10 +91,8 @@ class EditEgg extends EditRecord
|
||||
->helperText('')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
TextInput::make('update_url')
|
||||
->label('Update URL')
|
||||
->url()
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('URLs must point directly to the raw .json file.')
|
||||
->disabled()
|
||||
->helperText('Not implemented.')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
KeyValue::make('docker_images')
|
||||
->live()
|
||||
@@ -144,7 +142,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'] ??= '';
|
||||
|
||||
@@ -153,7 +151,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'] ??= '';
|
||||
|
||||
@@ -183,30 +181,7 @@ 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')
|
||||
@@ -245,13 +220,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([
|
||||
@@ -273,7 +249,6 @@ class EditEgg extends EditRecord
|
||||
->schema([
|
||||
TextInput::make('url')
|
||||
->label('URL')
|
||||
->default(fn (Egg $egg): ?string => $egg->update_url)
|
||||
->hint('Link to the egg file (eg. minecraft.json)')
|
||||
->url(),
|
||||
]),
|
||||
@@ -292,14 +267,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) {
|
||||
@@ -320,8 +297,8 @@ class EditEgg extends EditRecord
|
||||
->title('Import Success')
|
||||
->success()
|
||||
->send();
|
||||
})
|
||||
->authorize(fn () => auth()->user()->can('import egg')),
|
||||
}),
|
||||
|
||||
$this->getSaveFormAction()->formId('form'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
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
|
||||
{
|
||||
@@ -55,13 +55,11 @@ class ListEggs extends ListRecords
|
||||
->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')),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete egg')),
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
@@ -140,8 +138,7 @@ class ListEggs extends ListRecords
|
||||
->title('Import Success')
|
||||
->success()
|
||||
->send();
|
||||
})
|
||||
->authorize(fn () => auth()->user()->can('import egg')),
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,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')
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
namespace App\Filament\Resources\NodeResource\Pages;
|
||||
|
||||
use App\Filament\Resources\NodeResource;
|
||||
use App\Filament\Resources\NodeResource\Widgets\NodeMemoryChart;
|
||||
use App\Filament\Resources\NodeResource\Widgets\NodeStorageChart;
|
||||
use App\Models\Node;
|
||||
use App\Services\Nodes\NodeUpdateService;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
@@ -16,7 +17,6 @@ use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Components\View;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
@@ -41,32 +41,6 @@ class EditNode extends EditRecord
|
||||
->persistTabInQueryString()
|
||||
->columnSpanFull()
|
||||
->tabs([
|
||||
Tab::make('')
|
||||
->label('Overview')
|
||||
->icon('tabler-chart-area-line-filled')
|
||||
->columns(6)
|
||||
->schema([
|
||||
Fieldset::make()
|
||||
->label('Node Information')
|
||||
->columns(4)
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->label('Wings Version')
|
||||
->content(fn (Node $node) => $node->systemInformation()['version'] ?? 'Unknown'),
|
||||
Placeholder::make('')
|
||||
->label('CPU Threads')
|
||||
->content(fn (Node $node) => $node->systemInformation()['cpu_count'] ?? 0),
|
||||
Placeholder::make('')
|
||||
->label('Architecture')
|
||||
->content(fn (Node $node) => $node->systemInformation()['architecture'] ?? 'Unknown'),
|
||||
Placeholder::make('')
|
||||
->label('Kernel')
|
||||
->content(fn (Node $node) => $node->systemInformation()['kernel_version'] ?? 'Unknown'),
|
||||
]),
|
||||
View::make('filament.components.node-cpu-chart')->columnSpan(3),
|
||||
View::make('filament.components.node-memory-chart')->columnSpan(3),
|
||||
// TODO: Make purdy View::make('filament.components.node-storage-chart')->columnSpan(3),
|
||||
]),
|
||||
Tab::make('Basic Settings')
|
||||
->icon('tabler-server')
|
||||
->schema([
|
||||
@@ -463,17 +437,16 @@ class EditNode extends EditRecord
|
||||
];
|
||||
}
|
||||
|
||||
protected function getFooterWidgets(): array
|
||||
{
|
||||
return [
|
||||
NodeStorageChart::class,
|
||||
NodeMemoryChart::class,
|
||||
];
|
||||
}
|
||||
|
||||
protected function afterSave(): void
|
||||
{
|
||||
$this->fillForm();
|
||||
}
|
||||
|
||||
protected function getColumnSpan()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
protected function getColumnStart()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,8 +84,7 @@ class ListNodes extends ListRecords
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete node')),
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
])
|
||||
->emptyStateIcon('tabler-server-2')
|
||||
|
||||
@@ -7,12 +7,12 @@ use App\Models\Node;
|
||||
use App\Services\Allocations\AssignmentService;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\TextInputColumn;
|
||||
use Filament\Tables\Table;
|
||||
@@ -152,8 +152,7 @@ class AllocationsRelationManager extends RelationManager
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete allocation')),
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\NodeResource\Widgets;
|
||||
|
||||
use App\Models\Node;
|
||||
use Carbon\Carbon;
|
||||
use Filament\Support\RawJs;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class NodeCpuChart extends ChartWidget
|
||||
{
|
||||
protected static ?string $pollingInterval = '5s';
|
||||
protected static ?string $maxHeight = '300px';
|
||||
|
||||
public ?Model $record = null;
|
||||
|
||||
protected function getData(): array
|
||||
{
|
||||
/** @var Node $node */
|
||||
$node = $this->record;
|
||||
$threads = $node->systemInformation()['cpu_count'] ?? 0;
|
||||
|
||||
$cpu = collect(cache()->get("nodes.$node->id.cpu_percent"))
|
||||
->slice(-10)
|
||||
->map(fn ($value, $key) => [
|
||||
'cpu' => number_format($value * $threads, 2),
|
||||
'timestamp' => Carbon::createFromTimestamp($key, (auth()->user()->timezone ?? 'UTC'))->format('H:i:s'),
|
||||
])
|
||||
->all();
|
||||
|
||||
return [
|
||||
'datasets' => [
|
||||
[
|
||||
'data' => array_column($cpu, 'cpu'),
|
||||
'backgroundColor' => [
|
||||
'rgba(96, 165, 250, 0.3)',
|
||||
],
|
||||
'tension' => '0.3',
|
||||
'fill' => true,
|
||||
],
|
||||
],
|
||||
'labels' => array_column($cpu, 'timestamp'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getType(): string
|
||||
{
|
||||
return 'line';
|
||||
}
|
||||
|
||||
protected function getOptions(): RawJs
|
||||
{
|
||||
return RawJs::make(<<<'JS'
|
||||
{
|
||||
scales: {
|
||||
y: {
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
JS);
|
||||
}
|
||||
|
||||
public function getHeading(): string
|
||||
{
|
||||
/** @var Node $node */
|
||||
$node = $this->record;
|
||||
$threads = $node->systemInformation()['cpu_count'] ?? 0;
|
||||
|
||||
$cpu = number_format(collect(cache()->get("nodes.$node->id.cpu_percent"))->last() * $threads, 2);
|
||||
$max = number_format($threads * 100) . '%';
|
||||
|
||||
return 'CPU - ' . $cpu . '% Of ' . $max;
|
||||
}
|
||||
}
|
||||
@@ -3,83 +3,66 @@
|
||||
namespace App\Filament\Resources\NodeResource\Widgets;
|
||||
|
||||
use App\Models\Node;
|
||||
use Carbon\Carbon;
|
||||
use Filament\Support\RawJs;
|
||||
use Filament\Widgets\ChartWidget;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class NodeMemoryChart extends ChartWidget
|
||||
{
|
||||
protected static ?string $pollingInterval = '5s';
|
||||
protected static ?string $maxHeight = '300px';
|
||||
protected static ?string $heading = 'Memory';
|
||||
|
||||
protected static ?string $pollingInterval = '60s';
|
||||
|
||||
public ?Model $record = null;
|
||||
|
||||
protected static ?array $options = [
|
||||
'scales' => [
|
||||
'x' => [
|
||||
'grid' => [
|
||||
'display' => false,
|
||||
],
|
||||
'ticks' => [
|
||||
'display' => false,
|
||||
],
|
||||
],
|
||||
'y' => [
|
||||
'grid' => [
|
||||
'display' => false,
|
||||
],
|
||||
'ticks' => [
|
||||
'display' => false,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
protected function getData(): array
|
||||
{
|
||||
/** @var Node $node */
|
||||
$node = $this->record;
|
||||
|
||||
$memUsed = collect(cache()->get("nodes.$node->id.memory_used"))->slice(-10)
|
||||
->map(fn ($value, $key) => [
|
||||
'memory' => config('panel.use_binary_prefix') ? $value / 1024 / 1024 / 1024 : $value / 1000 / 1000 / 1000,
|
||||
'timestamp' => Carbon::createFromTimestamp($key, (auth()->user()->timezone ?? 'UTC'))->format('H:i:s'),
|
||||
])
|
||||
->all();
|
||||
$total = ($node->statistics()['memory_total'] ?? 0) / 1024 / 1024 / 1024;
|
||||
$used = ($node->statistics()['memory_used'] ?? 0) / 1024 / 1024 / 1024;
|
||||
$unused = $total - $used;
|
||||
|
||||
return [
|
||||
'datasets' => [
|
||||
[
|
||||
'data' => array_column($memUsed, 'memory'),
|
||||
'label' => 'Data Cool',
|
||||
'data' => [$used, $unused],
|
||||
'backgroundColor' => [
|
||||
'rgba(96, 165, 250, 0.3)',
|
||||
'rgb(255, 99, 132)',
|
||||
'rgb(54, 162, 235)',
|
||||
'rgb(255, 205, 86)',
|
||||
],
|
||||
'tension' => '0.3',
|
||||
'fill' => true,
|
||||
],
|
||||
// 'backgroundColor' => [],
|
||||
],
|
||||
'labels' => array_column($memUsed, 'timestamp'),
|
||||
'labels' => ['Used', 'Unused'],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getType(): string
|
||||
{
|
||||
return 'line';
|
||||
}
|
||||
|
||||
protected function getOptions(): RawJs
|
||||
{
|
||||
return RawJs::make(<<<'JS'
|
||||
{
|
||||
scales: {
|
||||
y: {
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
JS);
|
||||
}
|
||||
|
||||
public function getHeading(): string
|
||||
{
|
||||
/** @var Node $node */
|
||||
$node = $this->record;
|
||||
$latestMemoryUsed = collect(cache()->get("nodes.$node->id.memory_used"))->last();
|
||||
$totalMemory = collect(cache()->get("nodes.$node->id.memory_total"))->last();
|
||||
|
||||
$used = config('panel.use_binary_prefix')
|
||||
? number_format($latestMemoryUsed / 1024 / 1024 / 1024, 2) .' GiB'
|
||||
: number_format($latestMemoryUsed / 1000 / 1000 / 1000, 2) . ' GB';
|
||||
|
||||
$total = config('panel.use_binary_prefix')
|
||||
? number_format($totalMemory / 1024 / 1024 / 1024, 2) .' GiB'
|
||||
: number_format($totalMemory / 1000 / 1000 / 1000, 2) . ' GB';
|
||||
|
||||
return 'Memory - ' . $used . ' Of ' . $total;
|
||||
return 'pie';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ use Illuminate\Database\Eloquent\Model;
|
||||
class NodeStorageChart extends ChartWidget
|
||||
{
|
||||
protected static ?string $heading = 'Storage';
|
||||
|
||||
protected static ?string $pollingInterval = '60s';
|
||||
protected static ?string $maxHeight = '300px';
|
||||
|
||||
public ?Model $record = null;
|
||||
|
||||
@@ -47,6 +47,7 @@ class NodeStorageChart extends ChartWidget
|
||||
return [
|
||||
'datasets' => [
|
||||
[
|
||||
'label' => 'Data Cool',
|
||||
'data' => [$used, $unused],
|
||||
'backgroundColor' => [
|
||||
'rgb(255, 99, 132)',
|
||||
@@ -54,6 +55,7 @@ class NodeStorageChart extends ChartWidget
|
||||
'rgb(255, 205, 86)',
|
||||
],
|
||||
],
|
||||
// 'backgroundColor' => [],
|
||||
],
|
||||
'labels' => ['Used', 'Unused'],
|
||||
];
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Enums\RolePermissionModels;
|
||||
use App\Enums\RolePermissionPrefixes;
|
||||
use App\Filament\Resources\RoleResource\Pages;
|
||||
use App\Models\Role;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\Component;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Resources\Resource;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class RoleResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Role::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-users-group';
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'name';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::count() ?: null;
|
||||
}
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
$permissions = [];
|
||||
|
||||
foreach (RolePermissionModels::cases() as $model) {
|
||||
$options = [];
|
||||
|
||||
foreach (RolePermissionPrefixes::cases() as $prefix) {
|
||||
$options[$prefix->value . ' ' . strtolower($model->value)] = Str::headline($prefix->value);
|
||||
}
|
||||
|
||||
if (array_key_exists($model->value, Role::MODEL_SPECIFIC_PERMISSIONS)) {
|
||||
foreach (Role::MODEL_SPECIFIC_PERMISSIONS[$model->value] as $permission) {
|
||||
$options[$permission . ' ' . strtolower($model->value)] = Str::headline($permission);
|
||||
}
|
||||
}
|
||||
|
||||
$permissions[] = self::makeSection($model->value, $options);
|
||||
}
|
||||
|
||||
foreach (Role::SPECIAL_PERMISSIONS as $model => $prefixes) {
|
||||
$options = [];
|
||||
|
||||
foreach ($prefixes as $prefix) {
|
||||
$options[$prefix . ' ' . strtolower($model)] = Str::headline($prefix);
|
||||
}
|
||||
|
||||
$permissions[] = self::makeSection($model, $options);
|
||||
}
|
||||
|
||||
return $form
|
||||
->columns(1)
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label('Role Name')
|
||||
->required()
|
||||
->disabled(fn (Get $get) => $get('name') === Role::ROOT_ADMIN),
|
||||
TextInput::make('guard_name')
|
||||
->label('Guard Name')
|
||||
->default(Filament::getCurrentPanel()?->getAuthGuard() ?? '')
|
||||
->nullable()
|
||||
->hidden(),
|
||||
Fieldset::make('Permissions')
|
||||
->columns(3)
|
||||
->schema($permissions)
|
||||
->hidden(fn (Get $get) => $get('name') === Role::ROOT_ADMIN),
|
||||
Placeholder::make('permissions')
|
||||
->content('The Root Admin has all permissions.')
|
||||
->visible(fn (Get $get) => $get('name') === Role::ROOT_ADMIN),
|
||||
]);
|
||||
}
|
||||
|
||||
private static function makeSection(string $model, array $options): Section
|
||||
{
|
||||
$icon = null;
|
||||
|
||||
if (class_exists('\App\Filament\Resources\\' . $model . 'Resource')) {
|
||||
$icon = ('\App\Filament\Resources\\' . $model . 'Resource')::getNavigationIcon();
|
||||
} elseif (class_exists('\App\Filament\Pages\\' . $model)) {
|
||||
$icon = ('\App\Filament\Pages\\' . $model)::getNavigationIcon();
|
||||
}
|
||||
|
||||
return Section::make(Str::headline(Str::plural($model)))
|
||||
->columnSpan(1)
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->icon($icon)
|
||||
->headerActions([
|
||||
Action::make('count')
|
||||
->label(fn (Get $get) => count($get(strtolower($model) . '_list')))
|
||||
->badge(),
|
||||
])
|
||||
->schema([
|
||||
CheckboxList::make(strtolower($model) . '_list')
|
||||
->label('')
|
||||
->options($options)
|
||||
->columns()
|
||||
->gridDirection('row')
|
||||
->bulkToggleable()
|
||||
->live()
|
||||
->afterStateHydrated(
|
||||
function (Component $component, string $operation, ?Role $record) use ($options) {
|
||||
if (in_array($operation, ['edit', 'view'])) {
|
||||
|
||||
if (blank($record)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($component->isVisible()) {
|
||||
$component->state(
|
||||
collect($options)
|
||||
->filter(fn ($value, $key) => $record->checkPermissionTo($key))
|
||||
->keys()
|
||||
->toArray()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
->dehydrated(fn ($state) => !blank($state)),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListRoles::route('/'),
|
||||
'create' => Pages\CreateRole::route('/create'),
|
||||
'edit' => Pages\EditRole::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\RoleResource\Pages;
|
||||
|
||||
use App\Filament\Resources\RoleResource;
|
||||
use App\Models\Role;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
/**
|
||||
* @property Role $record
|
||||
*/
|
||||
class CreateRole extends CreateRecord
|
||||
{
|
||||
protected static string $resource = RoleResource::class;
|
||||
|
||||
protected static bool $canCreateAnother = false;
|
||||
|
||||
public Collection $permissions;
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
$this->permissions = collect($data)
|
||||
->filter(function ($permission, $key) {
|
||||
return !in_array($key, ['name', 'guard_name']);
|
||||
})
|
||||
->values()
|
||||
->flatten()
|
||||
->unique();
|
||||
|
||||
return Arr::only($data, ['name', 'guard_name']);
|
||||
}
|
||||
|
||||
protected function afterCreate(): void
|
||||
{
|
||||
$permissionModels = collect();
|
||||
$this->permissions->each(function ($permission) use ($permissionModels) {
|
||||
$permissionModels->push(Permission::firstOrCreate([
|
||||
'name' => $permission,
|
||||
'guard_name' => $this->data['guard_name'],
|
||||
]));
|
||||
});
|
||||
|
||||
$this->record->syncPermissions($permissionModels);
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\RoleResource\Pages;
|
||||
|
||||
use App\Filament\Resources\RoleResource;
|
||||
use App\Models\Role;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
/**
|
||||
* @property Role $record
|
||||
*/
|
||||
class EditRole extends EditRecord
|
||||
{
|
||||
protected static string $resource = RoleResource::class;
|
||||
|
||||
public Collection $permissions;
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
$this->permissions = collect($data)
|
||||
->filter(function ($permission, $key) {
|
||||
return !in_array($key, ['name', 'guard_name']);
|
||||
})
|
||||
->values()
|
||||
->flatten()
|
||||
->unique();
|
||||
|
||||
return Arr::only($data, ['name', 'guard_name']);
|
||||
}
|
||||
|
||||
protected function afterSave(): void
|
||||
{
|
||||
$permissionModels = collect();
|
||||
$this->permissions->each(function ($permission) use ($permissionModels) {
|
||||
$permissionModels->push(Permission::firstOrCreate([
|
||||
'name' => $permission,
|
||||
'guard_name' => $this->data['guard_name'],
|
||||
]));
|
||||
});
|
||||
|
||||
$this->record->syncPermissions($permissionModels);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make()
|
||||
->disabled(fn (Role $role) => $role->isRootAdmin() || $role->users_count >= 1)
|
||||
->label(fn (Role $role) => $role->isRootAdmin() ? 'Can\'t delete Root Admin' : ($role->users_count >= 1 ? 'In Use' : 'Delete')),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\RoleResource\Pages;
|
||||
|
||||
use App\Filament\Resources\RoleResource;
|
||||
use App\Models\Role;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\CreateAction as CreateActionTable;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ListRoles extends ListRecords
|
||||
{
|
||||
protected static string $resource = RoleResource::class;
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->sortable()
|
||||
->searchable(),
|
||||
TextColumn::make('guard_name')
|
||||
->hidden()
|
||||
->sortable()
|
||||
->searchable(),
|
||||
TextColumn::make('permissions_count')
|
||||
->label('Permissions')
|
||||
->badge()
|
||||
->counts('permissions')
|
||||
->formatStateUsing(fn (Role $role, $state) => $role->isRootAdmin() ? 'All' : $state),
|
||||
TextColumn::make('users_count')
|
||||
->label('Users')
|
||||
->counts('users')
|
||||
->icon('tabler-users'),
|
||||
])
|
||||
->actions([
|
||||
EditAction::make(),
|
||||
])
|
||||
->checkIfRecordIsSelectableUsing(fn (Role $role) => !$role->isRootAdmin() && $role->users_count <= 0)
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete role')),
|
||||
]),
|
||||
])
|
||||
->emptyStateIcon('tabler-users-group')
|
||||
->emptyStateDescription('')
|
||||
->emptyStateHeading('No Roles')
|
||||
->emptyStateActions([
|
||||
CreateActionTable::make('create')
|
||||
->label('Create Role')
|
||||
->button(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make()
|
||||
->label('Create Role'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -40,8 +40,8 @@ class CreateServer extends CreateRecord
|
||||
->icon('tabler-info-circle')
|
||||
->completedIcon('tabler-check')
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'default' => 2,
|
||||
'sm' => 2,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
@@ -61,7 +61,7 @@ class CreateServer extends CreateRecord
|
||||
}))
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'sm' => 4,
|
||||
'md' => 2,
|
||||
'lg' => 3,
|
||||
])
|
||||
@@ -75,13 +75,13 @@ class CreateServer extends CreateRecord
|
||||
->label('Owner')
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 3,
|
||||
'sm' => 4,
|
||||
'md' => 2,
|
||||
'lg' => 3,
|
||||
])
|
||||
->relationship('user', 'username')
|
||||
->searchable(['username', 'email'])
|
||||
->getOptionLabelFromRecordUsing(fn (User $user) => "$user->email | $user->username " . ($user->isRootAdmin() ? '(admin)' : ''))
|
||||
->searchable(['user', 'username', 'email'])
|
||||
->getOptionLabelFromRecordUsing(fn (User $user) => "$user->email | $user->username " . ($user->root_admin ? '(admin)' : ''))
|
||||
->createOptionForm([
|
||||
Forms\Components\TextInput::make('username')
|
||||
->alphaNum()
|
||||
@@ -98,6 +98,21 @@ class CreateServer extends CreateRecord
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('Providing a user password is optional. New user email will prompt users to create a password the first time they login.')
|
||||
->password(),
|
||||
|
||||
Forms\Components\ToggleButtons::make('root_admin')
|
||||
->label('Administrator (Root)')
|
||||
->options([
|
||||
false => 'No',
|
||||
true => 'Admin',
|
||||
])
|
||||
->colors([
|
||||
false => 'primary',
|
||||
true => 'danger',
|
||||
])
|
||||
->inline()
|
||||
->required()
|
||||
->default(false)
|
||||
->hidden(),
|
||||
])
|
||||
->createOptionUsing(function ($data) {
|
||||
resolve(UserCreationService::class)->handle($data);
|
||||
@@ -110,10 +125,10 @@ class CreateServer extends CreateRecord
|
||||
->prefixIcon('tabler-server-2')
|
||||
->default(fn () => ($this->node = Node::query()->latest()->first())?->id)
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 6,
|
||||
'lg' => 6,
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 2,
|
||||
'lg' => 2,
|
||||
])
|
||||
->live()
|
||||
->relationship('node', 'name')
|
||||
@@ -131,10 +146,10 @@ class CreateServer extends CreateRecord
|
||||
->prefixIcon('tabler-network')
|
||||
->label('Primary Allocation')
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 2,
|
||||
'lg' => 3,
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 1,
|
||||
'lg' => 2,
|
||||
])
|
||||
->disabled(fn (Forms\Get $get) => $get('node_id') === null)
|
||||
->searchable(['ip', 'port', 'ip_alias'])
|
||||
@@ -218,9 +233,7 @@ class CreateServer extends CreateRecord
|
||||
$end = min((int) $end, 2 ** 16 - 1);
|
||||
$range = $start <= $end ? range($start, $end) : range($end, $start);
|
||||
foreach ($range as $i) {
|
||||
if ($i > 1024 && $i <= 65535) {
|
||||
$ports->push($i);
|
||||
}
|
||||
$ports->push($i);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,6 +249,8 @@ class CreateServer extends CreateRecord
|
||||
$ports = $sortedPorts;
|
||||
}
|
||||
|
||||
$ports = $ports->filter(fn ($port) => $port > 1024 && $port < 65535)->values();
|
||||
|
||||
if ($update) {
|
||||
$set('allocation_ports', $ports->all());
|
||||
}
|
||||
@@ -253,10 +268,10 @@ class CreateServer extends CreateRecord
|
||||
Forms\Components\Repeater::make('allocation_additional')
|
||||
->label('Additional Allocations')
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 3,
|
||||
'lg' => 3,
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 1,
|
||||
'lg' => 2,
|
||||
])
|
||||
->addActionLabel('Add Allocation')
|
||||
->disabled(fn (Forms\Get $get) => $get('allocation_id') === null)
|
||||
@@ -288,13 +303,12 @@ class CreateServer extends CreateRecord
|
||||
),
|
||||
),
|
||||
|
||||
Forms\Components\Textarea::make('description')
|
||||
Forms\Components\TextInput::make('description')
|
||||
->placeholder('Description')
|
||||
->rows(3)
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 6,
|
||||
'md' => 6,
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 2,
|
||||
'lg' => 6,
|
||||
])
|
||||
->label('Notes'),
|
||||
@@ -306,9 +320,9 @@ class CreateServer extends CreateRecord
|
||||
->completedIcon('tabler-check')
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 4,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
'sm' => 2,
|
||||
'md' => 2,
|
||||
'lg' => 4,
|
||||
])
|
||||
->schema([
|
||||
Forms\Components\Select::make('egg_id')
|
||||
@@ -318,7 +332,7 @@ class CreateServer extends CreateRecord
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 2,
|
||||
'lg' => 4,
|
||||
'lg' => 3,
|
||||
])
|
||||
->searchable()
|
||||
->preload()
|
||||
@@ -375,51 +389,29 @@ class CreateServer extends CreateRecord
|
||||
->inline()
|
||||
->required(),
|
||||
|
||||
Forms\Components\ToggleButtons::make('start_on_completion')
|
||||
->label('Start Server After Install?')
|
||||
->default(true)
|
||||
->required()
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
])
|
||||
->options([
|
||||
true => 'Yes',
|
||||
false => 'No',
|
||||
])
|
||||
->colors([
|
||||
true => 'primary',
|
||||
false => 'danger',
|
||||
])
|
||||
->icons([
|
||||
true => 'tabler-code',
|
||||
false => 'tabler-code-off',
|
||||
])
|
||||
->inline(),
|
||||
|
||||
Forms\Components\Textarea::make('startup')
|
||||
->hintIcon('tabler-code')
|
||||
->label('Startup Command')
|
||||
->hidden(fn (Forms\Get $get) => $get('egg_id') === null)
|
||||
->required()
|
||||
->live()
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 2,
|
||||
'lg' => 4,
|
||||
])
|
||||
->rows(function ($state) {
|
||||
return str($state)->explode("\n")->reduce(
|
||||
fn (int $carry, $line) => $carry + floor(strlen($line) / 125),
|
||||
1
|
||||
);
|
||||
})
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 4,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
]),
|
||||
}),
|
||||
|
||||
Forms\Components\Hidden::make('environment')->default([]),
|
||||
|
||||
Forms\Components\Hidden::make('start_on_completion')->default(true),
|
||||
|
||||
Forms\Components\Section::make('Variables')
|
||||
->icon('tabler-eggs')
|
||||
->iconColor('primary')
|
||||
@@ -450,7 +442,8 @@ class CreateServer extends CreateRecord
|
||||
|
||||
$text = Forms\Components\TextInput::make('variable_value')
|
||||
->hidden($this->shouldHideComponent(...))
|
||||
->required(fn (Forms\Get $get) => in_array('required', $get('rules')))
|
||||
->maxLength(255)
|
||||
->required(fn (Forms\Get $get) => in_array('required', explode('|', $get('rules'))))
|
||||
->rules(
|
||||
fn (Forms\Get $get): Closure => function (string $attribute, $value, Closure $fail) use ($get) {
|
||||
$validator = Validator::make(['validatorkey' => $value], [
|
||||
@@ -477,7 +470,7 @@ class CreateServer extends CreateRecord
|
||||
->live(onBlur: true)
|
||||
->hintIcon('tabler-code')
|
||||
->label(fn (Forms\Get $get) => $get('name'))
|
||||
->hintIconTooltip(fn (Forms\Get $get) => implode('|', $get('rules')))
|
||||
->hintIconTooltip(fn (Forms\Get $get) => $get('rules'))
|
||||
->prefix(fn (Forms\Get $get) => '{{' . $get('env_variable') . '}}')
|
||||
->helperText(fn (Forms\Get $get) => empty($get('description')) ? '—' : $get('description'))
|
||||
->afterStateUpdated(function (Forms\Set $set, Forms\Get $get, $state) {
|
||||
@@ -498,7 +491,12 @@ class CreateServer extends CreateRecord
|
||||
->completedIcon('tabler-check')
|
||||
->schema([
|
||||
Forms\Components\Fieldset::make('Resource Limits')
|
||||
->columnSpan(6)
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 4,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
@@ -678,7 +676,12 @@ class CreateServer extends CreateRecord
|
||||
|
||||
Forms\Components\Fieldset::make('Feature Limits')
|
||||
->inlineLabel()
|
||||
->columnSpan(6)
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 4,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
@@ -709,13 +712,18 @@ class CreateServer extends CreateRecord
|
||||
->default(0),
|
||||
]),
|
||||
Forms\Components\Fieldset::make('Docker Settings')
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 4,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 3,
|
||||
'lg' => 4,
|
||||
'lg' => 3,
|
||||
])
|
||||
->columnSpan(6)
|
||||
->schema([
|
||||
Forms\Components\Select::make('select_image')
|
||||
->label('Image Name')
|
||||
@@ -734,12 +742,7 @@ class CreateServer extends CreateRecord
|
||||
return array_flip($images) + ['ghcr.io/custom-image' => 'Custom Image'];
|
||||
})
|
||||
->selectablePlaceholder(false)
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 3,
|
||||
'lg' => 2,
|
||||
]),
|
||||
->columnSpan(1),
|
||||
|
||||
Forms\Components\TextInput::make('image')
|
||||
->label('Image')
|
||||
@@ -755,18 +758,13 @@ class CreateServer extends CreateRecord
|
||||
}
|
||||
})
|
||||
->placeholder('Enter a custom Image')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 3,
|
||||
'lg' => 2,
|
||||
]),
|
||||
->columnSpan(2),
|
||||
|
||||
Forms\Components\KeyValue::make('docker_labels')
|
||||
->label('Container Labels')
|
||||
->keyLabel('Title')
|
||||
->valueLabel('Description')
|
||||
->columnSpanFull(),
|
||||
->columnSpan(3),
|
||||
|
||||
Forms\Components\CheckboxList::make('mounts')
|
||||
->live()
|
||||
@@ -814,7 +812,7 @@ class CreateServer extends CreateRecord
|
||||
|
||||
private function shouldHideComponent(Forms\Get $get, Forms\Components\Component $component): bool
|
||||
{
|
||||
$containsRuleIn = collect($get('rules'))->reduce(
|
||||
$containsRuleIn = str($get('rules'))->explode('|')->reduce(
|
||||
fn ($result, $value) => $result === true && !str($value)->startsWith('in:'), true
|
||||
);
|
||||
|
||||
@@ -831,7 +829,7 @@ class CreateServer extends CreateRecord
|
||||
|
||||
private function getSelectOptionsFromRules(Forms\Get $get): array
|
||||
{
|
||||
$inRule = collect($get('rules'))->reduce(
|
||||
$inRule = str($get('rules'))->explode('|')->reduce(
|
||||
fn ($result, $value) => str($value)->startsWith('in:') ? $value : $result, ''
|
||||
);
|
||||
|
||||
|
||||
@@ -7,8 +7,6 @@ use App\Services\Databases\DatabaseManagementService;
|
||||
use App\Services\Databases\DatabasePasswordService;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use LogicException;
|
||||
use App\Filament\Resources\ServerResource;
|
||||
use App\Http\Controllers\Admin\ServersController;
|
||||
@@ -29,7 +27,6 @@ use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
||||
|
||||
class EditServer extends EditRecord
|
||||
@@ -39,16 +36,22 @@ class EditServer extends EditRecord
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 2,
|
||||
'lg' => 4,
|
||||
])
|
||||
->schema([
|
||||
Tabs::make('Tabs')
|
||||
->persistTabInQueryString()
|
||||
->columnSpan(6)
|
||||
->columns([
|
||||
'default' => 2,
|
||||
'sm' => 2,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
->columnSpanFull()
|
||||
->tabs([
|
||||
Tabs\Tab::make('Information')
|
||||
->icon('tabler-info-circle')
|
||||
@@ -121,8 +124,7 @@ class EditServer extends EditRecord
|
||||
'md' => 2,
|
||||
'lg' => 3,
|
||||
])
|
||||
->readOnly()
|
||||
->dehydrated(false),
|
||||
->readOnly(),
|
||||
Forms\Components\TextInput::make('uuid_short')
|
||||
->label('Short UUID')
|
||||
->hintAction(CopyAction::make())
|
||||
@@ -132,8 +134,7 @@ class EditServer extends EditRecord
|
||||
'md' => 2,
|
||||
'lg' => 3,
|
||||
])
|
||||
->readOnly()
|
||||
->dehydrated(false),
|
||||
->readOnly(),
|
||||
Forms\Components\TextInput::make('external_id')
|
||||
->label('External ID')
|
||||
->columnSpan([
|
||||
@@ -158,6 +159,12 @@ class EditServer extends EditRecord
|
||||
->icon('tabler-brand-docker')
|
||||
->schema([
|
||||
Forms\Components\Fieldset::make('Resource Limits')
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 4,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
@@ -333,6 +340,12 @@ class EditServer extends EditRecord
|
||||
|
||||
Forms\Components\Fieldset::make('Feature Limits')
|
||||
->inlineLabel()
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 4,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
@@ -357,6 +370,12 @@ class EditServer extends EditRecord
|
||||
->numeric(),
|
||||
]),
|
||||
Forms\Components\Fieldset::make('Docker Settings')
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 4,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
@@ -419,10 +438,10 @@ class EditServer extends EditRecord
|
||||
->disabledOn('edit')
|
||||
->prefixIcon('tabler-egg')
|
||||
->columnSpan([
|
||||
'default' => 6,
|
||||
'default' => 1,
|
||||
'sm' => 3,
|
||||
'md' => 3,
|
||||
'lg' => 4,
|
||||
'lg' => 5,
|
||||
])
|
||||
->relationship('egg', 'name')
|
||||
->searchable()
|
||||
@@ -431,12 +450,6 @@ class EditServer extends EditRecord
|
||||
|
||||
Forms\Components\ToggleButtons::make('skip_scripts')
|
||||
->label('Run Egg Install Script?')->inline()
|
||||
->columnSpan([
|
||||
'default' => 6,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 2,
|
||||
])
|
||||
->options([
|
||||
false => 'Yes',
|
||||
true => 'Skip',
|
||||
@@ -454,7 +467,12 @@ class EditServer extends EditRecord
|
||||
Forms\Components\Textarea::make('startup')
|
||||
->label('Startup Command')
|
||||
->required()
|
||||
->columnSpan(6)
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 4,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
->rows(function ($state) {
|
||||
return str($state)->explode("\n")->reduce(
|
||||
fn (int $carry, $line) => $carry + floor(strlen($line) / 125),
|
||||
@@ -466,29 +484,20 @@ class EditServer extends EditRecord
|
||||
->hintAction(CopyAction::make())
|
||||
->label('Default Startup Command')
|
||||
->disabled()
|
||||
->formatStateUsing(function ($state, Get $get, Set $set) {
|
||||
->formatStateUsing(function ($state, Forms\Get $get, Forms\Set $set) {
|
||||
$egg = Egg::query()->find($get('egg_id'));
|
||||
|
||||
return $egg->startup;
|
||||
})
|
||||
->columnSpan(6),
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 4,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
]),
|
||||
|
||||
Forms\Components\Repeater::make('server_variables')
|
||||
->relationship('serverVariables', function (Builder $query) {
|
||||
/** @var Server $server */
|
||||
$server = $this->getRecord();
|
||||
|
||||
foreach ($server->variables as $variable) {
|
||||
ServerVariable::query()->firstOrCreate([
|
||||
'server_id' => $server->id,
|
||||
'variable_id' => $variable->id,
|
||||
], [
|
||||
'variable_value' => $variable->server_value ?? '',
|
||||
]);
|
||||
}
|
||||
|
||||
return $query;
|
||||
})
|
||||
->relationship('serverVariables')
|
||||
->grid()
|
||||
->mutateRelationshipDataBeforeSaveUsing(function (array &$data): array {
|
||||
foreach ($data as $key => $value) {
|
||||
@@ -504,7 +513,7 @@ class EditServer extends EditRecord
|
||||
|
||||
$text = Forms\Components\TextInput::make('variable_value')
|
||||
->hidden($this->shouldHideComponent(...))
|
||||
->required(fn (ServerVariable $serverVariable) => $serverVariable->variable->getRequiredAttribute())
|
||||
->required(fn (ServerVariable $serverVariable) => in_array('required', explode('|', $serverVariable->variable->rules)))
|
||||
->rules([
|
||||
fn (ServerVariable $serverVariable): Closure => function (string $attribute, $value, Closure $fail) use ($serverVariable) {
|
||||
$validator = Validator::make(['validatorkey' => $value], [
|
||||
@@ -531,7 +540,7 @@ class EditServer extends EditRecord
|
||||
->live(onBlur: true)
|
||||
->hintIcon('tabler-code')
|
||||
->label(fn (ServerVariable $serverVariable) => $serverVariable->variable->name)
|
||||
->hintIconTooltip(fn (ServerVariable $serverVariable) => implode('|', $serverVariable->variable->rules))
|
||||
->hintIconTooltip(fn (ServerVariable $serverVariable) => $serverVariable->variable->rules)
|
||||
->prefix(fn (ServerVariable $serverVariable) => '{{' . $serverVariable->variable->env_variable . '}}')
|
||||
->helperText(fn (ServerVariable $serverVariable) => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description);
|
||||
}
|
||||
@@ -735,16 +744,13 @@ class EditServer extends EditRecord
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\Action::make('Delete')
|
||||
Actions\DeleteAction::make('Delete')
|
||||
->successRedirectUrl(route('filament.admin.resources.servers.index'))
|
||||
->color('danger')
|
||||
->label('Delete')
|
||||
->requiresConfirmation()
|
||||
->action(function (Server $server) {
|
||||
resolve(ServerDeletionService::class)->handle($server);
|
||||
|
||||
return redirect(ListServers::getUrl());
|
||||
}),
|
||||
->disabled(fn (Server $server) => $server->databases()->count() > 0)
|
||||
->label(fn (Server $server) => $server->databases()->count() > 0 ? 'Server has a Database' : 'Delete')
|
||||
->after(fn (Server $server) => resolve(ServerDeletionService::class)->handle($server))
|
||||
->requiresConfirmation(),
|
||||
Actions\Action::make('console')
|
||||
->label('Console')
|
||||
->icon('tabler-terminal')
|
||||
@@ -776,24 +782,28 @@ class EditServer extends EditRecord
|
||||
];
|
||||
}
|
||||
|
||||
private function shouldHideComponent(ServerVariable $serverVariable, Forms\Components\Component $component): bool
|
||||
private function shouldHideComponent(Forms\Get $get, Forms\Components\Component $component): bool
|
||||
{
|
||||
$containsRuleIn = array_first($serverVariable->variable->rules, fn ($value) => str($value)->startsWith('in:'), false);
|
||||
$containsRuleIn = str($get('rules'))->explode('|')->reduce(
|
||||
fn ($result, $value) => $result === true && !str($value)->startsWith('in:'), true
|
||||
);
|
||||
|
||||
if ($component instanceof Forms\Components\Select) {
|
||||
return !$containsRuleIn;
|
||||
return $containsRuleIn;
|
||||
}
|
||||
|
||||
if ($component instanceof Forms\Components\TextInput) {
|
||||
return $containsRuleIn;
|
||||
return !$containsRuleIn;
|
||||
}
|
||||
|
||||
throw new \Exception('Component type not supported: ' . $component::class);
|
||||
}
|
||||
|
||||
private function getSelectOptionsFromRules(ServerVariable $serverVariable): array
|
||||
private function getSelectOptionsFromRules(Forms\Get $get): array
|
||||
{
|
||||
$inRule = array_first($serverVariable->variable->rules, fn ($value) => str($value)->startsWith('in:'));
|
||||
$inRule = str($get('rules'))->explode('|')->reduce(
|
||||
fn ($result, $value) => str($value)->startsWith('in:') ? $value : $result, ''
|
||||
);
|
||||
|
||||
return str($inRule)
|
||||
->after('in:')
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace App\Filament\Resources\ServerResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ServerResource;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\CreateAction;
|
||||
@@ -77,13 +76,7 @@ class ListServers extends ListRecords
|
||||
->actions([
|
||||
Tables\Actions\Action::make('View')
|
||||
->icon('tabler-terminal')
|
||||
->url(fn (Server $server) => "/server/$server->uuid_short")
|
||||
->visible(function (Server $server) {
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
return $user->isRootAdmin() || $user->id === $server->owner_id;
|
||||
}),
|
||||
->url(fn (Server $server) => "/server/$server->uuid_short"),
|
||||
Tables\Actions\EditAction::make(),
|
||||
])
|
||||
->emptyStateIcon('tabler-brand-docker')
|
||||
|
||||
@@ -4,15 +4,11 @@ namespace App\Filament\Resources\ServerResource\RelationManagers;
|
||||
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Server;
|
||||
use App\Services\Allocations\AssignmentService;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
/**
|
||||
* @method Server getOwnerRecord()
|
||||
@@ -25,7 +21,7 @@ class AllocationsRelationManager extends RelationManager
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
TextInput::make('ip')
|
||||
Forms\Components\TextInput::make('ip')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
]);
|
||||
@@ -66,87 +62,9 @@ class AllocationsRelationManager extends RelationManager
|
||||
->label(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id ? '' : 'Make Primary'),
|
||||
])
|
||||
->headerActions([
|
||||
Tables\Actions\CreateAction::make()->label('Create Allocation')
|
||||
->createAnother(false)
|
||||
->form(fn () => [
|
||||
TextInput::make('allocation_ip')
|
||||
->datalist($this->getOwnerRecord()->node->ipAddresses())
|
||||
->label('IP Address')
|
||||
->inlineLabel()
|
||||
->ipv4()
|
||||
->helperText("Usually your machine's public IP unless you are port forwarding.")
|
||||
->required(),
|
||||
TextInput::make('allocation_alias')
|
||||
->label('Alias')
|
||||
->inlineLabel()
|
||||
->default(null)
|
||||
->helperText('Optional display name to help you remember what these are.')
|
||||
->required(false),
|
||||
TagsInput::make('allocation_ports')
|
||||
->placeholder('Examples: 27015, 27017-27019')
|
||||
->helperText(new HtmlString('
|
||||
These are the ports that users can connect to this Server through.
|
||||
<br />
|
||||
You would have to port forward these on your home network.
|
||||
'))
|
||||
->label('Ports')
|
||||
->inlineLabel()
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, Set $set) {
|
||||
$ports = collect();
|
||||
$update = false;
|
||||
foreach ($state as $portEntry) {
|
||||
if (!str_contains($portEntry, '-')) {
|
||||
if (is_numeric($portEntry)) {
|
||||
$ports->push((int) $portEntry);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Do not add non numerical ports
|
||||
$update = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$update = true;
|
||||
[$start, $end] = explode('-', $portEntry);
|
||||
if (!is_numeric($start) || !is_numeric($end)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$start = max((int) $start, 0);
|
||||
$end = min((int) $end, 2 ** 16 - 1);
|
||||
foreach (range($start, $end) as $i) {
|
||||
$ports->push($i);
|
||||
}
|
||||
}
|
||||
|
||||
$uniquePorts = $ports->unique()->values();
|
||||
if ($ports->count() > $uniquePorts->count()) {
|
||||
$update = true;
|
||||
$ports = $uniquePorts;
|
||||
}
|
||||
|
||||
$sortedPorts = $ports->sort()->values();
|
||||
if ($sortedPorts->all() !== $ports->all()) {
|
||||
$update = true;
|
||||
$ports = $sortedPorts;
|
||||
}
|
||||
|
||||
$ports = $ports->filter(fn ($port) => $port > 1024 && $port < 65535)->values();
|
||||
|
||||
if ($update) {
|
||||
$set('allocation_ports', $ports->all());
|
||||
}
|
||||
})
|
||||
->splitKeys(['Tab', ' ', ','])
|
||||
->required(),
|
||||
])
|
||||
->action(fn (array $data) => resolve(AssignmentService::class)->handle($this->getOwnerRecord()->node, $data, $this->getOwnerRecord())),
|
||||
//TODO Tables\Actions\CreateAction::make()->label('Create Allocation'),
|
||||
Tables\Actions\AssociateAction::make()
|
||||
->multiple()
|
||||
->associateAnother(false)
|
||||
->preloadRecordSelect()
|
||||
->recordSelectOptionsQuery(fn ($query) => $query->whereBelongsTo($this->getOwnerRecord()->node))
|
||||
->label('Add Allocation'),
|
||||
|
||||
@@ -53,7 +53,6 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
->label(trans('strings.username'))
|
||||
->disabled()
|
||||
->readOnly()
|
||||
->dehydrated(false)
|
||||
->maxLength(255)
|
||||
->unique(ignoreRecord: true)
|
||||
->autofocus(),
|
||||
@@ -120,7 +119,6 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
->hidden(fn () => !cache()->get("users.{$this->getUser()->id}.2fa.tokens"))
|
||||
->rows(10)
|
||||
->readOnly()
|
||||
->dehydrated(false)
|
||||
->formatStateUsing(fn () => cache()->get("users.{$this->getUser()->id}.2fa.tokens"))
|
||||
->helperText('These will not be shown again!')
|
||||
->label('Backup Tokens:'),
|
||||
@@ -217,7 +215,7 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
Action::make('Create')
|
||||
->disabled(fn (Get $get) => $get('description') === null)
|
||||
->successRedirectUrl(route('filament.admin.auth.profile', ['tab' => '-api-keys-tab']))
|
||||
->action(function (Get $get, Action $action, User $user) {
|
||||
->action(function (Get $get, Action $action, $user) {
|
||||
$token = $user->createToken(
|
||||
$get('description'),
|
||||
$get('allowed_ips'),
|
||||
|
||||
@@ -3,16 +3,13 @@
|
||||
namespace App\Filament\Resources\UserResource\Pages;
|
||||
|
||||
use App\Filament\Resources\UserResource;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use App\Services\Exceptions\FilamentExceptionHandler;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use App\Models\User;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Form;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class EditUser extends EditRecord
|
||||
@@ -23,33 +20,54 @@ class EditUser extends EditRecord
|
||||
return $form
|
||||
->schema([
|
||||
Section::make()->schema([
|
||||
TextInput::make('username')->required()->maxLength(255),
|
||||
TextInput::make('email')->email()->required()->maxLength(255),
|
||||
TextInput::make('password')
|
||||
Forms\Components\TextInput::make('username')->required()->maxLength(255),
|
||||
Forms\Components\TextInput::make('email')->email()->required()->maxLength(255),
|
||||
|
||||
Forms\Components\TextInput::make('password')
|
||||
->dehydrateStateUsing(fn (string $state): string => Hash::make($state))
|
||||
->dehydrated(fn (?string $state): bool => filled($state))
|
||||
->required(fn (string $operation): bool => $operation === 'create')
|
||||
->password(),
|
||||
Select::make('language')
|
||||
|
||||
Forms\Components\ToggleButtons::make('root_admin')
|
||||
->label('Administrator (Root)')
|
||||
->options([
|
||||
false => 'No',
|
||||
true => 'Admin',
|
||||
])
|
||||
->colors([
|
||||
false => 'primary',
|
||||
true => 'danger',
|
||||
])
|
||||
->disableOptionWhen(function (string $operation, $value, User $user) {
|
||||
if ($operation !== 'edit' || $value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $user->isLastRootAdmin();
|
||||
})
|
||||
->hint(fn (User $user) => $user->isLastRootAdmin() ? 'This is the last root administrator!' : '')
|
||||
->helperText(fn (User $user) => $user->isLastRootAdmin() ? 'You must have at least one root administrator in your system.' : '')
|
||||
->hintColor('warning')
|
||||
->inline()
|
||||
->required()
|
||||
->default(false),
|
||||
|
||||
Forms\Components\Hidden::make('skipValidation')->default(true),
|
||||
|
||||
Forms\Components\Select::make('language')
|
||||
->required()
|
||||
->hidden()
|
||||
->default('en')
|
||||
->options(fn (User $user) => $user->getAvailableLanguages()),
|
||||
Hidden::make('skipValidation')->default(true),
|
||||
CheckboxList::make('roles')
|
||||
->disabled(fn (User $user) => $user->id === auth()->user()->id)
|
||||
->disableOptionWhen(fn (string $value): bool => $value == Role::getRootAdmin()->id)
|
||||
->relationship('roles', 'name')
|
||||
->label('Admin Roles')
|
||||
->columnSpanFull()
|
||||
->bulkToggleable(false),
|
||||
|
||||
])->columns(),
|
||||
]);
|
||||
}
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make()
|
||||
Actions\DeleteAction::make()
|
||||
->label(fn (User $user) => auth()->user()->id === $user->id ? 'Can\'t Delete Yourself' : ($user->servers()->count() > 0 ? 'User Has Servers' : 'Delete'))
|
||||
->disabled(fn (User $user) => auth()->user()->id === $user->id || $user->servers()->count() > 0),
|
||||
$this->getSaveFormAction()->formId('form'),
|
||||
@@ -60,4 +78,9 @@ class EditUser extends EditRecord
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function exception($exception, $stopPropagation): void
|
||||
{
|
||||
(new FilamentExceptionHandler())->handle($exception, $stopPropagation);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,22 +3,14 @@
|
||||
namespace App\Filament\Resources\UserResource\Pages;
|
||||
|
||||
use App\Filament\Resources\UserResource;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use App\Services\Users\UserCreationService;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Actions;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\ImageColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Tables;
|
||||
use Filament\Forms;
|
||||
|
||||
class ListUsers extends ListRecords
|
||||
{
|
||||
@@ -29,102 +21,101 @@ class ListUsers extends ListRecords
|
||||
return $table
|
||||
->searchable(false)
|
||||
->columns([
|
||||
ImageColumn::make('picture')
|
||||
Tables\Columns\ImageColumn::make('picture')
|
||||
->visibleFrom('lg')
|
||||
->label('')
|
||||
->extraImgAttributes(['class' => 'rounded-full'])
|
||||
->defaultImageUrl(fn (User $user) => 'https://gravatar.com/avatar/' . md5(strtolower($user->email))),
|
||||
TextColumn::make('external_id')
|
||||
Tables\Columns\TextColumn::make('external_id')
|
||||
->searchable()
|
||||
->hidden(),
|
||||
TextColumn::make('uuid')
|
||||
Tables\Columns\TextColumn::make('uuid')
|
||||
->label('UUID')
|
||||
->hidden()
|
||||
->searchable(),
|
||||
TextColumn::make('username')
|
||||
Tables\Columns\TextColumn::make('username')
|
||||
->searchable(),
|
||||
TextColumn::make('email')
|
||||
Tables\Columns\TextColumn::make('email')
|
||||
->searchable()
|
||||
->icon('tabler-mail'),
|
||||
IconColumn::make('use_totp')
|
||||
->label('2FA')
|
||||
Tables\Columns\IconColumn::make('root_admin')
|
||||
->visibleFrom('md')
|
||||
->label('Admin')
|
||||
->boolean()
|
||||
->trueIcon('tabler-star-filled')
|
||||
->falseIcon('tabler-star-off')
|
||||
->sortable(),
|
||||
Tables\Columns\IconColumn::make('use_totp')->label('2FA')
|
||||
->visibleFrom('lg')
|
||||
->icon(fn (User $user) => $user->use_totp ? 'tabler-lock' : 'tabler-lock-open-off')
|
||||
->boolean()->sortable(),
|
||||
TextColumn::make('roles_count')
|
||||
->counts('roles')
|
||||
->icon('tabler-users-group')
|
||||
->label('Roles')
|
||||
->formatStateUsing(fn (User $user, $state) => $state . ($user->isRootAdmin() ? ' (Root Admin)' : '')),
|
||||
TextColumn::make('servers_count')
|
||||
Tables\Columns\TextColumn::make('servers_count')
|
||||
->counts('servers')
|
||||
->icon('tabler-server')
|
||||
->label('Servers'),
|
||||
TextColumn::make('subusers_count')
|
||||
Tables\Columns\TextColumn::make('subusers_count')
|
||||
->visibleFrom('sm')
|
||||
->label('Subusers')
|
||||
->counts('subusers')
|
||||
->icon('tabler-users'),
|
||||
// ->formatStateUsing(fn (string $state, $record): string => (string) ($record->servers_count + $record->subusers_count))
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
EditAction::make(),
|
||||
Tables\Actions\EditAction::make(),
|
||||
])
|
||||
->checkIfRecordIsSelectableUsing(fn (User $user) => auth()->user()->id !== $user->id && !$user->servers_count)
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete user')),
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make('create')
|
||||
Actions\CreateAction::make('create')
|
||||
->label('Create User')
|
||||
->createAnother(false)
|
||||
->form([
|
||||
Grid::make()
|
||||
Forms\Components\Grid::make()
|
||||
->schema([
|
||||
TextInput::make('username')
|
||||
Forms\Components\TextInput::make('username')
|
||||
->alphaNum()
|
||||
->required()
|
||||
->maxLength(255),
|
||||
TextInput::make('email')
|
||||
Forms\Components\TextInput::make('email')
|
||||
->email()
|
||||
->required()
|
||||
->unique()
|
||||
->maxLength(255),
|
||||
TextInput::make('password')
|
||||
|
||||
Forms\Components\TextInput::make('password')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('Providing a user password is optional. New user email will prompt users to create a password the first time they login.')
|
||||
->password(),
|
||||
CheckboxList::make('roles')
|
||||
->disableOptionWhen(fn (string $value): bool => $value == Role::getRootAdmin()->id)
|
||||
->relationship('roles', 'name')
|
||||
->dehydrated()
|
||||
->label('Admin Roles')
|
||||
->columnSpanFull()
|
||||
->bulkToggleable(false),
|
||||
|
||||
Forms\Components\ToggleButtons::make('root_admin')
|
||||
->label('Administrator (Root)')
|
||||
->options([
|
||||
false => 'No',
|
||||
true => 'Admin',
|
||||
])
|
||||
->colors([
|
||||
false => 'primary',
|
||||
true => 'danger',
|
||||
])
|
||||
->inline()
|
||||
->required()
|
||||
->default(false),
|
||||
]),
|
||||
])
|
||||
->successRedirectUrl(route('filament.admin.resources.users.index'))
|
||||
->action(function (array $data) {
|
||||
$roles = $data['roles'];
|
||||
$roles = collect($roles)->map(fn ($role) => Role::findById($role));
|
||||
unset($data['roles']);
|
||||
|
||||
/** @var UserCreationService $creationService */
|
||||
$creationService = resolve(UserCreationService::class);
|
||||
$user = $creationService->handle($data);
|
||||
|
||||
$user->syncRoles($roles);
|
||||
|
||||
Notification::make()
|
||||
->title('User Created!')
|
||||
->success()
|
||||
->send();
|
||||
resolve(UserCreationService::class)->handle($data);
|
||||
Notification::make()->title('User Created!')->success()->send();
|
||||
|
||||
return redirect()->route('filament.admin.resources.users.index');
|
||||
}),
|
||||
|
||||
56
app/Http/Controllers/Admin/Settings/AdvancedController.php
Normal file
56
app/Http/Controllers/Admin/Settings/AdvancedController.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Settings;
|
||||
|
||||
use App\Models\Setting;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Prologue\Alerts\AlertsMessageBag;
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Settings\AdvancedSettingsFormRequest;
|
||||
|
||||
class AdvancedController extends Controller
|
||||
{
|
||||
/**
|
||||
* AdvancedController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private AlertsMessageBag $alert,
|
||||
private Kernel $kernel,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Render advanced Panel settings UI.
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
$showRecaptchaWarning = false;
|
||||
if (
|
||||
config('recaptcha._shipped_secret_key') === config('recaptcha.secret_key')
|
||||
|| config('recaptcha._shipped_website_key') === config('recaptcha.website_key')
|
||||
) {
|
||||
$showRecaptchaWarning = true;
|
||||
}
|
||||
|
||||
return view('admin.settings.advanced', [
|
||||
'showRecaptchaWarning' => $showRecaptchaWarning,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \App\Exceptions\Model\DataValidationException
|
||||
*/
|
||||
public function update(AdvancedSettingsFormRequest $request): RedirectResponse
|
||||
{
|
||||
foreach ($request->normalize() as $key => $value) {
|
||||
Setting::set('settings::' . $key, $value);
|
||||
}
|
||||
|
||||
$this->kernel->call('queue:restart');
|
||||
$this->alert->success('Advanced settings have been updated successfully and the queue worker was restarted to apply these changes.')->flash();
|
||||
|
||||
return redirect()->route('admin.settings.advanced');
|
||||
}
|
||||
}
|
||||
56
app/Http/Controllers/Admin/Settings/IndexController.php
Normal file
56
app/Http/Controllers/Admin/Settings/IndexController.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Settings;
|
||||
|
||||
use App\Models\Setting;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Prologue\Alerts\AlertsMessageBag;
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Traits\Helpers\AvailableLanguages;
|
||||
use App\Services\Helpers\SoftwareVersionService;
|
||||
use App\Http\Requests\Admin\Settings\BaseSettingsFormRequest;
|
||||
|
||||
class IndexController extends Controller
|
||||
{
|
||||
use AvailableLanguages;
|
||||
|
||||
/**
|
||||
* IndexController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private AlertsMessageBag $alert,
|
||||
private Kernel $kernel,
|
||||
private SoftwareVersionService $versionService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the UI for basic Panel settings.
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
return view('admin.settings.index', [
|
||||
'version' => $this->versionService,
|
||||
'languages' => $this->getAvailableLanguages(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle settings update.
|
||||
*
|
||||
* @throws \App\Exceptions\Model\DataValidationException
|
||||
*/
|
||||
public function update(BaseSettingsFormRequest $request): RedirectResponse
|
||||
{
|
||||
foreach ($request->normalize() as $key => $value) {
|
||||
Setting::set('settings::' . $key, $value);
|
||||
}
|
||||
|
||||
$this->kernel->call('queue:restart');
|
||||
$this->alert->success('Panel settings have been updated successfully and the queue worker was restarted to apply these changes.')->flash();
|
||||
|
||||
return redirect()->route('admin.settings');
|
||||
}
|
||||
}
|
||||
82
app/Http/Controllers/Admin/Settings/MailController.php
Normal file
82
app/Http/Controllers/Admin/Settings/MailController.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Settings;
|
||||
|
||||
use App\Models\Setting;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
use App\Notifications\MailTested;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use App\Exceptions\DisplayException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Providers\SettingsServiceProvider;
|
||||
use App\Http\Requests\Admin\Settings\MailSettingsFormRequest;
|
||||
|
||||
class MailController extends Controller
|
||||
{
|
||||
/**
|
||||
* MailController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private Kernel $kernel,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Render UI for editing mail settings. This UI should only display if
|
||||
* the server is configured to send mail using SMTP.
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
return view('admin.settings.mail', [
|
||||
'disabled' => config('mail.default') !== 'smtp',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle request to update SMTP mail settings.
|
||||
*
|
||||
* @throws DisplayException
|
||||
* @throws \App\Exceptions\Model\DataValidationException
|
||||
*/
|
||||
public function update(MailSettingsFormRequest $request): Response
|
||||
{
|
||||
if (config('mail.default') !== 'smtp') {
|
||||
throw new DisplayException('This feature is only available if SMTP is the selected email driver for the Panel.');
|
||||
}
|
||||
|
||||
$values = $request->normalize();
|
||||
if (array_get($values, 'mail:mailers:smtp:password') === '!e') {
|
||||
$values['mail:mailers:smtp:password'] = '';
|
||||
}
|
||||
|
||||
foreach ($values as $key => $value) {
|
||||
if (in_array($key, SettingsServiceProvider::getEncryptedKeys()) && !empty($value)) {
|
||||
$value = encrypt($value);
|
||||
}
|
||||
|
||||
Setting::set('settings::' . $key, $value);
|
||||
}
|
||||
|
||||
$this->kernel->call('queue:restart');
|
||||
|
||||
return response('', 204);
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a request to send a test mail message.
|
||||
*/
|
||||
public function test(Request $request): Response
|
||||
{
|
||||
try {
|
||||
Notification::route('mail', $request->user()->email)
|
||||
->notify(new MailTested($request->user()));
|
||||
} catch (\Exception $exception) {
|
||||
return response($exception->getMessage(), 500);
|
||||
}
|
||||
|
||||
return response('', 204);
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Application\Roles;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Models\Role;
|
||||
use Spatie\QueryBuilder\QueryBuilder;
|
||||
use App\Transformers\Api\Application\RoleTransformer;
|
||||
use App\Http\Controllers\Api\Application\ApplicationApiController;
|
||||
use App\Http\Requests\Api\Application\Roles\GetRoleRequest;
|
||||
use App\Http\Requests\Api\Application\Roles\StoreRoleRequest;
|
||||
use App\Http\Requests\Api\Application\Roles\DeleteRoleRequest;
|
||||
use App\Http\Requests\Api\Application\Roles\UpdateRoleRequest;
|
||||
|
||||
class RoleController extends ApplicationApiController
|
||||
{
|
||||
/**
|
||||
* Return all the roles currently registered on the Panel.
|
||||
*/
|
||||
public function index(GetRoleRequest $request): array
|
||||
{
|
||||
$roles = QueryBuilder::for(Role::query())
|
||||
->allowedFilters(['name'])
|
||||
->allowedSorts(['name'])
|
||||
->paginate($request->query('per_page') ?? 10);
|
||||
|
||||
return $this->fractal->collection($roles)
|
||||
->transformWith($this->getTransformer(RoleTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a single role.
|
||||
*/
|
||||
public function view(GetRoleRequest $request, Role $role): array
|
||||
{
|
||||
return $this->fractal->item($role)
|
||||
->transformWith($this->getTransformer(RoleTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a new role on the Panel and return an HTTP/201 response code with the
|
||||
* new role attached.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function store(StoreRoleRequest $request): JsonResponse
|
||||
{
|
||||
$role = Role::create($request->validated());
|
||||
|
||||
return $this->fractal->item($role)
|
||||
->transformWith($this->getTransformer(RoleTransformer::class))
|
||||
->addMeta([
|
||||
'resource' => route('api.application.roles.view', [
|
||||
'role' => $role->id,
|
||||
]),
|
||||
])
|
||||
->respond(201);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a role on the Panel and return the updated record to the user.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function update(UpdateRoleRequest $request, Role $role): array
|
||||
{
|
||||
$role->update($request->validated());
|
||||
|
||||
return $this->fractal->item($role)
|
||||
->transformWith($this->getTransformer(RoleTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a role from the Panel.
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function delete(DeleteRoleRequest $request, Role $role): Response
|
||||
{
|
||||
$role->delete();
|
||||
|
||||
return $this->returnNoContent();
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ use App\Http\Requests\Api\Application\Users\StoreUserRequest;
|
||||
use App\Http\Requests\Api\Application\Users\DeleteUserRequest;
|
||||
use App\Http\Requests\Api\Application\Users\UpdateUserRequest;
|
||||
use App\Http\Controllers\Api\Application\ApplicationApiController;
|
||||
use App\Http\Requests\Api\Application\Users\AssignUserRolesRequest;
|
||||
|
||||
class UserController extends ApplicationApiController
|
||||
{
|
||||
@@ -76,19 +75,6 @@ class UserController extends ApplicationApiController
|
||||
return $response->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign roles to a user.
|
||||
*/
|
||||
public function roles(AssignUserRolesRequest $request, User $user): array
|
||||
{
|
||||
$user->syncRoles($request->input('roles'));
|
||||
|
||||
$response = $this->fractal->item($user)
|
||||
->transformWith($this->getTransformer(UserTransformer::class));
|
||||
|
||||
return $response->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a new user on the system. Returns the created user and an HTTP/201
|
||||
* header on successful creation.
|
||||
|
||||
@@ -48,7 +48,7 @@ class ClientController extends ClientApiController
|
||||
if (in_array($type, ['admin', 'admin-all'])) {
|
||||
// If they aren't an admin but want all the admin servers don't fail the request, just
|
||||
// make it a query that will never return any results back.
|
||||
if (!$user->isRootAdmin()) {
|
||||
if (!$user->root_admin) {
|
||||
$builder->whereRaw('1 = 2');
|
||||
} else {
|
||||
$builder = $type === 'admin-all'
|
||||
|
||||
@@ -13,7 +13,6 @@ use Illuminate\Database\Query\JoinClause;
|
||||
use App\Http\Requests\Api\Client\ClientApiRequest;
|
||||
use App\Transformers\Api\Client\ActivityLogTransformer;
|
||||
use App\Http\Controllers\Api\Client\ClientApiController;
|
||||
use App\Models\Role;
|
||||
|
||||
class ActivityLogController extends ClientApiController
|
||||
{
|
||||
@@ -33,16 +32,15 @@ class ActivityLogController extends ClientApiController
|
||||
// We could do this with a query and a lot of joins, but that gets pretty
|
||||
// painful so for now we'll execute a simpler query.
|
||||
$subusers = $server->subusers()->pluck('user_id')->merge([$server->owner_id]);
|
||||
$rootAdmins = Role::getRootAdmin()->users()->pluck('id');
|
||||
|
||||
$builder->select('activity_logs.*')
|
||||
->leftJoin('users', function (JoinClause $join) {
|
||||
$join->on('users.id', 'activity_logs.actor_id')
|
||||
->where('activity_logs.actor_type', (new User())->getMorphClass());
|
||||
})
|
||||
->where(function (Builder $builder) use ($subusers, $rootAdmins) {
|
||||
->where(function (Builder $builder) use ($subusers) {
|
||||
$builder->whereNull('users.id')
|
||||
->orWhereNotIn('users.id', $rootAdmins)
|
||||
->orWhere('users.root_admin', 0)
|
||||
->orWhereIn('users.id', $subusers);
|
||||
});
|
||||
})
|
||||
|
||||
@@ -140,7 +140,7 @@ class SftpAuthenticationController extends Controller
|
||||
*/
|
||||
protected function validateSftpAccess(User $user, Server $server): void
|
||||
{
|
||||
if (!$user->isRootAdmin() && $server->owner_id !== $user->id) {
|
||||
if (!$user->root_admin && $server->owner_id !== $user->id) {
|
||||
$permissions = $this->permissions->handle($server, $user);
|
||||
|
||||
if (!in_array(Permission::ACTION_FILE_SFTP, $permissions)) {
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Filament\Pages\Installer\PanelInstaller;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -18,12 +17,8 @@ class LoginController extends AbstractLoginController
|
||||
* base authentication view component. React will take over at this point and
|
||||
* turn the login area into an SPA.
|
||||
*/
|
||||
public function index()
|
||||
public function index(): View
|
||||
{
|
||||
if (PanelInstaller::show()) {
|
||||
return redirect('/installer');
|
||||
}
|
||||
|
||||
return view('templates/auth.core');
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ class AdminAuthenticate
|
||||
*/
|
||||
public function handle(Request $request, \Closure $next): mixed
|
||||
{
|
||||
if (!$request->user() || !$request->user()->isRootAdmin()) {
|
||||
if (!$request->user() || !$request->user()->root_admin) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ class AuthenticateApplicationUser
|
||||
{
|
||||
/** @var \App\Models\User|null $user */
|
||||
$user = $request->user();
|
||||
if (!$user || !$user->isRootAdmin()) {
|
||||
if (!$user || !$user->root_admin) {
|
||||
throw new AccessDeniedHttpException('This account does not have permission to access the API.');
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ class AuthenticateServerAccess
|
||||
// At the very least, ensure that the user trying to make this request is the
|
||||
// server owner, a subuser, or a root admin. We'll leave it up to the controllers
|
||||
// to authenticate more detailed permissions if needed.
|
||||
if ($user->id !== $server->owner_id && !$user->isRootAdmin()) {
|
||||
if ($user->id !== $server->owner_id && !$user->root_admin) {
|
||||
// Check for subuser status.
|
||||
if (!$server->subusers->contains('user_id', $user->id)) {
|
||||
throw new NotFoundHttpException(trans('exceptions.api.resource_not_found'));
|
||||
@@ -55,7 +55,7 @@ class AuthenticateServerAccess
|
||||
if (($server->isSuspended() || $server->node->isUnderMaintenance()) && !$request->routeIs('api:client:server.resources')) {
|
||||
throw $exception;
|
||||
}
|
||||
if (!$user->isRootAdmin() || !$request->routeIs($this->except)) {
|
||||
if (!$user->root_admin || !$request->routeIs($this->except)) {
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ class RequireTwoFactorAuthentication
|
||||
// If the level is set as admin and the user is not an admin, pass them through as well.
|
||||
if ($level === self::LEVEL_NONE || $user->use_totp) {
|
||||
return $next($request);
|
||||
} elseif ($level === self::LEVEL_ADMIN && !$user->isRootAdmin()) {
|
||||
} elseif ($level === self::LEVEL_ADMIN && !$user->root_admin) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ abstract class AdminFormRequest extends FormRequest
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->user()->isRootAdmin();
|
||||
return (bool) $this->user()->root_admin;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,6 +22,7 @@ class NewUserFormRequest extends AdminFormRequest
|
||||
'name_last',
|
||||
'password',
|
||||
'language',
|
||||
'root_admin',
|
||||
])->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ class UserFormRequest extends AdminFormRequest
|
||||
'name_last',
|
||||
'password',
|
||||
'language',
|
||||
'root_admin',
|
||||
])->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ class StoreNodeRequest extends ApplicationApiRequest
|
||||
return collect($rules ?? Node::getRules())->only([
|
||||
'public',
|
||||
'name',
|
||||
'description',
|
||||
'fqdn',
|
||||
'scheme',
|
||||
'behind_proxy',
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\Application\Roles;
|
||||
|
||||
use App\Services\Acl\Api\AdminAcl;
|
||||
use App\Http\Requests\Api\Application\ApplicationApiRequest;
|
||||
|
||||
class DeleteRoleRequest extends ApplicationApiRequest
|
||||
{
|
||||
protected ?string $resource = AdminAcl::RESOURCE_ROLES;
|
||||
|
||||
protected int $permission = AdminAcl::WRITE;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\Application\Roles;
|
||||
|
||||
use App\Services\Acl\Api\AdminAcl;
|
||||
use App\Http\Requests\Api\Application\ApplicationApiRequest;
|
||||
|
||||
class GetRoleRequest extends ApplicationApiRequest
|
||||
{
|
||||
protected ?string $resource = AdminAcl::RESOURCE_ROLES;
|
||||
|
||||
protected int $permission = AdminAcl::READ;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\Application\Roles;
|
||||
|
||||
use App\Services\Acl\Api\AdminAcl;
|
||||
use App\Http\Requests\Api\Application\ApplicationApiRequest;
|
||||
|
||||
class StoreRoleRequest extends ApplicationApiRequest
|
||||
{
|
||||
protected ?string $resource = AdminAcl::RESOURCE_ROLES;
|
||||
|
||||
protected int $permission = AdminAcl::WRITE;
|
||||
|
||||
public function rules(array $rules = null): array
|
||||
{
|
||||
return [
|
||||
'name' => 'required|string',
|
||||
'guard_name' => 'nullable|string',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\Application\Roles;
|
||||
|
||||
class UpdateRoleRequest extends StoreRoleRequest
|
||||
{
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\Application\Users;
|
||||
|
||||
class AssignUserRolesRequest extends StoreUserRequest
|
||||
{
|
||||
/**
|
||||
* Return the validation rules for this request.
|
||||
*/
|
||||
public function rules(array $rules = null): array
|
||||
{
|
||||
return [
|
||||
'roles' => 'array',
|
||||
'roles.*' => 'string',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ class StoreUserRequest extends ApplicationApiRequest
|
||||
'password',
|
||||
'language',
|
||||
'timezone',
|
||||
'root_admin',
|
||||
])->toArray();
|
||||
|
||||
$response['first_name'] = $rules['name_first'];
|
||||
@@ -55,6 +56,7 @@ class StoreUserRequest extends ApplicationApiRequest
|
||||
'external_id' => 'Third Party Identifier',
|
||||
'name_first' => 'First Name',
|
||||
'name_last' => 'Last Name',
|
||||
'root_admin' => 'Root Administrator Status',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ class StoreTaskRequest extends ViewScheduleRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'action' => 'required|in:command,power,backup,delete_files',
|
||||
'action' => 'required|in:command,power,backup',
|
||||
'payload' => 'required_unless:action,backup|string|nullable',
|
||||
'time_offset' => 'required|numeric|min:0|max:900',
|
||||
'sequence_id' => 'sometimes|required|numeric|min:1',
|
||||
|
||||
@@ -56,7 +56,7 @@ abstract class SubuserRequest extends ClientApiRequest
|
||||
$server = $this->route()->parameter('server');
|
||||
|
||||
// If we are a root admin or the server owner, no need to perform these checks.
|
||||
if ($user->isRootAdmin() || $user->id === $server->owner_id) {
|
||||
if ($user->root_admin || $user->id === $server->owner_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Node;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class NodeStatistics implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
foreach (Node::all() as $node) {
|
||||
$stats = $node->statistics();
|
||||
$timestamp = now()->getTimestamp();
|
||||
|
||||
foreach ($stats as $key => $value) {
|
||||
$cacheKey = "nodes.{$node->id}.$key";
|
||||
$data = cache()->get($cacheKey, []);
|
||||
|
||||
// Add current timestamp and value to the data array
|
||||
$data[$timestamp] = $value;
|
||||
|
||||
// Update the cache with the new data, expires in 1 minute
|
||||
cache()->put($cacheKey, $data, now()->addMinute());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -12,7 +12,6 @@ use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||
use App\Services\Backups\InitiateBackupService;
|
||||
use App\Repositories\Daemon\DaemonPowerRepository;
|
||||
use App\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
use App\Services\Files\DeleteFilesService;
|
||||
|
||||
class RunTaskJob extends Job implements ShouldQueue
|
||||
{
|
||||
@@ -35,8 +34,7 @@ class RunTaskJob extends Job implements ShouldQueue
|
||||
*/
|
||||
public function handle(
|
||||
InitiateBackupService $backupService,
|
||||
DaemonPowerRepository $powerRepository,
|
||||
DeleteFilesService $deleteFilesService
|
||||
DaemonPowerRepository $powerRepository
|
||||
): void {
|
||||
// Do not process a task that is not set to active, unless it's been manually triggered.
|
||||
if (!$this->task->schedule->is_active && !$this->manualRun) {
|
||||
@@ -69,9 +67,6 @@ class RunTaskJob extends Job implements ShouldQueue
|
||||
case Task::ACTION_BACKUP:
|
||||
$backupService->setIgnoredFiles(explode(PHP_EOL, $this->task->payload))->handle($server, null, true);
|
||||
break;
|
||||
case Task::ACTION_DELETE_FILES:
|
||||
$deleteFilesService->handle($server, explode(PHP_EOL, $this->task->payload));
|
||||
break;
|
||||
default:
|
||||
throw new \InvalidArgumentException('Invalid task action provided: ' . $this->task->action);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Listeners\Auth;
|
||||
use App\Facades\Activity;
|
||||
use Illuminate\Auth\Events\Failed;
|
||||
use App\Events\Auth\DirectLogin;
|
||||
use Illuminate\Events\Dispatcher;
|
||||
|
||||
class AuthenticationListener
|
||||
{
|
||||
@@ -27,4 +28,10 @@ class AuthenticationListener
|
||||
|
||||
$activity->event($event instanceof Failed ? 'auth:fail' : 'auth:success')->log();
|
||||
}
|
||||
|
||||
public function subscribe(Dispatcher $events): void
|
||||
{
|
||||
$events->listen(Failed::class, self::class);
|
||||
$events->listen(DirectLogin::class, self::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,10 +140,6 @@ class ActivityLog extends Model
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::creating(function (self $model) {
|
||||
$model->timestamp = Carbon::now();
|
||||
});
|
||||
|
||||
static::created(function (self $model) {
|
||||
Event::dispatch(new ActivityLogged($model));
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@ use Illuminate\Support\Str;
|
||||
* @property array|null $features
|
||||
* @property string $docker_image -- deprecated, use $docker_images
|
||||
* @property array<string, string> $docker_images
|
||||
* @property string|null $update_url
|
||||
* @property string $update_url
|
||||
* @property bool $force_outgoing_ip
|
||||
* @property array|null $file_denylist
|
||||
* @property string|null $config_files
|
||||
@@ -95,7 +95,6 @@ class Egg extends Model
|
||||
'config_stop',
|
||||
'config_from',
|
||||
'startup',
|
||||
'update_url',
|
||||
'script_is_privileged',
|
||||
'script_install',
|
||||
'script_entry',
|
||||
|
||||
@@ -15,7 +15,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
* @property string $default_value
|
||||
* @property bool $user_viewable
|
||||
* @property bool $user_editable
|
||||
* @property array $rules
|
||||
* @property string $rules
|
||||
* @property \Carbon\CarbonImmutable $created_at
|
||||
* @property \Carbon\CarbonImmutable $updated_at
|
||||
* @property bool $required
|
||||
@@ -58,14 +58,12 @@ class EggVariable extends Model
|
||||
'default_value' => 'string',
|
||||
'user_viewable' => 'boolean',
|
||||
'user_editable' => 'boolean',
|
||||
'rules' => 'array',
|
||||
'rules.*' => 'string',
|
||||
'rules' => 'string',
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
'user_editable' => 0,
|
||||
'user_viewable' => 0,
|
||||
'rules' => '[]',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
@@ -74,7 +72,6 @@ class EggVariable extends Model
|
||||
'egg_id' => 'integer',
|
||||
'user_viewable' => 'bool',
|
||||
'user_editable' => 'bool',
|
||||
'rules' => 'array',
|
||||
'created_at' => 'immutable_datetime',
|
||||
'updated_at' => 'immutable_datetime',
|
||||
];
|
||||
@@ -82,7 +79,7 @@ class EggVariable extends Model
|
||||
|
||||
public function getRequiredAttribute(): bool
|
||||
{
|
||||
return in_array('required', $this->rules);
|
||||
return in_array('required', explode('|', $this->rules));
|
||||
}
|
||||
|
||||
public function egg(): HasOne
|
||||
|
||||
@@ -244,21 +244,21 @@ class Node extends Model
|
||||
*/
|
||||
public function isViable(int $memory, int $disk, int $cpu): bool
|
||||
{
|
||||
if ($this->memory > 0 && $this->memory_overallocate >= 0) {
|
||||
if ($this->memory_overallocate >= 0) {
|
||||
$memoryLimit = $this->memory * (1 + ($this->memory_overallocate / 100));
|
||||
if ($this->servers_sum_memory + $memory > $memoryLimit) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->disk > 0 && $this->disk_overallocate >= 0) {
|
||||
if ($this->disk_overallocate >= 0) {
|
||||
$diskLimit = $this->disk * (1 + ($this->disk_overallocate / 100));
|
||||
if ($this->servers_sum_disk + $disk > $diskLimit) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->cpu > 0 && $this->cpu_overallocate >= 0) {
|
||||
if ($this->cpu_overallocate >= 0) {
|
||||
$cpuLimit = $this->cpu * (1 + ($this->cpu_overallocate / 100));
|
||||
if ($this->servers_sum_cpu + $cpu > $cpuLimit) {
|
||||
return false;
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Spatie\Permission\Models\Role as BaseRole;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
* @property string $guard_name
|
||||
* @property \Illuminate\Database\Eloquent\Collection|\Spatie\Permission\Models\Permission[] $permissions
|
||||
* @property int|null $permissions_count
|
||||
* @property \Illuminate\Database\Eloquent\Collection|\App\Models\User[] $users
|
||||
* @property int|null $users_count
|
||||
*/
|
||||
class Role extends BaseRole
|
||||
{
|
||||
public const RESOURCE_NAME = 'role';
|
||||
|
||||
public const ROOT_ADMIN = 'Root Admin';
|
||||
|
||||
public const MODEL_SPECIFIC_PERMISSIONS = [
|
||||
'egg' => [
|
||||
'import',
|
||||
'export',
|
||||
],
|
||||
];
|
||||
|
||||
public const SPECIAL_PERMISSIONS = [
|
||||
'settings' => [
|
||||
'view',
|
||||
'update',
|
||||
],
|
||||
];
|
||||
|
||||
public function isRootAdmin(): bool
|
||||
{
|
||||
return $this->name === self::ROOT_ADMIN;
|
||||
}
|
||||
|
||||
public static function getRootAdmin(): self
|
||||
{
|
||||
/** @var self $role */
|
||||
$role = self::findOrCreate(self::ROOT_ADMIN);
|
||||
|
||||
return $role;
|
||||
}
|
||||
}
|
||||
@@ -24,4 +24,62 @@ class Setting extends Model
|
||||
'key' => 'required|string|between:1,255',
|
||||
'value' => 'string',
|
||||
];
|
||||
|
||||
private static array $cache = [];
|
||||
|
||||
private static array $databaseMiss = [];
|
||||
|
||||
/**
|
||||
* Store a new persistent setting in the database.
|
||||
*/
|
||||
public static function set(string $key, string $value = null): void
|
||||
{
|
||||
// Clear item from the cache.
|
||||
self::clearCache($key);
|
||||
|
||||
self::query()->updateOrCreate(['key' => $key], ['value' => $value ?? '']);
|
||||
|
||||
self::$cache[$key] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a persistent setting from the database.
|
||||
*/
|
||||
public static function get(string $key, mixed $default = null): mixed
|
||||
{
|
||||
// If item has already been requested return it from the cache. If
|
||||
// we already know it is missing, immediately return the default value.
|
||||
if (array_key_exists($key, self::$cache)) {
|
||||
return self::$cache[$key];
|
||||
} elseif (array_key_exists($key, self::$databaseMiss)) {
|
||||
return value($default);
|
||||
}
|
||||
|
||||
$instance = self::query()->where('key', $key)->first();
|
||||
if (is_null($instance)) {
|
||||
self::$databaseMiss[$key] = true;
|
||||
|
||||
return value($default);
|
||||
}
|
||||
|
||||
return self::$cache[$key] = $instance->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a key from the database cache.
|
||||
*/
|
||||
public static function forget(string $key)
|
||||
{
|
||||
self::clearCache($key);
|
||||
|
||||
return self::query()->where('key', $key)->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a key from the cache.
|
||||
*/
|
||||
private static function clearCache(string $key): void
|
||||
{
|
||||
unset(self::$cache[$key], self::$databaseMiss[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ class Task extends Model
|
||||
public const ACTION_POWER = 'power';
|
||||
public const ACTION_COMMAND = 'command';
|
||||
public const ACTION_BACKUP = 'backup';
|
||||
public const ACTION_DELETE_FILES = 'delete_files';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
|
||||
@@ -25,9 +25,6 @@ use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
|
||||
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
|
||||
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
|
||||
use App\Notifications\SendPasswordReset as ResetPasswordNotification;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Database\Eloquent\Model as IlluminateModel;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
|
||||
/**
|
||||
* App\Models\User.
|
||||
@@ -43,6 +40,7 @@ use Spatie\Permission\Traits\HasRoles;
|
||||
* @property string|null $remember_token
|
||||
* @property string $language
|
||||
* @property string $timezone
|
||||
* @property bool $root_admin
|
||||
* @property bool $use_totp
|
||||
* @property string|null $totp_secret
|
||||
* @property \Illuminate\Support\Carbon|null $totp_authenticated_at
|
||||
@@ -79,6 +77,7 @@ use Spatie\Permission\Traits\HasRoles;
|
||||
* @method static Builder|User whereNameLast($value)
|
||||
* @method static Builder|User wherePassword($value)
|
||||
* @method static Builder|User whereRememberToken($value)
|
||||
* @method static Builder|User whereRootAdmin($value)
|
||||
* @method static Builder|User whereTotpAuthenticatedAt($value)
|
||||
* @method static Builder|User whereTotpSecret($value)
|
||||
* @method static Builder|User whereUpdatedAt($value)
|
||||
@@ -95,7 +94,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
use AvailableLanguages;
|
||||
use CanResetPassword;
|
||||
use HasAccessTokens;
|
||||
use HasRoles;
|
||||
use Notifiable;
|
||||
|
||||
public const USER_LEVEL_USER = 0;
|
||||
@@ -133,6 +131,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
'totp_secret',
|
||||
'totp_authenticated_at',
|
||||
'gravatar',
|
||||
'root_admin',
|
||||
'oauth',
|
||||
];
|
||||
|
||||
@@ -146,6 +145,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
*/
|
||||
protected $attributes = [
|
||||
'external_id' => null,
|
||||
'root_admin' => false,
|
||||
'language' => 'en',
|
||||
'timezone' => 'UTC',
|
||||
'use_totp' => false,
|
||||
@@ -166,6 +166,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
'name_first' => 'nullable|string|between:0,255',
|
||||
'name_last' => 'nullable|string|between:0,255',
|
||||
'password' => 'sometimes|nullable|string',
|
||||
'root_admin' => 'boolean',
|
||||
'language' => 'string',
|
||||
'timezone' => 'string',
|
||||
'use_totp' => 'boolean',
|
||||
@@ -176,6 +177,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'root_admin' => 'boolean',
|
||||
'use_totp' => 'boolean',
|
||||
'gravatar' => 'boolean',
|
||||
'totp_authenticated_at' => 'datetime',
|
||||
@@ -224,10 +226,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
*/
|
||||
public function toReactObject(): array
|
||||
{
|
||||
return array_merge(collect($this->toArray())->except(['id', 'external_id'])->toArray(), [
|
||||
'root_admin' => $this->isRootAdmin(),
|
||||
'admin' => $this->canAccessPanel(Filament::getPanel('admin')),
|
||||
]);
|
||||
return collect($this->toArray())->except(['id', 'external_id'])->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -316,7 +315,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
|
||||
protected function checkPermission(Server $server, string $permission = ''): bool
|
||||
{
|
||||
if ($this->isRootAdmin() || $server->owner_id === $this->id) {
|
||||
if ($this->root_admin || $server->owner_id === $this->id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -352,23 +351,14 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
|
||||
public function isLastRootAdmin(): bool
|
||||
{
|
||||
$rootAdmins = User::all()->filter(fn ($user) => $user->isRootAdmin());
|
||||
$rootAdmins = User::query()->where('root_admin', true)->limit(2)->get();
|
||||
|
||||
return once(fn () => $rootAdmins->count() === 1 && $rootAdmins->first()->is($this));
|
||||
}
|
||||
|
||||
public function isRootAdmin(): bool
|
||||
{
|
||||
return $this->hasRole(Role::ROOT_ADMIN);
|
||||
}
|
||||
|
||||
public function canAccessPanel(Panel $panel): bool
|
||||
{
|
||||
if ($this->isRootAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->roles()->count() >= 1 && $this->getAllPermissions()->count() >= 1;
|
||||
return $this->root_admin;
|
||||
}
|
||||
|
||||
public function getFilamentName(): string
|
||||
@@ -380,13 +370,4 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
{
|
||||
return 'https://gravatar.com/avatar/' . md5(strtolower($this->email));
|
||||
}
|
||||
|
||||
public function canTarget(IlluminateModel $user): bool
|
||||
{
|
||||
if ($this->isRootAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $user instanceof User && !$user->isRootAdmin();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
class ApiKeyPolicy
|
||||
{
|
||||
use DefaultPolicies;
|
||||
|
||||
protected string $modelName = 'apikey';
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
class DatabaseHostPolicy
|
||||
{
|
||||
use DefaultPolicies;
|
||||
|
||||
protected string $modelName = 'databasehost';
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
class DatabasePolicy
|
||||
{
|
||||
use DefaultPolicies;
|
||||
|
||||
protected string $modelName = 'database';
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
trait DefaultPolicies
|
||||
{
|
||||
/**
|
||||
* Determine whether the user can view any models.
|
||||
*/
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return $user->can('viewList ' . $this->modelName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view the model.
|
||||
*/
|
||||
public function view(User $user, Model $model): bool
|
||||
{
|
||||
return $user->can('view ' . $this->modelName, $model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can create models.
|
||||
*/
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return $user->can('create ' . $this->modelName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the model.
|
||||
*/
|
||||
public function update(User $user, Model $model): bool
|
||||
{
|
||||
return $user->can('update ' . $this->modelName, $model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete the model.
|
||||
*/
|
||||
public function delete(User $user, Model $model): bool
|
||||
{
|
||||
return $user->can('delete ' . $this->modelName, $model);
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,12 @@
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
class EggPolicy
|
||||
{
|
||||
use DefaultPolicies;
|
||||
|
||||
protected string $modelName = 'egg';
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
class MountPolicy
|
||||
{
|
||||
use DefaultPolicies;
|
||||
|
||||
protected string $modelName = 'mount';
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user