mirror of
https://github.com/pelican-dev/panel.git
synced 2026-05-04 18:00:48 +03:00
Compare commits
215 Commits
release/v1
...
v1.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe4668a517 | ||
|
|
6125b07afa | ||
|
|
9717aa4b5f | ||
|
|
9491322d8c | ||
|
|
8ed6bb4d8b | ||
|
|
a787af7a06 | ||
|
|
d9016702d6 | ||
|
|
d565441b6a | ||
|
|
cb522b24ef | ||
|
|
b85b17f080 | ||
|
|
47bd7289b1 | ||
|
|
a9b76a0f51 | ||
|
|
8eebb82eba | ||
|
|
b3501be6ec | ||
|
|
ac67656d82 | ||
|
|
968239beb3 | ||
|
|
7514206186 | ||
|
|
1a8321c937 | ||
|
|
340ae8099b | ||
|
|
9d02aeb130 | ||
|
|
cf57c28c40 | ||
|
|
382dcb3868 | ||
|
|
f793b49a81 | ||
|
|
41ddae1ba0 | ||
|
|
e717e20996 | ||
|
|
b5145b016b | ||
|
|
95a8f72058 | ||
|
|
19548338ee | ||
|
|
a8356fc5d2 | ||
|
|
7a447b04d5 | ||
|
|
45699e1614 | ||
|
|
cde3546889 | ||
|
|
3f9c1dbc3c | ||
|
|
bc2df22d78 | ||
|
|
1a3dc5c743 | ||
|
|
fdd1b3798c | ||
|
|
288cbee32f | ||
|
|
a70a060350 | ||
|
|
590569a131 | ||
|
|
7acc8782bb | ||
|
|
f3de185508 | ||
|
|
291b514e24 | ||
|
|
86c369d7ce | ||
|
|
5f77deb1fd | ||
|
|
5f4429e2c3 | ||
|
|
1df3e8d5b0 | ||
|
|
ecb195b2c4 | ||
|
|
86e8a6371e | ||
|
|
d653edb22e | ||
|
|
741252e395 | ||
|
|
308601e6fe | ||
|
|
3933222d98 | ||
|
|
c53ef78d89 | ||
|
|
60792c05c2 | ||
|
|
94420d06be | ||
|
|
6655ccca6e | ||
|
|
a193b4f5ab | ||
|
|
3d5c8d14bd | ||
|
|
de002324d7 | ||
|
|
bcbacb47cd | ||
|
|
e9f6fbadd4 | ||
|
|
c621d2dad5 | ||
|
|
64943aa50c | ||
|
|
020e41cbbc | ||
|
|
e162374e15 | ||
|
|
81c75f7966 | ||
|
|
2be8168468 | ||
|
|
465a372000 | ||
|
|
f0c536c045 | ||
|
|
6a8e630444 | ||
|
|
71aed151d9 | ||
|
|
bb5955cff4 | ||
|
|
38be89a71e | ||
|
|
deb6603840 | ||
|
|
c7a307af6e | ||
|
|
8740f0f645 | ||
|
|
466f9f7edc | ||
|
|
d21740d458 | ||
|
|
1bf6a880fb | ||
|
|
96acd268be | ||
|
|
c0a41acf1f | ||
|
|
75e89b2d4c | ||
|
|
54ea55d426 | ||
|
|
207d875df8 | ||
|
|
ff0215afed | ||
|
|
f357c9501f | ||
|
|
71116e81ba | ||
|
|
f2063d7506 | ||
|
|
c5c05150d8 | ||
|
|
214eb5874f | ||
|
|
b14f6e1645 | ||
|
|
04b251d125 | ||
|
|
5f9ee09ebd | ||
|
|
2fb85f8236 | ||
|
|
4eba5b3f7a | ||
|
|
f95ba6447c | ||
|
|
c0eedc16e0 | ||
|
|
3c5da1cd70 | ||
|
|
8638e53f2b | ||
|
|
3ec90264bd | ||
|
|
e23a4a667a | ||
|
|
a946669dc8 | ||
|
|
6a8ff1a186 | ||
|
|
b003404aea | ||
|
|
45b73debc2 | ||
|
|
329a3993c1 | ||
|
|
da7cba3203 | ||
|
|
6c205a744d | ||
|
|
e78f7bc054 | ||
|
|
12a189f585 | ||
|
|
af4cba341a | ||
|
|
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 | ||
|
|
68e24896ae | ||
|
|
1864fff04f | ||
|
|
155f2d6476 | ||
|
|
bad5409d9c | ||
|
|
3158bdfef8 | ||
|
|
1fba700096 | ||
|
|
7f8fb3f650 | ||
|
|
d6e0421aaf | ||
|
|
e8e1958969 | ||
|
|
2e094605e9 | ||
|
|
953ee940aa | ||
|
|
496eaaaf83 | ||
|
|
18cf6e9338 | ||
|
|
525a106e81 | ||
|
|
d22f975684 | ||
|
|
c4864feaa5 | ||
|
|
b7b72d7336 | ||
|
|
686c4375bc | ||
|
|
3f40256f8b | ||
|
|
a58e159478 | ||
|
|
d89af243a8 | ||
|
|
bddd6af8af | ||
|
|
e1bdf95971 | ||
|
|
465a03bf0e | ||
|
|
2c2e52b18a | ||
|
|
fcef8d69ae | ||
|
|
8662806dfd | ||
|
|
acf43f2826 | ||
|
|
dfba8e3993 | ||
|
|
56484a2282 | ||
|
|
56b4938dc2 | ||
|
|
10806d6d6b | ||
|
|
a04937d698 | ||
|
|
8a3d67ada0 | ||
|
|
833ae30e59 | ||
|
|
1fdff43ae7 | ||
|
|
bb7c0e0e66 | ||
|
|
447e889a4f | ||
|
|
1c1c8c0cc6 | ||
|
|
7dad2d0e42 | ||
|
|
212c93c2ba | ||
|
|
7557dc1c8d | ||
|
|
07735464c7 | ||
|
|
8ba15538a9 | ||
|
|
c115c6ddf5 | ||
|
|
160ea1ed50 | ||
|
|
7164951085 | ||
|
|
40721a2cb8 | ||
|
|
c464b321dd | ||
|
|
0f8c27a297 |
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
|
||||
34
.env.example
34
.env.example
@@ -1,37 +1,7 @@
|
||||
APP_ENV=production
|
||||
APP_DEBUG=false
|
||||
APP_KEY=
|
||||
APP_TIMEZONE=UTC
|
||||
APP_URL=http://panel.test
|
||||
APP_INSTALLED=false
|
||||
APP_TIMEZONE=UTC
|
||||
APP_LOCALE=en
|
||||
APP_ENVIRONMENT_ONLY=true
|
||||
|
||||
LOG_CHANNEL=daily
|
||||
LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=sqlite
|
||||
|
||||
CACHE_STORE=file
|
||||
QUEUE_CONNECTION=database
|
||||
SESSION_DRIVER=file
|
||||
|
||||
MAIL_MAILER=log
|
||||
MAIL_HOST=smtp.example.com
|
||||
MAIL_PORT=25
|
||||
MAIL_USERNAME=
|
||||
MAIL_PASSWORD=
|
||||
MAIL_ENCRYPTION=tls
|
||||
MAIL_FROM_ADDRESS=no-reply@example.com
|
||||
MAIL_FROM_NAME="Pelican Admin"
|
||||
# Set this to your domain to prevent it defaulting to 'localhost', causing mail servers such as Gmail to reject your mail
|
||||
# MAIL_EHLO_DOMAIN=panel.example.com
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
# Set this to true, and set start & end ports to auto create allocations.
|
||||
PANEL_CLIENT_ALLOCATIONS_ENABLED=false
|
||||
PANEL_CLIENT_ALLOCATIONS_RANGE_START=
|
||||
PANEL_CLIENT_ALLOCATIONS_RANGE_END=
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -33,7 +33,6 @@ body:
|
||||
attributes:
|
||||
label: Panel Version
|
||||
description: Version number of your Panel (latest is not a version)
|
||||
placeholder: 1.4.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -42,7 +41,6 @@ body:
|
||||
attributes:
|
||||
label: Wings Version
|
||||
description: Version number of your Wings (latest is not a version)
|
||||
placeholder: 1.4.2
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -68,7 +66,7 @@ body:
|
||||
Run the following command to collect logs on your system.
|
||||
|
||||
Wings: `sudo wings diagnostics`
|
||||
Panel: `tail -n 150 /var/www/pelican/storage/logs/laravel-$(date +%F).log | nc pelipaste.com 99`
|
||||
Panel: `tail -n 150 /var/www/pelican/storage/logs/laravel-$(date +%F).log | curl -X POST -F 'c=@-' paste.pelistuff.com`
|
||||
placeholder: "https://pelipaste.com/a1h6z"
|
||||
render: bash
|
||||
validations:
|
||||
|
||||
88
.github/docker/entrypoint.sh
vendored
88
.github/docker/entrypoint.sh
vendored
@@ -1,81 +1,65 @@
|
||||
#!/bin/ash -e
|
||||
cd /app
|
||||
|
||||
mkdir -p /var/log/panel/logs/ /var/log/supervisord/ /var/log/nginx/ /var/log/php8/ \
|
||||
&& chmod 777 /var/log/panel/logs/ \
|
||||
&& ln -s /app/storage/logs/ /var/log/panel/
|
||||
#mkdir -p /var/log/supervisord/ /var/log/php8/ \
|
||||
|
||||
## check for .env file and generate app keys if missing
|
||||
if [ -f /app/var/.env ]; then
|
||||
if [ -f /pelican-data/.env ]; then
|
||||
echo "external vars exist."
|
||||
rm -rf /app/.env
|
||||
ln -s /app/var/.env /app/
|
||||
rm -rf /var/www/html/.env
|
||||
else
|
||||
echo "external vars don't exist."
|
||||
rm -rf /app/.env
|
||||
touch /app/var/.env
|
||||
rm -rf /var/www/html/.env
|
||||
touch /pelican-data/.env
|
||||
|
||||
## manually generate a key because key generate --force fails
|
||||
if [ -z $APP_KEY ]; then
|
||||
echo -e "Generating key."
|
||||
APP_KEY=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
|
||||
echo -e "Generated app key: $APP_KEY"
|
||||
echo -e "APP_KEY=$APP_KEY" > /app/var/.env
|
||||
echo -e "APP_KEY=$APP_KEY" > /pelican-data/.env
|
||||
else
|
||||
echo -e "APP_KEY exists in environment, using that."
|
||||
echo -e "APP_KEY=$APP_KEY" > /app/var/.env
|
||||
echo -e "APP_KEY=$APP_KEY" > /pelican-data/.env
|
||||
fi
|
||||
|
||||
ln -s /app/var/.env /app/
|
||||
## enable installer
|
||||
echo -e "APP_INSTALLED=false" >> /pelican-data/.env
|
||||
fi
|
||||
|
||||
echo "Checking if https is required."
|
||||
if [ -f /etc/nginx/http.d/panel.conf ]; then
|
||||
echo "Using nginx config already in place."
|
||||
if [ $LE_EMAIL ]; then
|
||||
echo "Checking for cert update"
|
||||
certbot certonly -d $(echo $APP_URL | sed 's~http[s]*://~~g') --standalone -m $LE_EMAIL --agree-tos -n
|
||||
else
|
||||
echo "No letsencrypt email is set"
|
||||
fi
|
||||
mkdir /pelican-data/database
|
||||
ln -s /pelican-data/.env /var/www/html/
|
||||
chown -h www-data:www-data /var/www/html/.env
|
||||
ln -s /pelican-data/database/database.sqlite /var/www/html/database/
|
||||
|
||||
if ! grep -q "APP_KEY=" .env || grep -q "APP_KEY=$" .env; then
|
||||
echo "Generating APP_KEY..."
|
||||
php artisan key:generate --force
|
||||
else
|
||||
echo "Checking if letsencrypt email is set."
|
||||
if [ -z $LE_EMAIL ]; then
|
||||
echo "No letsencrypt email is set using http config."
|
||||
cp .github/docker/default.conf /etc/nginx/http.d/panel.conf
|
||||
else
|
||||
echo "writing ssl config"
|
||||
cp .github/docker/default_ssl.conf /etc/nginx/http.d/panel.conf
|
||||
echo "updating ssl config for domain"
|
||||
sed -i "s|<domain>|$(echo $APP_URL | sed 's~http[s]*://~~g')|g" /etc/nginx/http.d/panel.conf
|
||||
echo "generating certs"
|
||||
certbot certonly -d $(echo $APP_URL | sed 's~http[s]*://~~g') --standalone -m $LE_EMAIL --agree-tos -n
|
||||
fi
|
||||
echo "Removing the default nginx config"
|
||||
rm -rf /etc/nginx/http.d/default.conf
|
||||
echo "APP_KEY is already set."
|
||||
fi
|
||||
|
||||
if [[ -z $DB_PORT ]]; then
|
||||
echo -e "DB_PORT not specified, defaulting to 3306"
|
||||
DB_PORT=3306
|
||||
fi
|
||||
|
||||
## check for DB up before starting the panel
|
||||
echo "Checking database status."
|
||||
until nc -z -v -w30 $DB_HOST $DB_PORT
|
||||
do
|
||||
echo "Waiting for database connection..."
|
||||
# wait for 1 seconds before check again
|
||||
sleep 1
|
||||
done
|
||||
|
||||
## make sure the db is set up
|
||||
echo -e "Migrating and Seeding D.B"
|
||||
php artisan migrate --seed --force
|
||||
echo -e "Migrating Database"
|
||||
php artisan migrate --force
|
||||
|
||||
echo -e "Optimizing Filament"
|
||||
php artisan filament:optimize
|
||||
|
||||
## start cronjobs for the queue
|
||||
echo -e "Starting cron jobs."
|
||||
crond -L /var/log/crond -l 5
|
||||
|
||||
echo -e "Starting supervisord."
|
||||
export SUPERVISORD_CADDY=false
|
||||
|
||||
## disable caddy if SKIP_CADDY is set
|
||||
if [[ "${SKIP_CADDY:-}" == "true" ]]; then
|
||||
echo "Starting PHP-FPM only"
|
||||
else
|
||||
echo "Starting PHP-FPM and Caddy"
|
||||
export SUPERVISORD_CADDY=true
|
||||
fi
|
||||
|
||||
chown -R www-data:www-data /pelican-data/.env /pelican-data/database
|
||||
|
||||
echo "Starting Supervisord"
|
||||
exec "$@"
|
||||
|
||||
18
.github/docker/supervisord.conf
vendored
18
.github/docker/supervisord.conf
vendored
@@ -1,5 +1,7 @@
|
||||
[unix_http_server]
|
||||
file=/tmp/supervisor.sock ; path to your socket file
|
||||
username=dummy
|
||||
password=dummy
|
||||
|
||||
[supervisord]
|
||||
logfile=/var/log/supervisord/supervisord.log ; supervisord log file
|
||||
@@ -18,6 +20,8 @@ supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
|
||||
|
||||
[supervisorctl]
|
||||
serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL for a unix socket
|
||||
username=dummy
|
||||
password=dummy
|
||||
|
||||
[program:php-fpm]
|
||||
command=/usr/local/sbin/php-fpm -F
|
||||
@@ -25,15 +29,15 @@ autostart=true
|
||||
autorestart=true
|
||||
|
||||
[program:queue-worker]
|
||||
command=/usr/local/bin/php /app/artisan queue:work --queue=high,standard,low --sleep=3 --tries=3
|
||||
user=nginx
|
||||
command=/usr/local/bin/php /var/www/html/artisan queue:work --tries=3
|
||||
user=www-data
|
||||
autostart=true
|
||||
autorestart=true
|
||||
|
||||
[program:nginx]
|
||||
command=/usr/sbin/nginx -g 'daemon off;'
|
||||
autostart=true
|
||||
autorestart=true
|
||||
[program:caddy]
|
||||
command=caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
|
||||
autostart=%(ENV_SUPERVISORD_CADDY)s
|
||||
autorestart=%(ENV_SUPERVISORD_CADDY)s
|
||||
priority=10
|
||||
stdout_events_enabled=true
|
||||
stderr_events_enabled=true
|
||||
stderr_events_enabled=true
|
||||
|
||||
15
.github/workflows/ci.yaml
vendored
15
.github/workflows/ci.yaml
vendored
@@ -3,10 +3,8 @@ name: Tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
mysql:
|
||||
@@ -32,7 +30,6 @@ jobs:
|
||||
APP_KEY: ThisIsARandomStringForTests12345
|
||||
APP_TIMEZONE: UTC
|
||||
APP_URL: http://localhost/
|
||||
APP_ENVIRONMENT_ONLY: "true"
|
||||
CACHE_DRIVER: array
|
||||
MAIL_MAILER: array
|
||||
SESSION_DRIVER: array
|
||||
@@ -41,6 +38,8 @@ jobs:
|
||||
DB_HOST: 127.0.0.1
|
||||
DB_DATABASE: testing
|
||||
DB_USERNAME: root
|
||||
GUZZLE_TIMEOUT: 60
|
||||
GUZZLE_CONNECT_TIMEOUT: 60
|
||||
steps:
|
||||
- name: Code Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -88,7 +87,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php: [8.2, 8.3]
|
||||
database: ["mariadb:10.3", "mariadb:10.11", "mariadb:11.4"]
|
||||
database: ["mariadb:10.6", "mariadb:10.11", "mariadb:11.4"]
|
||||
services:
|
||||
database:
|
||||
image: ${{ matrix.database }}
|
||||
@@ -104,7 +103,6 @@ jobs:
|
||||
APP_KEY: ThisIsARandomStringForTests12345
|
||||
APP_TIMEZONE: UTC
|
||||
APP_URL: http://localhost/
|
||||
APP_ENVIRONMENT_ONLY: "true"
|
||||
CACHE_DRIVER: array
|
||||
MAIL_MAILER: array
|
||||
SESSION_DRIVER: array
|
||||
@@ -113,6 +111,8 @@ jobs:
|
||||
DB_HOST: 127.0.0.1
|
||||
DB_DATABASE: testing
|
||||
DB_USERNAME: root
|
||||
GUZZLE_TIMEOUT: 60
|
||||
GUZZLE_CONNECT_TIMEOUT: 60
|
||||
steps:
|
||||
- name: Code Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -166,13 +166,14 @@ jobs:
|
||||
APP_KEY: ThisIsARandomStringForTests12345
|
||||
APP_TIMEZONE: UTC
|
||||
APP_URL: http://localhost/
|
||||
APP_ENVIRONMENT_ONLY: "true"
|
||||
CACHE_DRIVER: array
|
||||
MAIL_MAILER: array
|
||||
SESSION_DRIVER: array
|
||||
QUEUE_CONNECTION: sync
|
||||
DB_CONNECTION: sqlite
|
||||
DB_DATABASE: testing.sqlite
|
||||
GUZZLE_TIMEOUT: 60
|
||||
GUZZLE_CONNECT_TIMEOUT: 60
|
||||
steps:
|
||||
- name: Code Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
86
.github/workflows/docker-publish.yml
vendored
Normal file
86
.github/workflows/docker-publish.yml
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
name: Docker
|
||||
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
name: Build and Push
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
# Always run against a tag, even if the commit into the tag has [docker skip] within the commit message.
|
||||
if: "!contains(github.ref, 'main') || (!contains(github.event.head_commit.message, 'skip docker') && !contains(github.event.head_commit.message, 'docker skip'))"
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Docker metadata
|
||||
id: docker_meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
flavor: |
|
||||
latest=false
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.action == 'published' && github.event.release.prerelease == false }}
|
||||
type=ref,event=tag
|
||||
type=ref,event=branch
|
||||
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Setup Docker buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Get Build Information
|
||||
id: build_info
|
||||
run: |
|
||||
echo "version_tag=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_OUTPUT
|
||||
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and Push (tag)
|
||||
uses: docker/build-push-action@v5
|
||||
if: "github.event_name == 'release' && github.event.action == 'published'"
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
VERSION=${{ steps.build_info.outputs.version_tag }}
|
||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
|
||||
- name: Build and Push (main)
|
||||
uses: docker/build-push-action@v5
|
||||
if: "github.event_name == 'push' && contains(github.ref, 'main')"
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
VERSION=dev-${{ steps.build_info.outputs.short_sha }}
|
||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
12
Caddyfile
Normal file
12
Caddyfile
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
admin off
|
||||
email {$ADMIN_EMAIL}
|
||||
}
|
||||
|
||||
{$APP_URL} {
|
||||
root * /var/www/html/public
|
||||
encode gzip
|
||||
|
||||
php_fastcgi 127.0.0.1:9000
|
||||
file_server
|
||||
}
|
||||
82
Dockerfile
82
Dockerfile
@@ -1,41 +1,59 @@
|
||||
# Stage 0:
|
||||
# Build the assets that are needed for the frontend. This build stage is then discarded
|
||||
# since we won't need NodeJS anymore in the future. This Docker image ships a final production
|
||||
# level distribution
|
||||
FROM --platform=$TARGETOS/$TARGETARCH node:20-alpine
|
||||
WORKDIR /app
|
||||
# Pelican Production Dockerfile
|
||||
|
||||
FROM node:20-alpine AS yarn
|
||||
#FROM --platform=$TARGETOS/$TARGETARCH node:20-alpine AS yarn
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
COPY . ./
|
||||
RUN yarn install --frozen-lockfile \
|
||||
|
||||
RUN yarn config set network-timeout 300000 \
|
||||
&& yarn install --frozen-lockfile \
|
||||
&& yarn run build:production
|
||||
|
||||
# Stage 1:
|
||||
# Build the actual container with all of the needed PHP dependencies that will run the application.
|
||||
FROM --platform=$TARGETOS/$TARGETARCH php:8.3-fpm-alpine
|
||||
WORKDIR /app
|
||||
COPY . ./
|
||||
COPY --from=0 /app/public/assets ./public/assets
|
||||
RUN apk add --no-cache --update ca-certificates dcron curl git supervisor tar unzip nginx libpng-dev libxml2-dev libzip-dev icu-dev certbot certbot-nginx \
|
||||
&& docker-php-ext-configure zip \
|
||||
&& docker-php-ext-install bcmath gd intl pdo_mysql zip \
|
||||
&& curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \
|
||||
&& cp .env.example .env \
|
||||
&& mkdir -p bootstrap/cache/ storage/logs storage/framework/sessions storage/framework/views storage/framework/cache \
|
||||
&& chmod 777 -R bootstrap storage \
|
||||
&& composer install --no-dev --optimize-autoloader \
|
||||
&& rm -rf .env bootstrap/cache/*.php \
|
||||
&& mkdir -p /app/storage/logs/ \
|
||||
&& chown -R nginx:nginx .
|
||||
FROM php:8.3-fpm-alpine
|
||||
# FROM --platform=$TARGETOS/$TARGETARCH php:8.3-fpm-alpine
|
||||
|
||||
RUN rm /usr/local/etc/php-fpm.conf \
|
||||
&& echo "* * * * * /usr/local/bin/php /app/artisan schedule:run >> /dev/null 2>&1" >> /var/spool/cron/crontabs/root \
|
||||
&& echo "0 23 * * * certbot renew --nginx --quiet" >> /var/spool/cron/crontabs/root \
|
||||
&& sed -i s/ssl_session_cache/#ssl_session_cache/g /etc/nginx/nginx.conf \
|
||||
&& mkdir -p /var/run/php /var/run/nginx
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
|
||||
|
||||
COPY .github/docker/default.conf /etc/nginx/http.d/default.conf
|
||||
COPY .github/docker/www.conf /usr/local/etc/php-fpm.conf
|
||||
COPY .github/docker/supervisord.conf /etc/supervisord.conf
|
||||
WORKDIR /var/www/html
|
||||
|
||||
# Install dependencies
|
||||
RUN apk update && apk add --no-cache \
|
||||
libpng-dev libjpeg-turbo-dev freetype-dev libzip-dev icu-dev \
|
||||
zip unzip curl \
|
||||
caddy ca-certificates supervisor \
|
||||
&& docker-php-ext-install bcmath gd intl zip opcache pcntl posix pdo_mysql
|
||||
|
||||
# Copy the Caddyfile to the container
|
||||
COPY Caddyfile /etc/caddy/Caddyfile
|
||||
|
||||
# Copy the application code to the container
|
||||
COPY . .
|
||||
|
||||
COPY --from=yarn /build/public/assets ./public/assets
|
||||
|
||||
RUN touch .env
|
||||
|
||||
RUN composer install --no-dev --optimize-autoloader
|
||||
|
||||
# Set file permissions
|
||||
RUN chmod -R 755 storage bootstrap/cache \
|
||||
&& chown -R www-data:www-data ./
|
||||
|
||||
# Add scheduler to cron
|
||||
RUN echo "* * * * * php /var/www/html/artisan schedule:run >> /dev/null 2>&1" | crontab -u www-data -
|
||||
|
||||
## supervisord config and log dir
|
||||
RUN cp .github/docker/supervisord.conf /etc/supervisord.conf && \
|
||||
mkdir /var/log/supervisord/
|
||||
|
||||
HEALTHCHECK --interval=5m --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost/up || exit 1
|
||||
|
||||
EXPOSE 80 443
|
||||
|
||||
VOLUME /pelican-data
|
||||
|
||||
ENTRYPOINT [ "/bin/ash", ".github/docker/entrypoint.sh" ]
|
||||
CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ]
|
||||
|
||||
43
app/Console/Commands/Egg/CheckEggUpdatesCommand.php
Normal file
43
app/Console/Commands/Egg/CheckEggUpdatesCommand.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Egg;
|
||||
|
||||
use App\Models\Egg;
|
||||
use App\Services\Eggs\Sharing\EggExporterService;
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CheckEggUpdatesCommand extends Command
|
||||
{
|
||||
protected $signature = 'p:egg:check-updates';
|
||||
|
||||
public function handle(EggExporterService $exporterService): void
|
||||
{
|
||||
$eggs = Egg::all();
|
||||
foreach ($eggs as $egg) {
|
||||
try {
|
||||
if (is_null($egg->update_url)) {
|
||||
$this->comment("{$egg->name}: Skipping (no update url set)");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$currentJson = json_decode($exporterService->handle($egg->id));
|
||||
unset($currentJson->exported_at);
|
||||
|
||||
$updatedJson = json_decode(file_get_contents($egg->update_url));
|
||||
unset($updatedJson->exported_at);
|
||||
|
||||
if (md5(json_encode($currentJson)) === md5(json_encode($updatedJson))) {
|
||||
$this->info("{$egg->name}: Up-to-date");
|
||||
cache()->put("eggs.{$egg->uuid}.update", false, now()->addHour());
|
||||
} else {
|
||||
$this->warn("{$egg->name}: Found update");
|
||||
cache()->put("eggs.{$egg->uuid}.update", true, now()->addHour());
|
||||
}
|
||||
} catch (Exception $exception) {
|
||||
$this->error("{$egg->name}: Error ({$exception->getMessage()})");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,162 +3,27 @@
|
||||
namespace App\Console\Commands\Environment;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
use App\Traits\Commands\EnvironmentWriterTrait;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
class AppSettingsCommand extends Command
|
||||
{
|
||||
use EnvironmentWriterTrait;
|
||||
|
||||
public const CACHE_DRIVERS = [
|
||||
'file' => 'Filesystem (recommended)',
|
||||
'redis' => 'Redis',
|
||||
];
|
||||
|
||||
public const SESSION_DRIVERS = [
|
||||
'file' => 'Filesystem (recommended)',
|
||||
'redis' => 'Redis',
|
||||
'database' => 'Database',
|
||||
'cookie' => 'Cookie',
|
||||
];
|
||||
|
||||
public const QUEUE_DRIVERS = [
|
||||
'database' => 'Database (recommended)',
|
||||
'redis' => 'Redis',
|
||||
'sync' => 'Synchronous',
|
||||
];
|
||||
|
||||
protected $description = 'Configure basic environment settings for the Panel.';
|
||||
|
||||
protected $signature = 'p:environment:setup
|
||||
{--url= : The URL that this Panel is running on.}
|
||||
{--cache= : The cache driver backend to use.}
|
||||
{--session= : The session driver backend to use.}
|
||||
{--queue= : The queue driver backend to use.}
|
||||
{--redis-host= : Redis host to use for connections.}
|
||||
{--redis-pass= : Password used to connect to redis.}
|
||||
{--redis-port= : Port to connect to redis over.}
|
||||
{--settings-ui= : Enable or disable the settings UI.}';
|
||||
protected $signature = 'p:environment:setup';
|
||||
|
||||
protected array $variables = [];
|
||||
|
||||
/**
|
||||
* AppSettingsCommand constructor.
|
||||
*/
|
||||
public function __construct(private Kernel $console)
|
||||
public function handle(): void
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle command execution.
|
||||
*
|
||||
* @throws \App\Exceptions\PanelException
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$this->variables['APP_TIMEZONE'] = 'UTC';
|
||||
|
||||
$this->output->comment(__('commands.appsettings.comment.url'));
|
||||
$this->variables['APP_URL'] = $this->option('url') ?? $this->ask(
|
||||
'Application URL',
|
||||
config('app.url', 'https://example.com')
|
||||
);
|
||||
|
||||
$selected = config('cache.default', 'file');
|
||||
$this->variables['CACHE_STORE'] = $this->option('cache') ?? $this->choice(
|
||||
'Cache Driver',
|
||||
self::CACHE_DRIVERS,
|
||||
array_key_exists($selected, self::CACHE_DRIVERS) ? $selected : null
|
||||
);
|
||||
|
||||
$selected = config('session.driver', 'file');
|
||||
$this->variables['SESSION_DRIVER'] = $this->option('session') ?? $this->choice(
|
||||
'Session Driver',
|
||||
self::SESSION_DRIVERS,
|
||||
array_key_exists($selected, self::SESSION_DRIVERS) ? $selected : null
|
||||
);
|
||||
|
||||
$selected = config('queue.default', 'database');
|
||||
$this->variables['QUEUE_CONNECTION'] = $this->option('queue') ?? $this->choice(
|
||||
'Queue Driver',
|
||||
self::QUEUE_DRIVERS,
|
||||
array_key_exists($selected, self::QUEUE_DRIVERS) ? $selected : null
|
||||
);
|
||||
|
||||
if (!is_null($this->option('settings-ui'))) {
|
||||
$this->variables['APP_ENVIRONMENT_ONLY'] = $this->option('settings-ui') == 'true' ? 'false' : 'true';
|
||||
} else {
|
||||
$this->variables['APP_ENVIRONMENT_ONLY'] = $this->confirm(__('commands.appsettings.comment.settings_ui'), true) ? 'false' : 'true';
|
||||
}
|
||||
|
||||
// Make sure session cookies are set as "secure" when using HTTPS
|
||||
if (str_starts_with($this->variables['APP_URL'], 'https://')) {
|
||||
$this->variables['SESSION_SECURE_COOKIE'] = 'true';
|
||||
}
|
||||
|
||||
$redisUsed = count(collect($this->variables)->filter(function ($item) {
|
||||
return $item === 'redis';
|
||||
})) !== 0;
|
||||
|
||||
if ($redisUsed) {
|
||||
$this->requestRedisSettings();
|
||||
}
|
||||
|
||||
$path = base_path('.env');
|
||||
if (!file_exists($path)) {
|
||||
$this->comment('Copying example .env file');
|
||||
copy($path . '.example', $path);
|
||||
}
|
||||
|
||||
$this->writeToEnvironment($this->variables);
|
||||
|
||||
if (!config('app.key')) {
|
||||
$this->comment('Generating app key');
|
||||
Artisan::call('key:generate');
|
||||
}
|
||||
|
||||
if ($this->variables['QUEUE_CONNECTION'] !== 'sync') {
|
||||
$this->call('p:environment:queue-service', [
|
||||
'--use-redis' => $redisUsed,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->info($this->console->output());
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request redis connection details and verify them.
|
||||
*/
|
||||
private function requestRedisSettings(): void
|
||||
{
|
||||
$this->output->note(__('commands.appsettings.redis.note'));
|
||||
$this->variables['REDIS_HOST'] = $this->option('redis-host') ?? $this->ask(
|
||||
'Redis Host',
|
||||
config('database.redis.default.host')
|
||||
);
|
||||
|
||||
$askForRedisPassword = true;
|
||||
if (!empty(config('database.redis.default.password'))) {
|
||||
$this->variables['REDIS_PASSWORD'] = config('database.redis.default.password');
|
||||
$askForRedisPassword = $this->confirm('It seems a password is already defined for Redis, would you like to change it?');
|
||||
}
|
||||
|
||||
if ($askForRedisPassword) {
|
||||
$this->output->comment(__('commands.appsettings.redis.comment'));
|
||||
$this->variables['REDIS_PASSWORD'] = $this->option('redis-pass') ?? $this->output->askHidden(
|
||||
'Redis Password'
|
||||
);
|
||||
}
|
||||
|
||||
if (empty($this->variables['REDIS_PASSWORD'])) {
|
||||
$this->variables['REDIS_PASSWORD'] = 'null';
|
||||
}
|
||||
|
||||
$this->variables['REDIS_PORT'] = $this->option('redis-port') ?? $this->ask(
|
||||
'Redis Port',
|
||||
config('database.redis.default.port')
|
||||
);
|
||||
Artisan::call('filament:optimize');
|
||||
}
|
||||
}
|
||||
|
||||
68
app/Console/Commands/Environment/CacheSettingsCommand.php
Normal file
68
app/Console/Commands/Environment/CacheSettingsCommand.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Environment;
|
||||
|
||||
use App\Traits\Commands\RequestRedisSettingsTrait;
|
||||
use App\Traits\EnvironmentWriterTrait;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
|
||||
class CacheSettingsCommand extends Command
|
||||
{
|
||||
use EnvironmentWriterTrait;
|
||||
use RequestRedisSettingsTrait;
|
||||
|
||||
public const CACHE_DRIVERS = [
|
||||
'file' => 'Filesystem (default)',
|
||||
'database' => 'Database',
|
||||
'redis' => 'Redis',
|
||||
];
|
||||
|
||||
protected $description = 'Configure cache settings for the Panel.';
|
||||
|
||||
protected $signature = 'p:environment:cache
|
||||
{--driver= : The cache driver backend to use.}
|
||||
{--redis-host= : Redis host to use for connections.}
|
||||
{--redis-user= : User used to connect to redis.}
|
||||
{--redis-pass= : Password used to connect to redis.}
|
||||
{--redis-port= : Port to connect to redis over.}';
|
||||
|
||||
protected array $variables = [];
|
||||
|
||||
/**
|
||||
* CacheSettingsCommand constructor.
|
||||
*/
|
||||
public function __construct(private Kernel $console)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle command execution.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$selected = config('cache.default', 'file');
|
||||
$this->variables['CACHE_STORE'] = $this->option('driver') ?? $this->choice(
|
||||
'Cache Driver',
|
||||
self::CACHE_DRIVERS,
|
||||
array_key_exists($selected, self::CACHE_DRIVERS) ? $selected : null
|
||||
);
|
||||
|
||||
if ($this->variables['CACHE_STORE'] === 'redis') {
|
||||
$this->requestRedisSettings();
|
||||
|
||||
if (config('queue.default') !== 'sync') {
|
||||
$this->call('p:environment:queue-service', [
|
||||
'--overwrite' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->writeToEnvironment($this->variables);
|
||||
|
||||
$this->info($this->console->output());
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
namespace App\Console\Commands\Environment;
|
||||
|
||||
use App\Traits\EnvironmentWriterTrait;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
use Illuminate\Database\DatabaseManager;
|
||||
use App\Traits\Commands\EnvironmentWriterTrait;
|
||||
|
||||
class DatabaseSettingsCommand extends Command
|
||||
{
|
||||
@@ -42,6 +42,13 @@ class DatabaseSettingsCommand extends Command
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$this->error('Changing the database driver will NOT move any database data!');
|
||||
$this->error('Please make sure you made a database backup first!');
|
||||
$this->error('After changing the driver you will have to manually move the old data to the new database.');
|
||||
if (!$this->confirm('Do you want to continue?')) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
$selected = config('database.default', 'sqlite');
|
||||
$this->variables['DB_CONNECTION'] = $this->option('driver') ?? $this->choice(
|
||||
'Database Driver',
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace App\Console\Commands\Environment;
|
||||
|
||||
use App\Traits\EnvironmentWriterTrait;
|
||||
use Illuminate\Console\Command;
|
||||
use App\Traits\Commands\EnvironmentWriterTrait;
|
||||
|
||||
class EmailSettingsCommand extends Command
|
||||
{
|
||||
@@ -61,6 +61,8 @@ class EmailSettingsCommand extends Command
|
||||
|
||||
$this->writeToEnvironment($this->variables);
|
||||
|
||||
$this->call('queue:restart');
|
||||
|
||||
$this->line('Updating stored environment configuration file.');
|
||||
$this->line('');
|
||||
}
|
||||
@@ -68,7 +70,7 @@ class EmailSettingsCommand extends Command
|
||||
/**
|
||||
* Handle variables for SMTP driver.
|
||||
*/
|
||||
private function setupSmtpDriverVariables()
|
||||
private function setupSmtpDriverVariables(): void
|
||||
{
|
||||
$this->variables['MAIL_HOST'] = $this->option('host') ?? $this->ask(
|
||||
trans('command/messages.environment.mail.ask_smtp_host'),
|
||||
@@ -99,7 +101,7 @@ class EmailSettingsCommand extends Command
|
||||
/**
|
||||
* Handle variables for mailgun driver.
|
||||
*/
|
||||
private function setupMailgunDriverVariables()
|
||||
private function setupMailgunDriverVariables(): void
|
||||
{
|
||||
$this->variables['MAILGUN_DOMAIN'] = $this->option('host') ?? $this->ask(
|
||||
trans('command/messages.environment.mail.ask_mailgun_domain'),
|
||||
@@ -120,7 +122,7 @@ class EmailSettingsCommand extends Command
|
||||
/**
|
||||
* Handle variables for mandrill driver.
|
||||
*/
|
||||
private function setupMandrillDriverVariables()
|
||||
private function setupMandrillDriverVariables(): void
|
||||
{
|
||||
$this->variables['MANDRILL_SECRET'] = $this->option('password') ?? $this->ask(
|
||||
trans('command/messages.environment.mail.ask_mandrill_secret'),
|
||||
@@ -131,7 +133,7 @@ class EmailSettingsCommand extends Command
|
||||
/**
|
||||
* Handle variables for postmark driver.
|
||||
*/
|
||||
private function setupPostmarkDriverVariables()
|
||||
private function setupPostmarkDriverVariables(): void
|
||||
{
|
||||
$this->variables['MAIL_DRIVER'] = 'smtp';
|
||||
$this->variables['MAIL_HOST'] = 'smtp.postmarkapp.com';
|
||||
|
||||
66
app/Console/Commands/Environment/QueueSettingsCommand.php
Normal file
66
app/Console/Commands/Environment/QueueSettingsCommand.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Environment;
|
||||
|
||||
use App\Traits\Commands\RequestRedisSettingsTrait;
|
||||
use App\Traits\EnvironmentWriterTrait;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
|
||||
class QueueSettingsCommand extends Command
|
||||
{
|
||||
use EnvironmentWriterTrait;
|
||||
use RequestRedisSettingsTrait;
|
||||
|
||||
public const QUEUE_DRIVERS = [
|
||||
'database' => 'Database (default)',
|
||||
'redis' => 'Redis',
|
||||
'sync' => 'Synchronous',
|
||||
];
|
||||
|
||||
protected $description = 'Configure queue settings for the Panel.';
|
||||
|
||||
protected $signature = 'p:environment:queue
|
||||
{--driver= : The queue driver backend to use.}
|
||||
{--redis-host= : Redis host to use for connections.}
|
||||
{--redis-user= : User used to connect to redis.}
|
||||
{--redis-pass= : Password used to connect to redis.}
|
||||
{--redis-port= : Port to connect to redis over.}';
|
||||
|
||||
protected array $variables = [];
|
||||
|
||||
/**
|
||||
* QueueSettingsCommand constructor.
|
||||
*/
|
||||
public function __construct(private Kernel $console)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle command execution.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$selected = config('queue.default', 'database');
|
||||
$this->variables['QUEUE_CONNECTION'] = $this->option('driver') ?? $this->choice(
|
||||
'Queue Driver',
|
||||
self::QUEUE_DRIVERS,
|
||||
array_key_exists($selected, self::QUEUE_DRIVERS) ? $selected : null
|
||||
);
|
||||
|
||||
if ($this->variables['QUEUE_CONNECTION'] === 'redis') {
|
||||
$this->requestRedisSettings();
|
||||
|
||||
$this->call('p:environment:queue-service', [
|
||||
'--overwrite' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->writeToEnvironment($this->variables);
|
||||
|
||||
$this->info($this->console->output());
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@ class QueueWorkerServiceCommand extends Command
|
||||
{--service-name= : Name of the queue worker service.}
|
||||
{--user= : The user that PHP runs under.}
|
||||
{--group= : The group that PHP runs under.}
|
||||
{--use-redis : Whether redis is used.}
|
||||
{--overwrite : Force overwrite if the service file already exists.}';
|
||||
|
||||
public function handle(): void
|
||||
@@ -32,7 +31,8 @@ class QueueWorkerServiceCommand extends Command
|
||||
$user = $this->option('user') ?? $this->ask('Webserver User', 'www-data');
|
||||
$group = $this->option('group') ?? $this->ask('Webserver Group', 'www-data');
|
||||
|
||||
$afterRedis = $this->option('use-redis') ? '
|
||||
$redisUsed = config('queue.default') === 'redis' || config('session.driver') === 'redis' || config('cache.default') === 'redis';
|
||||
$afterRedis = $redisUsed ? '
|
||||
After=redis-server.service' : '';
|
||||
|
||||
$basePath = base_path();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
69
app/Console/Commands/Environment/SessionSettingsCommand.php
Normal file
69
app/Console/Commands/Environment/SessionSettingsCommand.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Environment;
|
||||
|
||||
use App\Traits\Commands\RequestRedisSettingsTrait;
|
||||
use App\Traits\EnvironmentWriterTrait;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Console\Kernel;
|
||||
|
||||
class SessionSettingsCommand extends Command
|
||||
{
|
||||
use EnvironmentWriterTrait;
|
||||
use RequestRedisSettingsTrait;
|
||||
|
||||
public const SESSION_DRIVERS = [
|
||||
'file' => 'Filesystem (default)',
|
||||
'redis' => 'Redis',
|
||||
'database' => 'Database',
|
||||
'cookie' => 'Cookie',
|
||||
];
|
||||
|
||||
protected $description = 'Configure session settings for the Panel.';
|
||||
|
||||
protected $signature = 'p:environment:session
|
||||
{--driver= : The session driver backend to use.}
|
||||
{--redis-host= : Redis host to use for connections.}
|
||||
{--redis-user= : User used to connect to redis.}
|
||||
{--redis-pass= : Password used to connect to redis.}
|
||||
{--redis-port= : Port to connect to redis over.}';
|
||||
|
||||
protected array $variables = [];
|
||||
|
||||
/**
|
||||
* SessionSettingsCommand constructor.
|
||||
*/
|
||||
public function __construct(private Kernel $console)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle command execution.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$selected = config('session.driver', 'file');
|
||||
$this->variables['SESSION_DRIVER'] = $this->option('driver') ?? $this->choice(
|
||||
'Session Driver',
|
||||
self::SESSION_DRIVERS,
|
||||
array_key_exists($selected, self::SESSION_DRIVERS) ? $selected : null
|
||||
);
|
||||
|
||||
if ($this->variables['SESSION_DRIVER'] === 'redis') {
|
||||
$this->requestRedisSettings();
|
||||
|
||||
if (config('queue.default') !== 'sync') {
|
||||
$this->call('p:environment:queue-service', [
|
||||
'--overwrite' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->writeToEnvironment($this->variables);
|
||||
|
||||
$this->info($this->console->output());
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -26,8 +26,8 @@ class InfoCommand extends Command
|
||||
{
|
||||
$this->output->title('Version Information');
|
||||
$this->table([], [
|
||||
['Panel Version', $this->versionService->versionData()['version']],
|
||||
['Latest Version', $this->versionService->getPanel()],
|
||||
['Panel Version', $this->versionService->currentPanelVersion()],
|
||||
['Latest Version', $this->versionService->latestPanelVersion()],
|
||||
['Up-to-Date', $this->versionService->isLatestPanel() ? 'Yes' : $this->formatText('No', 'bg=red')],
|
||||
], 'compact');
|
||||
|
||||
|
||||
60
app/Console/Commands/Maintenance/PruneImagesCommand.php
Normal file
60
app/Console/Commands/Maintenance/PruneImagesCommand.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Maintenance;
|
||||
|
||||
use App\Models\Node;
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class PruneImagesCommand extends Command
|
||||
{
|
||||
protected $signature = 'p:maintenance:prune-images {node?}';
|
||||
|
||||
protected $description = 'Clean up all dangling docker images to clear up disk space.';
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$node = $this->argument('node');
|
||||
|
||||
if (empty($node)) {
|
||||
$nodes = Node::all();
|
||||
/** @var Node $node */
|
||||
foreach ($nodes as $node) {
|
||||
$this->cleanupImages($node);
|
||||
}
|
||||
} else {
|
||||
$this->cleanupImages((int) $node);
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanupImages(int|Node $node): void
|
||||
{
|
||||
if (!$node instanceof Node) {
|
||||
$node = Node::query()->findOrFail($node);
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::daemon($node)
|
||||
->connectTimeout(5)
|
||||
->timeout(30)
|
||||
->delete('/api/system/docker/image/prune')
|
||||
->json() ?? [];
|
||||
|
||||
if (empty($response) || $response['ImagesDeleted'] === null) {
|
||||
$this->warn("Node {$node->id}: No images to clean up.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$count = count($response['ImagesDeleted']);
|
||||
|
||||
$useBinaryPrefix = config('panel.use_binary_prefix');
|
||||
$space = round($useBinaryPrefix ? $response['SpaceReclaimed'] / 1024 / 1024 : $response['SpaceReclaimed'] / 1000 / 1000, 2) . ($useBinaryPrefix ? ' MiB' : ' MB');
|
||||
|
||||
$this->info("Node {$node->id}: Cleaned up {$count} dangling docker images. ({$space})");
|
||||
} catch (Exception $exception) {
|
||||
$this->error($exception->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Console\Commands\Overrides;
|
||||
|
||||
use App\Console\RequiresDatabaseMigrations;
|
||||
use App\Traits\Commands\RequiresDatabaseMigrations;
|
||||
use Illuminate\Database\Console\Seeds\SeedCommand as BaseSeedCommand;
|
||||
|
||||
class SeedCommand extends BaseSeedCommand
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Console\Commands\Overrides;
|
||||
|
||||
use App\Console\RequiresDatabaseMigrations;
|
||||
use App\Traits\Commands\RequiresDatabaseMigrations;
|
||||
use Illuminate\Foundation\Console\UpCommand as BaseUpCommand;
|
||||
|
||||
class UpCommand extends BaseUpCommand
|
||||
|
||||
@@ -6,7 +6,6 @@ use Illuminate\Console\Command;
|
||||
use App\Models\Schedule;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use App\Services\Schedules\ProcessScheduleService;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class ProcessRunnableCommand extends Command
|
||||
{
|
||||
@@ -24,7 +23,7 @@ class ProcessRunnableCommand extends Command
|
||||
->whereRelation('server', fn (Builder $builder) => $builder->whereNull('status'))
|
||||
->where('is_active', true)
|
||||
->where('is_processing', false)
|
||||
->where('next_run_at', '<=', Carbon::now()->toDateTimeString())
|
||||
->where('next_run_at', '<=', now('UTC')->toDateTimeString())
|
||||
->get();
|
||||
|
||||
if ($schedules->count() < 1) {
|
||||
@@ -51,7 +50,7 @@ class ProcessRunnableCommand extends Command
|
||||
* never throw an exception out, otherwise you'll end up killing the entire run group causing
|
||||
* any other schedules to not process correctly.
|
||||
*/
|
||||
protected function processSchedule(Schedule $schedule)
|
||||
protected function processSchedule(Schedule $schedule): void
|
||||
{
|
||||
if ($schedule->tasks->isEmpty()) {
|
||||
return;
|
||||
|
||||
@@ -178,7 +178,7 @@ class UpgradeCommand extends Command
|
||||
$this->info(__('commands.upgrade.success'));
|
||||
}
|
||||
|
||||
protected function withProgress(ProgressBar $bar, \Closure $callback)
|
||||
protected function withProgress(ProgressBar $bar, \Closure $callback): void
|
||||
{
|
||||
$bar->clear();
|
||||
$callback();
|
||||
|
||||
@@ -15,7 +15,7 @@ class DeleteUserCommand extends Command
|
||||
public function handle(): int
|
||||
{
|
||||
$search = $this->option('user') ?? $this->ask(trans('command/messages.user.search_users'));
|
||||
Assert::notEmpty($search, 'Search term should be an email address, got: %s.');
|
||||
Assert::notEmpty($search, 'Search term should not be empty.');
|
||||
|
||||
$results = User::query()
|
||||
->where('id', 'LIKE', "$search%")
|
||||
@@ -42,6 +42,8 @@ class DeleteUserCommand extends Command
|
||||
if (!$deleteUser = $this->ask(trans('command/messages.user.select_search_user'))) {
|
||||
return $this->handle();
|
||||
}
|
||||
|
||||
$deleteUser = User::query()->findOrFail($deleteUser);
|
||||
} else {
|
||||
if (count($results) > 1) {
|
||||
$this->error(trans('command/messages.user.multiple_found'));
|
||||
@@ -53,8 +55,7 @@ class DeleteUserCommand extends Command
|
||||
}
|
||||
|
||||
if ($this->confirm(trans('command/messages.user.confirm_delete')) || !$this->input->isInteractive()) {
|
||||
$user = User::query()->findOrFail($deleteUser);
|
||||
$user->delete();
|
||||
$deleteUser->delete();
|
||||
|
||||
$this->info(trans('command/messages.user.deleted'));
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ class MakeUserCommand extends Command
|
||||
['UUID', $user->uuid],
|
||||
['Email', $user->email],
|
||||
['Username', $user->username],
|
||||
['Admin', $user->root_admin ? 'Yes' : 'No'],
|
||||
['Admin', $user->isRootAdmin() ? 'Yes' : 'No'],
|
||||
]);
|
||||
|
||||
return 0;
|
||||
|
||||
@@ -2,13 +2,17 @@
|
||||
|
||||
namespace App\Console;
|
||||
|
||||
use App\Console\Commands\Egg\CheckEggUpdatesCommand;
|
||||
use App\Console\Commands\Maintenance\CleanServiceBackupFilesCommand;
|
||||
use App\Console\Commands\Maintenance\PruneImagesCommand;
|
||||
use App\Console\Commands\Maintenance\PruneOrphanedBackupsCommand;
|
||||
use App\Console\Commands\Schedule\ProcessRunnableCommand;
|
||||
use App\Jobs\NodeStatistics;
|
||||
use App\Models\ActivityLog;
|
||||
use App\Models\Webhook;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Database\Console\PruneCommand;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
use App\Console\Commands\Schedule\ProcessRunnableCommand;
|
||||
use App\Console\Commands\Maintenance\PruneOrphanedBackupsCommand;
|
||||
use App\Console\Commands\Maintenance\CleanServiceBackupFilesCommand;
|
||||
|
||||
class Kernel extends ConsoleKernel
|
||||
{
|
||||
@@ -30,7 +34,12 @@ class Kernel extends ConsoleKernel
|
||||
|
||||
// Execute scheduled commands for servers every minute, as if there was a normal cron running.
|
||||
$schedule->command(ProcessRunnableCommand::class)->everyMinute()->withoutOverlapping();
|
||||
|
||||
$schedule->command(CleanServiceBackupFilesCommand::class)->daily();
|
||||
$schedule->command(PruneImagesCommand::class)->daily();
|
||||
$schedule->command(CheckEggUpdatesCommand::class)->hourly();
|
||||
|
||||
$schedule->job(new NodeStatistics())->everyFiveSeconds()->withoutOverlapping();
|
||||
|
||||
if (config('backups.prune_age')) {
|
||||
// Every 30 minutes, run the backup pruning command so that any abandoned backups can be deleted.
|
||||
@@ -40,5 +49,9 @@ class Kernel extends ConsoleKernel
|
||||
if (config('activity.prune_days')) {
|
||||
$schedule->command(PruneCommand::class, ['--model' => [ActivityLog::class]])->daily();
|
||||
}
|
||||
|
||||
if (config('panel.webhook.prune_days')) {
|
||||
$schedule->command(PruneCommand::class, ['--model' => [Webhook::class]])->daily();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,14 @@ enum ContainerStatus: string
|
||||
{
|
||||
// Docker Based
|
||||
case Created = 'created';
|
||||
case Starting = 'starting';
|
||||
case Running = 'running';
|
||||
case Restarting = 'restarting';
|
||||
case Exited = 'exited';
|
||||
case Paused = 'paused';
|
||||
case Dead = 'dead';
|
||||
case Removing = 'removing';
|
||||
case Stopping = 'stopping';
|
||||
case Offline = 'offline';
|
||||
|
||||
// HTTP Based
|
||||
@@ -20,15 +22,17 @@ enum ContainerStatus: string
|
||||
public function icon(): string
|
||||
{
|
||||
return match ($this) {
|
||||
|
||||
self::Created => 'tabler-heart-plus',
|
||||
self::Starting => 'tabler-heart-up',
|
||||
self::Running => 'tabler-heartbeat',
|
||||
self::Restarting => 'tabler-heart-bolt',
|
||||
self::Exited => 'tabler-heart-exclamation',
|
||||
self::Paused => 'tabler-heart-pause',
|
||||
self::Dead => 'tabler-heart-x',
|
||||
self::Dead, self::Offline => 'tabler-heart-x',
|
||||
self::Removing => 'tabler-heart-down',
|
||||
self::Missing => 'tabler-heart-question',
|
||||
self::Offline => 'tabler-heart-bolt',
|
||||
self::Missing => 'tabler-heart-search',
|
||||
self::Stopping => 'tabler-heart-minus',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -36,6 +40,7 @@ enum ContainerStatus: string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Created => 'primary',
|
||||
self::Starting => 'warning',
|
||||
self::Running => 'success',
|
||||
self::Restarting => 'info',
|
||||
self::Exited => 'danger',
|
||||
@@ -43,6 +48,7 @@ enum ContainerStatus: string
|
||||
self::Dead => 'danger',
|
||||
self::Removing => 'warning',
|
||||
self::Missing => 'danger',
|
||||
self::Stopping => 'warning',
|
||||
self::Offline => 'gray',
|
||||
};
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
@@ -12,7 +12,7 @@ class FailedCaptcha extends Event
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public string $ip, public string $domain)
|
||||
public function __construct(public string $ip, public ?string $message)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\Server;
|
||||
|
||||
use App\Events\Event;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Created extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public Server $server)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\Server;
|
||||
|
||||
use App\Events\Event;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Creating extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public Server $server)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\Server;
|
||||
|
||||
use App\Events\Event;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Deleted extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public Server $server)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\Server;
|
||||
|
||||
use App\Events\Event;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Deleting extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public Server $server)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\Server;
|
||||
|
||||
use App\Events\Event;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Saved extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public Server $server)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\Server;
|
||||
|
||||
use App\Events\Event;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Saving extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public Server $server)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\Server;
|
||||
|
||||
use App\Events\Event;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Updated extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public Server $server)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\Server;
|
||||
|
||||
use App\Events\Event;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Updating extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public Server $server)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\Subuser;
|
||||
|
||||
use App\Events\Event;
|
||||
use App\Models\Subuser;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Created extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public Subuser $subuser)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\Subuser;
|
||||
|
||||
use App\Events\Event;
|
||||
use App\Models\Subuser;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Creating extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public Subuser $subuser)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\Subuser;
|
||||
|
||||
use App\Events\Event;
|
||||
use App\Models\Subuser;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Deleted extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public Subuser $subuser)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\Subuser;
|
||||
|
||||
use App\Events\Event;
|
||||
use App\Models\Subuser;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Deleting extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public Subuser $subuser)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\User;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Events\Event;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Created extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public User $user)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\User;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Events\Event;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Creating extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public User $user)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\User;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Events\Event;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Deleted extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public User $user)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\User;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Events\Event;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Deleting extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public User $user)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
class AccountNotFoundException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
class AutoDeploymentException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -4,18 +4,22 @@ namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Container\Container;
|
||||
use Prologue\Alerts\AlertsMessageBag;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
||||
|
||||
class DisplayException extends PanelException implements HttpExceptionInterface
|
||||
{
|
||||
public const LEVEL_DEBUG = 'debug';
|
||||
|
||||
public const LEVEL_INFO = 'info';
|
||||
|
||||
public const LEVEL_WARNING = 'warning';
|
||||
|
||||
public const LEVEL_ERROR = 'error';
|
||||
|
||||
/**
|
||||
@@ -46,7 +50,7 @@ class DisplayException extends PanelException implements HttpExceptionInterface
|
||||
* and then redirecting them back to the page that they came from. If the
|
||||
* request originated from an API hit, return the error in JSONAPI spec format.
|
||||
*/
|
||||
public function render(Request $request)
|
||||
public function render(Request $request): bool|RedirectResponse|JsonResponse
|
||||
{
|
||||
if ($request->is('livewire/update')) {
|
||||
Notification::make()
|
||||
@@ -55,15 +59,13 @@ class DisplayException extends PanelException implements HttpExceptionInterface
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json(Handler::toArray($this), $this->getStatusCode(), $this->getHeaders());
|
||||
}
|
||||
|
||||
app(AlertsMessageBag::class)->danger($this->getMessage())->flash();
|
||||
|
||||
return redirect()->back()->withInput();
|
||||
}
|
||||
|
||||
@@ -73,10 +75,10 @@ class DisplayException extends PanelException implements HttpExceptionInterface
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function report()
|
||||
public function report(): void
|
||||
{
|
||||
if (!$this->getPrevious() instanceof \Exception || !Handler::isReportable($this->getPrevious())) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -85,6 +87,6 @@ class DisplayException extends PanelException implements HttpExceptionInterface
|
||||
throw $this->getPrevious();
|
||||
}
|
||||
|
||||
return $logger->{$this->getErrorLevel()}($this->getPrevious());
|
||||
$logger->{$this->getErrorLevel()}($this->getPrevious());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ class Handler extends ExceptionHandler
|
||||
/**
|
||||
* Render an exception into an HTTP response.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Illuminate\Http\Request $request
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
@@ -140,7 +140,7 @@ class Handler extends ExceptionHandler
|
||||
* Transform a validation exception into a consistent format to be returned for
|
||||
* calls to the API.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Illuminate\Http\Request $request
|
||||
*/
|
||||
public function invalidJson($request, ValidationException $exception): JsonResponse
|
||||
{
|
||||
@@ -236,7 +236,7 @@ class Handler extends ExceptionHandler
|
||||
/**
|
||||
* Convert an authentication exception into an unauthenticated response.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Illuminate\Http\Request $request
|
||||
*/
|
||||
protected function unauthenticated($request, AuthenticationException $exception): JsonResponse|RedirectResponse
|
||||
{
|
||||
@@ -273,6 +273,7 @@ class Handler extends ExceptionHandler
|
||||
*/
|
||||
public static function toArray(\Throwable $e): array
|
||||
{
|
||||
// @phpstan-ignore-next-line
|
||||
return (new self(app()))->convertExceptionToArray($e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,6 @@ use GuzzleHttp\Exception\GuzzleException;
|
||||
use App\Exceptions\DisplayException;
|
||||
use Illuminate\Support\Facades\Context;
|
||||
|
||||
/**
|
||||
* @method \GuzzleHttp\Exception\GuzzleException getPrevious()
|
||||
*/
|
||||
class DaemonConnectionException extends DisplayException
|
||||
{
|
||||
private int $statusCode = Response::HTTP_GATEWAY_TIMEOUT;
|
||||
|
||||
@@ -10,7 +10,7 @@ class HttpForbiddenException extends HttpException
|
||||
/**
|
||||
* HttpForbiddenException constructor.
|
||||
*/
|
||||
public function __construct(string $message = null, \Throwable $previous = null)
|
||||
public function __construct(?string $message = null, ?\Throwable $previous = null)
|
||||
{
|
||||
parent::__construct(Response::HTTP_FORBIDDEN, $message, $previous);
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Http\Server;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class FileTypeNotEditableException extends DisplayException
|
||||
{
|
||||
}
|
||||
@@ -12,7 +12,7 @@ class ServerStateConflictException extends ConflictHttpException
|
||||
* Exception thrown when the server is in an unsupported state for API access or
|
||||
* certain operations within the codebase.
|
||||
*/
|
||||
public function __construct(Server $server, \Throwable $previous = null)
|
||||
public function __construct(Server $server, ?\Throwable $previous = null)
|
||||
{
|
||||
$message = 'This server is currently in an unsupported state, please try again later.';
|
||||
if ($server->isSuspended()) {
|
||||
|
||||
@@ -11,7 +11,7 @@ class TwoFactorAuthRequiredException extends HttpException implements HttpExcept
|
||||
/**
|
||||
* TwoFactorAuthRequiredException constructor.
|
||||
*/
|
||||
public function __construct(\Throwable $previous = null)
|
||||
public function __construct(?\Throwable $previous = null)
|
||||
{
|
||||
parent::__construct(Response::HTTP_BAD_REQUEST, 'Two-factor authentication is required on this account in order to access this endpoint.', $previous);
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Repository\Daemon;
|
||||
|
||||
use App\Exceptions\Repository\RepositoryException;
|
||||
|
||||
class InvalidPowerSignalException extends RepositoryException
|
||||
{
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Repository;
|
||||
|
||||
use App\Exceptions\PanelException;
|
||||
|
||||
class RepositoryException extends PanelException
|
||||
{
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Allocation;
|
||||
|
||||
use App\Exceptions\PanelException;
|
||||
|
||||
class AllocationDoesNotBelongToServerException extends PanelException
|
||||
{
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Egg;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class BadJsonFormatException extends DisplayException
|
||||
{
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Egg;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class NoParentConfigurationFoundException extends DisplayException
|
||||
{
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Schedule\Task;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class TaskIntervalTooLongException extends DisplayException
|
||||
{
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Server;
|
||||
|
||||
use App\Exceptions\PanelException;
|
||||
|
||||
class RequiredVariableMissingException extends PanelException
|
||||
{
|
||||
}
|
||||
@@ -10,7 +10,7 @@ class ServiceLimitExceededException extends DisplayException
|
||||
* Exception thrown when something goes over a defined limit, such as allocated
|
||||
* ports, tasks, databases, etc.
|
||||
*/
|
||||
public function __construct(string $message, \Throwable $previous = null)
|
||||
public function __construct(string $message, ?\Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, $previous, self::LEVEL_WARNING);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Exceptions\DisplayException;
|
||||
class TwoFactorAuthenticationTokenInvalid extends DisplayException
|
||||
{
|
||||
public string $title = 'Invalid 2FA Code';
|
||||
|
||||
public string $icon = 'tabler-2fa';
|
||||
|
||||
public function __construct()
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Transformer;
|
||||
|
||||
use App\Exceptions\PanelException;
|
||||
|
||||
class InvalidTransformerLevelException extends PanelException
|
||||
{
|
||||
}
|
||||
@@ -34,7 +34,7 @@ class BackupManager
|
||||
/**
|
||||
* Returns a backup adapter instance.
|
||||
*/
|
||||
public function adapter(string $name = null): FilesystemAdapter
|
||||
public function adapter(?string $name = null): FilesystemAdapter
|
||||
{
|
||||
return $this->get($name ?: $this->getDefaultAdapter());
|
||||
}
|
||||
@@ -145,7 +145,7 @@ class BackupManager
|
||||
/**
|
||||
* Unset the given adapter instances.
|
||||
*
|
||||
* @param string|string[] $adapter
|
||||
* @param string|string[] $adapter
|
||||
*/
|
||||
public function forget(array|string $adapter): self
|
||||
{
|
||||
|
||||
@@ -7,7 +7,9 @@ use App\Models\DatabaseHost;
|
||||
class DynamicDatabaseConnection
|
||||
{
|
||||
public const DB_CHARSET = 'utf8';
|
||||
|
||||
public const DB_COLLATION = 'utf8_unicode_ci';
|
||||
|
||||
public const DB_DRIVER = 'mysql';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Facades;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
class Theme extends Facade
|
||||
{
|
||||
protected static function getFacadeAccessor(): string
|
||||
{
|
||||
return 'extensions.themes';
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Extensions\Themes;
|
||||
|
||||
class Theme
|
||||
{
|
||||
public function js($path): string
|
||||
{
|
||||
return sprintf('<script src="%s"></script>' . PHP_EOL, $this->getUrl($path));
|
||||
}
|
||||
|
||||
public function css($path): string
|
||||
{
|
||||
return sprintf('<link media="all" type="text/css" rel="stylesheet" href="%s"/>' . PHP_EOL, $this->getUrl($path));
|
||||
}
|
||||
|
||||
protected function getUrl($path): string
|
||||
{
|
||||
return '/themes/panel/' . ltrim($path, '/');
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Facades;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
use App\Services\Activity\ActivityLogBatchService;
|
||||
|
||||
class LogBatch extends Facade
|
||||
{
|
||||
protected static function getFacadeAccessor(): string
|
||||
{
|
||||
return ActivityLogBatchService::class;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Clusters;
|
||||
|
||||
use Filament\Clusters\Cluster;
|
||||
|
||||
class Settings extends Cluster
|
||||
{
|
||||
protected static ?string $navigationIcon = 'tabler-settings';
|
||||
}
|
||||
36
app/Filament/Pages/Auth/Login.php
Normal file
36
app/Filament/Pages/Auth/Login.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Auth;
|
||||
|
||||
use Coderflex\FilamentTurnstile\Forms\Components\Turnstile;
|
||||
use Filament\Pages\Auth\Login as BaseLogin;
|
||||
|
||||
class Login extends BaseLogin
|
||||
{
|
||||
protected function getForms(): array
|
||||
{
|
||||
return [
|
||||
'form' => $this->form(
|
||||
$this->makeForm()
|
||||
->schema([
|
||||
$this->getEmailFormComponent(),
|
||||
$this->getPasswordFormComponent(),
|
||||
$this->getRememberFormComponent(),
|
||||
Turnstile::make('captcha')
|
||||
->hidden(!config('turnstile.turnstile_enabled'))
|
||||
->validationMessages([
|
||||
'required' => config('turnstile.error_messages.turnstile_check_message'),
|
||||
]),
|
||||
])
|
||||
->statePath('data'),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
protected function throwFailureValidationException(): never
|
||||
{
|
||||
$this->dispatch('reset-captcha');
|
||||
|
||||
parent::throwFailureValidationException();
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use App\Filament\Resources\NodeResource\Pages\CreateNode;
|
||||
use App\Filament\Resources\NodeResource\Pages\ListNodes;
|
||||
use App\Models\Egg;
|
||||
use App\Models\Node;
|
||||
@@ -28,16 +29,20 @@ class Dashboard extends Page
|
||||
|
||||
public string $activeTab = 'nodes';
|
||||
|
||||
private SoftwareVersionService $softwareVersionService;
|
||||
|
||||
public function mount(SoftwareVersionService $softwareVersionService): void
|
||||
{
|
||||
$this->softwareVersionService = $softwareVersionService;
|
||||
}
|
||||
|
||||
public function getViewData(): array
|
||||
{
|
||||
/** @var SoftwareVersionService $softwareVersionService */
|
||||
$softwareVersionService = app(SoftwareVersionService::class);
|
||||
|
||||
return [
|
||||
'inDevelopment' => config('app.version') === 'canary',
|
||||
'version' => $softwareVersionService->versionData()['version'],
|
||||
'latestVersion' => $softwareVersionService->getPanel(),
|
||||
'isLatest' => $softwareVersionService->isLatestPanel(),
|
||||
'version' => $this->softwareVersionService->currentPanelVersion(),
|
||||
'latestVersion' => $this->softwareVersionService->latestPanelVersion(),
|
||||
'isLatest' => $this->softwareVersionService->isLatestPanel(),
|
||||
'eggsCount' => Egg::query()->count(),
|
||||
'nodesList' => ListNodes::getUrl(),
|
||||
'nodesCount' => Node::query()->count(),
|
||||
@@ -61,13 +66,13 @@ class Dashboard extends Page
|
||||
CreateAction::make()
|
||||
->label(trans('dashboard/index.sections.intro-first-node.button_label'))
|
||||
->icon('tabler-server-2')
|
||||
->url(route('filament.admin.resources.nodes.create')),
|
||||
->url(CreateNode::getUrl()),
|
||||
],
|
||||
'supportActions' => [
|
||||
CreateAction::make()
|
||||
->label(trans('dashboard/index.sections.intro-support.button_donate'))
|
||||
->icon('tabler-cash')
|
||||
->url($softwareVersionService->getDonations(), true)
|
||||
->url('https://pelican.dev/donate', true)
|
||||
->color('success'),
|
||||
],
|
||||
'helpActions' => [
|
||||
|
||||
184
app/Filament/Pages/Installer/PanelInstaller.php
Normal file
184
app/Filament/Pages/Installer/PanelInstaller.php
Normal file
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer;
|
||||
|
||||
use App\Filament\Pages\Dashboard;
|
||||
use App\Filament\Pages\Installer\Steps\CacheStep;
|
||||
use App\Filament\Pages\Installer\Steps\DatabaseStep;
|
||||
use App\Filament\Pages\Installer\Steps\EnvironmentStep;
|
||||
use App\Filament\Pages\Installer\Steps\QueueStep;
|
||||
use App\Filament\Pages\Installer\Steps\RequirementsStep;
|
||||
use App\Filament\Pages\Installer\Steps\SessionStep;
|
||||
use App\Models\User;
|
||||
use App\Services\Users\UserCreationService;
|
||||
use App\Traits\CheckMigrationsTrait;
|
||||
use App\Traits\EnvironmentWriterTrait;
|
||||
use Exception;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Wizard;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\SimplePage;
|
||||
use Filament\Support\Enums\MaxWidth;
|
||||
use Filament\Support\Exceptions\Halt;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Routing\Redirector;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
/**
|
||||
* @property Form $form
|
||||
*/
|
||||
class PanelInstaller extends SimplePage implements HasForms
|
||||
{
|
||||
use CheckMigrationsTrait;
|
||||
use EnvironmentWriterTrait;
|
||||
use InteractsWithForms;
|
||||
|
||||
public array $data = [];
|
||||
|
||||
protected static string $view = 'filament.pages.installer';
|
||||
|
||||
public function getMaxWidth(): MaxWidth|string
|
||||
{
|
||||
return MaxWidth::SevenExtraLarge;
|
||||
}
|
||||
|
||||
public static function isInstalled(): bool
|
||||
{
|
||||
// This defaults to true so existing panels count as "installed"
|
||||
return env('APP_INSTALLED', true);
|
||||
}
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
abort_if(self::isInstalled(), 404);
|
||||
|
||||
$this->form->fill();
|
||||
}
|
||||
|
||||
protected function getFormSchema(): array
|
||||
{
|
||||
return [
|
||||
Wizard::make([
|
||||
RequirementsStep::make(),
|
||||
EnvironmentStep::make($this),
|
||||
DatabaseStep::make($this),
|
||||
CacheStep::make($this),
|
||||
QueueStep::make($this),
|
||||
SessionStep::make(),
|
||||
])
|
||||
->persistStepInQueryString()
|
||||
->nextAction(fn (Action $action) => $action->keyBindings('enter'))
|
||||
->submitAction(new HtmlString(Blade::render(<<<'BLADE'
|
||||
<x-filament::button
|
||||
type="submit"
|
||||
size="sm"
|
||||
wire:loading.attr="disabled"
|
||||
>
|
||||
Finish
|
||||
<span wire:loading><x-filament::loading-indicator class="h-4 w-4" /></span>
|
||||
</x-filament::button>
|
||||
BLADE))),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getFormStatePath(): ?string
|
||||
{
|
||||
return 'data';
|
||||
}
|
||||
|
||||
public function submit(UserCreationService $userCreationService): Redirector|RedirectResponse
|
||||
{
|
||||
// Disable installer
|
||||
$this->writeToEnvironment(['APP_INSTALLED' => 'true']);
|
||||
|
||||
// Create admin user & login
|
||||
$user = $this->createAdminUser($userCreationService);
|
||||
auth()->guard()->login($user, true);
|
||||
|
||||
// Write session data at the very end to avoid "page expired" errors
|
||||
$this->writeToEnv('env_session');
|
||||
|
||||
// Redirect to admin panel
|
||||
return redirect(Dashboard::getUrl());
|
||||
}
|
||||
|
||||
public function writeToEnv(string $key): void
|
||||
{
|
||||
try {
|
||||
$variables = array_get($this->data, $key);
|
||||
$variables = array_filter($variables); // Filter array to remove NULL values
|
||||
$this->writeToEnvironment($variables);
|
||||
} catch (Exception $exception) {
|
||||
report($exception);
|
||||
|
||||
Notification::make()
|
||||
->title('Could not write to .env file')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->persistent()
|
||||
->send();
|
||||
|
||||
throw new Halt('Error while writing .env file');
|
||||
}
|
||||
|
||||
Artisan::call('config:clear');
|
||||
}
|
||||
|
||||
public function runMigrations(string $driver): void
|
||||
{
|
||||
try {
|
||||
Artisan::call('migrate', [
|
||||
'--force' => true,
|
||||
'--seed' => true,
|
||||
'--database' => $driver,
|
||||
]);
|
||||
} catch (Exception $exception) {
|
||||
report($exception);
|
||||
|
||||
Notification::make()
|
||||
->title('Migrations failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->persistent()
|
||||
->send();
|
||||
|
||||
throw new Halt('Error while running migrations');
|
||||
}
|
||||
|
||||
if (!$this->hasCompletedMigrations()) {
|
||||
Notification::make()
|
||||
->title('Migrations failed')
|
||||
->danger()
|
||||
->persistent()
|
||||
->send();
|
||||
|
||||
throw new Halt('Migrations failed');
|
||||
}
|
||||
}
|
||||
|
||||
public function createAdminUser(UserCreationService $userCreationService): User
|
||||
{
|
||||
try {
|
||||
$userData = array_get($this->data, 'user');
|
||||
$userData['root_admin'] = true;
|
||||
|
||||
return $userCreationService->handle($userData);
|
||||
} catch (Exception $exception) {
|
||||
report($exception);
|
||||
|
||||
Notification::make()
|
||||
->title('Could not create admin user')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->persistent()
|
||||
->send();
|
||||
|
||||
throw new Halt('Error while creating admin user');
|
||||
}
|
||||
}
|
||||
}
|
||||
123
app/Filament/Pages/Installer/Steps/CacheStep.php
Normal file
123
app/Filament/Pages/Installer/Steps/CacheStep.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use App\Filament\Pages\Installer\PanelInstaller;
|
||||
use Exception;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Exceptions\Halt;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Redis\RedisManager;
|
||||
|
||||
class CacheStep
|
||||
{
|
||||
public const CACHE_DRIVERS = [
|
||||
'file' => 'Filesystem',
|
||||
'redis' => 'Redis',
|
||||
];
|
||||
|
||||
public static function make(PanelInstaller $installer): Step
|
||||
{
|
||||
return Step::make('cache')
|
||||
->label('Cache')
|
||||
->columns()
|
||||
->schema([
|
||||
ToggleButtons::make('env_cache.CACHE_STORE')
|
||||
->label('Cache Driver')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The driver used for caching. We recommend "Filesystem".')
|
||||
->required()
|
||||
->inline()
|
||||
->options(self::CACHE_DRIVERS)
|
||||
->default(config('cache.default'))
|
||||
->columnSpanFull()
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, Set $set, Get $get) {
|
||||
if ($state !== 'redis') {
|
||||
$set('env_cache.REDIS_HOST', null);
|
||||
$set('env_cache.REDIS_PORT', null);
|
||||
$set('env_cache.REDIS_USERNAME', null);
|
||||
$set('env_cache.REDIS_PASSWORD', null);
|
||||
} else {
|
||||
$set('env_cache.REDIS_HOST', $get('env_cache.REDIS_HOST') ?? '127.0.0.1');
|
||||
$set('env_cache.REDIS_PORT', $get('env_cache.REDIS_PORT') ?? '6379');
|
||||
$set('env_cache.REDIS_USERNAME', null);
|
||||
}
|
||||
}),
|
||||
TextInput::make('env_cache.REDIS_HOST')
|
||||
->label('Redis Host')
|
||||
->placeholder('127.0.0.1')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The host of your redis server. Make sure it is reachable.')
|
||||
->required(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis')
|
||||
->default(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis' ? config('database.redis.default.host') : null)
|
||||
->visible(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis'),
|
||||
TextInput::make('env_cache.REDIS_PORT')
|
||||
->label('Redis Port')
|
||||
->placeholder('6379')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The port of your redis server.')
|
||||
->required(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis')
|
||||
->default(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis' ? config('database.redis.default.port') : null)
|
||||
->visible(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis'),
|
||||
TextInput::make('env_cache.REDIS_USERNAME')
|
||||
->label('Redis Username')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The name of your redis user. Can be empty')
|
||||
->default(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis' ? config('database.redis.default.username') : null)
|
||||
->visible(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis'),
|
||||
TextInput::make('env_cache.REDIS_PASSWORD')
|
||||
->label('Redis Password')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The password for your redis user. Can be empty.')
|
||||
->password()
|
||||
->revealable()
|
||||
->default(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis' ? config('database.redis.default.password') : null)
|
||||
->visible(fn (Get $get) => $get('env_cache.CACHE_STORE') === 'redis'),
|
||||
])
|
||||
->afterValidation(function (Get $get, Application $app) use ($installer) {
|
||||
$driver = $get('env_cache.CACHE_STORE');
|
||||
|
||||
if (!self::testConnection($app, $driver, $get('env_cache.REDIS_HOST'), $get('env_cache.REDIS_PORT'), $get('env_cache.REDIS_USERNAME'), $get('env_cache.REDIS_PASSWORD'))) {
|
||||
throw new Halt('Redis connection failed');
|
||||
}
|
||||
|
||||
$installer->writeToEnv('env_cache');
|
||||
});
|
||||
}
|
||||
|
||||
private static function testConnection(Application $app, string $driver, ?string $host, null|string|int $port, ?string $username, ?string $password): bool
|
||||
{
|
||||
if ($driver !== 'redis') {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
$redis = new RedisManager($app, 'predis', [
|
||||
'default' => [
|
||||
'host' => $host,
|
||||
'port' => $port,
|
||||
'username' => $username,
|
||||
'password' => $password,
|
||||
],
|
||||
]);
|
||||
|
||||
$redis->connection()->command('ping');
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Redis connection failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
139
app/Filament/Pages/Installer/Steps/DatabaseStep.php
Normal file
139
app/Filament/Pages/Installer/Steps/DatabaseStep.php
Normal file
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use App\Filament\Pages\Installer\PanelInstaller;
|
||||
use Exception;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Exceptions\Halt;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DatabaseStep
|
||||
{
|
||||
public const DATABASE_DRIVERS = [
|
||||
'sqlite' => 'SQLite',
|
||||
'mariadb' => 'MariaDB',
|
||||
'mysql' => 'MySQL',
|
||||
];
|
||||
|
||||
public static function make(PanelInstaller $installer): Step
|
||||
{
|
||||
return Step::make('database')
|
||||
->label('Database')
|
||||
->columns()
|
||||
->schema([
|
||||
ToggleButtons::make('env_database.DB_CONNECTION')
|
||||
->label('Database Driver')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The driver used for the panel database. We recommend "SQLite".')
|
||||
->required()
|
||||
->inline()
|
||||
->options(self::DATABASE_DRIVERS)
|
||||
->default(config('database.default'))
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, Set $set, Get $get) {
|
||||
$set('env_database.DB_DATABASE', $state === 'sqlite' ? 'database.sqlite' : 'panel');
|
||||
|
||||
if ($state === 'sqlite') {
|
||||
$set('env_database.DB_HOST', null);
|
||||
$set('env_database.DB_PORT', null);
|
||||
$set('env_database.DB_USERNAME', null);
|
||||
$set('env_database.DB_PASSWORD', null);
|
||||
} else {
|
||||
$set('env_database.DB_HOST', $get('env_database.DB_HOST') ?? '127.0.0.1');
|
||||
$set('env_database.DB_PORT', $get('env_database.DB_PORT') ?? '3306');
|
||||
$set('env_database.DB_USERNAME', $get('env_database.DB_USERNAME') ?? 'pelican');
|
||||
}
|
||||
}),
|
||||
TextInput::make('env_database.DB_DATABASE')
|
||||
->label(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite' ? 'Database Path' : 'Database Name')
|
||||
->placeholder(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite' ? 'database.sqlite' : 'panel')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite' ? 'The path of your .sqlite file relative to the database folder.' : 'The name of the panel database.')
|
||||
->required()
|
||||
->default('database.sqlite'),
|
||||
TextInput::make('env_database.DB_HOST')
|
||||
->label('Database Host')
|
||||
->placeholder('127.0.0.1')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The host of your database. Make sure it is reachable.')
|
||||
->required(fn (Get $get) => $get('env_database.DB_CONNECTION') !== 'sqlite')
|
||||
->hidden(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite'),
|
||||
TextInput::make('env_database.DB_PORT')
|
||||
->label('Database Port')
|
||||
->placeholder('3306')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The port of your database.')
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->required(fn (Get $get) => $get('env_database.DB_CONNECTION') !== 'sqlite')
|
||||
->hidden(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite'),
|
||||
TextInput::make('env_database.DB_USERNAME')
|
||||
->label('Database Username')
|
||||
->placeholder('pelican')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The name of your database user.')
|
||||
->required(fn (Get $get) => $get('env_database.DB_CONNECTION') !== 'sqlite')
|
||||
->hidden(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite'),
|
||||
TextInput::make('env_database.DB_PASSWORD')
|
||||
->label('Database Password')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The password of your database user. Can be empty.')
|
||||
->password()
|
||||
->revealable()
|
||||
->hidden(fn (Get $get) => $get('env_database.DB_CONNECTION') === 'sqlite'),
|
||||
])
|
||||
->afterValidation(function (Get $get) use ($installer) {
|
||||
$driver = $get('env_database.DB_CONNECTION');
|
||||
|
||||
if (!self::testConnection($driver, $get('env_database.DB_HOST'), $get('env_database.DB_PORT'), $get('env_database.DB_DATABASE'), $get('env_database.DB_USERNAME'), $get('env_database.DB_PASSWORD'))) {
|
||||
throw new Halt('Database connection failed');
|
||||
}
|
||||
|
||||
$installer->writeToEnv('env_database');
|
||||
|
||||
$installer->runMigrations($driver);
|
||||
});
|
||||
}
|
||||
|
||||
private static function testConnection(string $driver, ?string $host, null|string|int $port, ?string $database, ?string $username, ?string $password): bool
|
||||
{
|
||||
if ($driver === 'sqlite') {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
config()->set('database.connections._panel_install_test', [
|
||||
'driver' => $driver,
|
||||
'host' => $host,
|
||||
'port' => $port,
|
||||
'database' => $database,
|
||||
'username' => $username,
|
||||
'password' => $password,
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'strict' => true,
|
||||
]);
|
||||
|
||||
DB::connection('_panel_install_test')->getPdo();
|
||||
} catch (Exception $exception) {
|
||||
DB::disconnect('_panel_install_test');
|
||||
|
||||
Notification::make()
|
||||
->title('Database connection failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
52
app/Filament/Pages/Installer/Steps/EnvironmentStep.php
Normal file
52
app/Filament/Pages/Installer/Steps/EnvironmentStep.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use App\Filament\Pages\Installer\PanelInstaller;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
|
||||
class EnvironmentStep
|
||||
{
|
||||
public static function make(PanelInstaller $installer): Step
|
||||
{
|
||||
return Step::make('environment')
|
||||
->label('Environment')
|
||||
->columns()
|
||||
->schema([
|
||||
TextInput::make('env_general.APP_NAME')
|
||||
->label('App Name')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('This will be the Name of your Panel.')
|
||||
->required()
|
||||
->default(config('app.name')),
|
||||
TextInput::make('env_general.APP_URL')
|
||||
->label('App URL')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('This will be the URL you access your Panel from.')
|
||||
->required()
|
||||
->default(url('')),
|
||||
Fieldset::make('adminuser')
|
||||
->label('Admin User')
|
||||
->columns(3)
|
||||
->schema([
|
||||
TextInput::make('user.email')
|
||||
->label('E-Mail')
|
||||
->required()
|
||||
->email()
|
||||
->placeholder('admin@example.com'),
|
||||
TextInput::make('user.username')
|
||||
->label('Username')
|
||||
->required()
|
||||
->placeholder('admin'),
|
||||
TextInput::make('user.password')
|
||||
->label('Password')
|
||||
->required()
|
||||
->password()
|
||||
->revealable(),
|
||||
]),
|
||||
])
|
||||
->afterValidation(fn () => $installer->writeToEnv('env_general'));
|
||||
}
|
||||
}
|
||||
64
app/Filament/Pages/Installer/Steps/QueueStep.php
Normal file
64
app/Filament/Pages/Installer/Steps/QueueStep.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use App\Filament\Pages\Installer\PanelInstaller;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Filament\Forms\Get;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
||||
|
||||
class QueueStep
|
||||
{
|
||||
public const QUEUE_DRIVERS = [
|
||||
'database' => 'Database',
|
||||
'redis' => 'Redis',
|
||||
'sync' => 'Sync',
|
||||
];
|
||||
|
||||
public static function make(PanelInstaller $installer): Step
|
||||
{
|
||||
return Step::make('queue')
|
||||
->label('Queue')
|
||||
->columns()
|
||||
->schema([
|
||||
ToggleButtons::make('env_queue.QUEUE_CONNECTION')
|
||||
->label('Queue Driver')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The driver used for handling queues. We recommend "Database".')
|
||||
->required()
|
||||
->inline()
|
||||
->options(self::QUEUE_DRIVERS)
|
||||
->disableOptionWhen(fn ($value, Get $get) => $value === 'redis' && $get('env_cache.CACHE_STORE') !== 'redis')
|
||||
->default(config('queue.default')),
|
||||
Toggle::make('done')
|
||||
->label('I have done both steps below.')
|
||||
->accepted(fn () => !file_exists('/.dockerenv'))
|
||||
->inline(false)
|
||||
->validationMessages([
|
||||
'accepted' => 'You need to do both steps before continuing!',
|
||||
])
|
||||
->hidden(fn () => file_exists('/.dockerenv')),
|
||||
TextInput::make('crontab')
|
||||
->label(new HtmlString('Run the following command to set up your crontab. Note that <code>www-data</code> is your webserver user. On some systems this username might be different!'))
|
||||
->disabled()
|
||||
->hintAction(CopyAction::make())
|
||||
->default('(crontab -l -u www-data 2>/dev/null; echo "* * * * * php ' . base_path() . '/artisan schedule:run >> /dev/null 2>&1") | crontab -u www-data -')
|
||||
->hidden(fn () => file_exists('/.dockerenv'))
|
||||
->columnSpanFull(),
|
||||
TextInput::make('queueService')
|
||||
->label(new HtmlString('To setup the queue worker service you simply have to run the following command.'))
|
||||
->disabled()
|
||||
->hintAction(CopyAction::make())
|
||||
->default('sudo php ' . base_path() . '/artisan p:environment:queue-service')
|
||||
->hidden(fn () => file_exists('/.dockerenv'))
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->afterValidation(function () use ($installer) {
|
||||
$installer->writeToEnv('env_queue');
|
||||
});
|
||||
}
|
||||
}
|
||||
89
app/Filament/Pages/Installer/Steps/RequirementsStep.php
Normal file
89
app/Filament/Pages/Installer/Steps/RequirementsStep.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Exceptions\Halt;
|
||||
|
||||
class RequirementsStep
|
||||
{
|
||||
public const MIN_PHP_VERSION = '8.2';
|
||||
|
||||
public static function make(): Step
|
||||
{
|
||||
$correctPhpVersion = version_compare(PHP_VERSION, self::MIN_PHP_VERSION) >= 0;
|
||||
|
||||
$fields = [
|
||||
Section::make('PHP Version')
|
||||
->description(self::MIN_PHP_VERSION . ' or newer')
|
||||
->icon($correctPhpVersion ? 'tabler-check' : 'tabler-x')
|
||||
->iconColor($correctPhpVersion ? 'success' : 'danger')
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->content('Your PHP Version is ' . PHP_VERSION . '.'),
|
||||
]),
|
||||
];
|
||||
|
||||
$phpExtensions = [
|
||||
'BCMath' => extension_loaded('bcmath'),
|
||||
'cURL' => extension_loaded('curl'),
|
||||
'GD' => extension_loaded('gd'),
|
||||
'intl' => extension_loaded('intl'),
|
||||
'mbstring' => extension_loaded('mbstring'),
|
||||
'MySQL' => extension_loaded('pdo_mysql'),
|
||||
'SQLite3' => extension_loaded('pdo_sqlite'),
|
||||
'XML' => extension_loaded('xml'),
|
||||
'Zip' => extension_loaded('zip'),
|
||||
];
|
||||
$allExtensionsInstalled = !in_array(false, $phpExtensions);
|
||||
|
||||
$fields[] = Section::make('PHP Extensions')
|
||||
->description(implode(', ', array_keys($phpExtensions)))
|
||||
->icon($allExtensionsInstalled ? 'tabler-check' : 'tabler-x')
|
||||
->iconColor($allExtensionsInstalled ? 'success' : 'danger')
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->content('All needed PHP Extensions are installed.')
|
||||
->visible($allExtensionsInstalled),
|
||||
Placeholder::make('')
|
||||
->content('The following PHP Extensions are missing: ' . implode(', ', array_keys($phpExtensions, false)))
|
||||
->visible(!$allExtensionsInstalled),
|
||||
]);
|
||||
|
||||
$folderPermissions = [
|
||||
'Storage' => substr(sprintf('%o', fileperms(base_path('storage/'))), -4) >= 755,
|
||||
'Cache' => substr(sprintf('%o', fileperms(base_path('bootstrap/cache/'))), -4) >= 755,
|
||||
];
|
||||
$correctFolderPermissions = !in_array(false, $folderPermissions);
|
||||
|
||||
$fields[] = Section::make('Folder Permissions')
|
||||
->description(implode(', ', array_keys($folderPermissions)))
|
||||
->icon($correctFolderPermissions ? 'tabler-check' : 'tabler-x')
|
||||
->iconColor($correctFolderPermissions ? 'success' : 'danger')
|
||||
->schema([
|
||||
Placeholder::make('')
|
||||
->content('All Folders have the correct permissions.')
|
||||
->visible($correctFolderPermissions),
|
||||
Placeholder::make('')
|
||||
->content('The following Folders have wrong permissions: ' . implode(', ', array_keys($folderPermissions, false)))
|
||||
->visible(!$correctFolderPermissions),
|
||||
]);
|
||||
|
||||
return Step::make('requirements')
|
||||
->label('Server Requirements')
|
||||
->schema($fields)
|
||||
->afterValidation(function () use ($correctPhpVersion, $allExtensionsInstalled, $correctFolderPermissions) {
|
||||
if (!$correctPhpVersion || !$allExtensionsInstalled || !$correctFolderPermissions) {
|
||||
Notification::make()
|
||||
->title('Some requirements are missing!')
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
throw new Halt('Some requirements are missing');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
38
app/Filament/Pages/Installer/Steps/SessionStep.php
Normal file
38
app/Filament/Pages/Installer/Steps/SessionStep.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Installer\Steps;
|
||||
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Components\Wizard\Step;
|
||||
use Filament\Forms\Get;
|
||||
|
||||
class SessionStep
|
||||
{
|
||||
public const SESSION_DRIVERS = [
|
||||
'file' => 'Filesystem',
|
||||
'database' => 'Database',
|
||||
'cookie' => 'Cookie',
|
||||
'redis' => 'Redis',
|
||||
];
|
||||
|
||||
public static function make(): Step
|
||||
{
|
||||
return Step::make('session')
|
||||
->label('Session')
|
||||
->schema([
|
||||
ToggleButtons::make('env_session.SESSION_DRIVER')
|
||||
->label('Session Driver')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('The driver used for storing sessions. We recommend "Filesystem" or "Database".')
|
||||
->required()
|
||||
->inline()
|
||||
->options(self::SESSION_DRIVERS)
|
||||
->disableOptionWhen(fn ($value, Get $get) => $value === 'redis' && $get('env_cache.CACHE_STORE') !== 'redis')
|
||||
->default(config('session.driver')),
|
||||
TextInput::make('env_session.SESSION_SECURE_COOKIE')
|
||||
->hidden()
|
||||
->default(request()->isSecure()),
|
||||
]);
|
||||
}
|
||||
}
|
||||
631
app/Filament/Pages/Settings.php
Normal file
631
app/Filament/Pages/Settings.php
Normal file
@@ -0,0 +1,631 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use App\Models\Backup;
|
||||
use App\Notifications\MailTested;
|
||||
use App\Traits\EnvironmentWriterTrait;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\Actions\Action as FormAction;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Forms\Components\Tabs\Tab;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Concerns\HasUnsavedDataChangesAlert;
|
||||
use Filament\Pages\Concerns\InteractsWithHeaderActions;
|
||||
use Filament\Pages\Page;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Notification as MailNotification;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
/**
|
||||
* @property Form $form
|
||||
*/
|
||||
class Settings extends Page implements HasForms
|
||||
{
|
||||
use EnvironmentWriterTrait;
|
||||
use HasUnsavedDataChangesAlert;
|
||||
use InteractsWithForms;
|
||||
use InteractsWithHeaderActions;
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-settings';
|
||||
|
||||
protected static ?string $navigationGroup = 'Advanced';
|
||||
|
||||
protected static string $view = 'filament.pages.settings';
|
||||
|
||||
public ?array $data = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->form->fill();
|
||||
}
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
return auth()->user()->can('view settings');
|
||||
}
|
||||
|
||||
protected function getFormSchema(): array
|
||||
{
|
||||
return [
|
||||
Tabs::make('Tabs')
|
||||
->columns()
|
||||
->persistTabInQueryString()
|
||||
->disabled(fn () => !auth()->user()->can('update settings'))
|
||||
->tabs([
|
||||
Tab::make('general')
|
||||
->label('General')
|
||||
->icon('tabler-home')
|
||||
->schema($this->generalSettings()),
|
||||
Tab::make('captcha')
|
||||
->label('Captcha')
|
||||
->icon('tabler-shield')
|
||||
->schema($this->captchaSettings())
|
||||
->columns(3),
|
||||
Tab::make('mail')
|
||||
->label('Mail')
|
||||
->icon('tabler-mail')
|
||||
->schema($this->mailSettings()),
|
||||
Tab::make('backup')
|
||||
->label('Backup')
|
||||
->icon('tabler-box')
|
||||
->schema($this->backupSettings()),
|
||||
Tab::make('misc')
|
||||
->label('Misc')
|
||||
->icon('tabler-tool')
|
||||
->schema($this->miscSettings()),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
private function generalSettings(): array
|
||||
{
|
||||
return [
|
||||
TextInput::make('APP_NAME')
|
||||
->label('App Name')
|
||||
->required()
|
||||
->default(env('APP_NAME', 'Pelican')),
|
||||
TextInput::make('APP_FAVICON')
|
||||
->label('App Favicon')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('Favicons should be placed in the public folder, located in the root panel directory.')
|
||||
->required()
|
||||
->default(env('APP_FAVICON', '/pelican.ico')),
|
||||
Toggle::make('APP_DEBUG')
|
||||
->label('Enable Debug Mode?')
|
||||
->inline(false)
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('APP_DEBUG', (bool) $state))
|
||||
->default(env('APP_DEBUG', config('app.debug'))),
|
||||
ToggleButtons::make('FILAMENT_TOP_NAVIGATION')
|
||||
->label('Navigation')
|
||||
->inline()
|
||||
->options([
|
||||
false => 'Sidebar',
|
||||
true => 'Topbar',
|
||||
])
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('FILAMENT_TOP_NAVIGATION', (bool) $state))
|
||||
->default(env('FILAMENT_TOP_NAVIGATION', config('panel.filament.top-navigation'))),
|
||||
ToggleButtons::make('PANEL_USE_BINARY_PREFIX')
|
||||
->label('Unit prefix')
|
||||
->inline()
|
||||
->options([
|
||||
false => 'Decimal Prefix (MB/ GB)',
|
||||
true => 'Binary Prefix (MiB/ GiB)',
|
||||
])
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_USE_BINARY_PREFIX', (bool) $state))
|
||||
->default(env('PANEL_USE_BINARY_PREFIX', config('panel.use_binary_prefix'))),
|
||||
ToggleButtons::make('APP_2FA_REQUIRED')
|
||||
->label('2FA Requirement')
|
||||
->inline()
|
||||
->options([
|
||||
0 => 'Not required',
|
||||
1 => 'Required for only Admins',
|
||||
2 => 'Required for all Users',
|
||||
])
|
||||
->formatStateUsing(fn ($state): int => (int) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('APP_2FA_REQUIRED', (int) $state))
|
||||
->default(env('APP_2FA_REQUIRED', config('panel.auth.2fa_required'))),
|
||||
TagsInput::make('TRUSTED_PROXIES')
|
||||
->label('Trusted Proxies')
|
||||
->separator()
|
||||
->splitKeys(['Tab', ' '])
|
||||
->placeholder('New IP or IP Range')
|
||||
->default(env('TRUSTED_PROXIES', implode(',', config('trustedproxy.proxies'))))
|
||||
->hintActions([
|
||||
FormAction::make('clear')
|
||||
->label('Clear')
|
||||
->color('danger')
|
||||
->icon('tabler-trash')
|
||||
->requiresConfirmation()
|
||||
->authorize(fn () => auth()->user()->can('update settings'))
|
||||
->action(fn (Set $set) => $set('TRUSTED_PROXIES', [])),
|
||||
FormAction::make('cloudflare')
|
||||
->label('Set to Cloudflare IPs')
|
||||
->icon('tabler-brand-cloudflare')
|
||||
->authorize(fn () => auth()->user()->can('update settings'))
|
||||
->action(function (Client $client, Set $set) {
|
||||
$ips = collect();
|
||||
try {
|
||||
$response = $client->request(
|
||||
'GET',
|
||||
'https://api.cloudflare.com/client/v4/ips',
|
||||
config('panel.guzzle')
|
||||
);
|
||||
if ($response->getStatusCode() === 200) {
|
||||
$result = json_decode($response->getBody(), true)['result'];
|
||||
foreach (['ipv4_cidrs', 'ipv6_cidrs'] as $value) {
|
||||
$ips->push(...data_get($result, $value));
|
||||
}
|
||||
$ips->unique();
|
||||
}
|
||||
} catch (GuzzleException $e) {
|
||||
}
|
||||
|
||||
$set('TRUSTED_PROXIES', $ips->values()->all());
|
||||
}),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
private function captchaSettings(): array
|
||||
{
|
||||
return [
|
||||
Toggle::make('TURNSTILE_ENABLED')
|
||||
->label('Enable Turnstile Captcha?')
|
||||
->inline(false)
|
||||
->columnSpan(1)
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->live()
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('TURNSTILE_ENABLED', (bool) $state))
|
||||
->default(env('TURNSTILE_ENABLED', config('turnstile.turnstile_enabled'))),
|
||||
Placeholder::make('info')
|
||||
->columnSpan(2)
|
||||
->content(new HtmlString('<p>You can generate the keys on your <u><a href="https://developers.cloudflare.com/turnstile/get-started/#get-a-sitekey-and-secret-key" target="_blank">Cloudflare Dashboard</a></u>. A Cloudflare account is required.</p>')),
|
||||
TextInput::make('TURNSTILE_SITE_KEY')
|
||||
->label('Site Key')
|
||||
->required()
|
||||
->visible(fn (Get $get) => $get('TURNSTILE_ENABLED'))
|
||||
->default(env('TURNSTILE_SITE_KEY', config('turnstile.turnstile_site_key')))
|
||||
->placeholder('1x00000000000000000000AA'),
|
||||
TextInput::make('TURNSTILE_SECRET_KEY')
|
||||
->label('Secret Key')
|
||||
->required()
|
||||
->visible(fn (Get $get) => $get('TURNSTILE_ENABLED'))
|
||||
->default(env('TURNSTILE_SECRET_KEY', config('turnstile.secret_key')))
|
||||
->placeholder('1x0000000000000000000000000000000AA'),
|
||||
Toggle::make('TURNSTILE_VERIFY_DOMAIN')
|
||||
->label('Verify domain?')
|
||||
->inline(false)
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->visible(fn (Get $get) => $get('TURNSTILE_ENABLED'))
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('TURNSTILE_VERIFY_DOMAIN', (bool) $state))
|
||||
->default(env('TURNSTILE_VERIFY_DOMAIN', config('turnstile.turnstile_verify_domain'))),
|
||||
];
|
||||
}
|
||||
|
||||
private function mailSettings(): array
|
||||
{
|
||||
return [
|
||||
ToggleButtons::make('MAIL_MAILER')
|
||||
->label('Mail Driver')
|
||||
->columnSpanFull()
|
||||
->inline()
|
||||
->options([
|
||||
'log' => 'Print mails to Log',
|
||||
'smtp' => 'SMTP Server',
|
||||
'sendmail' => 'sendmail Binary',
|
||||
'mailgun' => 'Mailgun',
|
||||
'mandrill' => 'Mandrill',
|
||||
'postmark' => 'Postmark',
|
||||
])
|
||||
->live()
|
||||
->default(env('MAIL_MAILER', config('mail.default')))
|
||||
->hintAction(
|
||||
FormAction::make('test')
|
||||
->label('Send Test Mail')
|
||||
->icon('tabler-send')
|
||||
->hidden(fn (Get $get) => $get('MAIL_MAILER') === 'log')
|
||||
->authorize(fn () => auth()->user()->can('update settings'))
|
||||
->action(function () {
|
||||
try {
|
||||
MailNotification::route('mail', auth()->user()->email)
|
||||
->notify(new MailTested(auth()->user()));
|
||||
|
||||
Notification::make()
|
||||
->title('Test Mail sent')
|
||||
->success()
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Test Mail failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
})
|
||||
),
|
||||
Section::make('"From" Settings')
|
||||
->description('Set the Address and Name used as "From" in mails.')
|
||||
->columns()
|
||||
->schema([
|
||||
TextInput::make('MAIL_FROM_ADDRESS')
|
||||
->label('From Address')
|
||||
->required()
|
||||
->email()
|
||||
->default(env('MAIL_FROM_ADDRESS', config('mail.from.address'))),
|
||||
TextInput::make('MAIL_FROM_NAME')
|
||||
->label('From Name')
|
||||
->required()
|
||||
->default(env('MAIL_FROM_NAME', config('mail.from.name'))),
|
||||
]),
|
||||
Section::make('SMTP Configuration')
|
||||
->columns()
|
||||
->visible(fn (Get $get) => $get('MAIL_MAILER') === 'smtp')
|
||||
->schema([
|
||||
TextInput::make('MAIL_HOST')
|
||||
->label('Host')
|
||||
->required()
|
||||
->default(env('MAIL_HOST', config('mail.mailers.smtp.host'))),
|
||||
TextInput::make('MAIL_PORT')
|
||||
->label('Port')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(env('MAIL_PORT', config('mail.mailers.smtp.port'))),
|
||||
TextInput::make('MAIL_USERNAME')
|
||||
->label('Username')
|
||||
->default(env('MAIL_USERNAME', config('mail.mailers.smtp.username'))),
|
||||
TextInput::make('MAIL_PASSWORD')
|
||||
->label('Password')
|
||||
->password()
|
||||
->revealable()
|
||||
->default(env('MAIL_PASSWORD')),
|
||||
ToggleButtons::make('MAIL_ENCRYPTION')
|
||||
->label('Encryption')
|
||||
->inline()
|
||||
->options(['tls' => 'TLS', 'ssl' => 'SSL', '' => 'None'])
|
||||
->default(env('MAIL_ENCRYPTION', config('mail.mailers.smtp.encryption', 'tls'))),
|
||||
]),
|
||||
Section::make('Mailgun Configuration')
|
||||
->columns()
|
||||
->visible(fn (Get $get) => $get('MAIL_MAILER') === 'mailgun')
|
||||
->schema([
|
||||
TextInput::make('MAILGUN_DOMAIN')
|
||||
->label('Domain')
|
||||
->required()
|
||||
->default(env('MAILGUN_DOMAIN', config('services.mailgun.domain'))),
|
||||
TextInput::make('MAILGUN_SECRET')
|
||||
->label('Secret')
|
||||
->required()
|
||||
->default(env('MAILGUN_SECRET', config('services.mailgun.secret'))),
|
||||
TextInput::make('MAILGUN_ENDPOINT')
|
||||
->label('Endpoint')
|
||||
->required()
|
||||
->default(env('MAILGUN_ENDPOINT', config('services.mailgun.endpoint'))),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
private function backupSettings(): array
|
||||
{
|
||||
return [
|
||||
ToggleButtons::make('APP_BACKUP_DRIVER')
|
||||
->label('Backup Driver')
|
||||
->columnSpanFull()
|
||||
->inline()
|
||||
->options([
|
||||
Backup::ADAPTER_DAEMON => 'Wings',
|
||||
Backup::ADAPTER_AWS_S3 => 'S3',
|
||||
])
|
||||
->live()
|
||||
->default(env('APP_BACKUP_DRIVER', config('backups.default'))),
|
||||
Section::make('Throttles')
|
||||
->description('Configure how many backups can be created in a period. Set period to 0 to disable this throttle.')
|
||||
->columns()
|
||||
->schema([
|
||||
TextInput::make('BACKUP_THROTTLE_LIMIT')
|
||||
->label('Limit')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->default(config('backups.throttles.limit')),
|
||||
TextInput::make('BACKUP_THROTTLE_PERIOD')
|
||||
->label('Period')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->suffix('Seconds')
|
||||
->default(config('backups.throttles.period')),
|
||||
]),
|
||||
Section::make('S3 Configuration')
|
||||
->columns()
|
||||
->visible(fn (Get $get) => $get('APP_BACKUP_DRIVER') === Backup::ADAPTER_AWS_S3)
|
||||
->schema([
|
||||
TextInput::make('AWS_DEFAULT_REGION')
|
||||
->label('Default Region')
|
||||
->required()
|
||||
->default(config('backups.disks.s3.region')),
|
||||
TextInput::make('AWS_ACCESS_KEY_ID')
|
||||
->label('Access Key ID')
|
||||
->required()
|
||||
->default(config('backups.disks.s3.key')),
|
||||
TextInput::make('AWS_SECRET_ACCESS_KEY')
|
||||
->label('Secret Access Key')
|
||||
->required()
|
||||
->default(config('backups.disks.s3.secret')),
|
||||
TextInput::make('AWS_BACKUPS_BUCKET')
|
||||
->label('Bucket')
|
||||
->required()
|
||||
->default(config('backups.disks.s3.bucket')),
|
||||
TextInput::make('AWS_ENDPOINT')
|
||||
->label('Endpoint')
|
||||
->required()
|
||||
->default(config('backups.disks.s3.endpoint')),
|
||||
Toggle::make('AWS_USE_PATH_STYLE_ENDPOINT')
|
||||
->label('Use path style endpoint?')
|
||||
->inline(false)
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->live()
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('AWS_USE_PATH_STYLE_ENDPOINT', (bool) $state))
|
||||
->default(env('AWS_USE_PATH_STYLE_ENDPOINT', config('backups.disks.s3.use_path_style_endpoint'))),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
private function miscSettings(): array
|
||||
{
|
||||
return [
|
||||
Section::make('Automatic Allocation Creation')
|
||||
->description('Toggle if Users can create allocations via the client area.')
|
||||
->columns()
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
Toggle::make('PANEL_CLIENT_ALLOCATIONS_ENABLED')
|
||||
->label('Allow Users to create allocations?')
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_CLIENT_ALLOCATIONS_ENABLED', (bool) $state))
|
||||
->default(env('PANEL_CLIENT_ALLOCATIONS_ENABLED', config('panel.client_features.allocations.enabled'))),
|
||||
TextInput::make('PANEL_CLIENT_ALLOCATIONS_RANGE_START')
|
||||
->label('Starting Port')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1024)
|
||||
->maxValue(65535)
|
||||
->visible(fn (Get $get) => $get('PANEL_CLIENT_ALLOCATIONS_ENABLED'))
|
||||
->default(env('PANEL_CLIENT_ALLOCATIONS_RANGE_START')),
|
||||
TextInput::make('PANEL_CLIENT_ALLOCATIONS_RANGE_END')
|
||||
->label('Ending Port')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1024)
|
||||
->maxValue(65535)
|
||||
->visible(fn (Get $get) => $get('PANEL_CLIENT_ALLOCATIONS_ENABLED'))
|
||||
->default(env('PANEL_CLIENT_ALLOCATIONS_RANGE_END')),
|
||||
]),
|
||||
Section::make('Mail Notifications')
|
||||
->description('Toggle which mail notifications should be sent to Users.')
|
||||
->columns()
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
Toggle::make('PANEL_SEND_INSTALL_NOTIFICATION')
|
||||
->label('Server Installed')
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_INSTALL_NOTIFICATION', (bool) $state))
|
||||
->default(env('PANEL_SEND_INSTALL_NOTIFICATION', config('panel.email.send_install_notification'))),
|
||||
Toggle::make('PANEL_SEND_REINSTALL_NOTIFICATION')
|
||||
->label('Server Reinstalled')
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_REINSTALL_NOTIFICATION', (bool) $state))
|
||||
->default(env('PANEL_SEND_REINSTALL_NOTIFICATION', config('panel.email.send_reinstall_notification'))),
|
||||
]),
|
||||
Section::make('Connections')
|
||||
->description('Timeouts used when making requests.')
|
||||
->columns()
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
TextInput::make('GUZZLE_TIMEOUT')
|
||||
->label('Request Timeout')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(15)
|
||||
->maxValue(60)
|
||||
->suffix('Seconds')
|
||||
->default(env('GUZZLE_TIMEOUT', config('panel.guzzle.timeout'))),
|
||||
TextInput::make('GUZZLE_CONNECT_TIMEOUT')
|
||||
->label('Connect Timeout')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(5)
|
||||
->maxValue(60)
|
||||
->suffix('Seconds')
|
||||
->default(env('GUZZLE_CONNECT_TIMEOUT', config('panel.guzzle.connect_timeout'))),
|
||||
]),
|
||||
Section::make('Activity Logs')
|
||||
->description('Configure how often old activity logs should be pruned and whether admin activities should be logged.')
|
||||
->columns()
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
TextInput::make('APP_ACTIVITY_PRUNE_DAYS')
|
||||
->label('Prune age')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(365)
|
||||
->suffix('Days')
|
||||
->default(env('APP_ACTIVITY_PRUNE_DAYS', config('activity.prune_days'))),
|
||||
Toggle::make('APP_ACTIVITY_HIDE_ADMIN')
|
||||
->label('Hide admin activities?')
|
||||
->inline(false)
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->live()
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('APP_ACTIVITY_HIDE_ADMIN', (bool) $state))
|
||||
->default(env('APP_ACTIVITY_HIDE_ADMIN', config('activity.hide_admin_activity'))),
|
||||
]),
|
||||
Section::make('API')
|
||||
->description('Defines the rate limit for the number of requests per minute that can be executed.')
|
||||
->columns()
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
TextInput::make('APP_API_CLIENT_RATELIMIT')
|
||||
->label('Client API Rate Limit')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->suffix('Requests Per Minute')
|
||||
->default(env('APP_API_CLIENT_RATELIMIT', config('http.rate_limit.client'))),
|
||||
TextInput::make('APP_API_APPLICATION_RATELIMIT')
|
||||
->label('Application API Rate Limit')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->suffix('Requests Per Minute')
|
||||
->default(env('APP_API_APPLICATION_RATELIMIT', config('http.rate_limit.application'))),
|
||||
]),
|
||||
Section::make('Server')
|
||||
->description('Settings for Servers.')
|
||||
->columns()
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
Toggle::make('PANEL_EDITABLE_SERVER_DESCRIPTIONS')
|
||||
->label('Allow Users to edit Server Descriptions?')
|
||||
->onIcon('tabler-check')
|
||||
->offIcon('tabler-x')
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_EDITABLE_SERVER_DESCRIPTIONS', (bool) $state))
|
||||
->default(env('PANEL_EDITABLE_SERVER_DESCRIPTIONS', config('panel.editable_server_descriptions'))),
|
||||
]),
|
||||
Section::make('Webhook')
|
||||
->description('Configure how often old webhook logs should be pruned.')
|
||||
->columns()
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
TextInput::make('APP_WEBHOOK_PRUNE_DAYS')
|
||||
->label('Prune age')
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->maxValue(365)
|
||||
->suffix('Days')
|
||||
->default(env('APP_WEBHOOK_PRUNE_DAYS', config('panel.webhook.prune_days'))),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getFormStatePath(): ?string
|
||||
{
|
||||
return 'data';
|
||||
}
|
||||
|
||||
protected function hasUnsavedDataChangesAlert(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
try {
|
||||
$data = $this->form->getState();
|
||||
|
||||
// Convert bools to a string, so they are correctly written to the .env file
|
||||
$data = array_map(fn ($value) => is_bool($value) ? ($value ? 'true' : 'false') : $value, $data);
|
||||
|
||||
$this->writeToEnvironment($data);
|
||||
|
||||
Artisan::call('config:clear');
|
||||
Artisan::call('queue:restart');
|
||||
|
||||
$this->rememberData();
|
||||
|
||||
$this->redirect($this->getUrl());
|
||||
|
||||
Notification::make()
|
||||
->title('Settings saved')
|
||||
->success()
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Save failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('save')
|
||||
->action('save')
|
||||
->authorize(fn () => auth()->user()->can('update settings'))
|
||||
->keyBindings(['mod+s']),
|
||||
];
|
||||
|
||||
}
|
||||
}
|
||||
@@ -5,31 +5,28 @@ namespace App\Filament\Resources;
|
||||
use App\Filament\Resources\ApiKeyResource\Pages;
|
||||
use App\Models\ApiKey;
|
||||
use Filament\Resources\Resource;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ApiKeyResource extends Resource
|
||||
{
|
||||
protected static ?string $model = ApiKey::class;
|
||||
|
||||
protected static ?string $label = 'API Key';
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-key';
|
||||
|
||||
protected static ?string $navigationGroup = 'Advanced';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
{
|
||||
return static::getModel()::where('key_type', '2')->count() ?: null;
|
||||
return static::getModel()::where('key_type', ApiKey::TYPE_APPLICATION)->count() ?: null;
|
||||
}
|
||||
|
||||
public static function canEdit($record): bool
|
||||
public static function canEdit(Model $record): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -11,6 +11,7 @@ use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class CreateApiKey extends CreateRecord
|
||||
{
|
||||
@@ -41,7 +42,7 @@ class CreateApiKey extends CreateRecord
|
||||
'md' => 2,
|
||||
])
|
||||
->schema(
|
||||
collect(ApiKey::RESOURCES)->map(fn ($resource) => ToggleButtons::make("r_$resource")
|
||||
collect(ApiKey::getPermissionList())->map(fn ($resource) => ToggleButtons::make('permissions_' . $resource)
|
||||
->label(str($resource)->replace('_', ' ')->title())->inline()
|
||||
->options([
|
||||
0 => 'None',
|
||||
@@ -87,4 +88,20 @@ class CreateApiKey extends CreateRecord
|
||||
->columnSpanFull(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function handleRecordCreation(array $data): Model
|
||||
{
|
||||
$permissions = [];
|
||||
|
||||
foreach (ApiKey::getPermissionList() as $permission) {
|
||||
if (isset($data['permissions_' . $permission])) {
|
||||
$permissions[$permission] = intval($data['permissions_' . $permission]);
|
||||
unset($data['permissions_' . $permission]);
|
||||
}
|
||||
}
|
||||
|
||||
$data['permissions'] = $permissions;
|
||||
|
||||
return parent::handleRecordCreation($data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Filament\Resources\ApiKeyResource;
|
||||
use App\Models\ApiKey;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\CreateAction;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
@@ -51,13 +52,23 @@ class ListApiKeys extends ListRecords
|
||||
])
|
||||
->actions([
|
||||
DeleteAction::make(),
|
||||
])
|
||||
->emptyStateIcon('tabler-key')
|
||||
->emptyStateDescription('')
|
||||
->emptyStateHeading('No API Keys')
|
||||
->emptyStateActions([
|
||||
CreateAction::make('create')
|
||||
->label('Create API Key')
|
||||
->button(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
Actions\CreateAction::make()
|
||||
->label('Create API Key')
|
||||
->hidden(fn () => ApiKey::where('key_type', ApiKey::TYPE_APPLICATION)->count() <= 0),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,10 @@ class DatabaseHostResource extends Resource
|
||||
{
|
||||
protected static ?string $model = DatabaseHost::class;
|
||||
|
||||
protected static ?string $label = 'Databases';
|
||||
protected static ?string $label = 'Database Host';
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-database';
|
||||
|
||||
protected static ?string $navigationGroup = 'Advanced';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
@@ -20,13 +21,6 @@ class DatabaseHostResource extends Resource
|
||||
return static::getModel()::count() ?: null;
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -4,18 +4,22 @@ namespace App\Filament\Resources\DatabaseHostResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DatabaseHostResource;
|
||||
use App\Services\Databases\Hosts\HostCreationService;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use PDOException;
|
||||
|
||||
class CreateDatabaseHost extends CreateRecord
|
||||
{
|
||||
private HostCreationService $service;
|
||||
|
||||
protected static string $resource = DatabaseHostResource::class;
|
||||
|
||||
protected ?string $heading = 'Database Hosts';
|
||||
@@ -24,6 +28,11 @@ class CreateDatabaseHost extends CreateRecord
|
||||
|
||||
protected ?string $subheading = '(database servers that can have individual databases)';
|
||||
|
||||
public function boot(HostCreationService $service): void
|
||||
{
|
||||
$this->service = $service;
|
||||
}
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
@@ -94,10 +103,10 @@ class CreateDatabaseHost extends CreateRecord
|
||||
|
||||
protected function handleRecordCreation(array $data): Model
|
||||
{
|
||||
return resolve(HostCreationService::class)->handle($data);
|
||||
return $this->service->handle($data);
|
||||
}
|
||||
|
||||
public function exception($e, $stopPropagation): void
|
||||
public function exception(Exception $e, Closure $stopPropagation): void
|
||||
{
|
||||
if ($e instanceof PDOException) {
|
||||
Notification::make()
|
||||
|
||||
@@ -6,14 +6,16 @@ use App\Filament\Resources\DatabaseHostResource;
|
||||
use App\Filament\Resources\DatabaseHostResource\RelationManagers\DatabasesRelationManager;
|
||||
use App\Models\DatabaseHost;
|
||||
use App\Services\Databases\Hosts\HostUpdateService;
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use PDOException;
|
||||
|
||||
@@ -21,6 +23,13 @@ class EditDatabaseHost extends EditRecord
|
||||
{
|
||||
protected static string $resource = DatabaseHostResource::class;
|
||||
|
||||
private HostUpdateService $hostUpdateService;
|
||||
|
||||
public function boot(HostUpdateService $hostUpdateService): void
|
||||
{
|
||||
$this->hostUpdateService = $hostUpdateService;
|
||||
}
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
@@ -64,8 +73,7 @@ class EditDatabaseHost extends EditRecord
|
||||
->helperText('The password for the database user.')
|
||||
->password()
|
||||
->revealable()
|
||||
->maxLength(255)
|
||||
->required(),
|
||||
->maxLength(255),
|
||||
Select::make('node_id')
|
||||
->searchable()
|
||||
->preload()
|
||||
@@ -98,12 +106,16 @@ class EditDatabaseHost extends EditRecord
|
||||
];
|
||||
}
|
||||
|
||||
protected function handleRecordUpdate($record, array $data): Model
|
||||
protected function handleRecordUpdate(Model $record, array $data): Model
|
||||
{
|
||||
return resolve(HostUpdateService::class)->handle($record->id, $data);
|
||||
if (!$record instanceof DatabaseHost) {
|
||||
return $record;
|
||||
}
|
||||
|
||||
return $this->hostUpdateService->handle($record, $data);
|
||||
}
|
||||
|
||||
public function exception($e, $stopPropagation): void
|
||||
public function exception(Exception $e, Closure $stopPropagation): void
|
||||
{
|
||||
if ($e instanceof PDOException) {
|
||||
Notification::make()
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
namespace App\Filament\Resources\DatabaseHostResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DatabaseHostResource;
|
||||
use App\Models\DatabaseHost;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\CreateAction;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
@@ -30,27 +32,41 @@ class ListDatabaseHosts extends ListRecords
|
||||
->sortable(),
|
||||
TextColumn::make('username')
|
||||
->searchable(),
|
||||
TextColumn::make('max_databases')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
TextColumn::make('databases_count')
|
||||
->counts('databases')
|
||||
->icon('tabler-database')
|
||||
->label('Databases'),
|
||||
TextColumn::make('node.name')
|
||||
->numeric()
|
||||
->icon('tabler-server-2')
|
||||
->placeholder('No Nodes')
|
||||
->sortable(),
|
||||
])
|
||||
->checkIfRecordIsSelectableUsing(fn (DatabaseHost $databaseHost) => !$databaseHost->databases_count)
|
||||
->actions([
|
||||
EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete databasehost')),
|
||||
]),
|
||||
])
|
||||
->emptyStateIcon('tabler-database')
|
||||
->emptyStateDescription('')
|
||||
->emptyStateHeading('No Database Hosts')
|
||||
->emptyStateActions([
|
||||
CreateAction::make('create')
|
||||
->label('Create Database Host')
|
||||
->button(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make('create')->label('New Database Host'),
|
||||
Actions\CreateAction::make('create')
|
||||
->label('Create Database Host')
|
||||
->hidden(fn () => DatabaseHost::count() <= 0),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Actions\ViewAction;
|
||||
@@ -40,6 +41,7 @@ class DatabasesRelationManager extends RelationManager
|
||||
->formatStateUsing(fn (Get $get, Database $database) => 'jdbc:mysql://' . $get('username') . ':' . urlencode($database->password) . '@' . $database->host->host . ':' . $database->host->port . '/' . $get('database')),
|
||||
]);
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
@@ -60,7 +62,7 @@ class DatabasesRelationManager extends RelationManager
|
||||
]);
|
||||
}
|
||||
|
||||
protected function rotatePassword(DatabasePasswordService $service, Database $database, $set, $get): void
|
||||
protected function rotatePassword(DatabasePasswordService $service, Database $database, Set $set, Get $get): void
|
||||
{
|
||||
$newPassword = $service->handle($database);
|
||||
$jdbcString = 'jdbc:mysql://' . $get('username') . ':' . urlencode($newPassword) . '@' . $database->host->host . ':' . $database->host->port . '/' . $get('database');
|
||||
|
||||
@@ -13,6 +13,7 @@ class DatabaseResource extends Resource
|
||||
protected static ?string $navigationIcon = 'tabler-database';
|
||||
|
||||
protected static bool $shouldRegisterNavigation = false;
|
||||
|
||||
protected static ?string $navigationGroup = 'Advanced';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
@@ -20,13 +21,6 @@ class DatabaseResource extends Resource
|
||||
return static::getModel()::count() ?: null;
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -21,9 +21,12 @@ class CreateDatabase extends CreateRecord
|
||||
->searchable()
|
||||
->preload()
|
||||
->required(),
|
||||
TextInput::make('database_host_id')
|
||||
->required()
|
||||
->numeric(),
|
||||
Select::make('database_host_id')
|
||||
->relationship('host', 'name')
|
||||
->searchable()
|
||||
->selectablePlaceholder(false)
|
||||
->preload()
|
||||
->required(),
|
||||
TextInput::make('database')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
|
||||
@@ -4,10 +4,10 @@ namespace App\Filament\Resources\DatabaseResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DatabaseResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
@@ -48,7 +48,8 @@ class ListDatabases extends ListRecords
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete database')),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -21,13 +21,6 @@ class EggResource extends Resource
|
||||
return static::getModel()::count() ?: null;
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getGloballySearchableAttributes(): array
|
||||
{
|
||||
return ['name', 'tags', 'uuid', 'id'];
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Filament\Resources\EggResource\Pages;
|
||||
|
||||
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
|
||||
use App\Filament\Resources\EggResource;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
@@ -15,10 +16,9 @@ use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
@@ -27,6 +27,7 @@ class CreateEgg extends CreateRecord
|
||||
protected static string $resource = EggResource::class;
|
||||
|
||||
protected static bool $canCreateAnother = false;
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
@@ -74,9 +75,10 @@ class CreateEgg extends CreateRecord
|
||||
->helperText('')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
TextInput::make('update_url')
|
||||
->disabled()
|
||||
->helperText('Not implemented.')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('URLs must point directly to the raw .json file.')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
|
||||
->url(),
|
||||
KeyValue::make('docker_images')
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
@@ -133,7 +135,7 @@ class CreateEgg extends CreateRecord
|
||||
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
|
||||
$data['default_value'] ??= '';
|
||||
$data['description'] ??= '';
|
||||
$data['rules'] ??= '';
|
||||
$data['rules'] ??= [];
|
||||
$data['user_viewable'] ??= '';
|
||||
$data['user_editable'] ??= '';
|
||||
|
||||
@@ -142,7 +144,7 @@ class CreateEgg extends CreateRecord
|
||||
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
|
||||
$data['default_value'] ??= '';
|
||||
$data['description'] ??= '';
|
||||
$data['rules'] ??= '';
|
||||
$data['rules'] ??= [];
|
||||
$data['user_viewable'] ??= '';
|
||||
$data['user_editable'] ??= '';
|
||||
|
||||
@@ -154,7 +156,7 @@ class CreateEgg extends CreateRecord
|
||||
->debounce(750)
|
||||
->maxLength(255)
|
||||
->columnSpanFull()
|
||||
->afterStateUpdated(fn (Forms\Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString())
|
||||
->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString())
|
||||
)
|
||||
->required(),
|
||||
Textarea::make('description')->columnSpanFull(),
|
||||
@@ -172,7 +174,30 @@ class CreateEgg extends CreateRecord
|
||||
Checkbox::make('user_viewable')->label('Viewable'),
|
||||
Checkbox::make('user_editable')->label('Editable'),
|
||||
]),
|
||||
Textarea::make('rules')->columnSpanFull(),
|
||||
TagsInput::make('rules')
|
||||
->columnSpanFull()
|
||||
->placeholder('Add Rule')
|
||||
->reorderable()
|
||||
->suggestions([
|
||||
'required',
|
||||
'nullable',
|
||||
'string',
|
||||
'integer',
|
||||
'numeric',
|
||||
'boolean',
|
||||
'alpha',
|
||||
'alpha_dash',
|
||||
'alpha_num',
|
||||
'url',
|
||||
'email',
|
||||
'regex:',
|
||||
'min:',
|
||||
'max:',
|
||||
'between:',
|
||||
'between:1024,65535',
|
||||
'in:',
|
||||
'in:true,false',
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Install Script')
|
||||
@@ -186,7 +211,7 @@ class CreateEgg extends CreateRecord
|
||||
TextInput::make('script_container')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->default('alpine:3.4'),
|
||||
->default('ghcr.io/pelican-eggs/installers:debian'),
|
||||
|
||||
Select::make('script_entry')
|
||||
->selectablePlaceholder(false)
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
namespace App\Filament\Resources\EggResource\Pages;
|
||||
|
||||
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
|
||||
use App\Filament\Resources\EggResource;
|
||||
use App\Filament\Resources\EggResource\RelationManagers\ServersRelationManager;
|
||||
use App\Models\Egg;
|
||||
use App\Services\Eggs\Sharing\EggExporterService;
|
||||
use App\Services\Eggs\Sharing\EggImporterService;
|
||||
use Exception;
|
||||
use Filament\Actions;
|
||||
@@ -22,12 +24,10 @@ use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
|
||||
use App\Services\Eggs\Sharing\EggExporterService;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
|
||||
class EditEgg extends EditRecord
|
||||
{
|
||||
@@ -40,6 +40,7 @@ class EditEgg extends EditRecord
|
||||
Tabs::make()->tabs([
|
||||
Tab::make('Configuration')
|
||||
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
|
||||
->icon('tabler-egg')
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->required()
|
||||
@@ -80,6 +81,7 @@ class EditEgg extends EditRecord
|
||||
->helperText('')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
Toggle::make('force_outgoing_ip')
|
||||
->inline(false)
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip("Forces all outgoing network traffic to have its Source IP NATed to the IP of the server's primary allocation IP.
|
||||
Required for certain games to work properly when the Node has multiple public IP addresses.
|
||||
@@ -91,8 +93,10 @@ class EditEgg extends EditRecord
|
||||
->helperText('')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
TextInput::make('update_url')
|
||||
->disabled()
|
||||
->helperText('Not implemented.')
|
||||
->label('Update URL')
|
||||
->url()
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('URLs must point directly to the raw .json file.')
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
KeyValue::make('docker_images')
|
||||
->live()
|
||||
@@ -103,9 +107,9 @@ class EditEgg extends EditRecord
|
||||
->valueLabel('Image URI')
|
||||
->helperText('The docker images available to servers using this egg.'),
|
||||
]),
|
||||
|
||||
Tab::make('Process Management')
|
||||
->columns()
|
||||
->icon('tabler-server-cog')
|
||||
->schema([
|
||||
Select::make('config_from')
|
||||
->label('Copy Settings From')
|
||||
@@ -128,6 +132,7 @@ class EditEgg extends EditRecord
|
||||
]),
|
||||
Tab::make('Egg Variables')
|
||||
->columnSpanFull()
|
||||
->icon('tabler-variable')
|
||||
->schema([
|
||||
Repeater::make('variables')
|
||||
->label('')
|
||||
@@ -142,7 +147,7 @@ class EditEgg extends EditRecord
|
||||
->mutateRelationshipDataBeforeCreateUsing(function (array $data): array {
|
||||
$data['default_value'] ??= '';
|
||||
$data['description'] ??= '';
|
||||
$data['rules'] ??= '';
|
||||
$data['rules'] ??= [];
|
||||
$data['user_viewable'] ??= '';
|
||||
$data['user_editable'] ??= '';
|
||||
|
||||
@@ -151,7 +156,7 @@ class EditEgg extends EditRecord
|
||||
->mutateRelationshipDataBeforeSaveUsing(function (array $data): array {
|
||||
$data['default_value'] ??= '';
|
||||
$data['description'] ??= '';
|
||||
$data['rules'] ??= '';
|
||||
$data['rules'] ??= [];
|
||||
$data['user_viewable'] ??= '';
|
||||
$data['user_editable'] ??= '';
|
||||
|
||||
@@ -163,7 +168,7 @@ class EditEgg extends EditRecord
|
||||
->debounce(750)
|
||||
->maxLength(255)
|
||||
->columnSpanFull()
|
||||
->afterStateUpdated(fn (Forms\Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString())
|
||||
->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString())
|
||||
)
|
||||
->required(),
|
||||
Textarea::make('description')->columnSpanFull(),
|
||||
@@ -181,27 +186,47 @@ class EditEgg extends EditRecord
|
||||
Checkbox::make('user_viewable')->label('Viewable'),
|
||||
Checkbox::make('user_editable')->label('Editable'),
|
||||
]),
|
||||
TextInput::make('rules')->columnSpanFull(),
|
||||
TagsInput::make('rules')
|
||||
->columnSpanFull()
|
||||
->placeholder('Add Rule')
|
||||
->reorderable()
|
||||
->suggestions([
|
||||
'required',
|
||||
'nullable',
|
||||
'string',
|
||||
'integer',
|
||||
'numeric',
|
||||
'boolean',
|
||||
'alpha',
|
||||
'alpha_dash',
|
||||
'alpha_num',
|
||||
'url',
|
||||
'email',
|
||||
'regex:',
|
||||
'min:',
|
||||
'max:',
|
||||
'between:',
|
||||
'between:1024,65535',
|
||||
'in:',
|
||||
'in:true,false',
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Install Script')
|
||||
->columns(3)
|
||||
->icon('tabler-file-download')
|
||||
->schema([
|
||||
|
||||
Select::make('copy_script_from')
|
||||
->placeholder('None')
|
||||
->relationship('scriptFrom', 'name', ignoreRecord: true),
|
||||
|
||||
TextInput::make('script_container')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->default('alpine:3.4'),
|
||||
|
||||
TextInput::make('script_entry')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->default('ash'),
|
||||
|
||||
MonacoEditor::make('script_install')
|
||||
->label('Install Script')
|
||||
->columnSpanFull()
|
||||
@@ -209,7 +234,6 @@ class EditEgg extends EditRecord
|
||||
->language('shell')
|
||||
->view('filament.plugins.monaco-editor'),
|
||||
]),
|
||||
|
||||
])->columnSpanFull()->persistTabInQueryString(),
|
||||
]);
|
||||
}
|
||||
@@ -220,14 +244,13 @@ class EditEgg extends EditRecord
|
||||
Actions\DeleteAction::make('deleteEgg')
|
||||
->disabled(fn (Egg $egg): bool => $egg->servers()->count() > 0)
|
||||
->label(fn (Egg $egg): string => $egg->servers()->count() <= 0 ? 'Delete' : 'In Use'),
|
||||
|
||||
Actions\Action::make('exportEgg')
|
||||
->label('Export')
|
||||
->color('primary')
|
||||
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
|
||||
echo $service->handle($egg->id);
|
||||
}, 'egg-' . $egg->getKebabName() . '.json')),
|
||||
|
||||
}, 'egg-' . $egg->getKebabName() . '.json'))
|
||||
->authorize(fn () => auth()->user()->can('export egg')),
|
||||
Actions\Action::make('importEgg')
|
||||
->label('Import')
|
||||
->form([
|
||||
@@ -249,6 +272,7 @@ class EditEgg extends EditRecord
|
||||
->schema([
|
||||
TextInput::make('url')
|
||||
->label('URL')
|
||||
->default(fn (Egg $egg): ?string => $egg->update_url)
|
||||
->hint('Link to the egg file (eg. minecraft.json)')
|
||||
->url(),
|
||||
]),
|
||||
@@ -256,10 +280,7 @@ class EditEgg extends EditRecord
|
||||
->contained(false),
|
||||
|
||||
])
|
||||
->action(function (array $data, Egg $egg): void {
|
||||
/** @var EggImporterService $eggImportService */
|
||||
$eggImportService = resolve(EggImporterService::class);
|
||||
|
||||
->action(function (array $data, Egg $egg, EggImporterService $eggImportService): void {
|
||||
if (!empty($data['egg'])) {
|
||||
try {
|
||||
$eggImportService->fromFile($data['egg'], $egg);
|
||||
@@ -267,16 +288,14 @@ class EditEgg extends EditRecord
|
||||
Notification::make()
|
||||
->title('Import Failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->danger() // Will Robinson
|
||||
->send();
|
||||
|
||||
report($exception);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($data['url'])) {
|
||||
} elseif (!empty($data['url'])) {
|
||||
try {
|
||||
$eggImportService->fromUrl($data['url'], $egg);
|
||||
} catch (Exception $exception) {
|
||||
@@ -297,8 +316,8 @@ class EditEgg extends EditRecord
|
||||
->title('Import Success')
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
|
||||
})
|
||||
->authorize(fn () => auth()->user()->can('import egg')),
|
||||
$this->getSaveFormAction()->formId('form'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -14,13 +14,13 @@ use Filament\Forms\Components\Tabs\Tab;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
use Filament\Tables;
|
||||
|
||||
class ListEggs extends ListRecords
|
||||
{
|
||||
@@ -49,20 +49,56 @@ class ListEggs extends ListRecords
|
||||
])
|
||||
->actions([
|
||||
EditAction::make(),
|
||||
Tables\Actions\Action::make('export')
|
||||
Action::make('export')
|
||||
->icon('tabler-download')
|
||||
->label('Export')
|
||||
->color('primary')
|
||||
->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) {
|
||||
echo $service->handle($egg->id);
|
||||
}, 'egg-' . $egg->getKebabName() . '.json')),
|
||||
}, 'egg-' . $egg->getKebabName() . '.json'))
|
||||
->authorize(fn () => auth()->user()->can('export egg')),
|
||||
Action::make('update')
|
||||
->icon('tabler-cloud-download')
|
||||
->label('Update')
|
||||
->color('success')
|
||||
->requiresConfirmation()
|
||||
->modalHeading('Are you sure you want to update this egg?')
|
||||
->modalDescription('If you made any changes to the egg they will be overwritten!')
|
||||
->modalIconColor('danger')
|
||||
->modalSubmitAction(fn (Actions\StaticAction $action) => $action->color('danger'))
|
||||
->action(function (Egg $egg, EggImporterService $eggImporterService) {
|
||||
try {
|
||||
$eggImporterService->fromUrl($egg->update_url, $egg);
|
||||
|
||||
cache()->forget("eggs.{$egg->uuid}.update");
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Egg Update failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
report($exception);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title('Egg updated')
|
||||
->success()
|
||||
->send();
|
||||
})
|
||||
->authorize(fn () => auth()->user()->can('import egg'))
|
||||
->visible(fn (Egg $egg) => cache()->get("eggs.{$egg->uuid}.update", false)),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete egg')),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
@@ -95,10 +131,7 @@ class ListEggs extends ListRecords
|
||||
->contained(false),
|
||||
|
||||
])
|
||||
->action(function (array $data): void {
|
||||
/** @var EggImporterService $eggImportService */
|
||||
$eggImportService = resolve(EggImporterService::class);
|
||||
|
||||
->action(function (array $data, EggImporterService $eggImportService): void {
|
||||
if (!empty($data['egg'])) {
|
||||
/** @var TemporaryUploadedFile[] $eggFile */
|
||||
$eggFile = $data['egg'];
|
||||
@@ -109,6 +142,7 @@ class ListEggs extends ListRecords
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Import Failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
@@ -125,6 +159,7 @@ class ListEggs extends ListRecords
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
->title('Import Failed')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
@@ -138,7 +173,8 @@ class ListEggs extends ListRecords
|
||||
->title('Import Success')
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
})
|
||||
->authorize(fn () => auth()->user()->can('import egg')),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ class ServersRelationManager extends RelationManager
|
||||
{
|
||||
return $table
|
||||
->recordTitleAttribute('servers')
|
||||
->emptyStateDescription('No Servers')->emptyStateHeading('No servers are assigned this egg.')
|
||||
->emptyStateDescription('No Servers')->emptyStateHeading('No servers are assigned to this Egg.')
|
||||
->searchable(false)
|
||||
->columns([
|
||||
TextColumn::make('user.username')
|
||||
|
||||
@@ -11,6 +11,7 @@ class MountResource extends Resource
|
||||
protected static ?string $model = Mount::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'tabler-layers-linked';
|
||||
|
||||
protected static ?string $navigationGroup = 'Advanced';
|
||||
|
||||
public static function getNavigationBadge(): ?string
|
||||
@@ -18,13 +19,6 @@ class MountResource extends Resource
|
||||
return static::getModel()::count() ?: null;
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -4,14 +4,14 @@ namespace App\Filament\Resources\MountResource\Pages;
|
||||
|
||||
use App\Filament\Resources\MountResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Filament\Forms\Components\Group;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditMount extends EditRecord
|
||||
{
|
||||
@@ -96,6 +96,7 @@ class EditMount extends EditRecord
|
||||
'lg' => 2,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user