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_TIMEZONE=UTC
|
||||||
APP_URL=http://panel.test
|
APP_URL=http://panel.test
|
||||||
APP_LOCALE=en
|
APP_LOCALE=en
|
||||||
APP_INSTALLED=false
|
|
||||||
|
|
||||||
LOG_CHANNEL=daily
|
LOG_CHANNEL=daily
|
||||||
LOG_STACK=single
|
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:
|
attributes:
|
||||||
label: Panel Version
|
label: Panel Version
|
||||||
description: Version number of your Panel (latest is not a version)
|
description: Version number of your Panel (latest is not a version)
|
||||||
placeholder: 1.4.0
|
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -42,7 +41,6 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Wings Version
|
label: Wings Version
|
||||||
description: Version number of your Wings (latest is not a version)
|
description: Version number of your Wings (latest is not a version)
|
||||||
placeholder: 1.4.2
|
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -68,7 +66,7 @@ body:
|
|||||||
Run the following command to collect logs on your system.
|
Run the following command to collect logs on your system.
|
||||||
|
|
||||||
Wings: `sudo wings diagnostics`
|
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"
|
placeholder: "https://pelipaste.com/a1h6z"
|
||||||
render: bash
|
render: bash
|
||||||
validations:
|
validations:
|
||||||
|
|||||||
83
.github/docker/entrypoint.sh
vendored
83
.github/docker/entrypoint.sh
vendored
@@ -1,81 +1,58 @@
|
|||||||
#!/bin/ash -e
|
#!/bin/ash -e
|
||||||
cd /app
|
|
||||||
|
|
||||||
mkdir -p /var/log/panel/logs/ /var/log/supervisord/ /var/log/nginx/ /var/log/php8/ \
|
#mkdir -p /var/log/supervisord/ /var/log/php8/ \
|
||||||
&& chmod 777 /var/log/panel/logs/ \
|
|
||||||
&& ln -s /app/storage/logs/ /var/log/panel/
|
|
||||||
|
|
||||||
## check for .env file and generate app keys if missing
|
## 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."
|
echo "external vars exist."
|
||||||
rm -rf /app/.env
|
rm -rf /var/www/html/.env
|
||||||
ln -s /app/var/.env /app/
|
|
||||||
else
|
else
|
||||||
echo "external vars don't exist."
|
echo "external vars don't exist."
|
||||||
rm -rf /app/.env
|
rm -rf /var/www/html/.env
|
||||||
touch /app/var/.env
|
touch /pelican-data/.env
|
||||||
|
|
||||||
## manually generate a key because key generate --force fails
|
## manually generate a key because key generate --force fails
|
||||||
if [ -z $APP_KEY ]; then
|
if [ -z $APP_KEY ]; then
|
||||||
echo -e "Generating key."
|
echo -e "Generating key."
|
||||||
APP_KEY=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
|
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 "Generated app key: $APP_KEY"
|
||||||
echo -e "APP_KEY=$APP_KEY" > /app/var/.env
|
echo -e "APP_KEY=$APP_KEY" > /pelican-data/.env
|
||||||
else
|
else
|
||||||
echo -e "APP_KEY exists in environment, using that."
|
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
|
fi
|
||||||
|
|
||||||
ln -s /app/var/.env /app/
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Checking if https is required."
|
mkdir /pelican-data/database
|
||||||
if [ -f /etc/nginx/http.d/panel.conf ]; then
|
ln -s /pelican-data/.env /var/www/html/
|
||||||
echo "Using nginx config already in place."
|
ln -s /pelican-data/database/database.sqlite /var/www/html/database/
|
||||||
if [ $LE_EMAIL ]; then
|
|
||||||
echo "Checking for cert update"
|
if ! grep -q "APP_KEY=" .env || grep -q "APP_KEY=$" .env; then
|
||||||
certbot certonly -d $(echo $APP_URL | sed 's~http[s]*://~~g') --standalone -m $LE_EMAIL --agree-tos -n
|
echo "Generating APP_KEY..."
|
||||||
else
|
php artisan key:generate --force
|
||||||
echo "No letsencrypt email is set"
|
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
echo "Checking if letsencrypt email is set."
|
echo "APP_KEY is already set."
|
||||||
if [ -z $LE_EMAIL ]; then
|
|
||||||
echo "No letsencrypt email is set using http config."
|
|
||||||
cp .github/docker/default.conf /etc/nginx/http.d/panel.conf
|
|
||||||
else
|
|
||||||
echo "writing ssl config"
|
|
||||||
cp .github/docker/default_ssl.conf /etc/nginx/http.d/panel.conf
|
|
||||||
echo "updating ssl config for domain"
|
|
||||||
sed -i "s|<domain>|$(echo $APP_URL | sed 's~http[s]*://~~g')|g" /etc/nginx/http.d/panel.conf
|
|
||||||
echo "generating certs"
|
|
||||||
certbot certonly -d $(echo $APP_URL | sed 's~http[s]*://~~g') --standalone -m $LE_EMAIL --agree-tos -n
|
|
||||||
fi
|
|
||||||
echo "Removing the default nginx config"
|
|
||||||
rm -rf /etc/nginx/http.d/default.conf
|
|
||||||
fi
|
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
|
## make sure the db is set up
|
||||||
echo -e "Migrating and Seeding D.B"
|
echo -e "Migrating Database"
|
||||||
php artisan migrate --seed --force
|
php artisan migrate --force
|
||||||
|
|
||||||
## start cronjobs for the queue
|
## start cronjobs for the queue
|
||||||
echo -e "Starting cron jobs."
|
echo -e "Starting cron jobs."
|
||||||
crond -L /var/log/crond -l 5
|
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 "$@"
|
exec "$@"
|
||||||
|
|||||||
12
.github/docker/supervisord.conf
vendored
12
.github/docker/supervisord.conf
vendored
@@ -25,15 +25,15 @@ autostart=true
|
|||||||
autorestart=true
|
autorestart=true
|
||||||
|
|
||||||
[program:queue-worker]
|
[program:queue-worker]
|
||||||
command=/usr/local/bin/php /app/artisan queue:work --queue=high,standard,low --sleep=3 --tries=3
|
command=/usr/local/bin/php /var/www/html/artisan queue:work --queue=high,standard,low --sleep=3 --tries=3
|
||||||
user=nginx
|
user=www-data
|
||||||
autostart=true
|
autostart=true
|
||||||
autorestart=true
|
autorestart=true
|
||||||
|
|
||||||
[program:nginx]
|
[program:caddy]
|
||||||
command=/usr/sbin/nginx -g 'daemon off;'
|
command=caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
|
||||||
autostart=true
|
autostart=%(ENV_SUPERVISORD_CADDY)s
|
||||||
autorestart=true
|
autorestart=%(ENV_SUPERVISORD_CADDY)s
|
||||||
priority=10
|
priority=10
|
||||||
stdout_events_enabled=true
|
stdout_events_enabled=true
|
||||||
stderr_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:
|
# Pelican Production Dockerfile
|
||||||
# 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
|
FROM node:20-alpine AS yarn
|
||||||
# level distribution
|
#FROM --platform=$TARGETOS/$TARGETARCH node:20-alpine AS yarn
|
||||||
FROM --platform=$TARGETOS/$TARGETARCH node:20-alpine
|
|
||||||
WORKDIR /app
|
WORKDIR /build
|
||||||
|
|
||||||
COPY . ./
|
COPY . ./
|
||||||
RUN yarn install --frozen-lockfile \
|
|
||||||
&& yarn run build:production
|
|
||||||
|
|
||||||
# Stage 1:
|
RUN yarn install --frozen-lockfile && yarn run build:production
|
||||||
# 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 rm /usr/local/etc/php-fpm.conf \
|
FROM php:8.3-fpm-alpine
|
||||||
&& echo "* * * * * /usr/local/bin/php /app/artisan schedule:run >> /dev/null 2>&1" >> /var/spool/cron/crontabs/root \
|
# FROM --platform=$TARGETOS/$TARGETARCH php:8.3-fpm-alpine
|
||||||
&& echo "0 23 * * * certbot renew --nginx --quiet" >> /var/spool/cron/crontabs/root \
|
|
||||||
&& sed -i s/ssl_session_cache/#ssl_session_cache/g /etc/nginx/nginx.conf \
|
|
||||||
&& mkdir -p /var/run/php /var/run/nginx
|
|
||||||
|
|
||||||
COPY .github/docker/default.conf /etc/nginx/http.d/default.conf
|
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
|
||||||
COPY .github/docker/www.conf /usr/local/etc/php-fpm.conf
|
|
||||||
COPY .github/docker/supervisord.conf /etc/supervisord.conf
|
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" ]
|
ENTRYPOINT [ "/bin/ash", ".github/docker/entrypoint.sh" ]
|
||||||
CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ]
|
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],
|
['UUID', $user->uuid],
|
||||||
['Email', $user->email],
|
['Email', $user->email],
|
||||||
['Username', $user->username],
|
['Username', $user->username],
|
||||||
['Admin', $user->root_admin ? 'Yes' : 'No'],
|
['Admin', $user->isRootAdmin() ? 'Yes' : 'No'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return 0;
|
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\EnvironmentStep;
|
||||||
use App\Filament\Pages\Installer\Steps\RedisStep;
|
use App\Filament\Pages\Installer\Steps\RedisStep;
|
||||||
use App\Filament\Pages\Installer\Steps\RequirementsStep;
|
use App\Filament\Pages\Installer\Steps\RequirementsStep;
|
||||||
|
use App\Models\User;
|
||||||
use App\Services\Users\UserCreationService;
|
use App\Services\Users\UserCreationService;
|
||||||
use App\Traits\CheckMigrationsTrait;
|
use App\Traits\CheckMigrationsTrait;
|
||||||
use App\Traits\EnvironmentWriterTrait;
|
use App\Traits\EnvironmentWriterTrait;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Filament\Facades\Filament;
|
|
||||||
use Filament\Forms\Components\Wizard;
|
use Filament\Forms\Components\Wizard;
|
||||||
use Filament\Forms\Concerns\InteractsWithForms;
|
use Filament\Forms\Concerns\InteractsWithForms;
|
||||||
use Filament\Forms\Contracts\HasForms;
|
use Filament\Forms\Contracts\HasForms;
|
||||||
@@ -44,11 +44,22 @@ class PanelInstaller extends SimplePage implements HasForms
|
|||||||
return MaxWidth::SevenExtraLarge;
|
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()
|
public function mount()
|
||||||
{
|
{
|
||||||
if (is_installed()) {
|
abort_unless(self::show(), 404);
|
||||||
abort(404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->form->fill();
|
$this->form->fill();
|
||||||
}
|
}
|
||||||
@@ -103,10 +114,14 @@ class PanelInstaller extends SimplePage implements HasForms
|
|||||||
$variables = array_get($inputs, 'env');
|
$variables = array_get($inputs, 'env');
|
||||||
$this->writeToEnvironment($variables);
|
$this->writeToEnvironment($variables);
|
||||||
|
|
||||||
|
// Clear config cache
|
||||||
|
Artisan::call('config:clear');
|
||||||
|
|
||||||
// Run migrations
|
// Run migrations
|
||||||
Artisan::call('migrate', [
|
Artisan::call('migrate', [
|
||||||
'--force' => true,
|
'--force' => true,
|
||||||
'--seed' => true,
|
'--seed' => true,
|
||||||
|
'--database' => $variables['DB_CONNECTION'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!$this->hasCompletedMigrations()) {
|
if (!$this->hasCompletedMigrations()) {
|
||||||
@@ -116,10 +131,10 @@ class PanelInstaller extends SimplePage implements HasForms
|
|||||||
// Create first admin user
|
// Create first admin user
|
||||||
$userData = array_get($inputs, 'user');
|
$userData = array_get($inputs, 'user');
|
||||||
$userData['root_admin'] = true;
|
$userData['root_admin'] = true;
|
||||||
app(UserCreationService::class)->handle($userData);
|
$user = app(UserCreationService::class)->handle($userData);
|
||||||
|
|
||||||
// Install setup complete
|
// Install setup complete
|
||||||
$this->writeToEnvironment(['APP_INSTALLED' => 'true']);
|
$this->writeToEnvironment(['APP_INSTALLER' => 'false']);
|
||||||
|
|
||||||
$this->rememberData();
|
$this->rememberData();
|
||||||
|
|
||||||
@@ -128,7 +143,9 @@ class PanelInstaller extends SimplePage implements HasForms
|
|||||||
->success()
|
->success()
|
||||||
->send();
|
->send();
|
||||||
|
|
||||||
redirect()->intended(Filament::getUrl());
|
auth()->loginUsingId($user->id);
|
||||||
|
|
||||||
|
return redirect('/admin');
|
||||||
} catch (Exception $exception) {
|
} catch (Exception $exception) {
|
||||||
report($exception);
|
report($exception);
|
||||||
|
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ class AdminUserStep
|
|||||||
->label('Admin E-Mail')
|
->label('Admin E-Mail')
|
||||||
->required()
|
->required()
|
||||||
->email()
|
->email()
|
||||||
->default('admin@example.com'),
|
->placeholder('admin@example.com'),
|
||||||
TextInput::make('user.username')
|
TextInput::make('user.username')
|
||||||
->label('Admin Username')
|
->label('Admin Username')
|
||||||
->required()
|
->required()
|
||||||
->default('admin'),
|
->placeholder('admin'),
|
||||||
TextInput::make('user.password')
|
TextInput::make('user.password')
|
||||||
->label('Admin Password')
|
->label('Admin Password')
|
||||||
->required()
|
->required()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use Filament\Forms\Components\Wizard\Step;
|
|||||||
use Filament\Forms\Get;
|
use Filament\Forms\Get;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Support\Exceptions\Halt;
|
use Filament\Support\Exceptions\Halt;
|
||||||
use Illuminate\Database\DatabaseManager;
|
use Illuminate\Support\Facades\DB;
|
||||||
use PDOException;
|
use PDOException;
|
||||||
|
|
||||||
class DatabaseStep
|
class DatabaseStep
|
||||||
@@ -61,9 +61,6 @@ class DatabaseStep
|
|||||||
->afterValidation(function (Get $get) {
|
->afterValidation(function (Get $get) {
|
||||||
$driver = $get('env.DB_CONNECTION');
|
$driver = $get('env.DB_CONNECTION');
|
||||||
if ($driver !== 'sqlite') {
|
if ($driver !== 'sqlite') {
|
||||||
/** @var DatabaseManager $database */
|
|
||||||
$database = app(DatabaseManager::class);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
config()->set('database.connections._panel_install_test', [
|
config()->set('database.connections._panel_install_test', [
|
||||||
'driver' => $driver,
|
'driver' => $driver,
|
||||||
@@ -77,7 +74,7 @@ class DatabaseStep
|
|||||||
'strict' => true,
|
'strict' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$database->connection('_panel_install_test')->getPdo();
|
DB::connection('_panel_install_test')->getPdo();
|
||||||
} catch (PDOException $exception) {
|
} catch (PDOException $exception) {
|
||||||
Notification::make()
|
Notification::make()
|
||||||
->title('Database connection failed')
|
->title('Database connection failed')
|
||||||
@@ -85,7 +82,7 @@ class DatabaseStep
|
|||||||
->danger()
|
->danger()
|
||||||
->send();
|
->send();
|
||||||
|
|
||||||
$database->disconnect('_panel_install_test');
|
DB::disconnect('_panel_install_test');
|
||||||
|
|
||||||
throw new Halt('Database connection failed');
|
throw new Halt('Database connection failed');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ class EnvironmentStep
|
|||||||
];
|
];
|
||||||
|
|
||||||
public const QUEUE_DRIVERS = [
|
public const QUEUE_DRIVERS = [
|
||||||
|
'sync' => 'Sync',
|
||||||
'database' => 'Database',
|
'database' => 'Database',
|
||||||
'redis' => 'Redis',
|
'redis' => 'Redis',
|
||||||
'sync' => 'Synchronous',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
public const DATABASE_DRIVERS = [
|
public const DATABASE_DRIVERS = [
|
||||||
@@ -76,7 +76,7 @@ class EnvironmentStep
|
|||||||
ToggleButtons::make('env.QUEUE_CONNECTION')
|
ToggleButtons::make('env.QUEUE_CONNECTION')
|
||||||
->label('Queue Driver')
|
->label('Queue Driver')
|
||||||
->hintIcon('tabler-question-mark')
|
->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()
|
->required()
|
||||||
->inline()
|
->inline()
|
||||||
->options(self::QUEUE_DRIVERS)
|
->options(self::QUEUE_DRIVERS)
|
||||||
|
|||||||
@@ -2,8 +2,13 @@
|
|||||||
|
|
||||||
namespace App\Filament\Pages\Installer\Steps;
|
namespace App\Filament\Pages\Installer\Steps;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Forms\Components\Wizard\Step;
|
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
|
class RedisStep
|
||||||
{
|
{
|
||||||
@@ -37,6 +42,26 @@ class RedisStep
|
|||||||
->password()
|
->password()
|
||||||
->revealable()
|
->revealable()
|
||||||
->default(config('database.redis.default.password')),
|
->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();
|
$this->form->fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function canAccess(): bool
|
||||||
|
{
|
||||||
|
return auth()->user()->can('view settings');
|
||||||
|
}
|
||||||
|
|
||||||
protected function getFormSchema(): array
|
protected function getFormSchema(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Tabs::make('Tabs')
|
Tabs::make('Tabs')
|
||||||
->columns()
|
->columns()
|
||||||
->persistTabInQueryString()
|
->persistTabInQueryString()
|
||||||
|
->disabled(fn () => !auth()->user()->can('update settings'))
|
||||||
->tabs([
|
->tabs([
|
||||||
Tab::make('general')
|
Tab::make('general')
|
||||||
->label('General')
|
->label('General')
|
||||||
@@ -86,6 +92,7 @@ class Settings extends Page implements HasForms
|
|||||||
TextInput::make('APP_NAME')
|
TextInput::make('APP_NAME')
|
||||||
->label('App Name')
|
->label('App Name')
|
||||||
->required()
|
->required()
|
||||||
|
->alphaNum()
|
||||||
->default(env('APP_NAME', 'Pelican')),
|
->default(env('APP_NAME', 'Pelican')),
|
||||||
TextInput::make('APP_FAVICON')
|
TextInput::make('APP_FAVICON')
|
||||||
->label('App Favicon')
|
->label('App Favicon')
|
||||||
@@ -146,10 +153,12 @@ class Settings extends Page implements HasForms
|
|||||||
->color('danger')
|
->color('danger')
|
||||||
->icon('tabler-trash')
|
->icon('tabler-trash')
|
||||||
->requiresConfirmation()
|
->requiresConfirmation()
|
||||||
|
->authorize(fn () => auth()->user()->can('update settings'))
|
||||||
->action(fn (Set $set) => $set('TRUSTED_PROXIES', [])),
|
->action(fn (Set $set) => $set('TRUSTED_PROXIES', [])),
|
||||||
FormAction::make('cloudflare')
|
FormAction::make('cloudflare')
|
||||||
->label('Set to Cloudflare IPs')
|
->label('Set to Cloudflare IPs')
|
||||||
->icon('tabler-brand-cloudflare')
|
->icon('tabler-brand-cloudflare')
|
||||||
|
->authorize(fn () => auth()->user()->can('update settings'))
|
||||||
->action(fn (Set $set) => $set('TRUSTED_PROXIES', [
|
->action(fn (Set $set) => $set('TRUSTED_PROXIES', [
|
||||||
'173.245.48.0/20',
|
'173.245.48.0/20',
|
||||||
'103.21.244.0/22',
|
'103.21.244.0/22',
|
||||||
@@ -225,6 +234,7 @@ class Settings extends Page implements HasForms
|
|||||||
->label('Send Test Mail')
|
->label('Send Test Mail')
|
||||||
->icon('tabler-send')
|
->icon('tabler-send')
|
||||||
->hidden(fn (Get $get) => $get('MAIL_MAILER') === 'log')
|
->hidden(fn (Get $get) => $get('MAIL_MAILER') === 'log')
|
||||||
|
->authorize(fn () => auth()->user()->can('update settings'))
|
||||||
->action(function () {
|
->action(function () {
|
||||||
try {
|
try {
|
||||||
MailNotification::route('mail', auth()->user()->email)
|
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'))),
|
->default(env('MAIL_PORT', config('mail.mailers.smtp.port'))),
|
||||||
TextInput::make('MAIL_USERNAME')
|
TextInput::make('MAIL_USERNAME')
|
||||||
->label('Username')
|
->label('Username')
|
||||||
->required()
|
|
||||||
->default(env('MAIL_USERNAME', config('mail.mailers.smtp.username'))),
|
->default(env('MAIL_USERNAME', config('mail.mailers.smtp.username'))),
|
||||||
TextInput::make('MAIL_PASSWORD')
|
TextInput::make('MAIL_PASSWORD')
|
||||||
->label('Password')
|
->label('Password')
|
||||||
@@ -298,7 +307,7 @@ class Settings extends Page implements HasForms
|
|||||||
TextInput::make('MAILGUN_SECRET')
|
TextInput::make('MAILGUN_SECRET')
|
||||||
->label('Secret')
|
->label('Secret')
|
||||||
->required()
|
->required()
|
||||||
->default(env('MAIL_USERNAME', config('services.mailgun.secret'))),
|
->default(env('MAILGUN_SECRET', config('services.mailgun.secret'))),
|
||||||
TextInput::make('MAILGUN_ENDPOINT')
|
TextInput::make('MAILGUN_ENDPOINT')
|
||||||
->label('Endpoint')
|
->label('Endpoint')
|
||||||
->required()
|
->required()
|
||||||
@@ -561,12 +570,9 @@ class Settings extends Page implements HasForms
|
|||||||
return [
|
return [
|
||||||
Action::make('save')
|
Action::make('save')
|
||||||
->action('save')
|
->action('save')
|
||||||
|
->authorize(fn () => auth()->user()->can('update settings'))
|
||||||
->keyBindings(['mod+s']),
|
->keyBindings(['mod+s']),
|
||||||
];
|
];
|
||||||
|
|
||||||
}
|
}
|
||||||
protected function getFormActions(): array
|
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ class ListDatabaseHosts extends ListRecords
|
|||||||
])
|
])
|
||||||
->bulkActions([
|
->bulkActions([
|
||||||
BulkActionGroup::make([
|
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 App\Filament\Resources\DatabaseResource;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
use Filament\Tables\Actions\EditAction;
|
|
||||||
use Filament\Resources\Pages\ListRecords;
|
use Filament\Resources\Pages\ListRecords;
|
||||||
use Filament\Tables\Actions\BulkActionGroup;
|
use Filament\Tables\Actions\BulkActionGroup;
|
||||||
use Filament\Tables\Actions\DeleteBulkAction;
|
use Filament\Tables\Actions\DeleteBulkAction;
|
||||||
|
use Filament\Tables\Actions\EditAction;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
@@ -48,7 +48,8 @@ class ListDatabases extends ListRecords
|
|||||||
])
|
])
|
||||||
->bulkActions([
|
->bulkActions([
|
||||||
BulkActionGroup::make([
|
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 {
|
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
|
||||||
$data['default_value'] ??= '';
|
$data['default_value'] ??= '';
|
||||||
$data['description'] ??= '';
|
$data['description'] ??= '';
|
||||||
$data['rules'] ??= '';
|
$data['rules'] ??= [];
|
||||||
$data['user_viewable'] ??= '';
|
$data['user_viewable'] ??= '';
|
||||||
$data['user_editable'] ??= '';
|
$data['user_editable'] ??= '';
|
||||||
|
|
||||||
@@ -143,7 +143,7 @@ class CreateEgg extends CreateRecord
|
|||||||
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
|
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
|
||||||
$data['default_value'] ??= '';
|
$data['default_value'] ??= '';
|
||||||
$data['description'] ??= '';
|
$data['description'] ??= '';
|
||||||
$data['rules'] ??= '';
|
$data['rules'] ??= [];
|
||||||
$data['user_viewable'] ??= '';
|
$data['user_viewable'] ??= '';
|
||||||
$data['user_editable'] ??= '';
|
$data['user_editable'] ??= '';
|
||||||
|
|
||||||
@@ -173,7 +173,30 @@ class CreateEgg extends CreateRecord
|
|||||||
Checkbox::make('user_viewable')->label('Viewable'),
|
Checkbox::make('user_viewable')->label('Viewable'),
|
||||||
Checkbox::make('user_editable')->label('Editable'),
|
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')
|
Tab::make('Install Script')
|
||||||
|
|||||||
@@ -2,12 +2,15 @@
|
|||||||
|
|
||||||
namespace App\Filament\Resources\EggResource\Pages;
|
namespace App\Filament\Resources\EggResource\Pages;
|
||||||
|
|
||||||
|
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
|
||||||
use App\Filament\Resources\EggResource;
|
use App\Filament\Resources\EggResource;
|
||||||
use App\Filament\Resources\EggResource\RelationManagers\ServersRelationManager;
|
use App\Filament\Resources\EggResource\RelationManagers\ServersRelationManager;
|
||||||
use App\Models\Egg;
|
use App\Models\Egg;
|
||||||
|
use App\Services\Eggs\Sharing\EggExporterService;
|
||||||
use App\Services\Eggs\Sharing\EggImporterService;
|
use App\Services\Eggs\Sharing\EggImporterService;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
|
use Filament\Forms;
|
||||||
use Filament\Forms\Components\Checkbox;
|
use Filament\Forms\Components\Checkbox;
|
||||||
use Filament\Forms\Components\Fieldset;
|
use Filament\Forms\Components\Fieldset;
|
||||||
use Filament\Forms\Components\FileUpload;
|
use Filament\Forms\Components\FileUpload;
|
||||||
@@ -22,12 +25,9 @@ use Filament\Forms\Components\TagsInput;
|
|||||||
use Filament\Forms\Components\Textarea;
|
use Filament\Forms\Components\Textarea;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Forms\Components\Toggle;
|
use Filament\Forms\Components\Toggle;
|
||||||
|
use Filament\Forms\Form;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Resources\Pages\EditRecord;
|
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
|
class EditEgg extends EditRecord
|
||||||
{
|
{
|
||||||
@@ -144,7 +144,7 @@ class EditEgg extends EditRecord
|
|||||||
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
|
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
|
||||||
$data['default_value'] ??= '';
|
$data['default_value'] ??= '';
|
||||||
$data['description'] ??= '';
|
$data['description'] ??= '';
|
||||||
$data['rules'] ??= '';
|
$data['rules'] ??= [];
|
||||||
$data['user_viewable'] ??= '';
|
$data['user_viewable'] ??= '';
|
||||||
$data['user_editable'] ??= '';
|
$data['user_editable'] ??= '';
|
||||||
|
|
||||||
@@ -153,7 +153,7 @@ class EditEgg extends EditRecord
|
|||||||
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
|
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
|
||||||
$data['default_value'] ??= '';
|
$data['default_value'] ??= '';
|
||||||
$data['description'] ??= '';
|
$data['description'] ??= '';
|
||||||
$data['rules'] ??= '';
|
$data['rules'] ??= [];
|
||||||
$data['user_viewable'] ??= '';
|
$data['user_viewable'] ??= '';
|
||||||
$data['user_editable'] ??= '';
|
$data['user_editable'] ??= '';
|
||||||
|
|
||||||
@@ -183,7 +183,30 @@ class EditEgg extends EditRecord
|
|||||||
Checkbox::make('user_viewable')->label('Viewable'),
|
Checkbox::make('user_viewable')->label('Viewable'),
|
||||||
Checkbox::make('user_editable')->label('Editable'),
|
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')
|
Tab::make('Install Script')
|
||||||
@@ -222,14 +245,13 @@ class EditEgg extends EditRecord
|
|||||||
Actions\DeleteAction::make('deleteEgg')
|
Actions\DeleteAction::make('deleteEgg')
|
||||||
->disabled(fn (Egg $egg): bool => $egg->servers()->count() > 0)
|
->disabled(fn (Egg $egg): bool => $egg->servers()->count() > 0)
|
||||||
->label(fn (Egg $egg): string => $egg->servers()->count() <= 0 ? 'Delete' : 'In Use'),
|
->label(fn (Egg $egg): string => $egg->servers()->count() <= 0 ? 'Delete' : 'In Use'),
|
||||||
|
|
||||||
Actions\Action::make('exportEgg')
|
Actions\Action::make('exportEgg')
|
||||||
->label('Export')
|
->label('Export')
|
||||||
->color('primary')
|
->color('primary')
|
||||||
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
|
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
|
||||||
echo $service->handle($egg->id);
|
echo $service->handle($egg->id);
|
||||||
}, 'egg-' . $egg->getKebabName() . '.json')),
|
}, 'egg-' . $egg->getKebabName() . '.json'))
|
||||||
|
->authorize(fn () => auth()->user()->can('export egg')),
|
||||||
Actions\Action::make('importEgg')
|
Actions\Action::make('importEgg')
|
||||||
->label('Import')
|
->label('Import')
|
||||||
->form([
|
->form([
|
||||||
@@ -298,8 +320,8 @@ class EditEgg extends EditRecord
|
|||||||
->title('Import Success')
|
->title('Import Success')
|
||||||
->success()
|
->success()
|
||||||
->send();
|
->send();
|
||||||
}),
|
})
|
||||||
|
->authorize(fn () => auth()->user()->can('import egg')),
|
||||||
$this->getSaveFormAction()->formId('form'),
|
$this->getSaveFormAction()->formId('form'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ use Filament\Forms\Components\Tabs\Tab;
|
|||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Resources\Pages\ListRecords;
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
use Filament\Tables;
|
||||||
use Filament\Tables\Actions\BulkActionGroup;
|
use Filament\Tables\Actions\BulkActionGroup;
|
||||||
use Filament\Tables\Actions\DeleteBulkAction;
|
use Filament\Tables\Actions\DeleteBulkAction;
|
||||||
use Filament\Tables\Actions\EditAction;
|
use Filament\Tables\Actions\EditAction;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||||
use Filament\Tables;
|
|
||||||
|
|
||||||
class ListEggs extends ListRecords
|
class ListEggs extends ListRecords
|
||||||
{
|
{
|
||||||
@@ -55,11 +55,13 @@ class ListEggs extends ListRecords
|
|||||||
->color('primary')
|
->color('primary')
|
||||||
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
|
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
|
||||||
echo $service->handle($egg->id);
|
echo $service->handle($egg->id);
|
||||||
}, 'egg-' . $egg->getKebabName() . '.json')),
|
}, 'egg-' . $egg->getKebabName() . '.json'))
|
||||||
|
->authorize(fn () => auth()->user()->can('export egg')),
|
||||||
])
|
])
|
||||||
->bulkActions([
|
->bulkActions([
|
||||||
BulkActionGroup::make([
|
BulkActionGroup::make([
|
||||||
DeleteBulkAction::make(),
|
DeleteBulkAction::make()
|
||||||
|
->authorize(fn () => auth()->user()->can('delete egg')),
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -138,7 +140,8 @@ class ListEggs extends ListRecords
|
|||||||
->title('Import Success')
|
->title('Import Success')
|
||||||
->success()
|
->success()
|
||||||
->send();
|
->send();
|
||||||
}),
|
})
|
||||||
|
->authorize(fn () => auth()->user()->can('import egg')),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ class ListMounts extends ListRecords
|
|||||||
])
|
])
|
||||||
->bulkActions([
|
->bulkActions([
|
||||||
BulkActionGroup::make([
|
BulkActionGroup::make([
|
||||||
DeleteBulkAction::make(),
|
DeleteBulkAction::make()
|
||||||
|
->authorize(fn () => auth()->user()->can('delete mount')),
|
||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
->emptyStateIcon('tabler-layers-linked')
|
->emptyStateIcon('tabler-layers-linked')
|
||||||
|
|||||||
@@ -84,7 +84,8 @@ class ListNodes extends ListRecords
|
|||||||
])
|
])
|
||||||
->bulkActions([
|
->bulkActions([
|
||||||
BulkActionGroup::make([
|
BulkActionGroup::make([
|
||||||
DeleteBulkAction::make(),
|
DeleteBulkAction::make()
|
||||||
|
->authorize(fn () => auth()->user()->can('delete node')),
|
||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
->emptyStateIcon('tabler-server-2')
|
->emptyStateIcon('tabler-server-2')
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ use App\Models\Node;
|
|||||||
use App\Services\Allocations\AssignmentService;
|
use App\Services\Allocations\AssignmentService;
|
||||||
use Filament\Forms\Components\TagsInput;
|
use Filament\Forms\Components\TagsInput;
|
||||||
use Filament\Forms\Components\TextInput;
|
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\Form;
|
||||||
|
use Filament\Forms\Set;
|
||||||
use Filament\Resources\RelationManagers\RelationManager;
|
use Filament\Resources\RelationManagers\RelationManager;
|
||||||
use Filament\Tables;
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Actions\BulkActionGroup;
|
||||||
|
use Filament\Tables\Actions\DeleteBulkAction;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Tables\Columns\TextInputColumn;
|
use Filament\Tables\Columns\TextInputColumn;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
@@ -152,7 +152,8 @@ class AllocationsRelationManager extends RelationManager
|
|||||||
])
|
])
|
||||||
->bulkActions([
|
->bulkActions([
|
||||||
BulkActionGroup::make([
|
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')
|
->relationship('user', 'username')
|
||||||
->searchable(['username', 'email'])
|
->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([
|
->createOptionForm([
|
||||||
Forms\Components\TextInput::make('username')
|
Forms\Components\TextInput::make('username')
|
||||||
->alphaNum()
|
->alphaNum()
|
||||||
@@ -98,21 +98,6 @@ class CreateServer extends CreateRecord
|
|||||||
->hintIcon('tabler-question-mark')
|
->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.')
|
->hintIconTooltip('Providing a user password is optional. New user email will prompt users to create a password the first time they login.')
|
||||||
->password(),
|
->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) {
|
->createOptionUsing(function ($data) {
|
||||||
resolve(UserCreationService::class)->handle($data);
|
resolve(UserCreationService::class)->handle($data);
|
||||||
@@ -321,9 +306,9 @@ class CreateServer extends CreateRecord
|
|||||||
->completedIcon('tabler-check')
|
->completedIcon('tabler-check')
|
||||||
->columns([
|
->columns([
|
||||||
'default' => 1,
|
'default' => 1,
|
||||||
'sm' => 2,
|
'sm' => 4,
|
||||||
'md' => 2,
|
'md' => 4,
|
||||||
'lg' => 4,
|
'lg' => 6,
|
||||||
])
|
])
|
||||||
->schema([
|
->schema([
|
||||||
Forms\Components\Select::make('egg_id')
|
Forms\Components\Select::make('egg_id')
|
||||||
@@ -333,7 +318,7 @@ class CreateServer extends CreateRecord
|
|||||||
'default' => 1,
|
'default' => 1,
|
||||||
'sm' => 2,
|
'sm' => 2,
|
||||||
'md' => 2,
|
'md' => 2,
|
||||||
'lg' => 3,
|
'lg' => 4,
|
||||||
])
|
])
|
||||||
->searchable()
|
->searchable()
|
||||||
->preload()
|
->preload()
|
||||||
@@ -390,29 +375,51 @@ class CreateServer extends CreateRecord
|
|||||||
->inline()
|
->inline()
|
||||||
->required(),
|
->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')
|
Forms\Components\Textarea::make('startup')
|
||||||
->hintIcon('tabler-code')
|
->hintIcon('tabler-code')
|
||||||
->label('Startup Command')
|
->label('Startup Command')
|
||||||
->hidden(fn (Forms\Get $get) => $get('egg_id') === null)
|
->hidden(fn (Forms\Get $get) => $get('egg_id') === null)
|
||||||
->required()
|
->required()
|
||||||
->live()
|
->live()
|
||||||
->columnSpan([
|
|
||||||
'default' => 1,
|
|
||||||
'sm' => 2,
|
|
||||||
'md' => 2,
|
|
||||||
'lg' => 4,
|
|
||||||
])
|
|
||||||
->rows(function ($state) {
|
->rows(function ($state) {
|
||||||
return str($state)->explode("\n")->reduce(
|
return str($state)->explode("\n")->reduce(
|
||||||
fn (int $carry, $line) => $carry + floor(strlen($line) / 125),
|
fn (int $carry, $line) => $carry + floor(strlen($line) / 125),
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
}),
|
})
|
||||||
|
->columnSpan([
|
||||||
|
'default' => 1,
|
||||||
|
'sm' => 4,
|
||||||
|
'md' => 4,
|
||||||
|
'lg' => 6,
|
||||||
|
]),
|
||||||
|
|
||||||
Forms\Components\Hidden::make('environment')->default([]),
|
Forms\Components\Hidden::make('environment')->default([]),
|
||||||
|
|
||||||
Forms\Components\Hidden::make('start_on_completion')->default(true),
|
|
||||||
|
|
||||||
Forms\Components\Section::make('Variables')
|
Forms\Components\Section::make('Variables')
|
||||||
->icon('tabler-eggs')
|
->icon('tabler-eggs')
|
||||||
->iconColor('primary')
|
->iconColor('primary')
|
||||||
@@ -443,8 +450,7 @@ class CreateServer extends CreateRecord
|
|||||||
|
|
||||||
$text = Forms\Components\TextInput::make('variable_value')
|
$text = Forms\Components\TextInput::make('variable_value')
|
||||||
->hidden($this->shouldHideComponent(...))
|
->hidden($this->shouldHideComponent(...))
|
||||||
->maxLength(255)
|
->required(fn (Forms\Get $get) => in_array('required', $get('rules')))
|
||||||
->required(fn (Forms\Get $get) => in_array('required', explode('|', $get('rules'))))
|
|
||||||
->rules(
|
->rules(
|
||||||
fn (Forms\Get $get): Closure => function (string $attribute, $value, Closure $fail) use ($get) {
|
fn (Forms\Get $get): Closure => function (string $attribute, $value, Closure $fail) use ($get) {
|
||||||
$validator = Validator::make(['validatorkey' => $value], [
|
$validator = Validator::make(['validatorkey' => $value], [
|
||||||
@@ -471,7 +477,7 @@ class CreateServer extends CreateRecord
|
|||||||
->live(onBlur: true)
|
->live(onBlur: true)
|
||||||
->hintIcon('tabler-code')
|
->hintIcon('tabler-code')
|
||||||
->label(fn (Forms\Get $get) => $get('name'))
|
->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') . '}}')
|
->prefix(fn (Forms\Get $get) => '{{' . $get('env_variable') . '}}')
|
||||||
->helperText(fn (Forms\Get $get) => empty($get('description')) ? '—' : $get('description'))
|
->helperText(fn (Forms\Get $get) => empty($get('description')) ? '—' : $get('description'))
|
||||||
->afterStateUpdated(function (Forms\Set $set, Forms\Get $get, $state) {
|
->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
|
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
|
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
|
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, ''
|
fn ($result, $value) => str($value)->startsWith('in:') ? $value : $result, ''
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ use Filament\Notifications\Notification;
|
|||||||
use Filament\Resources\Pages\EditRecord;
|
use Filament\Resources\Pages\EditRecord;
|
||||||
use Illuminate\Support\Facades\Validator;
|
use Illuminate\Support\Facades\Validator;
|
||||||
use Closure;
|
use Closure;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
||||||
|
|
||||||
class EditServer extends EditRecord
|
class EditServer extends EditRecord
|
||||||
@@ -473,7 +474,21 @@ class EditServer extends EditRecord
|
|||||||
->columnSpan(6),
|
->columnSpan(6),
|
||||||
|
|
||||||
Forms\Components\Repeater::make('server_variables')
|
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()
|
->grid()
|
||||||
->mutateRelationshipDataBeforeSaveUsing(function (array &$data): array {
|
->mutateRelationshipDataBeforeSaveUsing(function (array &$data): array {
|
||||||
foreach ($data as $key => $value) {
|
foreach ($data as $key => $value) {
|
||||||
@@ -489,7 +504,7 @@ class EditServer extends EditRecord
|
|||||||
|
|
||||||
$text = Forms\Components\TextInput::make('variable_value')
|
$text = Forms\Components\TextInput::make('variable_value')
|
||||||
->hidden($this->shouldHideComponent(...))
|
->hidden($this->shouldHideComponent(...))
|
||||||
->required(fn (ServerVariable $serverVariable) => in_array('required', explode('|', $serverVariable->variable->rules)))
|
->required(fn (ServerVariable $serverVariable) => $serverVariable->variable->getRequiredAttribute())
|
||||||
->rules([
|
->rules([
|
||||||
fn (ServerVariable $serverVariable): Closure => function (string $attribute, $value, Closure $fail) use ($serverVariable) {
|
fn (ServerVariable $serverVariable): Closure => function (string $attribute, $value, Closure $fail) use ($serverVariable) {
|
||||||
$validator = Validator::make(['validatorkey' => $value], [
|
$validator = Validator::make(['validatorkey' => $value], [
|
||||||
@@ -516,7 +531,7 @@ class EditServer extends EditRecord
|
|||||||
->live(onBlur: true)
|
->live(onBlur: true)
|
||||||
->hintIcon('tabler-code')
|
->hintIcon('tabler-code')
|
||||||
->label(fn (ServerVariable $serverVariable) => $serverVariable->variable->name)
|
->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 . '}}')
|
->prefix(fn (ServerVariable $serverVariable) => '{{' . $serverVariable->variable->env_variable . '}}')
|
||||||
->helperText(fn (ServerVariable $serverVariable) => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description);
|
->helperText(fn (ServerVariable $serverVariable) => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description);
|
||||||
}
|
}
|
||||||
@@ -720,12 +735,16 @@ class EditServer extends EditRecord
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Actions\DeleteAction::make('Delete')
|
Actions\Action::make('Delete')
|
||||||
->successRedirectUrl(route('filament.admin.resources.servers.index'))
|
->successRedirectUrl(route('filament.admin.resources.servers.index'))
|
||||||
->color('danger')
|
->color('danger')
|
||||||
->label('Delete')
|
->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')
|
Actions\Action::make('console')
|
||||||
->label('Console')
|
->label('Console')
|
||||||
->icon('tabler-terminal')
|
->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(
|
$containsRuleIn = array_first($serverVariable->variable->rules, fn ($value) => str($value)->startsWith('in:'), false);
|
||||||
fn ($result, $value) => $result === true && !str($value)->startsWith('in:'), true
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($component instanceof Forms\Components\Select) {
|
if ($component instanceof Forms\Components\Select) {
|
||||||
return $containsRuleIn;
|
return !$containsRuleIn;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($component instanceof Forms\Components\TextInput) {
|
if ($component instanceof Forms\Components\TextInput) {
|
||||||
return !$containsRuleIn;
|
return $containsRuleIn;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new \Exception('Component type not supported: ' . $component::class);
|
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(
|
$inRule = array_first($serverVariable->variable->rules, fn ($value) => str($value)->startsWith('in:'));
|
||||||
fn ($result, $value) => str($value)->startsWith('in:') ? $value : $result, ''
|
|
||||||
);
|
|
||||||
|
|
||||||
return str($inRule)
|
return str($inRule)
|
||||||
->after('in:')
|
->after('in:')
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace App\Filament\Resources\ServerResource\Pages;
|
|||||||
|
|
||||||
use App\Filament\Resources\ServerResource;
|
use App\Filament\Resources\ServerResource;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
|
use App\Models\User;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\ListRecords;
|
use Filament\Resources\Pages\ListRecords;
|
||||||
use Filament\Tables\Actions\CreateAction;
|
use Filament\Tables\Actions\CreateAction;
|
||||||
@@ -76,7 +77,13 @@ class ListServers extends ListRecords
|
|||||||
->actions([
|
->actions([
|
||||||
Tables\Actions\Action::make('View')
|
Tables\Actions\Action::make('View')
|
||||||
->icon('tabler-terminal')
|
->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(),
|
Tables\Actions\EditAction::make(),
|
||||||
])
|
])
|
||||||
->emptyStateIcon('tabler-brand-docker')
|
->emptyStateIcon('tabler-brand-docker')
|
||||||
|
|||||||
@@ -3,13 +3,16 @@
|
|||||||
namespace App\Filament\Resources\UserResource\Pages;
|
namespace App\Filament\Resources\UserResource\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\UserResource;
|
use App\Filament\Resources\UserResource;
|
||||||
use App\Services\Exceptions\FilamentExceptionHandler;
|
use App\Models\Role;
|
||||||
use Filament\Actions;
|
|
||||||
use Filament\Resources\Pages\EditRecord;
|
|
||||||
use App\Models\User;
|
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\Section;
|
||||||
|
use Filament\Forms\Components\Select;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Forms\Form;
|
use Filament\Forms\Form;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
||||||
class EditUser extends EditRecord
|
class EditUser extends EditRecord
|
||||||
@@ -20,54 +23,33 @@ class EditUser extends EditRecord
|
|||||||
return $form
|
return $form
|
||||||
->schema([
|
->schema([
|
||||||
Section::make()->schema([
|
Section::make()->schema([
|
||||||
Forms\Components\TextInput::make('username')->required()->maxLength(255),
|
TextInput::make('username')->required()->maxLength(255),
|
||||||
Forms\Components\TextInput::make('email')->email()->required()->maxLength(255),
|
TextInput::make('email')->email()->required()->maxLength(255),
|
||||||
|
TextInput::make('password')
|
||||||
Forms\Components\TextInput::make('password')
|
|
||||||
->dehydrateStateUsing(fn (string $state): string => Hash::make($state))
|
->dehydrateStateUsing(fn (string $state): string => Hash::make($state))
|
||||||
->dehydrated(fn (?string $state): bool => filled($state))
|
->dehydrated(fn (?string $state): bool => filled($state))
|
||||||
->required(fn (string $operation): bool => $operation === 'create')
|
->required(fn (string $operation): bool => $operation === 'create')
|
||||||
->password(),
|
->password(),
|
||||||
|
Select::make('language')
|
||||||
Forms\Components\ToggleButtons::make('root_admin')
|
|
||||||
->label('Administrator (Root)')
|
|
||||||
->options([
|
|
||||||
false => 'No',
|
|
||||||
true => 'Admin',
|
|
||||||
])
|
|
||||||
->colors([
|
|
||||||
false => 'primary',
|
|
||||||
true => 'danger',
|
|
||||||
])
|
|
||||||
->disableOptionWhen(function (string $operation, $value, User $user) {
|
|
||||||
if ($operation !== 'edit' || $value) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $user->isLastRootAdmin();
|
|
||||||
})
|
|
||||||
->hint(fn (User $user) => $user->isLastRootAdmin() ? 'This is the last root administrator!' : '')
|
|
||||||
->helperText(fn (User $user) => $user->isLastRootAdmin() ? 'You must have at least one root administrator in your system.' : '')
|
|
||||||
->hintColor('warning')
|
|
||||||
->inline()
|
|
||||||
->required()
|
|
||||||
->default(false),
|
|
||||||
|
|
||||||
Forms\Components\Hidden::make('skipValidation')->default(true),
|
|
||||||
|
|
||||||
Forms\Components\Select::make('language')
|
|
||||||
->required()
|
->required()
|
||||||
->hidden()
|
->hidden()
|
||||||
->default('en')
|
->default('en')
|
||||||
->options(fn (User $user) => $user->getAvailableLanguages()),
|
->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(),
|
])->columns(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
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'))
|
->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),
|
->disabled(fn (User $user) => auth()->user()->id === $user->id || $user->servers()->count() > 0),
|
||||||
$this->getSaveFormAction()->formId('form'),
|
$this->getSaveFormAction()->formId('form'),
|
||||||
@@ -78,9 +60,4 @@ class EditUser extends EditRecord
|
|||||||
{
|
{
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function exception($exception, $stopPropagation): void
|
|
||||||
{
|
|
||||||
(new FilamentExceptionHandler())->handle($exception, $stopPropagation);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,22 @@
|
|||||||
namespace App\Filament\Resources\UserResource\Pages;
|
namespace App\Filament\Resources\UserResource\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\UserResource;
|
use App\Filament\Resources\UserResource;
|
||||||
|
use App\Models\Role;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\Users\UserCreationService;
|
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\Notifications\Notification;
|
||||||
use Filament\Resources\Pages\ListRecords;
|
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\Table;
|
||||||
use Filament\Tables;
|
|
||||||
use Filament\Forms;
|
|
||||||
|
|
||||||
class ListUsers extends ListRecords
|
class ListUsers extends ListRecords
|
||||||
{
|
{
|
||||||
@@ -21,101 +29,102 @@ class ListUsers extends ListRecords
|
|||||||
return $table
|
return $table
|
||||||
->searchable(false)
|
->searchable(false)
|
||||||
->columns([
|
->columns([
|
||||||
Tables\Columns\ImageColumn::make('picture')
|
ImageColumn::make('picture')
|
||||||
->visibleFrom('lg')
|
->visibleFrom('lg')
|
||||||
->label('')
|
->label('')
|
||||||
->extraImgAttributes(['class' => 'rounded-full'])
|
->extraImgAttributes(['class' => 'rounded-full'])
|
||||||
->defaultImageUrl(fn (User $user) => 'https://gravatar.com/avatar/' . md5(strtolower($user->email))),
|
->defaultImageUrl(fn (User $user) => 'https://gravatar.com/avatar/' . md5(strtolower($user->email))),
|
||||||
Tables\Columns\TextColumn::make('external_id')
|
TextColumn::make('external_id')
|
||||||
->searchable()
|
->searchable()
|
||||||
->hidden(),
|
->hidden(),
|
||||||
Tables\Columns\TextColumn::make('uuid')
|
TextColumn::make('uuid')
|
||||||
->label('UUID')
|
->label('UUID')
|
||||||
->hidden()
|
->hidden()
|
||||||
->searchable(),
|
->searchable(),
|
||||||
Tables\Columns\TextColumn::make('username')
|
TextColumn::make('username')
|
||||||
->searchable(),
|
->searchable(),
|
||||||
Tables\Columns\TextColumn::make('email')
|
TextColumn::make('email')
|
||||||
->searchable()
|
->searchable()
|
||||||
->icon('tabler-mail'),
|
->icon('tabler-mail'),
|
||||||
Tables\Columns\IconColumn::make('root_admin')
|
IconColumn::make('use_totp')
|
||||||
->visibleFrom('md')
|
->label('2FA')
|
||||||
->label('Admin')
|
|
||||||
->boolean()
|
|
||||||
->trueIcon('tabler-star-filled')
|
|
||||||
->falseIcon('tabler-star-off')
|
|
||||||
->sortable(),
|
|
||||||
Tables\Columns\IconColumn::make('use_totp')->label('2FA')
|
|
||||||
->visibleFrom('lg')
|
->visibleFrom('lg')
|
||||||
->icon(fn (User $user) => $user->use_totp ? 'tabler-lock' : 'tabler-lock-open-off')
|
->icon(fn (User $user) => $user->use_totp ? 'tabler-lock' : 'tabler-lock-open-off')
|
||||||
->boolean()->sortable(),
|
->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')
|
->counts('servers')
|
||||||
->icon('tabler-server')
|
->icon('tabler-server')
|
||||||
->label('Servers'),
|
->label('Servers'),
|
||||||
Tables\Columns\TextColumn::make('subusers_count')
|
TextColumn::make('subusers_count')
|
||||||
->visibleFrom('sm')
|
->visibleFrom('sm')
|
||||||
->label('Subusers')
|
->label('Subusers')
|
||||||
->counts('subusers')
|
->counts('subusers')
|
||||||
->icon('tabler-users'),
|
->icon('tabler-users'),
|
||||||
// ->formatStateUsing(fn (string $state, $record): string => (string) ($record->servers_count + $record->subusers_count))
|
// ->formatStateUsing(fn (string $state, $record): string => (string) ($record->servers_count + $record->subusers_count))
|
||||||
])
|
])
|
||||||
->filters([
|
|
||||||
//
|
|
||||||
])
|
|
||||||
->actions([
|
->actions([
|
||||||
Tables\Actions\EditAction::make(),
|
EditAction::make(),
|
||||||
])
|
])
|
||||||
->checkIfRecordIsSelectableUsing(fn (User $user) => auth()->user()->id !== $user->id && !$user->servers_count)
|
->checkIfRecordIsSelectableUsing(fn (User $user) => auth()->user()->id !== $user->id && !$user->servers_count)
|
||||||
->bulkActions([
|
->bulkActions([
|
||||||
Tables\Actions\BulkActionGroup::make([
|
BulkActionGroup::make([
|
||||||
Tables\Actions\DeleteBulkAction::make(),
|
DeleteBulkAction::make()
|
||||||
|
->authorize(fn () => auth()->user()->can('delete user')),
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Actions\CreateAction::make('create')
|
CreateAction::make('create')
|
||||||
->label('Create User')
|
->label('Create User')
|
||||||
->createAnother(false)
|
->createAnother(false)
|
||||||
->form([
|
->form([
|
||||||
Forms\Components\Grid::make()
|
Grid::make()
|
||||||
->schema([
|
->schema([
|
||||||
Forms\Components\TextInput::make('username')
|
TextInput::make('username')
|
||||||
->alphaNum()
|
->alphaNum()
|
||||||
->required()
|
->required()
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
Forms\Components\TextInput::make('email')
|
TextInput::make('email')
|
||||||
->email()
|
->email()
|
||||||
->required()
|
->required()
|
||||||
->unique()
|
->unique()
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
|
TextInput::make('password')
|
||||||
Forms\Components\TextInput::make('password')
|
|
||||||
->hintIcon('tabler-question-mark')
|
->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.')
|
->hintIconTooltip('Providing a user password is optional. New user email will prompt users to create a password the first time they login.')
|
||||||
->password(),
|
->password(),
|
||||||
|
CheckboxList::make('roles')
|
||||||
Forms\Components\ToggleButtons::make('root_admin')
|
->disableOptionWhen(fn (string $value): bool => $value == Role::getRootAdmin()->id)
|
||||||
->label('Administrator (Root)')
|
->relationship('roles', 'name')
|
||||||
->options([
|
->dehydrated()
|
||||||
false => 'No',
|
->label('Admin Roles')
|
||||||
true => 'Admin',
|
->columnSpanFull()
|
||||||
])
|
->bulkToggleable(false),
|
||||||
->colors([
|
|
||||||
false => 'primary',
|
|
||||||
true => 'danger',
|
|
||||||
])
|
|
||||||
->inline()
|
|
||||||
->required()
|
|
||||||
->default(false),
|
|
||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
->successRedirectUrl(route('filament.admin.resources.users.index'))
|
->successRedirectUrl(route('filament.admin.resources.users.index'))
|
||||||
->action(function (array $data) {
|
->action(function (array $data) {
|
||||||
resolve(UserCreationService::class)->handle($data);
|
$roles = $data['roles'];
|
||||||
Notification::make()->title('User Created!')->success()->send();
|
$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');
|
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\DeleteUserRequest;
|
||||||
use App\Http\Requests\Api\Application\Users\UpdateUserRequest;
|
use App\Http\Requests\Api\Application\Users\UpdateUserRequest;
|
||||||
use App\Http\Controllers\Api\Application\ApplicationApiController;
|
use App\Http\Controllers\Api\Application\ApplicationApiController;
|
||||||
|
use App\Http\Requests\Api\Application\Users\AssignUserRolesRequest;
|
||||||
|
|
||||||
class UserController extends ApplicationApiController
|
class UserController extends ApplicationApiController
|
||||||
{
|
{
|
||||||
@@ -75,6 +76,19 @@ class UserController extends ApplicationApiController
|
|||||||
return $response->toArray();
|
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
|
* Store a new user on the system. Returns the created user and an HTTP/201
|
||||||
* header on successful creation.
|
* header on successful creation.
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class ClientController extends ClientApiController
|
|||||||
if (in_array($type, ['admin', 'admin-all'])) {
|
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
|
// 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.
|
// make it a query that will never return any results back.
|
||||||
if (!$user->root_admin) {
|
if (!$user->isRootAdmin()) {
|
||||||
$builder->whereRaw('1 = 2');
|
$builder->whereRaw('1 = 2');
|
||||||
} else {
|
} else {
|
||||||
$builder = $type === 'admin-all'
|
$builder = $type === 'admin-all'
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use Illuminate\Database\Query\JoinClause;
|
|||||||
use App\Http\Requests\Api\Client\ClientApiRequest;
|
use App\Http\Requests\Api\Client\ClientApiRequest;
|
||||||
use App\Transformers\Api\Client\ActivityLogTransformer;
|
use App\Transformers\Api\Client\ActivityLogTransformer;
|
||||||
use App\Http\Controllers\Api\Client\ClientApiController;
|
use App\Http\Controllers\Api\Client\ClientApiController;
|
||||||
|
use App\Models\Role;
|
||||||
|
|
||||||
class ActivityLogController extends ClientApiController
|
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
|
// 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.
|
// painful so for now we'll execute a simpler query.
|
||||||
$subusers = $server->subusers()->pluck('user_id')->merge([$server->owner_id]);
|
$subusers = $server->subusers()->pluck('user_id')->merge([$server->owner_id]);
|
||||||
|
$rootAdmins = Role::getRootAdmin()->users()->pluck('id');
|
||||||
|
|
||||||
$builder->select('activity_logs.*')
|
$builder->select('activity_logs.*')
|
||||||
->leftJoin('users', function (JoinClause $join) {
|
->leftJoin('users', function (JoinClause $join) {
|
||||||
$join->on('users.id', 'activity_logs.actor_id')
|
$join->on('users.id', 'activity_logs.actor_id')
|
||||||
->where('activity_logs.actor_type', (new User())->getMorphClass());
|
->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')
|
$builder->whereNull('users.id')
|
||||||
->orWhere('users.root_admin', 0)
|
->orWhereNotIn('users.id', $rootAdmins)
|
||||||
->orWhereIn('users.id', $subusers);
|
->orWhereIn('users.id', $subusers);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ class SftpAuthenticationController extends Controller
|
|||||||
*/
|
*/
|
||||||
protected function validateSftpAccess(User $user, Server $server): void
|
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);
|
$permissions = $this->permissions->handle($server, $user);
|
||||||
|
|
||||||
if (!in_array(Permission::ACTION_FILE_SFTP, $permissions)) {
|
if (!in_array(Permission::ACTION_FILE_SFTP, $permissions)) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Auth;
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Filament\Pages\Installer\PanelInstaller;
|
||||||
use Carbon\CarbonImmutable;
|
use Carbon\CarbonImmutable;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -17,8 +18,12 @@ class LoginController extends AbstractLoginController
|
|||||||
* base authentication view component. React will take over at this point and
|
* base authentication view component. React will take over at this point and
|
||||||
* turn the login area into an SPA.
|
* 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');
|
return view('templates/auth.core');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class AdminAuthenticate
|
|||||||
*/
|
*/
|
||||||
public function handle(Request $request, \Closure $next): mixed
|
public function handle(Request $request, \Closure $next): mixed
|
||||||
{
|
{
|
||||||
if (!$request->user() || !$request->user()->root_admin) {
|
if (!$request->user() || !$request->user()->isRootAdmin()) {
|
||||||
throw new AccessDeniedHttpException();
|
throw new AccessDeniedHttpException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class AuthenticateApplicationUser
|
|||||||
{
|
{
|
||||||
/** @var \App\Models\User|null $user */
|
/** @var \App\Models\User|null $user */
|
||||||
$user = $request->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.');
|
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
|
// 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
|
// server owner, a subuser, or a root admin. We'll leave it up to the controllers
|
||||||
// to authenticate more detailed permissions if needed.
|
// 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.
|
// Check for subuser status.
|
||||||
if (!$server->subusers->contains('user_id', $user->id)) {
|
if (!$server->subusers->contains('user_id', $user->id)) {
|
||||||
throw new NotFoundHttpException(trans('exceptions.api.resource_not_found'));
|
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')) {
|
if (($server->isSuspended() || $server->node->isUnderMaintenance()) && !$request->routeIs('api:client:server.resources')) {
|
||||||
throw $exception;
|
throw $exception;
|
||||||
}
|
}
|
||||||
if (!$user->root_admin || !$request->routeIs($this->except)) {
|
if (!$user->isRootAdmin() || !$request->routeIs($this->except)) {
|
||||||
throw $exception;
|
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 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) {
|
if ($level === self::LEVEL_NONE || $user->use_totp) {
|
||||||
return $next($request);
|
return $next($request);
|
||||||
} elseif ($level === self::LEVEL_ADMIN && !$user->root_admin) {
|
} elseif ($level === self::LEVEL_ADMIN && !$user->isRootAdmin()) {
|
||||||
return $next($request);
|
return $next($request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ abstract class AdminFormRequest extends FormRequest
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (bool) $this->user()->root_admin;
|
return $this->user()->isRootAdmin();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ class NewUserFormRequest extends AdminFormRequest
|
|||||||
'name_last',
|
'name_last',
|
||||||
'password',
|
'password',
|
||||||
'language',
|
'language',
|
||||||
'root_admin',
|
|
||||||
])->toArray();
|
])->toArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ class UserFormRequest extends AdminFormRequest
|
|||||||
'name_last',
|
'name_last',
|
||||||
'password',
|
'password',
|
||||||
'language',
|
'language',
|
||||||
'root_admin',
|
|
||||||
])->toArray();
|
])->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',
|
'password',
|
||||||
'language',
|
'language',
|
||||||
'timezone',
|
'timezone',
|
||||||
'root_admin',
|
|
||||||
])->toArray();
|
])->toArray();
|
||||||
|
|
||||||
$response['first_name'] = $rules['name_first'];
|
$response['first_name'] = $rules['name_first'];
|
||||||
@@ -56,7 +55,6 @@ class StoreUserRequest extends ApplicationApiRequest
|
|||||||
'external_id' => 'Third Party Identifier',
|
'external_id' => 'Third Party Identifier',
|
||||||
'name_first' => 'First Name',
|
'name_first' => 'First Name',
|
||||||
'name_last' => 'Last Name',
|
'name_last' => 'Last Name',
|
||||||
'root_admin' => 'Root Administrator Status',
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ abstract class SubuserRequest extends ClientApiRequest
|
|||||||
$server = $this->route()->parameter('server');
|
$server = $this->route()->parameter('server');
|
||||||
|
|
||||||
// If we are a root admin or the server owner, no need to perform these checks.
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
|||||||
* @property string $default_value
|
* @property string $default_value
|
||||||
* @property bool $user_viewable
|
* @property bool $user_viewable
|
||||||
* @property bool $user_editable
|
* @property bool $user_editable
|
||||||
* @property string $rules
|
* @property array $rules
|
||||||
* @property \Carbon\CarbonImmutable $created_at
|
* @property \Carbon\CarbonImmutable $created_at
|
||||||
* @property \Carbon\CarbonImmutable $updated_at
|
* @property \Carbon\CarbonImmutable $updated_at
|
||||||
* @property bool $required
|
* @property bool $required
|
||||||
@@ -58,12 +58,14 @@ class EggVariable extends Model
|
|||||||
'default_value' => 'string',
|
'default_value' => 'string',
|
||||||
'user_viewable' => 'boolean',
|
'user_viewable' => 'boolean',
|
||||||
'user_editable' => 'boolean',
|
'user_editable' => 'boolean',
|
||||||
'rules' => 'string',
|
'rules' => 'array',
|
||||||
|
'rules.*' => 'string',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $attributes = [
|
protected $attributes = [
|
||||||
'user_editable' => 0,
|
'user_editable' => 0,
|
||||||
'user_viewable' => 0,
|
'user_viewable' => 0,
|
||||||
|
'rules' => '[]',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected function casts(): array
|
protected function casts(): array
|
||||||
@@ -72,6 +74,7 @@ class EggVariable extends Model
|
|||||||
'egg_id' => 'integer',
|
'egg_id' => 'integer',
|
||||||
'user_viewable' => 'bool',
|
'user_viewable' => 'bool',
|
||||||
'user_editable' => 'bool',
|
'user_editable' => 'bool',
|
||||||
|
'rules' => 'array',
|
||||||
'created_at' => 'immutable_datetime',
|
'created_at' => 'immutable_datetime',
|
||||||
'updated_at' => 'immutable_datetime',
|
'updated_at' => 'immutable_datetime',
|
||||||
];
|
];
|
||||||
@@ -79,7 +82,7 @@ class EggVariable extends Model
|
|||||||
|
|
||||||
public function getRequiredAttribute(): bool
|
public function getRequiredAttribute(): bool
|
||||||
{
|
{
|
||||||
return in_array('required', explode('|', $this->rules));
|
return in_array('required', $this->rules);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function egg(): HasOne
|
public function egg(): HasOne
|
||||||
|
|||||||
@@ -244,21 +244,21 @@ class Node extends Model
|
|||||||
*/
|
*/
|
||||||
public function isViable(int $memory, int $disk, int $cpu): bool
|
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));
|
$memoryLimit = $this->memory * (1 + ($this->memory_overallocate / 100));
|
||||||
if ($this->servers_sum_memory + $memory > $memoryLimit) {
|
if ($this->servers_sum_memory + $memory > $memoryLimit) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->disk_overallocate >= 0) {
|
if ($this->disk > 0 && $this->disk_overallocate >= 0) {
|
||||||
$diskLimit = $this->disk * (1 + ($this->disk_overallocate / 100));
|
$diskLimit = $this->disk * (1 + ($this->disk_overallocate / 100));
|
||||||
if ($this->servers_sum_disk + $disk > $diskLimit) {
|
if ($this->servers_sum_disk + $disk > $diskLimit) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->cpu_overallocate >= 0) {
|
if ($this->cpu > 0 && $this->cpu_overallocate >= 0) {
|
||||||
$cpuLimit = $this->cpu * (1 + ($this->cpu_overallocate / 100));
|
$cpuLimit = $this->cpu * (1 + ($this->cpu_overallocate / 100));
|
||||||
if ($this->servers_sum_cpu + $cpu > $cpuLimit) {
|
if ($this->servers_sum_cpu + $cpu > $cpuLimit) {
|
||||||
return false;
|
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\Access\Authorizable as AuthorizableContract;
|
||||||
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
|
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
|
||||||
use App\Notifications\SendPasswordReset as ResetPasswordNotification;
|
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.
|
* App\Models\User.
|
||||||
@@ -40,7 +43,6 @@ use App\Notifications\SendPasswordReset as ResetPasswordNotification;
|
|||||||
* @property string|null $remember_token
|
* @property string|null $remember_token
|
||||||
* @property string $language
|
* @property string $language
|
||||||
* @property string $timezone
|
* @property string $timezone
|
||||||
* @property bool $root_admin
|
|
||||||
* @property bool $use_totp
|
* @property bool $use_totp
|
||||||
* @property string|null $totp_secret
|
* @property string|null $totp_secret
|
||||||
* @property \Illuminate\Support\Carbon|null $totp_authenticated_at
|
* @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 whereNameLast($value)
|
||||||
* @method static Builder|User wherePassword($value)
|
* @method static Builder|User wherePassword($value)
|
||||||
* @method static Builder|User whereRememberToken($value)
|
* @method static Builder|User whereRememberToken($value)
|
||||||
* @method static Builder|User whereRootAdmin($value)
|
|
||||||
* @method static Builder|User whereTotpAuthenticatedAt($value)
|
* @method static Builder|User whereTotpAuthenticatedAt($value)
|
||||||
* @method static Builder|User whereTotpSecret($value)
|
* @method static Builder|User whereTotpSecret($value)
|
||||||
* @method static Builder|User whereUpdatedAt($value)
|
* @method static Builder|User whereUpdatedAt($value)
|
||||||
@@ -94,6 +95,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
|||||||
use AvailableLanguages;
|
use AvailableLanguages;
|
||||||
use CanResetPassword;
|
use CanResetPassword;
|
||||||
use HasAccessTokens;
|
use HasAccessTokens;
|
||||||
|
use HasRoles;
|
||||||
use Notifiable;
|
use Notifiable;
|
||||||
|
|
||||||
public const USER_LEVEL_USER = 0;
|
public const USER_LEVEL_USER = 0;
|
||||||
@@ -131,7 +133,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
|||||||
'totp_secret',
|
'totp_secret',
|
||||||
'totp_authenticated_at',
|
'totp_authenticated_at',
|
||||||
'gravatar',
|
'gravatar',
|
||||||
'root_admin',
|
|
||||||
'oauth',
|
'oauth',
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -145,7 +146,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
|||||||
*/
|
*/
|
||||||
protected $attributes = [
|
protected $attributes = [
|
||||||
'external_id' => null,
|
'external_id' => null,
|
||||||
'root_admin' => false,
|
|
||||||
'language' => 'en',
|
'language' => 'en',
|
||||||
'timezone' => 'UTC',
|
'timezone' => 'UTC',
|
||||||
'use_totp' => false,
|
'use_totp' => false,
|
||||||
@@ -166,7 +166,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
|||||||
'name_first' => 'nullable|string|between:0,255',
|
'name_first' => 'nullable|string|between:0,255',
|
||||||
'name_last' => 'nullable|string|between:0,255',
|
'name_last' => 'nullable|string|between:0,255',
|
||||||
'password' => 'sometimes|nullable|string',
|
'password' => 'sometimes|nullable|string',
|
||||||
'root_admin' => 'boolean',
|
|
||||||
'language' => 'string',
|
'language' => 'string',
|
||||||
'timezone' => 'string',
|
'timezone' => 'string',
|
||||||
'use_totp' => 'boolean',
|
'use_totp' => 'boolean',
|
||||||
@@ -177,7 +176,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
|||||||
protected function casts(): array
|
protected function casts(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'root_admin' => 'boolean',
|
|
||||||
'use_totp' => 'boolean',
|
'use_totp' => 'boolean',
|
||||||
'gravatar' => 'boolean',
|
'gravatar' => 'boolean',
|
||||||
'totp_authenticated_at' => 'datetime',
|
'totp_authenticated_at' => 'datetime',
|
||||||
@@ -226,7 +224,10 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
|||||||
*/
|
*/
|
||||||
public function toReactObject(): array
|
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
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,14 +352,23 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
|||||||
|
|
||||||
public function isLastRootAdmin(): bool
|
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));
|
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
|
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
|
public function getFilamentName(): string
|
||||||
@@ -370,4 +380,13 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
|
|||||||
{
|
{
|
||||||
return 'https://gravatar.com/avatar/' . md5(strtolower($this->email));
|
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;
|
namespace App\Policies;
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
|
|
||||||
class EggPolicy
|
class EggPolicy
|
||||||
{
|
{
|
||||||
public function create(User $user): bool
|
use DefaultPolicies;
|
||||||
{
|
|
||||||
return true;
|
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;
|
namespace App\Policies;
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
class ServerPolicy
|
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();
|
// For "viewAny" the $server param is the class name
|
||||||
if (!$subuser || empty($permission)) {
|
if (is_string($server)) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return in_array($permission, $subuser->permissions);
|
// Owner has full server permissions
|
||||||
}
|
if ($server->owner_id === $user->id) {
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) {
|
|
||||||
return true;
|
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;
|
||||||
use App\Models\ApiKey;
|
use App\Models\ApiKey;
|
||||||
use App\Models\Node;
|
use App\Models\Node;
|
||||||
|
use App\Models\User;
|
||||||
use App\Services\Helpers\SoftwareVersionService;
|
use App\Services\Helpers\SoftwareVersionService;
|
||||||
use Dedoc\Scramble\Scramble;
|
use Dedoc\Scramble\Scramble;
|
||||||
use Dedoc\Scramble\Support\Generator\OpenApi;
|
use Dedoc\Scramble\Support\Generator\OpenApi;
|
||||||
@@ -91,6 +92,10 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
'success' => Color::Green,
|
'success' => Color::Green,
|
||||||
'warning' => Color::Amber,
|
'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_DATABASE_HOSTS = 'database_hosts';
|
||||||
public const RESOURCE_SERVER_DATABASES = 'server_databases';
|
public const RESOURCE_SERVER_DATABASES = 'server_databases';
|
||||||
public const RESOURCE_MOUNTS = 'mounts';
|
public const RESOURCE_MOUNTS = 'mounts';
|
||||||
|
public const RESOURCE_ROLES = 'roles';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if an API key has permission to perform a specific read/write operation.
|
* 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.
|
// Update existing variables or create new ones.
|
||||||
foreach ($parsed['variables'] ?? [] as $variable) {
|
foreach ($parsed['variables'] ?? [] as $variable) {
|
||||||
EggVariable::unguarded(function () use ($egg, $variable) {
|
EggVariable::unguarded(function () use ($egg, $variable) {
|
||||||
|
$variable['rules'] = is_array($variable['rules']) ? $variable['rules'] : explode('|', $variable['rules']);
|
||||||
|
|
||||||
$egg->variables()->updateOrCreate([
|
$egg->variables()->updateOrCreate([
|
||||||
'env_variable' => $variable['env_variable'],
|
'env_variable' => $variable['env_variable'],
|
||||||
], Collection::make($variable)->except(['egg_id', 'env_variable'])->toArray());
|
], 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')));
|
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']);
|
$this->validateRules($data['rules']);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ class VariableCreationService
|
|||||||
'default_value' => $data['default_value'] ?? '',
|
'default_value' => $data['default_value'] ?? '',
|
||||||
'user_viewable' => in_array('user_viewable', $options),
|
'user_viewable' => in_array('user_viewable', $options),
|
||||||
'user_editable' => in_array('user_editable', $options),
|
'user_editable' => in_array('user_editable', $options),
|
||||||
'rules' => $data['rules'] ?? '',
|
'rules' => $data['rules'] ?? [],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $eggVariable;
|
return $eggVariable;
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Services\Eggs\Variables;
|
namespace App\Services\Eggs\Variables;
|
||||||
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use App\Models\EggVariable;
|
use App\Models\EggVariable;
|
||||||
use App\Exceptions\DisplayException;
|
use App\Exceptions\DisplayException;
|
||||||
use App\Traits\Services\ValidatesValidationRules;
|
use App\Traits\Services\ValidatesValidationRules;
|
||||||
@@ -54,12 +53,8 @@ class VariableUpdateService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($data['rules'] ?? '')) {
|
if (!empty($data['rules'] ?? [])) {
|
||||||
$this->validateRules(
|
$this->validateRules($data['rules']);
|
||||||
(is_string($data['rules']) && Str::contains($data['rules'], ';;'))
|
|
||||||
? explode(';;', $data['rules'])
|
|
||||||
: $data['rules']
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$options = array_get($data, 'options') ?? [];
|
$options = array_get($data, 'options') ?? [];
|
||||||
@@ -71,7 +66,7 @@ class VariableUpdateService
|
|||||||
'default_value' => $data['default_value'] ?? '',
|
'default_value' => $data['default_value'] ?? '',
|
||||||
'user_viewable' => in_array('user_viewable', $options),
|
'user_viewable' => in_array('user_viewable', $options),
|
||||||
'user_editable' => in_array('user_editable', $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
|
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 = ['*'];
|
$permissions = ['*'];
|
||||||
|
|
||||||
if ($user->root_admin) {
|
if ($user->isRootAdmin()) {
|
||||||
$permissions[] = 'admin.websocket.errors';
|
$permissions[] = 'admin.websocket.errors';
|
||||||
$permissions[] = 'admin.websocket.install';
|
$permissions[] = 'admin.websocket.install';
|
||||||
$permissions[] = 'admin.websocket.transfer';
|
$permissions[] = 'admin.websocket.transfer';
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Services\Users;
|
namespace App\Services\Users;
|
||||||
|
|
||||||
|
use App\Models\Role;
|
||||||
use Ramsey\Uuid\Uuid;
|
use Ramsey\Uuid\Uuid;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Contracts\Hashing\Hasher;
|
use Illuminate\Contracts\Hashing\Hasher;
|
||||||
@@ -39,10 +40,17 @@ class UserCreationService
|
|||||||
$data['password'] = $this->hasher->make(str_random(30));
|
$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, [
|
$user = User::query()->forceCreate(array_merge($data, [
|
||||||
'uuid' => Uuid::uuid4()->toString(),
|
'uuid' => Uuid::uuid4()->toString(),
|
||||||
]));
|
]));
|
||||||
|
|
||||||
|
if ($isRootAdmin) {
|
||||||
|
$user->syncRoles(Role::getRootAdmin());
|
||||||
|
}
|
||||||
|
|
||||||
if (isset($generateResetToken)) {
|
if (isset($generateResetToken)) {
|
||||||
$token = $this->passwordBroker->createToken($user);
|
$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
|
// the user is a root admin at the moment. In a future release we'll be rolling
|
||||||
// out more specific permissions for keys.
|
// out more specific permissions for keys.
|
||||||
if ($token->key_type === ApiKey::TYPE_ACCOUNT) {
|
if ($token->key_type === ApiKey::TYPE_ACCOUNT) {
|
||||||
return $this->request->user()->root_admin;
|
return $this->request->user()->isRootAdmin();
|
||||||
}
|
}
|
||||||
|
|
||||||
return AdminAcl::check($token, $resource);
|
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;
|
namespace App\Transformers\Api\Application;
|
||||||
|
|
||||||
|
use App\Models\Role;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use League\Fractal\Resource\Collection;
|
use League\Fractal\Resource\Collection;
|
||||||
use League\Fractal\Resource\NullResource;
|
use League\Fractal\Resource\NullResource;
|
||||||
@@ -12,7 +13,10 @@ class UserTransformer extends BaseTransformer
|
|||||||
/**
|
/**
|
||||||
* List of resources that can be included.
|
* List of resources that can be included.
|
||||||
*/
|
*/
|
||||||
protected array $availableIncludes = ['servers'];
|
protected array $availableIncludes = [
|
||||||
|
'servers',
|
||||||
|
'roles',
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the resource name for the JSONAPI output.
|
* Return the resource name for the JSONAPI output.
|
||||||
@@ -36,7 +40,7 @@ class UserTransformer extends BaseTransformer
|
|||||||
'first_name' => $user->name_first,
|
'first_name' => $user->name_first,
|
||||||
'last_name' => $user->name_last,
|
'last_name' => $user->name_last,
|
||||||
'language' => $user->language,
|
'language' => $user->language,
|
||||||
'root_admin' => (bool) $user->root_admin,
|
'root_admin' => $user->isRootAdmin(),
|
||||||
'2fa_enabled' => (bool) $user->use_totp,
|
'2fa_enabled' => (bool) $user->use_totp,
|
||||||
'2fa' => (bool) $user->use_totp, // deprecated, use "2fa_enabled"
|
'2fa' => (bool) $user->use_totp, // deprecated, use "2fa_enabled"
|
||||||
'created_at' => $this->formatTimestamp($user->created_at),
|
'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 $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
|
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,
|
'default_value' => $variable->default_value,
|
||||||
'server_value' => $variable->server_value,
|
'server_value' => $variable->server_value,
|
||||||
'is_editable' => $variable->user_editable,
|
'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,
|
'last_name' => $user->name_last,
|
||||||
'language' => $user->language,
|
'language' => $user->language,
|
||||||
'image' => 'https://gravatar.com/avatar/' . md5(Str::lower($user->email)), // deprecated
|
'image' => 'https://gravatar.com/avatar/' . md5(Str::lower($user->email)), // deprecated
|
||||||
'admin' => (bool) $user->root_admin, // deprecated, use "root_admin"
|
'admin' => $user->isRootAdmin(), // deprecated, use "root_admin"
|
||||||
'root_admin' => (bool) $user->root_admin,
|
'root_admin' => $user->isRootAdmin(),
|
||||||
'2fa_enabled' => (bool) $user->use_totp,
|
'2fa_enabled' => (bool) $user->use_totp,
|
||||||
'created_at' => $this->formatTimestamp($user->created_at),
|
'created_at' => $this->formatTimestamp($user->created_at),
|
||||||
'updated_at' => $this->formatTimestamp($user->updated_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;
|
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-json": "*",
|
||||||
"ext-mbstring": "*",
|
"ext-mbstring": "*",
|
||||||
"ext-pdo": "*",
|
"ext-pdo": "*",
|
||||||
"ext-pdo_mysql": "*",
|
|
||||||
"ext-zip": "*",
|
"ext-zip": "*",
|
||||||
"abdelhamiderrahmouni/filament-monaco-editor": "0.2.1",
|
"abdelhamiderrahmouni/filament-monaco-editor": "0.2.1",
|
||||||
"aws/aws-sdk-php": "~3.288.1",
|
"aws/aws-sdk-php": "~3.288.1",
|
||||||
@@ -34,6 +33,7 @@
|
|||||||
"s1lentium/iptools": "~1.2.0",
|
"s1lentium/iptools": "~1.2.0",
|
||||||
"socialiteproviders/discord": "^4.2",
|
"socialiteproviders/discord": "^4.2",
|
||||||
"spatie/laravel-fractal": "^6.2",
|
"spatie/laravel-fractal": "^6.2",
|
||||||
|
"spatie/laravel-permission": "^6.9",
|
||||||
"spatie/laravel-query-builder": "^5.8.1",
|
"spatie/laravel-query-builder": "^5.8.1",
|
||||||
"spatie/temporary-directory": "^2.2",
|
"spatie/temporary-directory": "^2.2",
|
||||||
"symfony/http-client": "^7.1",
|
"symfony/http-client": "^7.1",
|
||||||
@@ -90,4 +90,4 @@
|
|||||||
},
|
},
|
||||||
"minimum-stability": "stable",
|
"minimum-stability": "stable",
|
||||||
"prefer-stable": true
|
"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
|
<?php
|
||||||
|
|
||||||
|
$database = env('DB_DATABASE', 'database.sqlite');
|
||||||
|
$datapasePath = database_path($database);
|
||||||
|
if (str($database)->startsWith('/')) {
|
||||||
|
$databasePath = $database;
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
'default' => env('DB_CONNECTION', 'sqlite'),
|
'default' => env('DB_CONNECTION', 'sqlite'),
|
||||||
@@ -8,7 +14,7 @@ return [
|
|||||||
'sqlite' => [
|
'sqlite' => [
|
||||||
'driver' => 'sqlite',
|
'driver' => 'sqlite',
|
||||||
'url' => env('DB_URL'),
|
'url' => env('DB_URL'),
|
||||||
'database' => database_path(env('DB_DATABASE', 'database.sqlite')),
|
'database' => $datapasePath,
|
||||||
'prefix' => '',
|
'prefix' => '',
|
||||||
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ return [
|
|||||||
| Client Features
|
| Client Features
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| Allow clients to create their own databases.
|
| Allow clients to turn features on or off
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'client_features' => [
|
'client_features' => [
|
||||||
@@ -93,6 +93,10 @@ return [
|
|||||||
'range_start' => env('PANEL_CLIENT_ALLOCATIONS_RANGE_START'),
|
'range_start' => env('PANEL_CLIENT_ALLOCATIONS_RANGE_START'),
|
||||||
'range_end' => env('PANEL_CLIENT_ALLOCATIONS_RANGE_END'),
|
'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(),
|
'name_last' => $this->faker->lastName(),
|
||||||
'password' => $password ?: $password = bcrypt('password'),
|
'password' => $password ?: $password = bcrypt('password'),
|
||||||
'language' => 'en',
|
'language' => 'en',
|
||||||
'root_admin' => false,
|
|
||||||
'use_totp' => false,
|
'use_totp' => false,
|
||||||
'oauth' => [],
|
'oauth' => [],
|
||||||
'created_at' => Carbon::now(),
|
'created_at' => Carbon::now(),
|
||||||
'updated_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;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\Role;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
class DatabaseSeeder extends Seeder
|
class DatabaseSeeder extends Seeder
|
||||||
@@ -12,5 +13,7 @@ class DatabaseSeeder extends Seeder
|
|||||||
public function run()
|
public function run()
|
||||||
{
|
{
|
||||||
$this->call(EggSeeder::class);
|
$this->call(EggSeeder::class);
|
||||||
|
|
||||||
|
Role::firstOrCreate(['name' => Role::ROOT_ADMIN]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"version": "PTDL_v2",
|
"version": "PTDL_v2",
|
||||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/minecraft\/egg-bungeecord.json"
|
"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",
|
"name": "Bungeecord",
|
||||||
"author": "panel@example.com",
|
"author": "panel@example.com",
|
||||||
"uuid": "9e6b409e-4028-4947-aea8-50a2c404c271",
|
"uuid": "9e6b409e-4028-4947-aea8-50a2c404c271",
|
||||||
@@ -44,8 +44,12 @@
|
|||||||
"default_value": "latest",
|
"default_value": "latest",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "required|alpha_num|between:1,6",
|
"rules": [
|
||||||
"sort": null,
|
"required",
|
||||||
|
"alpha_num",
|
||||||
|
"between:1,6"
|
||||||
|
],
|
||||||
|
"sort": 1,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -55,9 +59,12 @@
|
|||||||
"default_value": "bungeecord.jar",
|
"default_value": "bungeecord.jar",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/",
|
"rules": [
|
||||||
"sort": null,
|
"required",
|
||||||
|
"regex:\/^([\\w\\d._-]+)(\\.jar)$\/"
|
||||||
|
],
|
||||||
|
"sort": 2,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"version": "PTDL_v2",
|
"version": "PTDL_v2",
|
||||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/minecraft\/egg-forge-minecraft.json"
|
"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",
|
"name": "Forge Minecraft",
|
||||||
"author": "panel@example.com",
|
"author": "panel@example.com",
|
||||||
"uuid": "ed072427-f209-4603-875c-f540c6dd5a65",
|
"uuid": "ed072427-f209-4603-875c-f540c6dd5a65",
|
||||||
@@ -44,7 +44,10 @@
|
|||||||
"default_value": "server.jar",
|
"default_value": "server.jar",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/",
|
"rules": [
|
||||||
|
"required",
|
||||||
|
"regex:\/^([\\w\\d._-]+)(\\.jar)$\/"
|
||||||
|
],
|
||||||
"sort": 1,
|
"sort": 1,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -55,7 +58,11 @@
|
|||||||
"default_value": "latest",
|
"default_value": "latest",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "required|string|max:9",
|
"rules": [
|
||||||
|
"required",
|
||||||
|
"string",
|
||||||
|
"max:9"
|
||||||
|
],
|
||||||
"sort": 2,
|
"sort": 2,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -66,7 +73,11 @@
|
|||||||
"default_value": "recommended",
|
"default_value": "recommended",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "required|string|in:recommended,latest",
|
"rules": [
|
||||||
|
"required",
|
||||||
|
"string",
|
||||||
|
"in:recommended,latest"
|
||||||
|
],
|
||||||
"sort": 3,
|
"sort": 3,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -77,7 +88,10 @@
|
|||||||
"default_value": "",
|
"default_value": "",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "nullable|regex:\/^[0-9\\.\\-]+$\/",
|
"rules": [
|
||||||
|
"nullable",
|
||||||
|
"regex:\/^[0-9\\.\\-]+$\/"
|
||||||
|
],
|
||||||
"sort": 4,
|
"sort": 4,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"version": "PTDL_v2",
|
"version": "PTDL_v2",
|
||||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/minecraft\/egg-paper.json"
|
"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",
|
"name": "Paper",
|
||||||
"author": "parker@example.com",
|
"author": "parker@example.com",
|
||||||
"uuid": "5da37ef6-58da-4169-90a6-e683e1721247",
|
"uuid": "5da37ef6-58da-4169-90a6-e683e1721247",
|
||||||
@@ -44,7 +44,11 @@
|
|||||||
"default_value": "latest",
|
"default_value": "latest",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "nullable|string|max:20",
|
"rules": [
|
||||||
|
"nullable",
|
||||||
|
"string",
|
||||||
|
"max:20"
|
||||||
|
],
|
||||||
"sort": 1,
|
"sort": 1,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -55,7 +59,10 @@
|
|||||||
"default_value": "server.jar",
|
"default_value": "server.jar",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/",
|
"rules": [
|
||||||
|
"required",
|
||||||
|
"regex:\/^([\\w\\d._-]+)(\\.jar)$\/"
|
||||||
|
],
|
||||||
"sort": 2,
|
"sort": 2,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -66,7 +73,10 @@
|
|||||||
"default_value": "",
|
"default_value": "",
|
||||||
"user_viewable": false,
|
"user_viewable": false,
|
||||||
"user_editable": false,
|
"user_editable": false,
|
||||||
"rules": "nullable|string",
|
"rules": [
|
||||||
|
"nullable",
|
||||||
|
"string"
|
||||||
|
],
|
||||||
"sort": 3,
|
"sort": 3,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -77,7 +87,11 @@
|
|||||||
"default_value": "latest",
|
"default_value": "latest",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "required|string|max:20",
|
"rules": [
|
||||||
|
"required",
|
||||||
|
"string",
|
||||||
|
"max:20"
|
||||||
|
],
|
||||||
"sort": 4,
|
"sort": 4,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"version": "PTDL_v2",
|
"version": "PTDL_v2",
|
||||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/minecraft\/egg-sponge--sponge-vanilla.json"
|
"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)",
|
"name": "Sponge (SpongeVanilla)",
|
||||||
"author": "panel@example.com",
|
"author": "panel@example.com",
|
||||||
"uuid": "f0d2f88f-1ff3-42a0-b03f-ac44c5571e6d",
|
"uuid": "f0d2f88f-1ff3-42a0-b03f-ac44c5571e6d",
|
||||||
@@ -44,7 +44,10 @@
|
|||||||
"default_value": "1.12.2-7.3.0",
|
"default_value": "1.12.2-7.3.0",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "required|regex:\/^([a-zA-Z0-9.\\-_]+)$\/",
|
"rules": [
|
||||||
|
"required",
|
||||||
|
"regex:\/^([a-zA-Z0-9.\\-_]+)$\/"
|
||||||
|
],
|
||||||
"sort": 1,
|
"sort": 1,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -55,7 +58,10 @@
|
|||||||
"default_value": "server.jar",
|
"default_value": "server.jar",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/",
|
"rules": [
|
||||||
|
"required",
|
||||||
|
"regex:\/^([\\w\\d._-]+)(\\.jar)$\/"
|
||||||
|
],
|
||||||
"sort": 2,
|
"sort": 2,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"version": "PTDL_v2",
|
"version": "PTDL_v2",
|
||||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/minecraft\/egg-vanilla-minecraft.json"
|
"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",
|
"name": "Vanilla Minecraft",
|
||||||
"author": "panel@example.com",
|
"author": "panel@example.com",
|
||||||
"uuid": "9ac39f3d-0c34-4d93-8174-c52ab9e6c57b",
|
"uuid": "9ac39f3d-0c34-4d93-8174-c52ab9e6c57b",
|
||||||
@@ -44,7 +44,10 @@
|
|||||||
"default_value": "server.jar",
|
"default_value": "server.jar",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/",
|
"rules": [
|
||||||
|
"required",
|
||||||
|
"regex:\/^([\\w\\d._-]+)(\\.jar)$\/"
|
||||||
|
],
|
||||||
"sort": 1,
|
"sort": 1,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -55,7 +58,11 @@
|
|||||||
"default_value": "latest",
|
"default_value": "latest",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "required|string|between:3,15",
|
"rules": [
|
||||||
|
"required",
|
||||||
|
"string",
|
||||||
|
"between:3,15"
|
||||||
|
],
|
||||||
"sort": 2,
|
"sort": 2,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"version": "PTDL_v2",
|
"version": "PTDL_v2",
|
||||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/rust\/egg-rust.json"
|
"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",
|
"name": "Rust",
|
||||||
"author": "panel@example.com",
|
"author": "panel@example.com",
|
||||||
"uuid": "bace2dfb-209c-452a-9459-7d6f340b07ae",
|
"uuid": "bace2dfb-209c-452a-9459-7d6f340b07ae",
|
||||||
@@ -38,7 +38,11 @@
|
|||||||
"default_value": "A Rust Server",
|
"default_value": "A Rust Server",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "required|string|max:60",
|
"rules": [
|
||||||
|
"required",
|
||||||
|
"string",
|
||||||
|
"max:60"
|
||||||
|
],
|
||||||
"sort": 1,
|
"sort": 1,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -49,7 +53,10 @@
|
|||||||
"default_value": "vanilla",
|
"default_value": "vanilla",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "required|in:vanilla,oxide,carbon",
|
"rules": [
|
||||||
|
"required",
|
||||||
|
"in:vanilla,oxide,carbon"
|
||||||
|
],
|
||||||
"sort": 2,
|
"sort": 2,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -60,7 +67,11 @@
|
|||||||
"default_value": "Procedural Map",
|
"default_value": "Procedural Map",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "required|string|max:20",
|
"rules": [
|
||||||
|
"required",
|
||||||
|
"string",
|
||||||
|
"max:20"
|
||||||
|
],
|
||||||
"sort": 3,
|
"sort": 3,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -71,7 +82,10 @@
|
|||||||
"default_value": "Powered by Panel",
|
"default_value": "Powered by Panel",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "required|string",
|
"rules": [
|
||||||
|
"required",
|
||||||
|
"string"
|
||||||
|
],
|
||||||
"sort": 4,
|
"sort": 4,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -82,7 +96,10 @@
|
|||||||
"default_value": "http:\/\/example.com",
|
"default_value": "http:\/\/example.com",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "nullable|url",
|
"rules": [
|
||||||
|
"nullable",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
"sort": 5,
|
"sort": 5,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -93,7 +110,10 @@
|
|||||||
"default_value": "3000",
|
"default_value": "3000",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "required|integer",
|
"rules": [
|
||||||
|
"required",
|
||||||
|
"integer"
|
||||||
|
],
|
||||||
"sort": 6,
|
"sort": 6,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -104,7 +124,10 @@
|
|||||||
"default_value": "",
|
"default_value": "",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "nullable|string",
|
"rules": [
|
||||||
|
"nullable",
|
||||||
|
"string"
|
||||||
|
],
|
||||||
"sort": 7,
|
"sort": 7,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -115,7 +138,10 @@
|
|||||||
"default_value": "40",
|
"default_value": "40",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "required|integer",
|
"rules": [
|
||||||
|
"required",
|
||||||
|
"integer"
|
||||||
|
],
|
||||||
"sort": 8,
|
"sort": 8,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -126,7 +152,10 @@
|
|||||||
"default_value": "",
|
"default_value": "",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "nullable|url",
|
"rules": [
|
||||||
|
"nullable",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
"sort": 9,
|
"sort": 9,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -137,7 +166,10 @@
|
|||||||
"default_value": "27017",
|
"default_value": "27017",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": false,
|
"user_editable": false,
|
||||||
"rules": "required|integer",
|
"rules": [
|
||||||
|
"required",
|
||||||
|
"integer"
|
||||||
|
],
|
||||||
"sort": 10,
|
"sort": 10,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -148,7 +180,10 @@
|
|||||||
"default_value": "28016",
|
"default_value": "28016",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": false,
|
"user_editable": false,
|
||||||
"rules": "required|integer",
|
"rules": [
|
||||||
|
"required",
|
||||||
|
"integer"
|
||||||
|
],
|
||||||
"sort": 11,
|
"sort": 11,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -159,7 +194,11 @@
|
|||||||
"default_value": "",
|
"default_value": "",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "required|regex:\/^[\\w.-]*$\/|max:64",
|
"rules": [
|
||||||
|
"required",
|
||||||
|
"regex:\/^[\\w.-]*$\/",
|
||||||
|
"max:64"
|
||||||
|
],
|
||||||
"sort": 12,
|
"sort": 12,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -170,7 +209,10 @@
|
|||||||
"default_value": "60",
|
"default_value": "60",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "required|integer",
|
"rules": [
|
||||||
|
"required",
|
||||||
|
"integer"
|
||||||
|
],
|
||||||
"sort": 13,
|
"sort": 13,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -181,7 +223,10 @@
|
|||||||
"default_value": "",
|
"default_value": "",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "nullable|string",
|
"rules": [
|
||||||
|
"nullable",
|
||||||
|
"string"
|
||||||
|
],
|
||||||
"sort": 14,
|
"sort": 14,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -192,7 +237,10 @@
|
|||||||
"default_value": "28082",
|
"default_value": "28082",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": false,
|
"user_editable": false,
|
||||||
"rules": "required|integer",
|
"rules": [
|
||||||
|
"required",
|
||||||
|
"integer"
|
||||||
|
],
|
||||||
"sort": 15,
|
"sort": 15,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -203,7 +251,10 @@
|
|||||||
"default_value": "",
|
"default_value": "",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "nullable|url",
|
"rules": [
|
||||||
|
"nullable",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
"sort": 16,
|
"sort": 16,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -214,7 +265,10 @@
|
|||||||
"default_value": "",
|
"default_value": "",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "nullable|url",
|
"rules": [
|
||||||
|
"nullable",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
"sort": 17,
|
"sort": 17,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -225,7 +279,11 @@
|
|||||||
"default_value": "258550",
|
"default_value": "258550",
|
||||||
"user_viewable": false,
|
"user_viewable": false,
|
||||||
"user_editable": false,
|
"user_editable": false,
|
||||||
"rules": "required|string|in:258550",
|
"rules": [
|
||||||
|
"required",
|
||||||
|
"string",
|
||||||
|
"in:258550"
|
||||||
|
],
|
||||||
"sort": 18,
|
"sort": 18,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"version": "PTDL_v2",
|
"version": "PTDL_v2",
|
||||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/source-engine\/egg-counter--strike--global-offensive.json"
|
"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",
|
"name": "Counter-Strike: Global Offensive",
|
||||||
"author": "panel@example.com",
|
"author": "panel@example.com",
|
||||||
"uuid": "437c367d-06be-498f-a604-fdad135504d7",
|
"uuid": "437c367d-06be-498f-a604-fdad135504d7",
|
||||||
@@ -39,7 +39,11 @@
|
|||||||
"default_value": "de_dust2",
|
"default_value": "de_dust2",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "required|string|alpha_dash",
|
"rules": [
|
||||||
|
"required",
|
||||||
|
"string",
|
||||||
|
"alpha_dash"
|
||||||
|
],
|
||||||
"sort": 1,
|
"sort": 1,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -50,7 +54,12 @@
|
|||||||
"default_value": "",
|
"default_value": "",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "required|string|alpha_num|size:32",
|
"rules": [
|
||||||
|
"required",
|
||||||
|
"string",
|
||||||
|
"alpha_num",
|
||||||
|
"size:32"
|
||||||
|
],
|
||||||
"sort": 2,
|
"sort": 2,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -61,7 +70,11 @@
|
|||||||
"default_value": "740",
|
"default_value": "740",
|
||||||
"user_viewable": false,
|
"user_viewable": false,
|
||||||
"user_editable": false,
|
"user_editable": false,
|
||||||
"rules": "required|string|max:20",
|
"rules": [
|
||||||
|
"required",
|
||||||
|
"string",
|
||||||
|
"max:20"
|
||||||
|
],
|
||||||
"sort": 3,
|
"sort": 3,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"version": "PTDL_v2",
|
"version": "PTDL_v2",
|
||||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/source-engine\/egg-custom-source-engine-game.json"
|
"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",
|
"name": "Custom Source Engine Game",
|
||||||
"author": "panel@example.com",
|
"author": "panel@example.com",
|
||||||
"uuid": "2a42d0c2-c0ba-4067-9a0a-9b95d77a3490",
|
"uuid": "2a42d0c2-c0ba-4067-9a0a-9b95d77a3490",
|
||||||
@@ -38,7 +38,11 @@
|
|||||||
"default_value": "",
|
"default_value": "",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": false,
|
"user_editable": false,
|
||||||
"rules": "required|numeric|digits_between:1,6",
|
"rules": [
|
||||||
|
"required",
|
||||||
|
"numeric",
|
||||||
|
"digits_between:1,6"
|
||||||
|
],
|
||||||
"sort": 1,
|
"sort": 1,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -49,7 +53,11 @@
|
|||||||
"default_value": "",
|
"default_value": "",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": false,
|
"user_editable": false,
|
||||||
"rules": "required|alpha_dash|between:1,100",
|
"rules": [
|
||||||
|
"required",
|
||||||
|
"alpha_dash",
|
||||||
|
"between:1,100"
|
||||||
|
],
|
||||||
"sort": 2,
|
"sort": 2,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -60,7 +68,11 @@
|
|||||||
"default_value": "",
|
"default_value": "",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "required|string|alpha_dash",
|
"rules": [
|
||||||
|
"required",
|
||||||
|
"string",
|
||||||
|
"alpha_dash"
|
||||||
|
],
|
||||||
"sort": 3,
|
"sort": 3,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -71,7 +83,10 @@
|
|||||||
"default_value": "",
|
"default_value": "",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "nullable|string",
|
"rules": [
|
||||||
|
"nullable",
|
||||||
|
"string"
|
||||||
|
],
|
||||||
"sort": 4,
|
"sort": 4,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -82,7 +97,10 @@
|
|||||||
"default_value": "",
|
"default_value": "",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "nullable|string",
|
"rules": [
|
||||||
|
"nullable",
|
||||||
|
"string"
|
||||||
|
],
|
||||||
"sort": 5,
|
"sort": 5,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -93,7 +111,10 @@
|
|||||||
"default_value": "",
|
"default_value": "",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "nullable|string",
|
"rules": [
|
||||||
|
"nullable",
|
||||||
|
"string"
|
||||||
|
],
|
||||||
"sort": 6,
|
"sort": 6,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"version": "PTDL_v2",
|
"version": "PTDL_v2",
|
||||||
"update_url": "https:\/\/github.com\/pelican-dev\/panel\/raw\/main\/database\/Seeders\/eggs\/source-engine\/egg-garrys-mod.json"
|
"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",
|
"name": "Garrys Mod",
|
||||||
"author": "panel@example.com",
|
"author": "panel@example.com",
|
||||||
"uuid": "60ef81d4-30a2-4d98-ab64-f59c69e2f915",
|
"uuid": "60ef81d4-30a2-4d98-ab64-f59c69e2f915",
|
||||||
@@ -39,7 +39,11 @@
|
|||||||
"default_value": "gm_flatgrass",
|
"default_value": "gm_flatgrass",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "required|string|alpha_dash",
|
"rules": [
|
||||||
|
"required",
|
||||||
|
"string",
|
||||||
|
"alpha_dash"
|
||||||
|
],
|
||||||
"sort": 1,
|
"sort": 1,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -50,7 +54,12 @@
|
|||||||
"default_value": "",
|
"default_value": "",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "nullable|string|alpha_num|size:32",
|
"rules": [
|
||||||
|
"nullable",
|
||||||
|
"string",
|
||||||
|
"alpha_num",
|
||||||
|
"size:32"
|
||||||
|
],
|
||||||
"sort": 2,
|
"sort": 2,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -61,7 +70,11 @@
|
|||||||
"default_value": "4020",
|
"default_value": "4020",
|
||||||
"user_viewable": false,
|
"user_viewable": false,
|
||||||
"user_editable": false,
|
"user_editable": false,
|
||||||
"rules": "required|string|max:20",
|
"rules": [
|
||||||
|
"required",
|
||||||
|
"string",
|
||||||
|
"max:20"
|
||||||
|
],
|
||||||
"sort": 3,
|
"sort": 3,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -72,7 +85,10 @@
|
|||||||
"default_value": "",
|
"default_value": "",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "nullable|integer",
|
"rules": [
|
||||||
|
"nullable",
|
||||||
|
"integer"
|
||||||
|
],
|
||||||
"sort": 4,
|
"sort": 4,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -83,7 +99,10 @@
|
|||||||
"default_value": "sandbox",
|
"default_value": "sandbox",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "required|string",
|
"rules": [
|
||||||
|
"required",
|
||||||
|
"string"
|
||||||
|
],
|
||||||
"sort": 5,
|
"sort": 5,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -94,7 +113,11 @@
|
|||||||
"default_value": "32",
|
"default_value": "32",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "required|integer|max:128",
|
"rules": [
|
||||||
|
"required",
|
||||||
|
"integer",
|
||||||
|
"max:128"
|
||||||
|
],
|
||||||
"sort": 6,
|
"sort": 6,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -105,7 +128,11 @@
|
|||||||
"default_value": "22",
|
"default_value": "22",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "required|integer|max:100",
|
"rules": [
|
||||||
|
"required",
|
||||||
|
"integer",
|
||||||
|
"max:100"
|
||||||
|
],
|
||||||
"sort": 7,
|
"sort": 7,
|
||||||
"field_type": "text"
|
"field_type": "text"
|
||||||
},
|
},
|
||||||
@@ -116,7 +143,10 @@
|
|||||||
"default_value": "0",
|
"default_value": "0",
|
||||||
"user_viewable": true,
|
"user_viewable": true,
|
||||||
"user_editable": true,
|
"user_editable": true,
|
||||||
"rules": "required|boolean",
|
"rules": [
|
||||||
|
"required",
|
||||||
|
"boolean"
|
||||||
|
],
|
||||||
"sort": 8,
|
"sort": 8,
|
||||||
"field_type": "text"
|
"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