mirror of
https://github.com/pelican-dev/panel.git
synced 2026-05-04 18:00:48 +03:00
Compare commits
54 Commits
release/v1
...
v1.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aafe17174f | ||
|
|
a067419d6e | ||
|
|
6117282909 | ||
|
|
967d02612d | ||
|
|
0cd20eb444 | ||
|
|
4dba73163b | ||
|
|
aab3817244 | ||
|
|
1785883c55 | ||
|
|
4c19144640 | ||
|
|
a8a2668754 | ||
|
|
6734fe3be6 | ||
|
|
ff0cde5152 | ||
|
|
b098d20afb | ||
|
|
3ca77765e6 | ||
|
|
476eccca53 | ||
|
|
f686eda718 | ||
|
|
0f58643cf2 | ||
|
|
83ba05d7fb | ||
|
|
66841f5fab | ||
|
|
c03ef43767 | ||
|
|
805461aaf0 | ||
|
|
6f15537d77 | ||
|
|
4fc8d98a0f | ||
|
|
9779365432 | ||
|
|
6e998498e3 | ||
|
|
7d0b9af21a | ||
|
|
116175ba60 | ||
|
|
1e841ac40d | ||
|
|
3401703ccd | ||
|
|
f7cb42e008 | ||
|
|
b6e55795c1 | ||
|
|
17c0041bfd | ||
|
|
478948c81b | ||
|
|
6b706de23d | ||
|
|
508e1c9645 | ||
|
|
3e7c29d264 | ||
|
|
fc643f57f9 | ||
|
|
68a0cbbf10 | ||
|
|
8497e8b009 | ||
|
|
8c64a4ad55 | ||
|
|
49e93c1379 | ||
|
|
d7b5966e1b | ||
|
|
e152efc5f9 | ||
|
|
58307c15a3 | ||
|
|
40810877e0 | ||
|
|
818781ca66 | ||
|
|
05477c711f | ||
|
|
20b06b7b39 | ||
|
|
c2b1a98d29 | ||
|
|
0ff429215d | ||
|
|
d1ca21de9f | ||
|
|
d0c89b0729 | ||
|
|
ffadf9ac16 | ||
|
|
bf23389dba |
10
.dockerignore
Normal file
10
.dockerignore
Normal file
@@ -0,0 +1,10 @@
|
||||
.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,7 +4,6 @@ APP_KEY=
|
||||
APP_TIMEZONE=UTC
|
||||
APP_URL=http://panel.test
|
||||
APP_LOCALE=en
|
||||
APP_INSTALLED=false
|
||||
|
||||
LOG_CHANNEL=daily
|
||||
LOG_STACK=single
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -33,7 +33,6 @@ body:
|
||||
attributes:
|
||||
label: Panel Version
|
||||
description: Version number of your Panel (latest is not a version)
|
||||
placeholder: 1.4.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -42,7 +41,6 @@ body:
|
||||
attributes:
|
||||
label: Wings Version
|
||||
description: Version number of your Wings (latest is not a version)
|
||||
placeholder: 1.4.2
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -68,7 +66,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 | nc pelipaste.com 99`
|
||||
Panel: `tail -n 150 /var/www/pelican/storage/logs/laravel-$(date +%F).log | curl -X POST -F 'c=@-' paste.pelistuff.com`
|
||||
placeholder: "https://pelipaste.com/a1h6z"
|
||||
render: bash
|
||||
validations:
|
||||
|
||||
83
.github/docker/entrypoint.sh
vendored
83
.github/docker/entrypoint.sh
vendored
@@ -1,81 +1,58 @@
|
||||
#!/bin/ash -e
|
||||
cd /app
|
||||
|
||||
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/
|
||||
#mkdir -p /var/log/supervisord/ /var/log/php8/ \
|
||||
|
||||
## check for .env file and generate app keys if missing
|
||||
if [ -f /app/var/.env ]; then
|
||||
if [ -f /pelican-data/.env ]; then
|
||||
echo "external vars exist."
|
||||
rm -rf /app/.env
|
||||
ln -s /app/var/.env /app/
|
||||
rm -rf /var/www/html/.env
|
||||
else
|
||||
echo "external vars don't exist."
|
||||
rm -rf /app/.env
|
||||
touch /app/var/.env
|
||||
rm -rf /var/www/html/.env
|
||||
touch /pelican-data/.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" > /app/var/.env
|
||||
echo -e "APP_KEY=$APP_KEY" > /pelican-data/.env
|
||||
else
|
||||
echo -e "APP_KEY exists in environment, using that."
|
||||
echo -e "APP_KEY=$APP_KEY" > /app/var/.env
|
||||
echo -e "APP_KEY=$APP_KEY" > /pelican-data/.env
|
||||
fi
|
||||
|
||||
ln -s /app/var/.env /app/
|
||||
fi
|
||||
|
||||
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
|
||||
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
|
||||
else
|
||||
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
|
||||
echo "APP_KEY is already set."
|
||||
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 and Seeding D.B"
|
||||
php artisan migrate --seed --force
|
||||
echo -e "Migrating Database"
|
||||
php artisan migrate --force
|
||||
|
||||
## start cronjobs for the queue
|
||||
echo -e "Starting cron jobs."
|
||||
crond -L /var/log/crond -l 5
|
||||
|
||||
echo -e "Starting supervisord."
|
||||
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"
|
||||
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 /app/artisan queue:work --queue=high,standard,low --sleep=3 --tries=3
|
||||
user=nginx
|
||||
command=/usr/local/bin/php /var/www/html/artisan queue:work --queue=high,standard,low --sleep=3 --tries=3
|
||||
user=www-data
|
||||
autostart=true
|
||||
autorestart=true
|
||||
|
||||
[program:nginx]
|
||||
command=/usr/sbin/nginx -g 'daemon off;'
|
||||
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
|
||||
priority=10
|
||||
stdout_events_enabled=true
|
||||
stderr_events_enabled=true
|
||||
11
Caddyfile
Normal file
11
Caddyfile
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
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,41 +1,58 @@
|
||||
# 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
|
||||
# Pelican Production Dockerfile
|
||||
|
||||
FROM node:20-alpine AS yarn
|
||||
#FROM --platform=$TARGETOS/$TARGETARCH node:20-alpine AS yarn
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
COPY . ./
|
||||
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 .
|
||||
RUN yarn install --frozen-lockfile && yarn run build:production
|
||||
|
||||
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
|
||||
FROM php:8.3-fpm-alpine
|
||||
# FROM --platform=$TARGETOS/$TARGETARCH php:8.3-fpm-alpine
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
EXPOSE 80 443
|
||||
ENTRYPOINT [ "/bin/ash", ".github/docker/entrypoint.sh" ]
|
||||
CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ]
|
||||
|
||||
54
app/Console/Commands/Environment/RedisSetupCommand.php
Normal file
54
app/Console/Commands/Environment/RedisSetupCommand.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,7 @@ class MakeUserCommand extends Command
|
||||
['UUID', $user->uuid],
|
||||
['Email', $user->email],
|
||||
['Username', $user->username],
|
||||
['Admin', $user->root_admin ? 'Yes' : 'No'],
|
||||
['Admin', $user->isRootAdmin() ? 'Yes' : 'No'],
|
||||
]);
|
||||
|
||||
return 0;
|
||||
|
||||
16
app/Enums/RolePermissionModels.php
Normal file
16
app/Enums/RolePermissionModels.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?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';
|
||||
}
|
||||
12
app/Enums/RolePermissionPrefixes.php
Normal file
12
app/Enums/RolePermissionPrefixes.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum RolePermissionPrefixes: string
|
||||
{
|
||||
case ViewAny = 'viewList';
|
||||
case View = 'view';
|
||||
case Create = 'create';
|
||||
case Update = 'update';
|
||||
case Delete = 'delete';
|
||||
}
|
||||
@@ -7,11 +7,11 @@ 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\Facades\Filament;
|
||||
use Filament\Forms\Components\Wizard;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
@@ -44,11 +44,22 @@ class PanelInstaller extends SimplePage implements HasForms
|
||||
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()
|
||||
{
|
||||
if (is_installed()) {
|
||||
abort(404);
|
||||
}
|
||||
abort_unless(self::show(), 404);
|
||||
|
||||
$this->form->fill();
|
||||
}
|
||||
@@ -103,10 +114,14 @@ class PanelInstaller extends SimplePage implements HasForms
|
||||
$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()) {
|
||||
@@ -116,10 +131,10 @@ class PanelInstaller extends SimplePage implements HasForms
|
||||
// Create first admin user
|
||||
$userData = array_get($inputs, 'user');
|
||||
$userData['root_admin'] = true;
|
||||
app(UserCreationService::class)->handle($userData);
|
||||
$user = app(UserCreationService::class)->handle($userData);
|
||||
|
||||
// Install setup complete
|
||||
$this->writeToEnvironment(['APP_INSTALLED' => 'true']);
|
||||
$this->writeToEnvironment(['APP_INSTALLER' => 'false']);
|
||||
|
||||
$this->rememberData();
|
||||
|
||||
@@ -128,7 +143,9 @@ class PanelInstaller extends SimplePage implements HasForms
|
||||
->success()
|
||||
->send();
|
||||
|
||||
redirect()->intended(Filament::getUrl());
|
||||
auth()->loginUsingId($user->id);
|
||||
|
||||
return redirect('/admin');
|
||||
} catch (Exception $exception) {
|
||||
report($exception);
|
||||
|
||||
|
||||
@@ -16,11 +16,11 @@ class AdminUserStep
|
||||
->label('Admin E-Mail')
|
||||
->required()
|
||||
->email()
|
||||
->default('admin@example.com'),
|
||||
->placeholder('admin@example.com'),
|
||||
TextInput::make('user.username')
|
||||
->label('Admin Username')
|
||||
->required()
|
||||
->default('admin'),
|
||||
->placeholder('admin'),
|
||||
TextInput::make('user.password')
|
||||
->label('Admin Password')
|
||||
->required()
|
||||
|
||||
@@ -7,7 +7,7 @@ use Filament\Forms\Components\Wizard\Step;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Exceptions\Halt;
|
||||
use Illuminate\Database\DatabaseManager;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use PDOException;
|
||||
|
||||
class DatabaseStep
|
||||
@@ -61,9 +61,6 @@ class DatabaseStep
|
||||
->afterValidation(function (Get $get) {
|
||||
$driver = $get('env.DB_CONNECTION');
|
||||
if ($driver !== 'sqlite') {
|
||||
/** @var DatabaseManager $database */
|
||||
$database = app(DatabaseManager::class);
|
||||
|
||||
try {
|
||||
config()->set('database.connections._panel_install_test', [
|
||||
'driver' => $driver,
|
||||
@@ -77,7 +74,7 @@ class DatabaseStep
|
||||
'strict' => true,
|
||||
]);
|
||||
|
||||
$database->connection('_panel_install_test')->getPdo();
|
||||
DB::connection('_panel_install_test')->getPdo();
|
||||
} catch (PDOException $exception) {
|
||||
Notification::make()
|
||||
->title('Database connection failed')
|
||||
@@ -85,7 +82,7 @@ class DatabaseStep
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
$database->disconnect('_panel_install_test');
|
||||
DB::disconnect('_panel_install_test');
|
||||
|
||||
throw new Halt('Database connection failed');
|
||||
}
|
||||
|
||||
@@ -23,9 +23,9 @@ class EnvironmentStep
|
||||
];
|
||||
|
||||
public const QUEUE_DRIVERS = [
|
||||
'sync' => 'Sync',
|
||||
'database' => 'Database',
|
||||
'redis' => 'Redis',
|
||||
'sync' => 'Synchronous',
|
||||
];
|
||||
|
||||
public const DATABASE_DRIVERS = [
|
||||
@@ -76,7 +76,7 @@ class EnvironmentStep
|
||||
ToggleButtons::make('env.QUEUE_CONNECTION')
|
||||
->label('Queue Driver')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The driver used for handling queues. We recommend "Database".')
|
||||
->hintIconTooltip('The driver used for handling queues. We recommend "Sync" or "Database".')
|
||||
->required()
|
||||
->inline()
|
||||
->options(self::QUEUE_DRIVERS)
|
||||
|
||||
@@ -2,8 +2,13 @@
|
||||
|
||||
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
|
||||
{
|
||||
@@ -37,6 +42,26 @@ class RedisStep
|
||||
->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');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,12 +49,18 @@ class Settings extends Page implements HasForms
|
||||
$this->form->fill();
|
||||
}
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
return auth()->user()->can('view settings');
|
||||
}
|
||||
|
||||
protected function getFormSchema(): array
|
||||
{
|
||||
return [
|
||||
Tabs::make('Tabs')
|
||||
->columns()
|
||||
->persistTabInQueryString()
|
||||
->disabled(fn () => !auth()->user()->can('update settings'))
|
||||
->tabs([
|
||||
Tab::make('general')
|
||||
->label('General')
|
||||
@@ -86,6 +92,7 @@ class Settings extends Page implements HasForms
|
||||
TextInput::make('APP_NAME')
|
||||
->label('App Name')
|
||||
->required()
|
||||
->alphaNum()
|
||||
->default(env('APP_NAME', 'Pelican')),
|
||||
TextInput::make('APP_FAVICON')
|
||||
->label('App Favicon')
|
||||
@@ -146,10 +153,12 @@ class Settings extends Page implements HasForms
|
||||
->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',
|
||||
@@ -225,6 +234,7 @@ class Settings extends Page implements HasForms
|
||||
->label('Send Test Mail')
|
||||
->icon('tabler-send')
|
||||
->hidden(fn (Get $get) => $get('MAIL_MAILER') === 'log')
|
||||
->authorize(fn () => auth()->user()->can('update settings'))
|
||||
->action(function () {
|
||||
try {
|
||||
MailNotification::route('mail', auth()->user()->email)
|
||||
@@ -274,7 +284,6 @@ class Settings extends Page implements HasForms
|
||||
->default(env('MAIL_PORT', config('mail.mailers.smtp.port'))),
|
||||
TextInput::make('MAIL_USERNAME')
|
||||
->label('Username')
|
||||
->required()
|
||||
->default(env('MAIL_USERNAME', config('mail.mailers.smtp.username'))),
|
||||
TextInput::make('MAIL_PASSWORD')
|
||||
->label('Password')
|
||||
@@ -298,7 +307,7 @@ class Settings extends Page implements HasForms
|
||||
TextInput::make('MAILGUN_SECRET')
|
||||
->label('Secret')
|
||||
->required()
|
||||
->default(env('MAIL_USERNAME', config('services.mailgun.secret'))),
|
||||
->default(env('MAILGUN_SECRET', config('services.mailgun.secret'))),
|
||||
TextInput::make('MAILGUN_ENDPOINT')
|
||||
->label('Endpoint')
|
||||
->required()
|
||||
@@ -561,12 +570,9 @@ class Settings extends Page implements HasForms
|
||||
return [
|
||||
Action::make('save')
|
||||
->action('save')
|
||||
->authorize(fn () => auth()->user()->can('update settings'))
|
||||
->keyBindings(['mod+s']),
|
||||
];
|
||||
|
||||
}
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,8 @@ class ListDatabaseHosts extends ListRecords
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete databasehost')),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -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,7 +48,8 @@ class ListDatabases extends ListRecords
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete database')),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ class CreateEgg extends CreateRecord
|
||||
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
|
||||
$data['default_value'] ??= '';
|
||||
$data['description'] ??= '';
|
||||
$data['rules'] ??= '';
|
||||
$data['rules'] ??= [];
|
||||
$data['user_viewable'] ??= '';
|
||||
$data['user_editable'] ??= '';
|
||||
|
||||
@@ -143,7 +143,7 @@ class CreateEgg extends CreateRecord
|
||||
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
|
||||
$data['default_value'] ??= '';
|
||||
$data['description'] ??= '';
|
||||
$data['rules'] ??= '';
|
||||
$data['rules'] ??= [];
|
||||
$data['user_viewable'] ??= '';
|
||||
$data['user_editable'] ??= '';
|
||||
|
||||
@@ -173,7 +173,30 @@ class CreateEgg extends CreateRecord
|
||||
Checkbox::make('user_viewable')->label('Viewable'),
|
||||
Checkbox::make('user_editable')->label('Editable'),
|
||||
]),
|
||||
Textarea::make('rules')->columnSpanFull(),
|
||||
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',
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Install Script')
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
|
||||
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;
|
||||
@@ -22,12 +25,9 @@ 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
|
||||
{
|
||||
@@ -144,7 +144,7 @@ class EditEgg extends EditRecord
|
||||
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
|
||||
$data['default_value'] ??= '';
|
||||
$data['description'] ??= '';
|
||||
$data['rules'] ??= '';
|
||||
$data['rules'] ??= [];
|
||||
$data['user_viewable'] ??= '';
|
||||
$data['user_editable'] ??= '';
|
||||
|
||||
@@ -153,7 +153,7 @@ class EditEgg extends EditRecord
|
||||
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
|
||||
$data['default_value'] ??= '';
|
||||
$data['description'] ??= '';
|
||||
$data['rules'] ??= '';
|
||||
$data['rules'] ??= [];
|
||||
$data['user_viewable'] ??= '';
|
||||
$data['user_editable'] ??= '';
|
||||
|
||||
@@ -183,7 +183,30 @@ class EditEgg extends EditRecord
|
||||
Checkbox::make('user_viewable')->label('Viewable'),
|
||||
Checkbox::make('user_editable')->label('Editable'),
|
||||
]),
|
||||
TextInput::make('rules')->columnSpanFull(),
|
||||
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',
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Install Script')
|
||||
@@ -222,14 +245,13 @@ 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')),
|
||||
|
||||
}, 'egg-' . $egg->getKebabName() . '.json'))
|
||||
->authorize(fn () => auth()->user()->can('export egg')),
|
||||
Actions\Action::make('importEgg')
|
||||
->label('Import')
|
||||
->form([
|
||||
@@ -298,8 +320,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,11 +55,13 @@ 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')),
|
||||
}, 'egg-' . $egg->getKebabName() . '.json'))
|
||||
->authorize(fn () => auth()->user()->can('export egg')),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete egg')),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
@@ -138,7 +140,8 @@ class ListEggs extends ListRecords
|
||||
->title('Import Success')
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
})
|
||||
->authorize(fn () => auth()->user()->can('import egg')),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,8 @@ class ListMounts extends ListRecords
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete mount')),
|
||||
]),
|
||||
])
|
||||
->emptyStateIcon('tabler-layers-linked')
|
||||
|
||||
@@ -84,7 +84,8 @@ class ListNodes extends ListRecords
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete node')),
|
||||
]),
|
||||
])
|
||||
->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\Set;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
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\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\TextInputColumn;
|
||||
use Filament\Tables\Table;
|
||||
@@ -152,7 +152,8 @@ class AllocationsRelationManager extends RelationManager
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete allocation')),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
146
app/Filament/Resources/RoleResource.php
Normal file
146
app/Filament/Resources/RoleResource.php
Normal file
@@ -0,0 +1,146 @@
|
||||
<?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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
48
app/Filament/Resources/RoleResource/Pages/CreateRole.php
Normal file
48
app/Filament/Resources/RoleResource/Pages/CreateRole.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
56
app/Filament/Resources/RoleResource/Pages/EditRole.php
Normal file
56
app/Filament/Resources/RoleResource/Pages/EditRole.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?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')),
|
||||
];
|
||||
}
|
||||
}
|
||||
68
app/Filament/Resources/RoleResource/Pages/ListRoles.php
Normal file
68
app/Filament/Resources/RoleResource/Pages/ListRoles.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,7 @@ class CreateServer extends CreateRecord
|
||||
])
|
||||
->relationship('user', 'username')
|
||||
->searchable(['username', 'email'])
|
||||
->getOptionLabelFromRecordUsing(fn (User $user) => "$user->email | $user->username " . ($user->root_admin ? '(admin)' : ''))
|
||||
->getOptionLabelFromRecordUsing(fn (User $user) => "$user->email | $user->username " . ($user->isRootAdmin() ? '(admin)' : ''))
|
||||
->createOptionForm([
|
||||
Forms\Components\TextInput::make('username')
|
||||
->alphaNum()
|
||||
@@ -98,21 +98,6 @@ 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);
|
||||
@@ -321,9 +306,9 @@ class CreateServer extends CreateRecord
|
||||
->completedIcon('tabler-check')
|
||||
->columns([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 2,
|
||||
'lg' => 4,
|
||||
'sm' => 4,
|
||||
'md' => 4,
|
||||
'lg' => 6,
|
||||
])
|
||||
->schema([
|
||||
Forms\Components\Select::make('egg_id')
|
||||
@@ -333,7 +318,7 @@ class CreateServer extends CreateRecord
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 2,
|
||||
'lg' => 3,
|
||||
'lg' => 4,
|
||||
])
|
||||
->searchable()
|
||||
->preload()
|
||||
@@ -390,29 +375,51 @@ 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')
|
||||
@@ -443,8 +450,7 @@ class CreateServer extends CreateRecord
|
||||
|
||||
$text = Forms\Components\TextInput::make('variable_value')
|
||||
->hidden($this->shouldHideComponent(...))
|
||||
->maxLength(255)
|
||||
->required(fn (Forms\Get $get) => in_array('required', explode('|', $get('rules'))))
|
||||
->required(fn (Forms\Get $get) => in_array('required', $get('rules')))
|
||||
->rules(
|
||||
fn (Forms\Get $get): Closure => function (string $attribute, $value, Closure $fail) use ($get) {
|
||||
$validator = Validator::make(['validatorkey' => $value], [
|
||||
@@ -471,7 +477,7 @@ class CreateServer extends CreateRecord
|
||||
->live(onBlur: true)
|
||||
->hintIcon('tabler-code')
|
||||
->label(fn (Forms\Get $get) => $get('name'))
|
||||
->hintIconTooltip(fn (Forms\Get $get) => $get('rules'))
|
||||
->hintIconTooltip(fn (Forms\Get $get) => implode('|', $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) {
|
||||
@@ -808,7 +814,7 @@ class CreateServer extends CreateRecord
|
||||
|
||||
private function shouldHideComponent(Forms\Get $get, Forms\Components\Component $component): bool
|
||||
{
|
||||
$containsRuleIn = str($get('rules'))->explode('|')->reduce(
|
||||
$containsRuleIn = collect($get('rules'))->reduce(
|
||||
fn ($result, $value) => $result === true && !str($value)->startsWith('in:'), true
|
||||
);
|
||||
|
||||
@@ -825,7 +831,7 @@ class CreateServer extends CreateRecord
|
||||
|
||||
private function getSelectOptionsFromRules(Forms\Get $get): array
|
||||
{
|
||||
$inRule = str($get('rules'))->explode('|')->reduce(
|
||||
$inRule = collect($get('rules'))->reduce(
|
||||
fn ($result, $value) => str($value)->startsWith('in:') ? $value : $result, ''
|
||||
);
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ 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
|
||||
@@ -473,7 +474,21 @@ class EditServer extends EditRecord
|
||||
->columnSpan(6),
|
||||
|
||||
Forms\Components\Repeater::make('server_variables')
|
||||
->relationship('serverVariables')
|
||||
->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;
|
||||
})
|
||||
->grid()
|
||||
->mutateRelationshipDataBeforeSaveUsing(function (array &$data): array {
|
||||
foreach ($data as $key => $value) {
|
||||
@@ -489,7 +504,7 @@ class EditServer extends EditRecord
|
||||
|
||||
$text = Forms\Components\TextInput::make('variable_value')
|
||||
->hidden($this->shouldHideComponent(...))
|
||||
->required(fn (ServerVariable $serverVariable) => in_array('required', explode('|', $serverVariable->variable->rules)))
|
||||
->required(fn (ServerVariable $serverVariable) => $serverVariable->variable->getRequiredAttribute())
|
||||
->rules([
|
||||
fn (ServerVariable $serverVariable): Closure => function (string $attribute, $value, Closure $fail) use ($serverVariable) {
|
||||
$validator = Validator::make(['validatorkey' => $value], [
|
||||
@@ -516,7 +531,7 @@ class EditServer extends EditRecord
|
||||
->live(onBlur: true)
|
||||
->hintIcon('tabler-code')
|
||||
->label(fn (ServerVariable $serverVariable) => $serverVariable->variable->name)
|
||||
->hintIconTooltip(fn (ServerVariable $serverVariable) => $serverVariable->variable->rules)
|
||||
->hintIconTooltip(fn (ServerVariable $serverVariable) => implode('|', $serverVariable->variable->rules))
|
||||
->prefix(fn (ServerVariable $serverVariable) => '{{' . $serverVariable->variable->env_variable . '}}')
|
||||
->helperText(fn (ServerVariable $serverVariable) => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description);
|
||||
}
|
||||
@@ -720,12 +735,16 @@ class EditServer extends EditRecord
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make('Delete')
|
||||
Actions\Action::make('Delete')
|
||||
->successRedirectUrl(route('filament.admin.resources.servers.index'))
|
||||
->color('danger')
|
||||
->label('Delete')
|
||||
->after(fn (Server $server) => resolve(ServerDeletionService::class)->handle($server))
|
||||
->requiresConfirmation(),
|
||||
->requiresConfirmation()
|
||||
->action(function (Server $server) {
|
||||
resolve(ServerDeletionService::class)->handle($server);
|
||||
|
||||
return redirect(ListServers::getUrl());
|
||||
}),
|
||||
Actions\Action::make('console')
|
||||
->label('Console')
|
||||
->icon('tabler-terminal')
|
||||
@@ -757,28 +776,24 @@ class EditServer extends EditRecord
|
||||
];
|
||||
}
|
||||
|
||||
private function shouldHideComponent(Forms\Get $get, Forms\Components\Component $component): bool
|
||||
private function shouldHideComponent(ServerVariable $serverVariable, Forms\Components\Component $component): bool
|
||||
{
|
||||
$containsRuleIn = str($get('rules'))->explode('|')->reduce(
|
||||
fn ($result, $value) => $result === true && !str($value)->startsWith('in:'), true
|
||||
);
|
||||
$containsRuleIn = array_first($serverVariable->variable->rules, fn ($value) => str($value)->startsWith('in:'), false);
|
||||
|
||||
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(Forms\Get $get): array
|
||||
private function getSelectOptionsFromRules(ServerVariable $serverVariable): array
|
||||
{
|
||||
$inRule = str($get('rules'))->explode('|')->reduce(
|
||||
fn ($result, $value) => str($value)->startsWith('in:') ? $value : $result, ''
|
||||
);
|
||||
$inRule = array_first($serverVariable->variable->rules, fn ($value) => str($value)->startsWith('in:'));
|
||||
|
||||
return str($inRule)
|
||||
->after('in:')
|
||||
|
||||
@@ -4,6 +4,7 @@ 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;
|
||||
@@ -76,7 +77,13 @@ class ListServers extends ListRecords
|
||||
->actions([
|
||||
Tables\Actions\Action::make('View')
|
||||
->icon('tabler-terminal')
|
||||
->url(fn (Server $server) => "/server/$server->uuid_short"),
|
||||
->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;
|
||||
}),
|
||||
Tables\Actions\EditAction::make(),
|
||||
])
|
||||
->emptyStateIcon('tabler-brand-docker')
|
||||
|
||||
@@ -3,13 +3,16 @@
|
||||
namespace App\Filament\Resources\UserResource\Pages;
|
||||
|
||||
use App\Filament\Resources\UserResource;
|
||||
use App\Services\Exceptions\FilamentExceptionHandler;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use Filament\Forms;
|
||||
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 Filament\Resources\Pages\EditRecord;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class EditUser extends EditRecord
|
||||
@@ -20,54 +23,33 @@ class EditUser extends EditRecord
|
||||
return $form
|
||||
->schema([
|
||||
Section::make()->schema([
|
||||
Forms\Components\TextInput::make('username')->required()->maxLength(255),
|
||||
Forms\Components\TextInput::make('email')->email()->required()->maxLength(255),
|
||||
|
||||
Forms\Components\TextInput::make('password')
|
||||
TextInput::make('username')->required()->maxLength(255),
|
||||
TextInput::make('email')->email()->required()->maxLength(255),
|
||||
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(),
|
||||
|
||||
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')
|
||||
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 [
|
||||
Actions\DeleteAction::make()
|
||||
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'),
|
||||
@@ -78,9 +60,4 @@ class EditUser extends EditRecord
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function exception($exception, $stopPropagation): void
|
||||
{
|
||||
(new FilamentExceptionHandler())->handle($exception, $stopPropagation);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,22 @@
|
||||
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;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
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
|
||||
{
|
||||
@@ -21,101 +29,102 @@ class ListUsers extends ListRecords
|
||||
return $table
|
||||
->searchable(false)
|
||||
->columns([
|
||||
Tables\Columns\ImageColumn::make('picture')
|
||||
ImageColumn::make('picture')
|
||||
->visibleFrom('lg')
|
||||
->label('')
|
||||
->extraImgAttributes(['class' => 'rounded-full'])
|
||||
->defaultImageUrl(fn (User $user) => 'https://gravatar.com/avatar/' . md5(strtolower($user->email))),
|
||||
Tables\Columns\TextColumn::make('external_id')
|
||||
TextColumn::make('external_id')
|
||||
->searchable()
|
||||
->hidden(),
|
||||
Tables\Columns\TextColumn::make('uuid')
|
||||
TextColumn::make('uuid')
|
||||
->label('UUID')
|
||||
->hidden()
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('username')
|
||||
TextColumn::make('username')
|
||||
->searchable(),
|
||||
Tables\Columns\TextColumn::make('email')
|
||||
TextColumn::make('email')
|
||||
->searchable()
|
||||
->icon('tabler-mail'),
|
||||
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')
|
||||
IconColumn::make('use_totp')
|
||||
->label('2FA')
|
||||
->visibleFrom('lg')
|
||||
->icon(fn (User $user) => $user->use_totp ? 'tabler-lock' : 'tabler-lock-open-off')
|
||||
->boolean()->sortable(),
|
||||
Tables\Columns\TextColumn::make('servers_count')
|
||||
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')
|
||||
->counts('servers')
|
||||
->icon('tabler-server')
|
||||
->label('Servers'),
|
||||
Tables\Columns\TextColumn::make('subusers_count')
|
||||
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([
|
||||
Tables\Actions\EditAction::make(),
|
||||
EditAction::make(),
|
||||
])
|
||||
->checkIfRecordIsSelectableUsing(fn (User $user) => auth()->user()->id !== $user->id && !$user->servers_count)
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete user')),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make('create')
|
||||
CreateAction::make('create')
|
||||
->label('Create User')
|
||||
->createAnother(false)
|
||||
->form([
|
||||
Forms\Components\Grid::make()
|
||||
Grid::make()
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('username')
|
||||
TextInput::make('username')
|
||||
->alphaNum()
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Forms\Components\TextInput::make('email')
|
||||
TextInput::make('email')
|
||||
->email()
|
||||
->required()
|
||||
->unique()
|
||||
->maxLength(255),
|
||||
|
||||
Forms\Components\TextInput::make('password')
|
||||
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(),
|
||||
|
||||
Forms\Components\ToggleButtons::make('root_admin')
|
||||
->label('Administrator (Root)')
|
||||
->options([
|
||||
false => 'No',
|
||||
true => 'Admin',
|
||||
])
|
||||
->colors([
|
||||
false => 'primary',
|
||||
true => 'danger',
|
||||
])
|
||||
->inline()
|
||||
->required()
|
||||
->default(false),
|
||||
CheckboxList::make('roles')
|
||||
->disableOptionWhen(fn (string $value): bool => $value == Role::getRootAdmin()->id)
|
||||
->relationship('roles', 'name')
|
||||
->dehydrated()
|
||||
->label('Admin Roles')
|
||||
->columnSpanFull()
|
||||
->bulkToggleable(false),
|
||||
]),
|
||||
])
|
||||
->successRedirectUrl(route('filament.admin.resources.users.index'))
|
||||
->action(function (array $data) {
|
||||
resolve(UserCreationService::class)->handle($data);
|
||||
Notification::make()->title('User Created!')->success()->send();
|
||||
$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();
|
||||
|
||||
return redirect()->route('filament.admin.resources.users.index');
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<?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,6 +13,7 @@ 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
|
||||
{
|
||||
@@ -75,6 +76,19 @@ 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->root_admin) {
|
||||
if (!$user->isRootAdmin()) {
|
||||
$builder->whereRaw('1 = 2');
|
||||
} else {
|
||||
$builder = $type === 'admin-all'
|
||||
|
||||
@@ -13,6 +13,7 @@ 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
|
||||
{
|
||||
@@ -32,15 +33,16 @@ 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) {
|
||||
->where(function (Builder $builder) use ($subusers, $rootAdmins) {
|
||||
$builder->whereNull('users.id')
|
||||
->orWhere('users.root_admin', 0)
|
||||
->orWhereNotIn('users.id', $rootAdmins)
|
||||
->orWhereIn('users.id', $subusers);
|
||||
});
|
||||
})
|
||||
|
||||
@@ -140,7 +140,7 @@ class SftpAuthenticationController extends Controller
|
||||
*/
|
||||
protected function validateSftpAccess(User $user, Server $server): void
|
||||
{
|
||||
if (!$user->root_admin && $server->owner_id !== $user->id) {
|
||||
if (!$user->isRootAdmin() && $server->owner_id !== $user->id) {
|
||||
$permissions = $this->permissions->handle($server, $user);
|
||||
|
||||
if (!in_array(Permission::ACTION_FILE_SFTP, $permissions)) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Filament\Pages\Installer\PanelInstaller;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -17,8 +18,12 @@ 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(): View
|
||||
public function index()
|
||||
{
|
||||
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()->root_admin) {
|
||||
if (!$request->user() || !$request->user()->isRootAdmin()) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ class AuthenticateApplicationUser
|
||||
{
|
||||
/** @var \App\Models\User|null $user */
|
||||
$user = $request->user();
|
||||
if (!$user || !$user->root_admin) {
|
||||
if (!$user || !$user->isRootAdmin()) {
|
||||
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->root_admin) {
|
||||
if ($user->id !== $server->owner_id && !$user->isRootAdmin()) {
|
||||
// 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->root_admin || !$request->routeIs($this->except)) {
|
||||
if (!$user->isRootAdmin() || !$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->root_admin) {
|
||||
} elseif ($level === self::LEVEL_ADMIN && !$user->isRootAdmin()) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ abstract class AdminFormRequest extends FormRequest
|
||||
return false;
|
||||
}
|
||||
|
||||
return (bool) $this->user()->root_admin;
|
||||
return $this->user()->isRootAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,7 +22,6 @@ class NewUserFormRequest extends AdminFormRequest
|
||||
'name_last',
|
||||
'password',
|
||||
'language',
|
||||
'root_admin',
|
||||
])->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ class UserFormRequest extends AdminFormRequest
|
||||
'name_last',
|
||||
'password',
|
||||
'language',
|
||||
'root_admin',
|
||||
])->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?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;
|
||||
}
|
||||
13
app/Http/Requests/Api/Application/Roles/GetRoleRequest.php
Normal file
13
app/Http/Requests/Api/Application/Roles/GetRoleRequest.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?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;
|
||||
}
|
||||
21
app/Http/Requests/Api/Application/Roles/StoreRoleRequest.php
Normal file
21
app/Http/Requests/Api/Application/Roles/StoreRoleRequest.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\Application\Roles;
|
||||
|
||||
class UpdateRoleRequest extends StoreRoleRequest
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?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,7 +26,6 @@ class StoreUserRequest extends ApplicationApiRequest
|
||||
'password',
|
||||
'language',
|
||||
'timezone',
|
||||
'root_admin',
|
||||
])->toArray();
|
||||
|
||||
$response['first_name'] = $rules['name_first'];
|
||||
@@ -56,7 +55,6 @@ class StoreUserRequest extends ApplicationApiRequest
|
||||
'external_id' => 'Third Party Identifier',
|
||||
'name_first' => 'First Name',
|
||||
'name_last' => 'Last Name',
|
||||
'root_admin' => 'Root Administrator Status',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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->root_admin || $user->id === $server->owner_id) {
|
||||
if ($user->isRootAdmin() || $user->id === $server->owner_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
* @property string $default_value
|
||||
* @property bool $user_viewable
|
||||
* @property bool $user_editable
|
||||
* @property string $rules
|
||||
* @property array $rules
|
||||
* @property \Carbon\CarbonImmutable $created_at
|
||||
* @property \Carbon\CarbonImmutable $updated_at
|
||||
* @property bool $required
|
||||
@@ -58,12 +58,14 @@ class EggVariable extends Model
|
||||
'default_value' => 'string',
|
||||
'user_viewable' => 'boolean',
|
||||
'user_editable' => 'boolean',
|
||||
'rules' => 'string',
|
||||
'rules' => 'array',
|
||||
'rules.*' => 'string',
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
'user_editable' => 0,
|
||||
'user_viewable' => 0,
|
||||
'rules' => '[]',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
@@ -72,6 +74,7 @@ class EggVariable extends Model
|
||||
'egg_id' => 'integer',
|
||||
'user_viewable' => 'bool',
|
||||
'user_editable' => 'bool',
|
||||
'rules' => 'array',
|
||||
'created_at' => 'immutable_datetime',
|
||||
'updated_at' => 'immutable_datetime',
|
||||
];
|
||||
@@ -79,7 +82,7 @@ class EggVariable extends Model
|
||||
|
||||
public function getRequiredAttribute(): bool
|
||||
{
|
||||
return in_array('required', explode('|', $this->rules));
|
||||
return in_array('required', $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_overallocate >= 0) {
|
||||
if ($this->memory > 0 && $this->memory_overallocate >= 0) {
|
||||
$memoryLimit = $this->memory * (1 + ($this->memory_overallocate / 100));
|
||||
if ($this->servers_sum_memory + $memory > $memoryLimit) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->disk_overallocate >= 0) {
|
||||
if ($this->disk > 0 && $this->disk_overallocate >= 0) {
|
||||
$diskLimit = $this->disk * (1 + ($this->disk_overallocate / 100));
|
||||
if ($this->servers_sum_disk + $disk > $diskLimit) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->cpu_overallocate >= 0) {
|
||||
if ($this->cpu > 0 && $this->cpu_overallocate >= 0) {
|
||||
$cpuLimit = $this->cpu * (1 + ($this->cpu_overallocate / 100));
|
||||
if ($this->servers_sum_cpu + $cpu > $cpuLimit) {
|
||||
return false;
|
||||
|
||||
48
app/Models/Role.php
Normal file
48
app/Models/Role.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,9 @@ 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.
|
||||
@@ -40,7 +43,6 @@ use App\Notifications\SendPasswordReset as ResetPasswordNotification;
|
||||
* @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
|
||||
@@ -77,7 +79,6 @@ use App\Notifications\SendPasswordReset as ResetPasswordNotification;
|
||||
* @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)
|
||||
@@ -94,6 +95,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
use AvailableLanguages;
|
||||
use CanResetPassword;
|
||||
use HasAccessTokens;
|
||||
use HasRoles;
|
||||
use Notifiable;
|
||||
|
||||
public const USER_LEVEL_USER = 0;
|
||||
@@ -131,7 +133,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
'totp_secret',
|
||||
'totp_authenticated_at',
|
||||
'gravatar',
|
||||
'root_admin',
|
||||
'oauth',
|
||||
];
|
||||
|
||||
@@ -145,7 +146,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
*/
|
||||
protected $attributes = [
|
||||
'external_id' => null,
|
||||
'root_admin' => false,
|
||||
'language' => 'en',
|
||||
'timezone' => 'UTC',
|
||||
'use_totp' => false,
|
||||
@@ -166,7 +166,6 @@ 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',
|
||||
@@ -177,7 +176,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'root_admin' => 'boolean',
|
||||
'use_totp' => 'boolean',
|
||||
'gravatar' => 'boolean',
|
||||
'totp_authenticated_at' => 'datetime',
|
||||
@@ -226,7 +224,10 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
*/
|
||||
public function toReactObject(): array
|
||||
{
|
||||
return collect($this->toArray())->except(['id', 'external_id'])->toArray();
|
||||
return array_merge(collect($this->toArray())->except(['id', 'external_id'])->toArray(), [
|
||||
'root_admin' => $this->isRootAdmin(),
|
||||
'admin' => $this->canAccessPanel(Filament::getPanel('admin')),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -315,7 +316,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
|
||||
protected function checkPermission(Server $server, string $permission = ''): bool
|
||||
{
|
||||
if ($this->root_admin || $server->owner_id === $this->id) {
|
||||
if ($this->isRootAdmin() || $server->owner_id === $this->id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -351,14 +352,23 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
||||
|
||||
public function isLastRootAdmin(): bool
|
||||
{
|
||||
$rootAdmins = User::query()->where('root_admin', true)->limit(2)->get();
|
||||
$rootAdmins = User::all()->filter(fn ($user) => $user->isRootAdmin());
|
||||
|
||||
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
|
||||
{
|
||||
return $this->root_admin;
|
||||
if ($this->isRootAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->roles()->count() >= 1 && $this->getAllPermissions()->count() >= 1;
|
||||
}
|
||||
|
||||
public function getFilamentName(): string
|
||||
@@ -370,4 +380,13 @@ 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();
|
||||
}
|
||||
}
|
||||
|
||||
10
app/Policies/ApiKeyPolicy.php
Normal file
10
app/Policies/ApiKeyPolicy.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
class ApiKeyPolicy
|
||||
{
|
||||
use DefaultPolicies;
|
||||
|
||||
protected string $modelName = 'apikey';
|
||||
}
|
||||
10
app/Policies/DatabaseHostPolicy.php
Normal file
10
app/Policies/DatabaseHostPolicy.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
class DatabaseHostPolicy
|
||||
{
|
||||
use DefaultPolicies;
|
||||
|
||||
protected string $modelName = 'databasehost';
|
||||
}
|
||||
10
app/Policies/DatabasePolicy.php
Normal file
10
app/Policies/DatabasePolicy.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
class DatabasePolicy
|
||||
{
|
||||
use DefaultPolicies;
|
||||
|
||||
protected string $modelName = 'database';
|
||||
}
|
||||
49
app/Policies/DefaultPolicies.php
Normal file
49
app/Policies/DefaultPolicies.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?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,12 +2,9 @@
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
class EggPolicy
|
||||
{
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
use DefaultPolicies;
|
||||
|
||||
protected string $modelName = 'egg';
|
||||
}
|
||||
|
||||
10
app/Policies/MountPolicy.php
Normal file
10
app/Policies/MountPolicy.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
class MountPolicy
|
||||
{
|
||||
use DefaultPolicies;
|
||||
|
||||
protected string $modelName = 'mount';
|
||||
}
|
||||
10
app/Policies/NodePolicy.php
Normal file
10
app/Policies/NodePolicy.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
class NodePolicy
|
||||
{
|
||||
use DefaultPolicies;
|
||||
|
||||
protected string $modelName = 'node';
|
||||
}
|
||||
10
app/Policies/RolePolicy.php
Normal file
10
app/Policies/RolePolicy.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
class RolePolicy
|
||||
{
|
||||
use DefaultPolicies;
|
||||
|
||||
protected string $modelName = 'role';
|
||||
}
|
||||
@@ -2,34 +2,38 @@
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
|
||||
class ServerPolicy
|
||||
{
|
||||
use DefaultPolicies;
|
||||
|
||||
protected string $modelName = 'server';
|
||||
|
||||
/**
|
||||
* Checks if the user has the given permission on/for the server.
|
||||
* Runs before any of the functions are called. Used to determine if the (sub-)user has permissions.
|
||||
*/
|
||||
protected function checkPermission(User $user, Server $server, string $permission): bool
|
||||
public function before(User $user, string $ability, string|Server $server): ?bool
|
||||
{
|
||||
$subuser = $server->subusers->where('user_id', $user->id)->first();
|
||||
if (!$subuser || empty($permission)) {
|
||||
return false;
|
||||
// For "viewAny" the $server param is the class name
|
||||
if (is_string($server)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return in_array($permission, $subuser->permissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs before any of the functions are called. Used to determine if user is root admin, if so, ignore permissions.
|
||||
*/
|
||||
public function before(User $user, string $ability, Server $server): bool
|
||||
{
|
||||
if ($user->root_admin || $server->owner_id === $user->id) {
|
||||
// Owner has full server permissions
|
||||
if ($server->owner_id === $user->id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->checkPermission($user, $server, $ability);
|
||||
$subuser = $server->subusers->where('user_id', $user->id)->first();
|
||||
// If the user is a subuser check their permissions
|
||||
if ($subuser) {
|
||||
return in_array($ability, $subuser->permissions);
|
||||
}
|
||||
|
||||
// Return null to let default policies take over
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
26
app/Policies/UserPolicy.php
Normal file
26
app/Policies/UserPolicy.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class UserPolicy
|
||||
{
|
||||
use DefaultPolicies {
|
||||
update as defaultUpdate;
|
||||
delete as defaultDelete;
|
||||
}
|
||||
|
||||
protected string $modelName = 'user';
|
||||
|
||||
public function update(User $user, Model $model): bool
|
||||
{
|
||||
return $user->canTarget($model) && $this->defaultUpdate($user, $model);
|
||||
}
|
||||
|
||||
public function delete(User $user, Model $model): bool
|
||||
{
|
||||
return $user->canTarget($model) && $this->defaultDelete($user, $model);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ use App\Extensions\Themes\Theme;
|
||||
use App\Models;
|
||||
use App\Models\ApiKey;
|
||||
use App\Models\Node;
|
||||
use App\Models\User;
|
||||
use App\Services\Helpers\SoftwareVersionService;
|
||||
use Dedoc\Scramble\Scramble;
|
||||
use Dedoc\Scramble\Support\Generator\OpenApi;
|
||||
@@ -91,6 +92,10 @@ class AppServiceProvider extends ServiceProvider
|
||||
'success' => Color::Green,
|
||||
'warning' => Color::Amber,
|
||||
]);
|
||||
|
||||
Gate::before(function (User $user, $ability) {
|
||||
return $user->isRootAdmin() ? true : null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,6 +32,7 @@ class AdminAcl
|
||||
public const RESOURCE_DATABASE_HOSTS = 'database_hosts';
|
||||
public const RESOURCE_SERVER_DATABASES = 'server_databases';
|
||||
public const RESOURCE_MOUNTS = 'mounts';
|
||||
public const RESOURCE_ROLES = 'roles';
|
||||
|
||||
/**
|
||||
* Determine if an API key has permission to perform a specific read/write operation.
|
||||
|
||||
@@ -54,6 +54,8 @@ class EggImporterService
|
||||
// Update existing variables or create new ones.
|
||||
foreach ($parsed['variables'] ?? [] as $variable) {
|
||||
EggVariable::unguarded(function () use ($egg, $variable) {
|
||||
$variable['rules'] = is_array($variable['rules']) ? $variable['rules'] : explode('|', $variable['rules']);
|
||||
|
||||
$egg->variables()->updateOrCreate([
|
||||
'env_variable' => $variable['env_variable'],
|
||||
], Collection::make($variable)->except(['egg_id', 'env_variable'])->toArray());
|
||||
|
||||
@@ -40,7 +40,7 @@ class VariableCreationService
|
||||
throw new ReservedVariableNameException(sprintf('Cannot use the protected name %s for this environment variable.', array_get($data, 'env_variable')));
|
||||
}
|
||||
|
||||
if (!empty($data['rules'] ?? '')) {
|
||||
if (!empty($data['rules'] ?? [])) {
|
||||
$this->validateRules($data['rules']);
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ class VariableCreationService
|
||||
'default_value' => $data['default_value'] ?? '',
|
||||
'user_viewable' => in_array('user_viewable', $options),
|
||||
'user_editable' => in_array('user_editable', $options),
|
||||
'rules' => $data['rules'] ?? '',
|
||||
'rules' => $data['rules'] ?? [],
|
||||
]);
|
||||
|
||||
return $eggVariable;
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Services\Eggs\Variables;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use App\Models\EggVariable;
|
||||
use App\Exceptions\DisplayException;
|
||||
use App\Traits\Services\ValidatesValidationRules;
|
||||
@@ -54,12 +53,8 @@ class VariableUpdateService
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($data['rules'] ?? '')) {
|
||||
$this->validateRules(
|
||||
(is_string($data['rules']) && Str::contains($data['rules'], ';;'))
|
||||
? explode(';;', $data['rules'])
|
||||
: $data['rules']
|
||||
);
|
||||
if (!empty($data['rules'] ?? [])) {
|
||||
$this->validateRules($data['rules']);
|
||||
}
|
||||
|
||||
$options = array_get($data, 'options') ?? [];
|
||||
@@ -71,7 +66,7 @@ class VariableUpdateService
|
||||
'default_value' => $data['default_value'] ?? '',
|
||||
'user_viewable' => in_array('user_viewable', $options),
|
||||
'user_editable' => in_array('user_editable', $options),
|
||||
'rules' => $data['rules'] ?? '',
|
||||
'rules' => $data['rules'] ?? [],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,10 @@ class GetUserPermissionsService
|
||||
*/
|
||||
public function handle(Server $server, User $user): array
|
||||
{
|
||||
if ($user->root_admin || $user->id === $server->owner_id) {
|
||||
if ($user->isRootAdmin() || $user->id === $server->owner_id) {
|
||||
$permissions = ['*'];
|
||||
|
||||
if ($user->root_admin) {
|
||||
if ($user->isRootAdmin()) {
|
||||
$permissions[] = 'admin.websocket.errors';
|
||||
$permissions[] = 'admin.websocket.install';
|
||||
$permissions[] = 'admin.websocket.transfer';
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Services\Users;
|
||||
|
||||
use App\Models\Role;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Hashing\Hasher;
|
||||
@@ -39,10 +40,17 @@ class UserCreationService
|
||||
$data['password'] = $this->hasher->make(str_random(30));
|
||||
}
|
||||
|
||||
$isRootAdmin = array_key_exists('root_admin', $data) && $data['root_admin'];
|
||||
unset($data['root_admin']);
|
||||
|
||||
$user = User::query()->forceCreate(array_merge($data, [
|
||||
'uuid' => Uuid::uuid4()->toString(),
|
||||
]));
|
||||
|
||||
if ($isRootAdmin) {
|
||||
$user->syncRoles(Role::getRootAdmin());
|
||||
}
|
||||
|
||||
if (isset($generateResetToken)) {
|
||||
$token = $this->passwordBroker->createToken($user);
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ abstract class BaseTransformer extends TransformerAbstract
|
||||
// the user is a root admin at the moment. In a future release we'll be rolling
|
||||
// out more specific permissions for keys.
|
||||
if ($token->key_type === ApiKey::TYPE_ACCOUNT) {
|
||||
return $this->request->user()->root_admin;
|
||||
return $this->request->user()->isRootAdmin();
|
||||
}
|
||||
|
||||
return AdminAcl::check($token, $resource);
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Transformers\Api\Application;
|
||||
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
class RolePermissionTransformer extends BaseTransformer
|
||||
{
|
||||
public function getResourceName(): string
|
||||
{
|
||||
return 'permissions';
|
||||
}
|
||||
|
||||
public function transform(Permission $model): array
|
||||
{
|
||||
return [
|
||||
'name' => $model->name,
|
||||
'guard_name' => $model->guard_name,
|
||||
'created_at' => $model->created_at->toAtomString(),
|
||||
'updated_at' => $model->updated_at->toAtomString(),
|
||||
];
|
||||
}
|
||||
}
|
||||
47
app/Transformers/Api/Application/RoleTransformer.php
Normal file
47
app/Transformers/Api/Application/RoleTransformer.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Transformers\Api\Application;
|
||||
|
||||
use App\Models\Role;
|
||||
use League\Fractal\Resource\Collection;
|
||||
use League\Fractal\Resource\NullResource;
|
||||
|
||||
class RoleTransformer extends BaseTransformer
|
||||
{
|
||||
protected array $availableIncludes = [
|
||||
'permissions',
|
||||
];
|
||||
|
||||
/**
|
||||
* Return the resource name for the JSONAPI output.
|
||||
*/
|
||||
public function getResourceName(): string
|
||||
{
|
||||
return Role::RESOURCE_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform role into a representation for the application API.
|
||||
*/
|
||||
public function transform(Role $model): array
|
||||
{
|
||||
return [
|
||||
'name' => $model->name,
|
||||
'guard_name' => $model->guard_name,
|
||||
'created_at' => $model->created_at->toAtomString(),
|
||||
'updated_at' => $model->updated_at->toAtomString(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Include the permissions associated with this role.
|
||||
*
|
||||
* @throws \App\Exceptions\Transformer\InvalidTransformerLevelException
|
||||
*/
|
||||
public function includePermissions(Role $model): Collection|NullResource
|
||||
{
|
||||
$model->loadMissing('permissions');
|
||||
|
||||
return $this->collection($model->getRelation('permissions'), $this->makeTransformer(RolePermissionTransformer::class), 'permissions');
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Transformers\Api\Application;
|
||||
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use League\Fractal\Resource\Collection;
|
||||
use League\Fractal\Resource\NullResource;
|
||||
@@ -12,7 +13,10 @@ class UserTransformer extends BaseTransformer
|
||||
/**
|
||||
* List of resources that can be included.
|
||||
*/
|
||||
protected array $availableIncludes = ['servers'];
|
||||
protected array $availableIncludes = [
|
||||
'servers',
|
||||
'roles',
|
||||
];
|
||||
|
||||
/**
|
||||
* Return the resource name for the JSONAPI output.
|
||||
@@ -36,7 +40,7 @@ class UserTransformer extends BaseTransformer
|
||||
'first_name' => $user->name_first,
|
||||
'last_name' => $user->name_last,
|
||||
'language' => $user->language,
|
||||
'root_admin' => (bool) $user->root_admin,
|
||||
'root_admin' => $user->isRootAdmin(),
|
||||
'2fa_enabled' => (bool) $user->use_totp,
|
||||
'2fa' => (bool) $user->use_totp, // deprecated, use "2fa_enabled"
|
||||
'created_at' => $this->formatTimestamp($user->created_at),
|
||||
@@ -59,4 +63,20 @@ class UserTransformer extends BaseTransformer
|
||||
|
||||
return $this->collection($user->getRelation('servers'), $this->makeTransformer(ServerTransformer::class), 'server');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the roles associated with this user.
|
||||
*
|
||||
* @throws \App\Exceptions\Transformer\InvalidTransformerLevelException
|
||||
*/
|
||||
public function includeRoles(User $user): Collection|NullResource
|
||||
{
|
||||
if (!$this->authorize(AdminAcl::RESOURCE_ROLES)) {
|
||||
return $this->null();
|
||||
}
|
||||
|
||||
$user->loadMissing('roles');
|
||||
|
||||
return $this->collection($user->getRelation('roles'), $this->makeTransformer(RoleTransformer::class), Role::RESOURCE_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +113,6 @@ class ActivityLogTransformer extends BaseClientTransformer
|
||||
*/
|
||||
protected function canViewIP(Model $actor = null): bool
|
||||
{
|
||||
return $actor?->is($this->request->user()) || $this->request->user()->root_admin;
|
||||
return $actor?->is($this->request->user()) || $this->request->user()->isRootAdmin();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ class EggVariableTransformer extends BaseClientTransformer
|
||||
'default_value' => $variable->default_value,
|
||||
'server_value' => $variable->server_value,
|
||||
'is_editable' => $variable->user_editable,
|
||||
'rules' => $variable->rules,
|
||||
'rules' => implode('|', $variable->rules),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,8 +29,8 @@ class UserTransformer extends BaseClientTransformer
|
||||
'last_name' => $user->name_last,
|
||||
'language' => $user->language,
|
||||
'image' => 'https://gravatar.com/avatar/' . md5(Str::lower($user->email)), // deprecated
|
||||
'admin' => (bool) $user->root_admin, // deprecated, use "root_admin"
|
||||
'root_admin' => (bool) $user->root_admin,
|
||||
'admin' => $user->isRootAdmin(), // deprecated, use "root_admin"
|
||||
'root_admin' => $user->isRootAdmin(),
|
||||
'2fa_enabled' => (bool) $user->use_totp,
|
||||
'created_at' => $this->formatTimestamp($user->created_at),
|
||||
'updated_at' => $this->formatTimestamp($user->updated_at),
|
||||
|
||||
@@ -17,34 +17,3 @@ if (!function_exists('is_ip')) {
|
||||
return $address !== null && filter_var($address, FILTER_VALIDATE_IP) !== false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('object_get_strict')) {
|
||||
/**
|
||||
* Get an object using dot notation. An object key with a value of null is still considered valid
|
||||
* and will not trigger the response of a default value (unlike object_get).
|
||||
*/
|
||||
function object_get_strict(object $object, ?string $key, mixed $default = null): mixed
|
||||
{
|
||||
if (is_null($key) || trim($key) == '') {
|
||||
return $object;
|
||||
}
|
||||
|
||||
foreach (explode('.', $key) as $segment) {
|
||||
if (!is_object($object) || !property_exists($object, $segment)) {
|
||||
return value($default);
|
||||
}
|
||||
|
||||
$object = $object->{$segment};
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('is_installed')) {
|
||||
function is_installed(): bool
|
||||
{
|
||||
// This defaults to true so existing panels count as "installed"
|
||||
return env('APP_INSTALLED', true);
|
||||
}
|
||||
}
|
||||
|
||||
58
compose.yml
Normal file
58
compose.yml
Normal file
@@ -0,0 +1,58 @@
|
||||
x-common:
|
||||
panel:
|
||||
&panel-environment
|
||||
APP_URL: "https://localhost" # can be set to 'http://localhost' on port 80 only
|
||||
ADMIN_EMAIL: "USEYOUROWNEMAILHERE@example.com"
|
||||
|
||||
APP_DEBUG: "false"
|
||||
APP_ENVIRONMENT_ONLY: "false"
|
||||
APP_ENV: "production"
|
||||
SESSION_DRIVER: "file"
|
||||
|
||||
mail:
|
||||
&mail-environment
|
||||
MAIL_DRIVER: "log"
|
||||
# MAIL_HOST: ""
|
||||
# MAIL_PORT: ""
|
||||
# MAIL_FROM: ""
|
||||
# MAIL_USERNAME: ""
|
||||
# MAIL_PASSWORD: ""
|
||||
# MAIL_ENCRYPTION: ""
|
||||
|
||||
#
|
||||
# ------------------------------------------------------------------------------------------
|
||||
# DANGER ZONE BELOW
|
||||
#
|
||||
# The remainder of this file likely does not need to be changed. Please only make modifications
|
||||
# below if you understand what you are doing.
|
||||
#
|
||||
|
||||
services:
|
||||
panel:
|
||||
image: ghcr.io/pelican-dev/panel:latest
|
||||
restart: always
|
||||
networks:
|
||||
- default
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
# - "9000:9000" # enable when not using caddy to be abel to reach php-fpm
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway" # shows the panel on te internal docker network as well. usually '172.17.0.1'
|
||||
volumes:
|
||||
- pelican-data:/pelican-data
|
||||
- pelican-logs:/var/www/html/storage/logs
|
||||
environment:
|
||||
<<: [*panel-environment, *mail-environment]
|
||||
XDG_DATA_HOME: /pelican-data
|
||||
# SKIP_CADDY: true # enable when not using caddy.
|
||||
|
||||
volumes:
|
||||
pelican-data:
|
||||
pelican-logs:
|
||||
|
||||
networks:
|
||||
default:
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.20.0.0/16
|
||||
@@ -7,7 +7,6 @@
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"ext-pdo": "*",
|
||||
"ext-pdo_mysql": "*",
|
||||
"ext-zip": "*",
|
||||
"abdelhamiderrahmouni/filament-monaco-editor": "0.2.1",
|
||||
"aws/aws-sdk-php": "~3.288.1",
|
||||
@@ -34,6 +33,7 @@
|
||||
"s1lentium/iptools": "~1.2.0",
|
||||
"socialiteproviders/discord": "^4.2",
|
||||
"spatie/laravel-fractal": "^6.2",
|
||||
"spatie/laravel-permission": "^6.9",
|
||||
"spatie/laravel-query-builder": "^5.8.1",
|
||||
"spatie/temporary-directory": "^2.2",
|
||||
"symfony/http-client": "^7.1",
|
||||
@@ -90,4 +90,4 @@
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
}
|
||||
|
||||
609
composer.lock
generated
609
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,11 @@
|
||||
<?php
|
||||
|
||||
$database = env('DB_DATABASE', 'database.sqlite');
|
||||
$datapasePath = database_path($database);
|
||||
if (str($database)->startsWith('/')) {
|
||||
$databasePath = $database;
|
||||
}
|
||||
|
||||
return [
|
||||
|
||||
'default' => env('DB_CONNECTION', 'sqlite'),
|
||||
@@ -8,7 +14,7 @@ return [
|
||||
'sqlite' => [
|
||||
'driver' => 'sqlite',
|
||||
'url' => env('DB_URL'),
|
||||
'database' => database_path(env('DB_DATABASE', 'database.sqlite')),
|
||||
'database' => $datapasePath,
|
||||
'prefix' => '',
|
||||
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||
],
|
||||
|
||||
@@ -74,7 +74,7 @@ return [
|
||||
| Client Features
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Allow clients to create their own databases.
|
||||
| Allow clients to turn features on or off
|
||||
*/
|
||||
|
||||
'client_features' => [
|
||||
@@ -93,6 +93,10 @@ return [
|
||||
'range_start' => env('PANEL_CLIENT_ALLOCATIONS_RANGE_START'),
|
||||
'range_end' => env('PANEL_CLIENT_ALLOCATIONS_RANGE_END'),
|
||||
],
|
||||
|
||||
'installer' => [
|
||||
'enabled' => env('APP_INSTALLER', false),
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|
||||
13
config/permission.php
Normal file
13
config/permission.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'models' => [
|
||||
|
||||
'permission' => Spatie\Permission\Models\Permission::class,
|
||||
|
||||
'role' => \App\Models\Role::class,
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
@@ -33,19 +33,10 @@ class UserFactory extends Factory
|
||||
'name_last' => $this->faker->lastName(),
|
||||
'password' => $password ?: $password = bcrypt('password'),
|
||||
'language' => 'en',
|
||||
'root_admin' => false,
|
||||
'use_totp' => false,
|
||||
'oauth' => [],
|
||||
'created_at' => Carbon::now(),
|
||||
'updated_at' => Carbon::now(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the user is an admin.
|
||||
*/
|
||||
public function admin(): static
|
||||
{
|
||||
return $this->state(['root_admin' => true]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Role;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
@@ -12,5 +13,7 @@ class DatabaseSeeder extends Seeder
|
||||
public function run()
|
||||
{
|
||||
$this->call(EggSeeder::class);
|
||||
|
||||
Role::firstOrCreate(['name' => Role::ROOT_ADMIN]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"version": "PTDL_v2",
|
||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/minecraft\/egg-bungeecord.json"
|
||||
},
|
||||
"exported_at": "2024-07-03T14:33:51+00:00",
|
||||
"exported_at": "2024-07-25T12:03:12+00:00",
|
||||
"name": "Bungeecord",
|
||||
"author": "panel@example.com",
|
||||
"uuid": "9e6b409e-4028-4947-aea8-50a2c404c271",
|
||||
@@ -44,8 +44,12 @@
|
||||
"default_value": "latest",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|alpha_num|between:1,6",
|
||||
"sort": null,
|
||||
"rules": [
|
||||
"required",
|
||||
"alpha_num",
|
||||
"between:1,6"
|
||||
],
|
||||
"sort": 1,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
@@ -55,9 +59,12 @@
|
||||
"default_value": "bungeecord.jar",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/",
|
||||
"sort": null,
|
||||
"rules": [
|
||||
"required",
|
||||
"regex:\/^([\\w\\d._-]+)(\\.jar)$\/"
|
||||
],
|
||||
"sort": 2,
|
||||
"field_type": "text"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"version": "PTDL_v2",
|
||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/minecraft\/egg-forge-minecraft.json"
|
||||
},
|
||||
"exported_at": "2024-07-03T14:33:51+00:00",
|
||||
"exported_at": "2024-07-25T12:03:19+00:00",
|
||||
"name": "Forge Minecraft",
|
||||
"author": "panel@example.com",
|
||||
"uuid": "ed072427-f209-4603-875c-f540c6dd5a65",
|
||||
@@ -44,7 +44,10 @@
|
||||
"default_value": "server.jar",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/",
|
||||
"rules": [
|
||||
"required",
|
||||
"regex:\/^([\\w\\d._-]+)(\\.jar)$\/"
|
||||
],
|
||||
"sort": 1,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -55,7 +58,11 @@
|
||||
"default_value": "latest",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|max:9",
|
||||
"rules": [
|
||||
"required",
|
||||
"string",
|
||||
"max:9"
|
||||
],
|
||||
"sort": 2,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -66,7 +73,11 @@
|
||||
"default_value": "recommended",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|in:recommended,latest",
|
||||
"rules": [
|
||||
"required",
|
||||
"string",
|
||||
"in:recommended,latest"
|
||||
],
|
||||
"sort": 3,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -77,7 +88,10 @@
|
||||
"default_value": "",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|regex:\/^[0-9\\.\\-]+$\/",
|
||||
"rules": [
|
||||
"nullable",
|
||||
"regex:\/^[0-9\\.\\-]+$\/"
|
||||
],
|
||||
"sort": 4,
|
||||
"field_type": "text"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"version": "PTDL_v2",
|
||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/minecraft\/egg-paper.json"
|
||||
},
|
||||
"exported_at": "2024-07-03T14:33:52+00:00",
|
||||
"exported_at": "2024-07-25T12:03:43+00:00",
|
||||
"name": "Paper",
|
||||
"author": "parker@example.com",
|
||||
"uuid": "5da37ef6-58da-4169-90a6-e683e1721247",
|
||||
@@ -44,7 +44,11 @@
|
||||
"default_value": "latest",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|string|max:20",
|
||||
"rules": [
|
||||
"nullable",
|
||||
"string",
|
||||
"max:20"
|
||||
],
|
||||
"sort": 1,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -55,7 +59,10 @@
|
||||
"default_value": "server.jar",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/",
|
||||
"rules": [
|
||||
"required",
|
||||
"regex:\/^([\\w\\d._-]+)(\\.jar)$\/"
|
||||
],
|
||||
"sort": 2,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -66,7 +73,10 @@
|
||||
"default_value": "",
|
||||
"user_viewable": false,
|
||||
"user_editable": false,
|
||||
"rules": "nullable|string",
|
||||
"rules": [
|
||||
"nullable",
|
||||
"string"
|
||||
],
|
||||
"sort": 3,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -77,7 +87,11 @@
|
||||
"default_value": "latest",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|max:20",
|
||||
"rules": [
|
||||
"required",
|
||||
"string",
|
||||
"max:20"
|
||||
],
|
||||
"sort": 4,
|
||||
"field_type": "text"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"version": "PTDL_v2",
|
||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/minecraft\/egg-sponge--sponge-vanilla.json"
|
||||
},
|
||||
"exported_at": "2024-07-03T14:34:02+00:00",
|
||||
"exported_at": "2024-07-25T12:03:55+00:00",
|
||||
"name": "Sponge (SpongeVanilla)",
|
||||
"author": "panel@example.com",
|
||||
"uuid": "f0d2f88f-1ff3-42a0-b03f-ac44c5571e6d",
|
||||
@@ -44,7 +44,10 @@
|
||||
"default_value": "1.12.2-7.3.0",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|regex:\/^([a-zA-Z0-9.\\-_]+)$\/",
|
||||
"rules": [
|
||||
"required",
|
||||
"regex:\/^([a-zA-Z0-9.\\-_]+)$\/"
|
||||
],
|
||||
"sort": 1,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -55,7 +58,10 @@
|
||||
"default_value": "server.jar",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/",
|
||||
"rules": [
|
||||
"required",
|
||||
"regex:\/^([\\w\\d._-]+)(\\.jar)$\/"
|
||||
],
|
||||
"sort": 2,
|
||||
"field_type": "text"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"version": "PTDL_v2",
|
||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/minecraft\/egg-vanilla-minecraft.json"
|
||||
},
|
||||
"exported_at": "2024-07-03T14:34:02+00:00",
|
||||
"exported_at": "2024-07-25T12:04:05+00:00",
|
||||
"name": "Vanilla Minecraft",
|
||||
"author": "panel@example.com",
|
||||
"uuid": "9ac39f3d-0c34-4d93-8174-c52ab9e6c57b",
|
||||
@@ -44,7 +44,10 @@
|
||||
"default_value": "server.jar",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/",
|
||||
"rules": [
|
||||
"required",
|
||||
"regex:\/^([\\w\\d._-]+)(\\.jar)$\/"
|
||||
],
|
||||
"sort": 1,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -55,7 +58,11 @@
|
||||
"default_value": "latest",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|between:3,15",
|
||||
"rules": [
|
||||
"required",
|
||||
"string",
|
||||
"between:3,15"
|
||||
],
|
||||
"sort": 2,
|
||||
"field_type": "text"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"version": "PTDL_v2",
|
||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/rust\/egg-rust.json"
|
||||
},
|
||||
"exported_at": "2024-07-03T14:34:09+00:00",
|
||||
"exported_at": "2024-07-25T12:06:17+00:00",
|
||||
"name": "Rust",
|
||||
"author": "panel@example.com",
|
||||
"uuid": "bace2dfb-209c-452a-9459-7d6f340b07ae",
|
||||
@@ -38,7 +38,11 @@
|
||||
"default_value": "A Rust Server",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|max:60",
|
||||
"rules": [
|
||||
"required",
|
||||
"string",
|
||||
"max:60"
|
||||
],
|
||||
"sort": 1,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -49,7 +53,10 @@
|
||||
"default_value": "vanilla",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|in:vanilla,oxide,carbon",
|
||||
"rules": [
|
||||
"required",
|
||||
"in:vanilla,oxide,carbon"
|
||||
],
|
||||
"sort": 2,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -60,7 +67,11 @@
|
||||
"default_value": "Procedural Map",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|max:20",
|
||||
"rules": [
|
||||
"required",
|
||||
"string",
|
||||
"max:20"
|
||||
],
|
||||
"sort": 3,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -71,7 +82,10 @@
|
||||
"default_value": "Powered by Panel",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string",
|
||||
"rules": [
|
||||
"required",
|
||||
"string"
|
||||
],
|
||||
"sort": 4,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -82,7 +96,10 @@
|
||||
"default_value": "http:\/\/example.com",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|url",
|
||||
"rules": [
|
||||
"nullable",
|
||||
"url"
|
||||
],
|
||||
"sort": 5,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -93,7 +110,10 @@
|
||||
"default_value": "3000",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|integer",
|
||||
"rules": [
|
||||
"required",
|
||||
"integer"
|
||||
],
|
||||
"sort": 6,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -104,7 +124,10 @@
|
||||
"default_value": "",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|string",
|
||||
"rules": [
|
||||
"nullable",
|
||||
"string"
|
||||
],
|
||||
"sort": 7,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -115,7 +138,10 @@
|
||||
"default_value": "40",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|integer",
|
||||
"rules": [
|
||||
"required",
|
||||
"integer"
|
||||
],
|
||||
"sort": 8,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -126,7 +152,10 @@
|
||||
"default_value": "",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|url",
|
||||
"rules": [
|
||||
"nullable",
|
||||
"url"
|
||||
],
|
||||
"sort": 9,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -137,7 +166,10 @@
|
||||
"default_value": "27017",
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "required|integer",
|
||||
"rules": [
|
||||
"required",
|
||||
"integer"
|
||||
],
|
||||
"sort": 10,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -148,7 +180,10 @@
|
||||
"default_value": "28016",
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "required|integer",
|
||||
"rules": [
|
||||
"required",
|
||||
"integer"
|
||||
],
|
||||
"sort": 11,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -159,7 +194,11 @@
|
||||
"default_value": "",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|regex:\/^[\\w.-]*$\/|max:64",
|
||||
"rules": [
|
||||
"required",
|
||||
"regex:\/^[\\w.-]*$\/",
|
||||
"max:64"
|
||||
],
|
||||
"sort": 12,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -170,7 +209,10 @@
|
||||
"default_value": "60",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|integer",
|
||||
"rules": [
|
||||
"required",
|
||||
"integer"
|
||||
],
|
||||
"sort": 13,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -181,7 +223,10 @@
|
||||
"default_value": "",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|string",
|
||||
"rules": [
|
||||
"nullable",
|
||||
"string"
|
||||
],
|
||||
"sort": 14,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -192,7 +237,10 @@
|
||||
"default_value": "28082",
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "required|integer",
|
||||
"rules": [
|
||||
"required",
|
||||
"integer"
|
||||
],
|
||||
"sort": 15,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -203,7 +251,10 @@
|
||||
"default_value": "",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|url",
|
||||
"rules": [
|
||||
"nullable",
|
||||
"url"
|
||||
],
|
||||
"sort": 16,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -214,7 +265,10 @@
|
||||
"default_value": "",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|url",
|
||||
"rules": [
|
||||
"nullable",
|
||||
"url"
|
||||
],
|
||||
"sort": 17,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -225,7 +279,11 @@
|
||||
"default_value": "258550",
|
||||
"user_viewable": false,
|
||||
"user_editable": false,
|
||||
"rules": "required|string|in:258550",
|
||||
"rules": [
|
||||
"required",
|
||||
"string",
|
||||
"in:258550"
|
||||
],
|
||||
"sort": 18,
|
||||
"field_type": "text"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"version": "PTDL_v2",
|
||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/source-engine\/egg-counter--strike--global-offensive.json"
|
||||
},
|
||||
"exported_at": "2024-07-03T14:34:03+00:00",
|
||||
"exported_at": "2024-07-25T12:04:25+00:00",
|
||||
"name": "Counter-Strike: Global Offensive",
|
||||
"author": "panel@example.com",
|
||||
"uuid": "437c367d-06be-498f-a604-fdad135504d7",
|
||||
@@ -39,7 +39,11 @@
|
||||
"default_value": "de_dust2",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|alpha_dash",
|
||||
"rules": [
|
||||
"required",
|
||||
"string",
|
||||
"alpha_dash"
|
||||
],
|
||||
"sort": 1,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -50,7 +54,12 @@
|
||||
"default_value": "",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|alpha_num|size:32",
|
||||
"rules": [
|
||||
"required",
|
||||
"string",
|
||||
"alpha_num",
|
||||
"size:32"
|
||||
],
|
||||
"sort": 2,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -61,7 +70,11 @@
|
||||
"default_value": "740",
|
||||
"user_viewable": false,
|
||||
"user_editable": false,
|
||||
"rules": "required|string|max:20",
|
||||
"rules": [
|
||||
"required",
|
||||
"string",
|
||||
"max:20"
|
||||
],
|
||||
"sort": 3,
|
||||
"field_type": "text"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"version": "PTDL_v2",
|
||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/source-engine\/egg-custom-source-engine-game.json"
|
||||
},
|
||||
"exported_at": "2024-07-03T14:34:04+00:00",
|
||||
"exported_at": "2024-07-25T12:03:27+00:00",
|
||||
"name": "Custom Source Engine Game",
|
||||
"author": "panel@example.com",
|
||||
"uuid": "2a42d0c2-c0ba-4067-9a0a-9b95d77a3490",
|
||||
@@ -38,7 +38,11 @@
|
||||
"default_value": "",
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "required|numeric|digits_between:1,6",
|
||||
"rules": [
|
||||
"required",
|
||||
"numeric",
|
||||
"digits_between:1,6"
|
||||
],
|
||||
"sort": 1,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -49,7 +53,11 @@
|
||||
"default_value": "",
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "required|alpha_dash|between:1,100",
|
||||
"rules": [
|
||||
"required",
|
||||
"alpha_dash",
|
||||
"between:1,100"
|
||||
],
|
||||
"sort": 2,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -60,7 +68,11 @@
|
||||
"default_value": "",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|alpha_dash",
|
||||
"rules": [
|
||||
"required",
|
||||
"string",
|
||||
"alpha_dash"
|
||||
],
|
||||
"sort": 3,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -71,7 +83,10 @@
|
||||
"default_value": "",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|string",
|
||||
"rules": [
|
||||
"nullable",
|
||||
"string"
|
||||
],
|
||||
"sort": 4,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -82,7 +97,10 @@
|
||||
"default_value": "",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|string",
|
||||
"rules": [
|
||||
"nullable",
|
||||
"string"
|
||||
],
|
||||
"sort": 5,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -93,7 +111,10 @@
|
||||
"default_value": "",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|string",
|
||||
"rules": [
|
||||
"nullable",
|
||||
"string"
|
||||
],
|
||||
"sort": 6,
|
||||
"field_type": "text"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"version": "PTDL_v2",
|
||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/source-engine\/egg-garrys-mod.json"
|
||||
},
|
||||
"exported_at": "2024-07-03T14:34:04+00:00",
|
||||
"exported_at": "2024-07-25T12:05:02+00:00",
|
||||
"name": "Garrys Mod",
|
||||
"author": "panel@example.com",
|
||||
"uuid": "60ef81d4-30a2-4d98-ab64-f59c69e2f915",
|
||||
@@ -39,7 +39,11 @@
|
||||
"default_value": "gm_flatgrass",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|alpha_dash",
|
||||
"rules": [
|
||||
"required",
|
||||
"string",
|
||||
"alpha_dash"
|
||||
],
|
||||
"sort": 1,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -50,7 +54,12 @@
|
||||
"default_value": "",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|string|alpha_num|size:32",
|
||||
"rules": [
|
||||
"nullable",
|
||||
"string",
|
||||
"alpha_num",
|
||||
"size:32"
|
||||
],
|
||||
"sort": 2,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -61,7 +70,11 @@
|
||||
"default_value": "4020",
|
||||
"user_viewable": false,
|
||||
"user_editable": false,
|
||||
"rules": "required|string|max:20",
|
||||
"rules": [
|
||||
"required",
|
||||
"string",
|
||||
"max:20"
|
||||
],
|
||||
"sort": 3,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -72,7 +85,10 @@
|
||||
"default_value": "",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|integer",
|
||||
"rules": [
|
||||
"nullable",
|
||||
"integer"
|
||||
],
|
||||
"sort": 4,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -83,7 +99,10 @@
|
||||
"default_value": "sandbox",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string",
|
||||
"rules": [
|
||||
"required",
|
||||
"string"
|
||||
],
|
||||
"sort": 5,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -94,7 +113,11 @@
|
||||
"default_value": "32",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|integer|max:128",
|
||||
"rules": [
|
||||
"required",
|
||||
"integer",
|
||||
"max:128"
|
||||
],
|
||||
"sort": 6,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -105,7 +128,11 @@
|
||||
"default_value": "22",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|integer|max:100",
|
||||
"rules": [
|
||||
"required",
|
||||
"integer",
|
||||
"max:100"
|
||||
],
|
||||
"sort": 7,
|
||||
"field_type": "text"
|
||||
},
|
||||
@@ -116,7 +143,10 @@
|
||||
"default_value": "0",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|boolean",
|
||||
"rules": [
|
||||
"required",
|
||||
"boolean"
|
||||
],
|
||||
"sort": 8,
|
||||
"field_type": "text"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user