Compare commits

..

No commits in common. "main" and "v1.0.0-beta11" have entirely different histories.

1869 changed files with 83920 additions and 29523 deletions

View File

@ -1,29 +1,10 @@
**.DS_Store
.env
.devcontainer
.dockerignore
.editorconfig
.git
.github
**.gitignore
.php-cs-fixer.dist.php
.prettierrc.json
.vscode
Dockerfile
bounties.md
compose.yml
contributing.md
contributor_license_agreement.md
database/database.sqlite
docker/README.md
node_modules
phpstan.neon
phpunit.xml
readme.md
vendor
database/database.sqlite
storage/debugbar/*.json
storage/logs/*.log
storage/framework/cache/data/*
storage/framework/sessions/*
storage/framework/testing
storage/framework/views/*.php
storage/logs/*.log
vendor

View File

@ -3,4 +3,5 @@ APP_DEBUG=false
APP_KEY=
APP_URL=http://panel.test
APP_INSTALLED=false
APP_TIMEZONE=UTC
APP_LOCALE=en

6
.eslintignore Normal file
View File

@ -0,0 +1,6 @@
public
node_modules
resources/views
babel.config.js
tailwind.config.js
webpack.config.js

52
.eslintrc.js Normal file
View File

@ -0,0 +1,52 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 6,
ecmaFeatures: {
jsx: true,
},
project: './tsconfig.json',
tsconfigRootDir: './',
},
settings: {
react: {
pragma: 'React',
version: 'detect',
},
linkComponents: [
{ name: 'Link', linkAttribute: 'to' },
{ name: 'NavLink', linkAttribute: 'to' },
],
},
env: {
browser: true,
es6: true,
},
plugins: ['react', 'react-hooks', 'prettier', '@typescript-eslint'],
extends: [
// 'standard',
'eslint:recommended',
'plugin:react/recommended',
'plugin:@typescript-eslint/recommended',
'plugin:jest-dom/recommended',
],
rules: {
eqeqeq: 'error',
'prettier/prettier': ['error', {}, { usePrettierrc: true }],
// TypeScript can infer this significantly better than eslint ever can.
'react/prop-types': 0,
'react/display-name': 0,
'@typescript-eslint/no-explicit-any': 0,
'@typescript-eslint/no-non-null-assertion': 0,
// 'react/no-unknown-property': ['error', { ignore: ['css'] }],
// This setup is required to avoid a spam of errors when running eslint about React being
// used before it is defined.
//
// @see https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-use-before-define.md#how-to-use
'no-use-before-define': 0,
'@typescript-eslint/no-use-before-define': 'warn',
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
'@typescript-eslint/ban-ts-comment': ['error', { 'ts-expect-error': 'allow-with-description' }],
},
};

76
.github/docker/README.md vendored Normal file
View File

@ -0,0 +1,76 @@
# Pelican Panel - Docker Image
This is a ready to use docker image for the panel.
## Requirements
This docker image requires some additional software to function. The software can either be provided in other containers (see the [docker-compose.yml](https://github.com/pelican-dev/panel/blob/develop/docker-compose.example.yml) as an example) or as existing instances.
A mysql database is required. We recommend the stock [MariaDB Image](https://hub.docker.com/_/mariadb/) image if you prefer to run it in a docker container. As a non-containerized option we recommend mariadb.
A caching software is required as well. We recommend the stock [Redis Image](https://hub.docker.com/_/redis/) image. You can choose any of the [supported options](#cache-drivers).
You can provide additional settings using a custom `.env` file or by setting the appropriate environment variables in the docker-compose file.
## Setup
Start the docker container and the required dependencies (either provide existing ones or start containers as well, see the [docker-compose.yml](https://github.com/pelican-dev/panel/blob/develop/docker-compose.example.yml) file as an example.
After the startup is complete you'll need to create a user.
If you are running the docker container without docker-compose, use:
```
docker exec -it <container id> php artisan p:user:make
```
If you are using docker compose use
```
docker-compose exec panel php artisan p:user:make
```
## Environment Variables
There are multiple environment variables to configure the panel when not providing your own `.env` file, see the following table for details on each available option.
Note: If your `APP_URL` starts with `https://` you need to provide an `LE_EMAIL` as well so Certificates can be generated.
| Variable | Description | Required |
|-------------------| ------------------------------------------------------------------------------ | -------- |
| `APP_URL` | The URL the panel will be reachable with (including protocol) | yes |
| `APP_TIMEZONE` | The timezone to use for the panel | yes |
| `LE_EMAIL` | The email used for letsencrypt certificate generation | yes |
| `DB_HOST` | The host of the mysql instance | yes |
| `DB_PORT` | The port of the mysql instance | yes |
| `DB_DATABASE` | The name of the mysql database | yes |
| `DB_USERNAME` | The mysql user | yes |
| `DB_PASSWORD` | The mysql password for the specified user | yes |
| `CACHE_STORE` | The cache driver (see [Cache drivers](#cache-drivers) for detais) | yes |
| `SESSION_DRIVER` | | yes |
| `QUEUE_DRIVER` | | yes |
| `REDIS_HOST` | The hostname or IP address of the redis database | yes |
| `REDIS_PASSWORD` | The password used to secure the redis database | maybe |
| `REDIS_PORT` | The port the redis database is using on the host | maybe |
| `MAIL_DRIVER` | The email driver (see [Mail drivers](#mail-drivers) for details) | yes |
| `MAIL_FROM` | The email that should be used as the sender email | yes |
| `MAIL_HOST` | The host of your mail driver instance | maybe |
| `MAIL_PORT` | The port of your mail driver instance | maybe |
| `MAIL_USERNAME` | The username for your mail driver | maybe |
| `MAIL_PASSWORD` | The password for your mail driver | maybe |
### Cache drivers
You can choose between different cache drivers depending on what you prefer.
We recommend redis when using docker as it can be started in a container easily.
| Driver | Description | Required variables |
| -------- | ------------------------------------ | ------------------------------------------------------ |
| redis | host where redis is running | `REDIS_HOST` |
| redis | port redis is running on | `REDIS_PORT` |
| redis | redis database password | `REDIS_PASSWORD` |
### Mail drivers
You can choose between different mail drivers according to your needs.
Every driver requires `MAIL_FROM` to be set.
| Driver | Description | Required variables |
| -------- | ------------------------------------ | ------------------------------------------------------------- |
| mail | uses the installed php mail | |
| mandrill | [Mandrill](http://www.mandrill.com/) | `MAIL_USERNAME` |
| postmark | [Postmark](https://postmarkapp.com/) | `MAIL_USERNAME` |
| mailgun | [Mailgun](https://www.mailgun.com/) | `MAIL_USERNAME`, `MAIL_HOST` |
| smtp | Any SMTP server can be configured | `MAIL_USERNAME`, `MAIL_HOST`, `MAIL_PASSWORD`, `MAIL_PORT` |

75
.github/docker/default.conf vendored Normal file
View File

@ -0,0 +1,75 @@
# If using Ubuntu this file should be placed in:
# /etc/nginx/sites-available/
#
# If using CentOS this file should be placed in:
# /etc/nginx/conf.d/
#
# The MIT License (MIT)
#
# Pterodactyl®
# Copyright © Dane Everitt <dane@daneeveritt.com> and contributors
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
server {
listen 80;
server_name _;
root /app/public;
index index.html index.htm index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
access_log off;
error_log /var/log/nginx/panel.app-error.log error;
# allow larger file uploads and longer script runtimes
client_max_body_size 100m;
client_body_timeout 120s;
sendfile off;
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
# the fastcgi_pass path needs to be changed accordingly when using CentOS
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param PHP_VALUE "upload_max_filesize = 100M \n post_max_size=100M";
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param HTTP_PROXY "";
fastcgi_intercept_errors off;
fastcgi_buffer_size 16k;
fastcgi_buffers 4 16k;
fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
fastcgi_read_timeout 300;
}
location ~ /\.ht {
deny all;
}
}

70
.github/docker/default_ssl.conf vendored Normal file
View File

@ -0,0 +1,70 @@
# If using Ubuntu this file should be placed in:
# /etc/nginx/sites-available/
#
server {
listen 80;
server_name <domain>;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name <domain>;
root /app/public;
index index.php;
access_log /var/log/nginx/panel.app-access.log;
error_log /var/log/nginx/panel.app-error.log error;
# allow larger file uploads and longer script runtimes
client_max_body_size 100m;
client_body_timeout 120s;
sendfile off;
# strengthen ssl security
ssl_certificate /etc/letsencrypt/live/<domain>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<domain>/privkey.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:ECDHE-RSA-AES128-GCM-SHA256:AES256+EECDH:DHE-RSA-AES128-GCM-SHA256:AES256+EDH:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
# See the link below for more SSL information:
# https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
#
# ssl_dhparam /etc/ssl/certs/dhparam.pem;
# Add headers to serve security related headers
add_header Strict-Transport-Security "max-age=15768000; preload;";
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header X-Robots-Tag none;
add_header Content-Security-Policy "frame-ancestors 'self'";
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param PHP_VALUE "upload_max_filesize = 100M \n post_max_size=100M";
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param HTTP_PROXY "";
fastcgi_intercept_errors off;
fastcgi_buffer_size 16k;
fastcgi_buffers 4 16k;
fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
fastcgi_read_timeout 300;
include /etc/nginx/fastcgi_params;
}
location ~ /\.ht {
deny all;
}
}

View File

@ -1,11 +1,14 @@
#!/bin/ash -e
## check for .env file or symlink and generate app keys if missing
if [ -f /var/www/html/.env ]; then
#mkdir -p /var/log/supervisord/ /var/log/php8/ \
## check for .env file and generate app keys if missing
if [ -f /pelican-data/.env ]; then
echo "external vars exist."
rm -rf /var/www/html/.env
else
echo "external vars don't exist."
# webroot .env is symlinked to this path
rm -rf /var/www/html/.env
touch /pelican-data/.env
## manually generate a key because key generate --force fails
@ -23,7 +26,10 @@ else
echo -e "APP_INSTALLED=false" >> /pelican-data/.env
fi
mkdir -p /pelican-data/database /pelican-data/storage/avatars /pelican-data/storage/fonts /var/www/html/storage/logs/supervisord 2>/dev/null
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..."
@ -39,15 +45,21 @@ 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
export SUPERVISORD_CADDY=false
## disable caddy if SKIP_CADDY is set
if [[ "${SKIP_CADDY:-}" == "true" ]]; then
echo "Starting PHP-FPM only"
else
if [[ -z $SKIP_CADDY ]]; then
echo "Starting PHP-FPM and Caddy"
export SUPERVISORD_CADDY=true
else
echo "Starting PHP-FPM only"
fi
chown -R www-data:www-data /pelican-data/.env /pelican-data/database
echo "Starting Supervisord"
exec "$@"

View File

@ -4,14 +4,16 @@ username=dummy
password=dummy
[supervisord]
logfile=/var/www/html/storage/logs/supervisord/supervisord.log ; supervisord log file
logfile=/var/log/supervisord/supervisord.log ; supervisord log file
logfile_maxbytes=50MB ; maximum size of logfile before rotation
logfile_backups=2 ; number of backed up logfiles
loglevel=error ; info, debug, warn, trace
pidfile=/var/run/supervisord/supervisord.pid ; pidfile location
nodaemon=true ; run supervisord as a daemon
pidfile=/var/run/supervisord.pid ; pidfile location
nodaemon=false ; run supervisord as a daemon
minfds=1024 ; number of startup file descriptors
minprocs=200 ; number of process descriptors
user=root ; default user
childlogdir=/var/log/supervisord/ ; where child log files will live
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
@ -28,6 +30,7 @@ autorestart=true
[program:queue-worker]
command=/usr/local/bin/php /var/www/html/artisan queue:work --tries=3
user=www-data
autostart=true
autorestart=true
@ -36,12 +39,5 @@ command=caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
autostart=%(ENV_SUPERVISORD_CADDY)s
autorestart=%(ENV_SUPERVISORD_CADDY)s
priority=10
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
redirect_stderr=true
[program:supercronic]
command=supercronic -overlapping /etc/supercronic/crontab
autostart=true
autorestart=true
redirect_stderr=true
stdout_events_enabled=true
stderr_events_enabled=true

16
.github/docker/www.conf vendored Normal file
View File

@ -0,0 +1,16 @@
[www]
user = nginx
group = nginx
listen = 127.0.0.1:9000
listen.owner = nginx
listen.group = nginx
listen.mode = 0750
pm = ondemand
pm.max_children = 9
pm.process_idle_timeout = 10s
pm.max_requests = 200
clear_env = no

View File

@ -3,8 +3,10 @@ name: Build
on:
push:
branches:
- main
- '**'
pull_request:
branches:
- '**'
jobs:
ui:
@ -18,25 +20,14 @@ jobs:
- name: Code Checkout
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2
coverage: none
- name: Install PHP dependencies
run: composer install --no-interaction --no-suggest --no-progress --no-autoloader --no-scripts --no-dev
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: "yarn"
- name: Install JS dependencies
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Build
run: yarn build
run: yarn build:production

View File

@ -13,7 +13,7 @@ jobs:
strategy:
fail-fast: false
matrix:
php: [8.2, 8.3, 8.4]
php: [8.2, 8.3]
database: ["mysql:8"]
services:
database:
@ -66,16 +66,16 @@ jobs:
coverage: none
- name: Install dependencies
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
run: composer install --no-interaction --no-suggest --prefer-dist
- name: Unit tests
run: vendor/bin/pest tests/Unit
run: vendor/bin/phpunit tests/Unit
env:
DB_HOST: UNIT_NO_DB
SKIP_MIGRATIONS: true
- name: Integration tests
run: vendor/bin/pest tests/Integration
run: vendor/bin/phpunit tests/Integration
env:
DB_PORT: ${{ job.services.database.ports[3306] }}
DB_USERNAME: root
@ -86,8 +86,8 @@ jobs:
strategy:
fail-fast: false
matrix:
php: [8.2, 8.3, 8.4]
database: ["mariadb:10.6", "mariadb:10.11", "mariadb:11.4"]
php: [8.2, 8.3]
database: ["mariadb:10.3", "mariadb:10.11", "mariadb:11.4"]
services:
database:
image: ${{ matrix.database }}
@ -139,16 +139,16 @@ jobs:
coverage: none
- name: Install dependencies
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
run: composer install --no-interaction --no-suggest --prefer-dist
- name: Unit tests
run: vendor/bin/pest tests/Unit
run: vendor/bin/phpunit tests/Unit
env:
DB_HOST: UNIT_NO_DB
SKIP_MIGRATIONS: true
- name: Integration tests
run: vendor/bin/pest tests/Integration
run: vendor/bin/phpunit tests/Integration
env:
DB_PORT: ${{ job.services.database.ports[3306] }}
DB_USERNAME: root
@ -159,7 +159,7 @@ jobs:
strategy:
fail-fast: false
matrix:
php: [8.2, 8.3, 8.4]
php: [8.2, 8.3]
env:
APP_ENV: testing
APP_DEBUG: "false"
@ -200,92 +200,16 @@ jobs:
coverage: none
- name: Install dependencies
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
run: composer install --no-interaction --no-suggest --prefer-dist
- name: Create SQLite file
run: touch database/testing.sqlite
- name: Unit tests
run: vendor/bin/pest tests/Unit
run: vendor/bin/phpunit tests/Unit
env:
DB_HOST: UNIT_NO_DB
SKIP_MIGRATIONS: true
- name: Integration tests
run: vendor/bin/pest tests/Integration
postgresql:
name: PostgreSQL
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: [8.2, 8.3, 8.4]
database: ["postgres:14"]
services:
database:
image: ${{ matrix.database }}
env:
POSTGRES_DB: testing
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_HOST_AUTH_METHOD: trust
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
APP_ENV: testing
APP_DEBUG: "false"
APP_KEY: ThisIsARandomStringForTests12345
APP_TIMEZONE: UTC
APP_URL: http://localhost/
CACHE_DRIVER: array
MAIL_MAILER: array
SESSION_DRIVER: array
QUEUE_CONNECTION: sync
DB_CONNECTION: pgsql
DB_HOST: 127.0.0.1
DB_DATABASE: testing
DB_USERNAME: postgres
DB_PASSWORD: postgres
GUZZLE_TIMEOUT: 60
GUZZLE_CONNECT_TIMEOUT: 60
steps:
- name: Code Checkout
uses: actions/checkout@v4
- name: Get cache directory
id: composer-cache
run: |
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
restore-keys: |
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2
coverage: none
- name: Install dependencies
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
- name: Unit tests
run: vendor/bin/pest tests/Unit
env:
DB_HOST: UNIT_NO_DB
SKIP_MIGRATIONS: true
- name: Integration tests
run: vendor/bin/pest tests/Integration
run: vendor/bin/phpunit tests/Integration

View File

@ -1,5 +1,6 @@
name: Docker
on:
push:
branches:
@ -13,73 +14,18 @@ env:
IMAGE_NAME: ${{ github.repository }}
jobs:
build-php-base:
name: Build PHP base image on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-24.04
arch: amd64
platform: linux/amd64
- os: ubuntu-24.04-arm
arch: arm64
platform: linux/arm64
steps:
- name: Code checkout
uses: actions/checkout@v4
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v3
- name: Build the base PHP image
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile.base
push: false
load: true
platforms: ${{ matrix.platform }}
tags: base-php:${{ matrix.arch }}
cache-from: type=gha,scope=base-php${{ matrix.arch }}
cache-to: type=gha,scope=base-php${{ matrix.arch }}
- name: Export image to file
run: docker save -o base-php-${{ matrix.arch }}.tar base-php:${{ matrix.arch }}
- name: Push the docker build to the artifacts
uses: actions/upload-artifact@v4
with:
name: base-php-${{ matrix.arch }}.tar
path: base-php-${{ matrix.arch }}.tar
retention-days: 7
build-and-push:
name: Build and Push ubuntu-24.04
runs-on: ubuntu-24.04
needs: build-php-base
name: Build and Push
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
fail-fast: false
# Start a temp local registry because workflow can not pull from localy loaded images
services:
registry:
image: registry:2
ports:
- 5000:5000
# 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
@ -92,14 +38,11 @@ jobs:
type=ref,event=tag
type=ref,event=branch
- name: Set up QEMU
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
# We Need to start it in host mode else it can't acces the local registry on port 5000
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v3
with:
driver-opts: network=host
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
@ -114,52 +57,30 @@ jobs:
echo "version_tag=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_OUTPUT
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
# Download the base PHP image AMD64
- uses: actions/download-artifact@v4
with:
name: base-php-amd64.tar
# Download the base PHP image ARM64
- uses: actions/download-artifact@v4
with:
name: base-php-arm64.tar
- name: Load base images into local registry
run: |
docker load -i base-php-amd64.tar
docker load -i base-php-arm64.tar
docker tag base-php:amd64 localhost:5000/base-php:amd64
docker tag base-php:arm64 localhost:5000/base-php:arm64
docker push localhost:5000/base-php:amd64
docker push localhost:5000/base-php:arm64
rm base-php-arm64.tar base-php-amd64.tar
- name: Build and Push (tag)
uses: docker/build-push-action@v6
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'
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 }}
cache-from: type=gha,scope=tagged${{ matrix.os }}
cache-to: type=gha,scope=tagged${{ matrix.os }},mode=max
- name: Build and Push (main)
uses: docker/build-push-action@v6
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'
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,scope=${{ matrix.os }}
cache-to: type=gha,scope=${{ matrix.os }},mode=max
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@ -25,38 +25,21 @@ jobs:
run: cp .env.example .env
- name: Install dependencies
run: composer install --no-interaction --no-suggest --no-progress --no-autoloader --no-scripts
run: composer install --no-interaction --no-progress --prefer-dist
- name: Pint
run: vendor/bin/pint --test
phpstan:
name: PHPStan
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: [ 8.2, 8.3, 8.4 ]
steps:
- name: Code Checkout
uses: actions/checkout@v4
- name: Get cache directory
id: composer-cache
run: |
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-${{ matrix.php }}-
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
php-version: "8.3"
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2
coverage: none
@ -65,7 +48,7 @@ jobs:
run: cp .env.example .env
- name: Install dependencies
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
run: composer install --no-interaction --no-progress --prefer-dist
- name: PHPStan
run: vendor/bin/phpstan --memory-limit=-1 --error-format=github
run: vendor/bin/phpstan --memory-limit=-1

View File

@ -11,33 +11,22 @@ jobs:
runs-on: ubuntu-22.04
permissions:
contents: write
steps:
- name: Code checkout
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2
coverage: none
- name: Install PHP dependencies
run: composer install --no-interaction --no-suggest --no-progress --no-autoloader --no-scripts --no-dev
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: "yarn"
- name: Install JS dependencies
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Build
run: yarn build
run: yarn build:production
- name: Create release branch and bump version
env:
@ -55,8 +44,8 @@ jobs:
- name: Create release archive
run: |
rm -rf node_modules vendor tests CODE_OF_CONDUCT.md CONTRIBUTING.md phpunit.xml shell.nix
tar -czf panel.tar.gz * .env.example
rm -rf node_modules tests CODE_OF_CONDUCT.md CONTRIBUTING.md flake.lock flake.nix phpunit.xml shell.nix
tar -czf panel.tar.gz * .editorconfig .env.example .eslintignore .eslintrc.js .gitignore .prettierrc.json
- name: Create checksum
run: |

5
.gitignore vendored
View File

@ -1,9 +1,9 @@
/.phpunit.cache
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/storage/pail
/storage/clockwork/*
/vendor
*.DS_Store*
@ -19,11 +19,10 @@ npm-debug.log
yarn-error.log
/.fleet
/.idea
/.nova
/.vscode
public/assets/manifest.json
/database/*.sqlite*
/database/*.sqlite
filament-monaco-editor/
_ide_helper*
/.phpstorm.meta.php

52
.php-cs-fixer.dist.php Normal file
View File

@ -0,0 +1,52 @@
<?php
use PhpCsFixer\Config;
use PhpCsFixer\Finder;
$finder = (new Finder())
->in(__DIR__)
->exclude([
'vendor',
'node_modules',
'storage',
'bootstrap/cache',
])
->notName(['_ide_helper*']);
return (new Config())
->setRiskyAllowed(true)
->setFinder($finder)
->setRules([
'@Symfony' => true,
'@PSR1' => true,
'@PSR2' => true,
'@PSR12' => true,
'align_multiline_comment' => ['comment_type' => 'phpdocs_like'],
'combine_consecutive_unsets' => true,
'concat_space' => ['spacing' => 'one'],
'heredoc_to_nowdoc' => true,
'no_alias_functions' => true,
'no_unreachable_default_argument_value' => true,
'no_useless_return' => true,
'ordered_imports' => [
'sort_algorithm' => 'length',
],
'phpdoc_align' => [
'align' => 'left',
'tags' => [
'param',
'property',
'return',
'throws',
'type',
'var',
],
],
'random_api_migration' => true,
'ternary_to_null_coalescing' => true,
'yoda_style' => [
'equal' => false,
'identical' => false,
'less_and_greater' => false,
],
]);

View File

@ -1,99 +1,52 @@
# syntax=docker.io/docker/dockerfile:1.13-labs
# Pelican Production Dockerfile
##
# If you want to build this locally you want to run `docker build -f Dockerfile.dev`
##
# ================================
# Stage 1-1: Composer Install
# ================================
FROM --platform=$TARGETOS/$TARGETARCH localhost:5000/base-php:$TARGETARCH AS composer
FROM node:20-alpine AS yarn
#FROM --platform=$TARGETOS/$TARGETARCH node:20-alpine AS yarn
WORKDIR /build
COPY . ./
RUN yarn config set network-timeout 300000 \
&& yarn install --frozen-lockfile \
&& yarn run build:production
FROM php:8.3-fpm-alpine
# FROM --platform=$TARGETOS/$TARGETARCH php:8.3-fpm-alpine
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
# Copy bare minimum to install Composer dependencies
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-interaction --no-autoloader --no-scripts
# ================================
# Stage 1-2: Yarn Install
# ================================
FROM --platform=$TARGETOS/$TARGETARCH node:20-alpine AS yarn
WORKDIR /build
# Copy bare minimum to install Yarn dependencies
COPY package.json yarn.lock ./
RUN yarn config set network-timeout 300000 \
&& yarn install --frozen-lockfile
# ================================
# Stage 2-1: Composer Optimize
# ================================
FROM --platform=$TARGETOS/$TARGETARCH composer AS composerbuild
# Copy full code to optimize autoload
COPY --exclude=Caddyfile --exclude=docker/ . ./
RUN composer dump-autoload --optimize
# ================================
# Stage 2-2: Build Frontend Assets
# ================================
FROM --platform=$TARGETOS/$TARGETARCH yarn AS yarnbuild
WORKDIR /build
# Copy full code
COPY --exclude=Caddyfile --exclude=docker/ . ./
COPY --from=composer /build .
RUN yarn run build
# ================================
# Stage 5: Build Final Application Image
# ================================
FROM --platform=$TARGETOS/$TARGETARCH localhost:5000/base-php:$TARGETARCH AS final
WORKDIR /var/www/html
# Install additional required libraries
# Install dependencies
RUN apk update && apk add --no-cache \
caddy ca-certificates supervisor supercronic
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 --chown=root:www-data --chmod=640 --from=composerbuild /build .
COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public
# Copy the Caddyfile to the container
COPY Caddyfile /etc/caddy/Caddyfile
# Set permissions
# First ensure all files are owned by root and restrict www-data to read access
RUN chown root:www-data ./ \
&& chmod 750 ./ \
# Files should not have execute set, but directories need it
&& find ./ -type d -exec chmod 750 {} \; \
# Create necessary directories
&& mkdir -p /pelican-data/storage /var/www/html/storage/app/public /var/run/supervisord /etc/supercronic \
# Symlinks for env, database, and avatars
&& ln -s /pelican-data/.env ./.env \
&& ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \
&& ln -sf /var/www/html/storage/app/public /var/www/html/public/storage \
&& ln -s /pelican-data/storage/avatars /var/www/html/storage/app/public/avatars \
&& ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \
# Allow www-data write permissions where necessary
&& chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord
# Copy the application code to the container
COPY . .
# Configure Supervisor
COPY docker/supervisord.conf /etc/supervisord.conf
COPY docker/Caddyfile /etc/caddy/Caddyfile
# Add Laravel scheduler to crontab
COPY docker/crontab /etc/supercronic/crontab
COPY --from=yarn /build/public/assets ./public/assets
COPY docker/entrypoint.sh ./docker/entrypoint.sh
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
@ -102,7 +55,5 @@ EXPOSE 80 443
VOLUME /pelican-data
USER www-data
ENTRYPOINT [ "/bin/ash", "docker/entrypoint.sh" ]
ENTRYPOINT [ "/bin/ash", ".github/docker/entrypoint.sh" ]
CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ]

View File

@ -1,10 +0,0 @@
# ================================
# Stage 0: Build PHP Base Image
# ================================
FROM --platform=$TARGETOS/$TARGETARCH php:8.4-fpm-alpine
ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
RUN install-php-extensions bcmath gd intl zip opcache pcntl posix pdo_mysql pdo_pgsql
RUN rm /usr/local/bin/install-php-extensions

View File

@ -1,112 +0,0 @@
# syntax=docker.io/docker/dockerfile:1.13-labs
# Pelican Development Dockerfile
FROM --platform=$TARGETOS/$TARGETARCH php:8.4-fpm-alpine AS base
ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
RUN install-php-extensions bcmath gd intl zip opcache pcntl posix pdo_mysql pdo_pgsql
RUN rm /usr/local/bin/install-php-extensions
# ================================
# Stage 1-1: Composer Install
# ================================
FROM --platform=$TARGETOS/$TARGETARCH base AS composer
WORKDIR /build
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
# Copy bare minimum to install Composer dependencies
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-interaction --no-autoloader --no-scripts
# ================================
# Stage 1-2: Yarn Install
# ================================
FROM --platform=$TARGETOS/$TARGETARCH node:20-alpine AS yarn
WORKDIR /build
# Copy bare minimum to install Yarn dependencies
COPY package.json yarn.lock ./
RUN yarn config set network-timeout 300000 \
&& yarn install --frozen-lockfile
# ================================
# Stage 2-1: Composer Optimize
# ================================
FROM --platform=$TARGETOS/$TARGETARCH composer AS composerbuild
# Copy full code to optimize autoload
COPY --exclude=Caddyfile --exclude=docker/ . ./
RUN composer dump-autoload --optimize
# ================================
# Stage 2-2: Build Frontend Assets
# ================================
FROM --platform=$TARGETOS/$TARGETARCH yarn AS yarnbuild
WORKDIR /build
# Copy full code
COPY --exclude=Caddyfile --exclude=docker/ . ./
COPY --from=composer /build .
RUN yarn run build
# ================================
# Stage 5: Build Final Application Image
# ================================
FROM --platform=$TARGETOS/$TARGETARCH base AS final
WORKDIR /var/www/html
# Install additional required libraries
RUN apk update && apk add --no-cache \
caddy ca-certificates supervisor supercronic
COPY --chown=root:www-data --chmod=640 --from=composerbuild /build .
COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public
# Set permissions
# First ensure all files are owned by root and restrict www-data to read access
RUN chown root:www-data ./ \
&& chmod 750 ./ \
# Files should not have execute set, but directories need it
&& find ./ -type d -exec chmod 750 {} \; \
# Create necessary directories
&& mkdir -p /pelican-data/storage /var/www/html/storage/app/public /var/run/supervisord /etc/supercronic \
# Symlinks for env, database, and avatars
&& ln -s /pelican-data/.env ./.env \
&& ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \
&& ln -sf /var/www/html/storage/app/public /var/www/html/public/storage \
&& ln -s /pelican-data/storage/avatars /var/www/html/storage/app/public/avatars \
&& ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \
# Allow www-data write permissions where necessary
&& chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord
# Configure Supervisor
COPY docker/supervisord.conf /etc/supervisord.conf
COPY docker/Caddyfile /etc/caddy/Caddyfile
# Add Laravel scheduler to crontab
COPY docker/crontab /etc/supercronic/crontab
COPY docker/entrypoint.sh ./docker/entrypoint.sh
HEALTHCHECK --interval=5m --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost/up || exit 1
EXPOSE 80 443
VOLUME /pelican-data
USER www-data
ENTRYPOINT [ "/bin/ash", "docker/entrypoint.sh" ]
CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ]

View File

@ -1,58 +0,0 @@
<?php
namespace App\Checks;
use Exception;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use Spatie\Health\Checks\Check;
use Spatie\Health\Checks\Result;
class CacheCheck extends Check
{
protected ?string $driver = null;
public function driver(string $driver): self
{
$this->driver = $driver;
return $this;
}
public function run(): Result
{
$driver = $this->driver ?? $this->defaultDriver();
$result = Result::make()->meta([
'driver' => $driver,
]);
try {
return $this->canWriteValuesToCache($driver)
? $result->ok(trans('admin/health.results.cache.ok'))
: $result->failed(trans('admin/health.results.cache.failed_retrieve'));
} catch (Exception $exception) {
return $result->failed(trans('admin/health.results.cache.failed', ['error' => $exception->getMessage()]));
}
}
protected function defaultDriver(): ?string
{
return config('cache.default', 'file');
}
protected function canWriteValuesToCache(?string $driver): bool
{
$expectedValue = Str::random(5);
$cacheName = "laravel-health:check-{$expectedValue}";
Cache::driver($driver)->put($cacheName, $expectedValue, 10);
$actualValue = Cache::driver($driver)->get($cacheName);
Cache::driver($driver)->forget($cacheName);
return $actualValue === $expectedValue;
}
}

View File

@ -1,42 +0,0 @@
<?php
namespace App\Checks;
use Exception;
use Illuminate\Support\Facades\DB;
use Spatie\Health\Checks\Check;
use Spatie\Health\Checks\Result;
class DatabaseCheck extends Check
{
protected ?string $connectionName = null;
public function connectionName(string $connectionName): self
{
$this->connectionName = $connectionName;
return $this;
}
public function run(): Result
{
$connectionName = $this->connectionName ?? $this->getDefaultConnectionName();
$result = Result::make()->meta([
'connection_name' => $connectionName,
]);
try {
DB::connection($connectionName)->getPdo();
return $result->ok(trans('admin/health.results.database.ok'));
} catch (Exception $exception) {
return $result->failed(trans('admin/health.results.database.failed', ['error' => $exception->getMessage()]));
}
}
protected function getDefaultConnectionName(): string
{
return config('database.default');
}
}

View File

@ -1,44 +0,0 @@
<?php
namespace App\Checks;
use Spatie\Health\Checks\Check;
use Spatie\Health\Checks\Result;
use function config;
class DebugModeCheck extends Check
{
protected bool $expected = false;
public function expectedToBe(bool $bool): self
{
$this->expected = $bool;
return $this;
}
public function run(): Result
{
$actual = config('app.debug');
$result = Result::make()
->meta([
'actual' => $actual,
'expected' => $this->expected,
])
->shortSummary($this->convertToWord($actual));
return $this->expected === $actual
? $result->ok()
: $result->failed(trans('admin/health.results.debugmode.failed', [
'actual' => $this->convertToWord($actual),
'expected' => $this->convertToWord($this->expected),
]));
}
protected function convertToWord(bool $boolean): string
{
return $boolean ? 'true' : 'false';
}
}

View File

@ -1,38 +0,0 @@
<?php
namespace App\Checks;
use Illuminate\Support\Facades\App;
use Spatie\Health\Checks\Check;
use Spatie\Health\Checks\Result;
class EnvironmentCheck extends Check
{
protected string $expectedEnvironment = 'production';
public function expectEnvironment(string $expectedEnvironment): self
{
$this->expectedEnvironment = $expectedEnvironment;
return $this;
}
public function run(): Result
{
$actualEnvironment = (string) App::environment();
$result = Result::make()
->meta([
'actual' => $actualEnvironment,
'expected' => $this->expectedEnvironment,
])
->shortSummary($actualEnvironment);
return $this->expectedEnvironment === $actualEnvironment
? $result->ok(trans('admin/health.results.environment.ok'))
: $result->failed(trans('admin/health.results.environment.failed', [
'actual' => $actualEnvironment,
'expected' => $this->expectedEnvironment,
]));
}
}

View File

@ -1,47 +0,0 @@
<?php
namespace App\Checks;
use App\Models\Node;
use App\Services\Helpers\SoftwareVersionService;
use Spatie\Health\Checks\Check;
use Spatie\Health\Checks\Result;
use Spatie\Health\Enums\Status;
class NodeVersionsCheck extends Check
{
public function __construct(private SoftwareVersionService $versionService) {}
public function run(): Result
{
$all = Node::all();
if ($all->isEmpty()) {
$result = Result::make()
->notificationMessage(trans('admin/health.results.nodeversions.no_nodes_created'))
->shortSummary(trans('admin/health.results.nodeversions.no_nodes'));
$result->status = Status::skipped();
return $result;
}
$outdated = $all
->filter(fn (Node $node) => !isset($node->systemInformation()['exception']) && !$this->versionService->isLatestWings($node->systemInformation()['version']))
->count();
$all = $all->count();
$latestVersion = $this->versionService->latestWingsVersion();
$result = Result::make()
->meta([
'all' => $all,
'outdated' => $outdated,
'latestVersion' => $latestVersion,
])
->shortSummary($outdated === 0 ? trans('admin/health.results.nodeversions.all_up_to_date') : trans('admin/health.results.nodeversions.outdated', ['outdated' => $outdated, 'all' => $all]));
return $outdated === 0
? $result->ok(trans('admin/health.results.nodeversions.ok'))
: $result->failed(trans('admin/health.results.nodeversions.failed', ['outdated' => $outdated, 'all' => $all]));
}
}

View File

@ -1,34 +0,0 @@
<?php
namespace App\Checks;
use App\Services\Helpers\SoftwareVersionService;
use Spatie\Health\Checks\Check;
use Spatie\Health\Checks\Result;
class PanelVersionCheck extends Check
{
public function __construct(private SoftwareVersionService $versionService) {}
public function run(): Result
{
$isLatest = $this->versionService->isLatestPanel();
$currentVersion = $this->versionService->currentPanelVersion();
$latestVersion = $this->versionService->latestPanelVersion();
$result = Result::make()
->meta([
'isLatest' => $isLatest,
'currentVersion' => $currentVersion,
'latestVersion' => $latestVersion,
])
->shortSummary($isLatest ? trans('admin/health.results.panelversion.up_to_date') : trans('admin/health.results.panelversion.outdated'));
return $isLatest
? $result->ok(trans('admin/health.results.panelversion.ok'))
: $result->failed(trans('admin/health.results.panelversion.failed', [
'currentVersion' => $currentVersion,
'latestVersion' => $latestVersion,
]));
}
}

View File

@ -1,41 +0,0 @@
<?php
namespace App\Checks;
use Carbon\Carbon;
use Composer\InstalledVersions;
use Spatie\Health\Checks\Checks\ScheduleCheck as BaseCheck;
use Spatie\Health\Checks\Result;
class ScheduleCheck extends BaseCheck
{
public function run(): Result
{
$result = Result::make()->ok(trans('admin/health.results.schedule.ok'));
$lastHeartbeatTimestamp = cache()->store($this->cacheStoreName)->get($this->cacheKey);
if (!$lastHeartbeatTimestamp) {
return $result->failed(trans('admin/health.results.schedule.failed_not_ran'));
}
$latestHeartbeatAt = Carbon::createFromTimestamp($lastHeartbeatTimestamp);
$carbonVersion = InstalledVersions::getVersion('nesbot/carbon');
$minutesAgo = $latestHeartbeatAt->diffInMinutes();
if (version_compare($carbonVersion,
'3.0.0', '<')) {
$minutesAgo += 1;
}
if ($minutesAgo > $this->heartbeatMaxAgeInMinutes) {
return $result->failed(trans('admin/health.results.schedule.failed_last_ran', [
'time' => $minutesAgo,
]));
}
return $result;
}
}

View File

@ -1,16 +0,0 @@
<?php
namespace App\Checks;
use Spatie\Health\Checks\Checks\UsedDiskSpaceCheck as BaseCheck;
class UsedDiskSpaceCheck extends BaseCheck
{
protected function getDiskUsagePercentage(): int
{
$freeSpace = disk_free_space($this->filesystemName ?? '/');
$totalSpace = disk_total_space($this->filesystemName ?? '/');
return 100 - ($freeSpace * 100 / $totalSpace);
}
}

View File

@ -16,37 +16,28 @@ class CheckEggUpdatesCommand extends Command
$eggs = Egg::all();
foreach ($eggs as $egg) {
try {
$this->check($egg, $exporterService);
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()})");
}
}
}
private function check(Egg $egg, EggExporterService $exporterService): void
{
if (is_null($egg->update_url)) {
$this->comment("$egg->name: Skipping (no update url set)");
return;
}
$currentJson = json_decode($exporterService->handle($egg->id));
unset($currentJson->exported_at);
$updatedEgg = file_get_contents($egg->update_url);
assert($updatedEgg !== false);
$updatedJson = json_decode($updatedEgg);
unset($updatedJson->exported_at);
if (md5(json_encode($currentJson, JSON_THROW_ON_ERROR)) === md5(json_encode($updatedJson, JSON_THROW_ON_ERROR))) {
$this->info("$egg->name: Up-to-date");
cache()->put("eggs.$egg->uuid.update", false, now()->addHour());
return;
}
$this->warn("$egg->name: Found update");
cache()->put("eggs.$egg->uuid.update", true, now()->addHour());
}
}

View File

@ -3,6 +3,7 @@
namespace App\Console\Commands\Environment;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
class AppSettingsCommand extends Command
{
@ -20,13 +21,9 @@ class AppSettingsCommand extends Command
if (!config('app.key')) {
$this->comment('Generating app key');
$this->call('key:generate');
Artisan::call('key:generate');
}
$this->comment('Creating storage link');
$this->call('storage:link');
$this->comment('Caching components & icons');
$this->call('filament:optimize');
Artisan::call('filament:optimize');
}
}

View File

@ -27,6 +27,8 @@ class CacheSettingsCommand extends Command
{--redis-pass= : Password used to connect to redis.}
{--redis-port= : Port to connect to redis over.}';
protected array $variables = [];
/**
* CacheSettingsCommand constructor.
*/

View File

@ -27,7 +27,6 @@ class DatabaseSettingsCommand extends Command
{--username= : Username to use when connecting to the MySQL/ MariaDB server.}
{--password= : Password to use for the MySQL/ MariaDB database.}';
/** @var array<array-key, mixed> */
protected array $variables = [];
/**
@ -58,7 +57,7 @@ class DatabaseSettingsCommand extends Command
);
if ($this->variables['DB_CONNECTION'] === 'mysql') {
$this->output->note(trans('commands.database_settings.DB_HOST_note'));
$this->output->note(__('commands.database_settings.DB_HOST_note'));
$this->variables['DB_HOST'] = $this->option('host') ?? $this->ask(
'Database Host',
config('database.connections.mysql.host', '127.0.0.1')
@ -74,7 +73,7 @@ class DatabaseSettingsCommand extends Command
config('database.connections.mysql.database', 'panel')
);
$this->output->note(trans('commands.database_settings.DB_USERNAME_note'));
$this->output->note(__('commands.database_settings.DB_USERNAME_note'));
$this->variables['DB_USERNAME'] = $this->option('username') ?? $this->ask(
'Database Username',
config('database.connections.mysql.username', 'pelican')
@ -83,7 +82,7 @@ class DatabaseSettingsCommand extends Command
$askForMySQLPassword = true;
if (!empty(config('database.connections.mysql.password')) && $this->input->isInteractive()) {
$this->variables['DB_PASSWORD'] = config('database.connections.mysql.password');
$askForMySQLPassword = $this->confirm(trans('commands.database_settings.DB_PASSWORD_note'));
$askForMySQLPassword = $this->confirm(__('commands.database_settings.DB_PASSWORD_note'));
}
if ($askForMySQLPassword) {
@ -107,9 +106,9 @@ class DatabaseSettingsCommand extends Command
$this->database->connection('_panel_command_test')->getPdo();
} catch (\PDOException $exception) {
$this->output->error(sprintf('Unable to connect to the MySQL server using the provided credentials. The error returned was "%s".', $exception->getMessage()));
$this->output->error(trans('commands.database_settings.DB_error_2'));
$this->output->error(__('commands.database_settings.DB_error_2'));
if ($this->confirm(trans('commands.database_settings.go_back'))) {
if ($this->confirm(__('commands.database_settings.go_back'))) {
$this->database->disconnect('_panel_command_test');
return $this->handle();
@ -118,7 +117,7 @@ class DatabaseSettingsCommand extends Command
return 1;
}
} elseif ($this->variables['DB_CONNECTION'] === 'mariadb') {
$this->output->note(trans('commands.database_settings.DB_HOST_note'));
$this->output->note(__('commands.database_settings.DB_HOST_note'));
$this->variables['DB_HOST'] = $this->option('host') ?? $this->ask(
'Database Host',
config('database.connections.mariadb.host', '127.0.0.1')
@ -134,7 +133,7 @@ class DatabaseSettingsCommand extends Command
config('database.connections.mariadb.database', 'panel')
);
$this->output->note(trans('commands.database_settings.DB_USERNAME_note'));
$this->output->note(__('commands.database_settings.DB_USERNAME_note'));
$this->variables['DB_USERNAME'] = $this->option('username') ?? $this->ask(
'Database Username',
config('database.connections.mariadb.username', 'pelican')
@ -143,7 +142,7 @@ class DatabaseSettingsCommand extends Command
$askForMariaDBPassword = true;
if (!empty(config('database.connections.mariadb.password')) && $this->input->isInteractive()) {
$this->variables['DB_PASSWORD'] = config('database.connections.mariadb.password');
$askForMariaDBPassword = $this->confirm(trans('commands.database_settings.DB_PASSWORD_note'));
$askForMariaDBPassword = $this->confirm(__('commands.database_settings.DB_PASSWORD_note'));
}
if ($askForMariaDBPassword) {
@ -167,9 +166,9 @@ class DatabaseSettingsCommand extends Command
$this->database->connection('_panel_command_test')->getPdo();
} catch (\PDOException $exception) {
$this->output->error(sprintf('Unable to connect to the MariaDB server using the provided credentials. The error returned was "%s".', $exception->getMessage()));
$this->output->error(trans('commands.database_settings.DB_error_2'));
$this->output->error(__('commands.database_settings.DB_error_2'));
if ($this->confirm(trans('commands.database_settings.go_back'))) {
if ($this->confirm(__('commands.database_settings.go_back'))) {
$this->database->disconnect('_panel_command_test');
return $this->handle();
@ -180,7 +179,7 @@ class DatabaseSettingsCommand extends Command
} elseif ($this->variables['DB_CONNECTION'] === 'sqlite') {
$this->variables['DB_DATABASE'] = $this->option('database') ?? $this->ask(
'Database Path',
(string) env('DB_DATABASE', 'database.sqlite')
env('DB_DATABASE', 'database.sqlite')
);
}

View File

@ -22,7 +22,6 @@ class EmailSettingsCommand extends Command
{--username=}
{--password=}';
/** @var array<array-key, mixed> */
protected array $variables = [];
/**
@ -92,7 +91,7 @@ class EmailSettingsCommand extends Command
trans('command/messages.environment.mail.ask_smtp_password')
);
$this->variables['MAIL_SCHEME'] = $this->option('encryption') ?? $this->choice(
$this->variables['MAIL_ENCRYPTION'] = $this->option('encryption') ?? $this->choice(
trans('command/messages.environment.mail.ask_encryption'),
['tls' => 'TLS', 'ssl' => 'SSL', '' => 'None'],
config('mail.mailers.smtp.encryption', 'tls')

View File

@ -27,6 +27,8 @@ class QueueSettingsCommand extends Command
{--redis-pass= : Password used to connect to redis.}
{--redis-port= : Port to connect to redis over.}';
protected array $variables = [];
/**
* QueueSettingsCommand constructor.
*/

View File

@ -18,21 +18,10 @@ class QueueWorkerServiceCommand extends Command
public function handle(): void
{
if (@file_exists('/.dockerenv')) {
$result = Process::run('supervisorctl restart queue-worker');
if ($result->failed()) {
$this->error('Error restarting service: ' . $result->errorOutput());
return;
}
$this->line('Queue worker service file updated successfully.');
return;
}
$serviceName = $this->option('service-name') ?? $this->ask('Queue worker service name', 'pelican-queue');
$path = '/etc/systemd/system/' . $serviceName . '.service';
$fileExists = @file_exists($path);
$fileExists = file_exists($path);
if ($fileExists && !$this->option('overwrite') && !$this->confirm('The service file already exists. Do you want to overwrite it?')) {
$this->line('Creation of queue worker service file aborted because service file already exists.');

View File

@ -20,6 +20,8 @@ class RedisSetupCommand extends Command
{--redis-pass= : Password used to connect to redis.}
{--redis-port= : Port to connect to redis over.}';
protected array $variables = [];
/**
* RedisSetupCommand constructor.
*/
@ -35,7 +37,7 @@ class RedisSetupCommand extends Command
{
$this->variables['CACHE_STORE'] = 'redis';
$this->variables['QUEUE_CONNECTION'] = 'redis';
$this->variables['SESSION_DRIVER'] = 'redis';
$this->variables['SESSION_DRIVERS'] = 'redis';
$this->requestRedisSettings();

View File

@ -28,6 +28,8 @@ class SessionSettingsCommand extends Command
{--redis-pass= : Password used to connect to redis.}
{--redis-port= : Port to connect to redis over.}';
protected array $variables = [];
/**
* SessionSettingsCommand constructor.
*/

View File

@ -3,6 +3,7 @@
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Services\Helpers\SoftwareVersionService;
class InfoCommand extends Command
{
@ -10,8 +11,98 @@ class InfoCommand extends Command
protected $signature = 'p:info';
/**
* InfoCommand constructor.
*/
public function __construct(private SoftwareVersionService $versionService)
{
parent::__construct();
}
/**
* Handle execution of command.
*/
public function handle(): void
{
$this->call('about');
$this->output->title('Version Information');
$this->table([], [
['Panel Version', $this->versionService->versionData()['version']],
['Latest Version', $this->versionService->getPanel()],
['Up-to-Date', $this->versionService->isLatestPanel() ? 'Yes' : $this->formatText('No', 'bg=red')],
], 'compact');
$this->output->title('Application Configuration');
$this->table([], [
['Environment', config('app.env') === 'production' ? config('app.env') : $this->formatText(config('app.env'), 'bg=red')],
['Debug Mode', config('app.debug') ? $this->formatText('Yes', 'bg=red') : 'No'],
['Application Name', config('app.name')],
['Application URL', config('app.url')],
['Installation Directory', base_path()],
['Cache Driver', config('cache.default')],
['Queue Driver', config('queue.default') === 'sync' ? $this->formatText(config('queue.default'), 'bg=red') : config('queue.default')],
['Session Driver', config('session.driver')],
['Filesystem Driver', config('filesystems.default')],
], 'compact');
$this->output->title('Database Configuration');
$driver = config('database.default');
if ($driver === 'sqlite') {
$this->table([], [
['Driver', $driver],
['Database', config("database.connections.$driver.database")],
], 'compact');
} else {
$this->table([], [
['Driver', $driver],
['Host', config("database.connections.$driver.host")],
['Port', config("database.connections.$driver.port")],
['Database', config("database.connections.$driver.database")],
['Username', config("database.connections.$driver.username")],
], 'compact');
}
$this->output->title('Email Configuration');
$driver = config('mail.default');
if ($driver === 'smtp') {
$this->table([], [
['Driver', $driver],
['Host', config("mail.mailers.$driver.host")],
['Port', config("mail.mailers.$driver.port")],
['Username', config("mail.mailers.$driver.username")],
['Encryption', config("mail.mailers.$driver.encryption")],
['From Address', config('mail.from.address')],
['From Name', config('mail.from.name')],
], 'compact');
} else {
$this->table([], [
['Driver', $driver],
['From Address', config('mail.from.address')],
['From Name', config('mail.from.name')],
], 'compact');
}
$this->output->title('Backup Configuration');
$driver = config('backups.default');
if ($driver === 's3') {
$this->table([], [
['Driver', $driver],
['Region', config("backups.disks.$driver.region")],
['Bucket', config("backups.disks.$driver.bucket")],
['Endpoint', config("backups.disks.$driver.endpoint")],
['Use path style endpoint', config("backups.disks.$driver.use_path_style_endpoint") ? 'Yes' : 'No'],
], 'compact');
} else {
$this->table([], [
['Driver', $driver],
], 'compact');
}
}
/**
* Format output in a Name: Value manner.
*/
private function formatText(string $value, string $opts = ''): string
{
return sprintf('<%s>%s</>', $opts, $value);
}
}

View File

@ -6,7 +6,6 @@ use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory;
use SplFileInfo;
class CleanServiceBackupFilesCommand extends Command
{
@ -33,10 +32,9 @@ class CleanServiceBackupFilesCommand extends Command
*/
public function handle(): void
{
/** @var SplFileInfo[] */
$files = $this->disk->files('services/.bak');
collect($files)->each(function ($file) {
collect($files)->each(function (\SplFileInfo $file) {
$lastModified = Carbon::createFromTimestamp($this->disk->lastModified($file->getPath()));
if ($lastModified->diffInMinutes(Carbon::now()) > self::BACKUP_THRESHOLD_MINUTES) {
$this->disk->delete($file->getPath());

View File

@ -2,8 +2,8 @@
namespace App\Console\Commands\Node;
use App\Models\Node;
use Illuminate\Console\Command;
use App\Services\Nodes\NodeCreationService;
class MakeNodeCommand extends Command
{
@ -24,13 +24,20 @@ class MakeNodeCommand extends Command
{--overallocateCpu= : Enter the amount of cpu to overallocate (% or -1 to overallocate the maximum).}
{--uploadSize= : Enter the maximum upload filesize.}
{--daemonListeningPort= : Enter the daemon listening port.}
{--daemonConnectingPort= : Enter the daemon connecting port.}
{--daemonSFTPPort= : Enter the daemon SFTP listening port.}
{--daemonSFTPAlias= : Enter the daemon SFTP alias.}
{--daemonBase= : Enter the base folder.}';
protected $description = 'Creates a new node on the system via the CLI.';
/**
* MakeNodeCommand constructor.
*/
public function __construct(private NodeCreationService $creationService)
{
parent::__construct();
}
/**
* Handle the command execution process.
*
@ -38,32 +45,31 @@ class MakeNodeCommand extends Command
*/
public function handle(): void
{
$data['name'] = $this->option('name') ?? $this->ask(trans('commands.make_node.name'));
$data['description'] = $this->option('description') ?? $this->ask(trans('commands.make_node.description'));
$data['name'] = $this->option('name') ?? $this->ask(__('commands.make_node.name'));
$data['description'] = $this->option('description') ?? $this->ask(__('commands.make_node.description'));
$data['scheme'] = $this->option('scheme') ?? $this->anticipate(
trans('commands.make_node.scheme'),
__('commands.make_node.scheme'),
['https', 'http'],
'https'
);
$data['fqdn'] = $this->option('fqdn') ?? $this->ask(trans('commands.make_node.fqdn'));
$data['public'] = $this->option('public') ?? $this->confirm(trans('commands.make_node.public'), true);
$data['behind_proxy'] = $this->option('proxy') ?? $this->confirm(trans('commands.make_node.behind_proxy'));
$data['maintenance_mode'] = $this->option('maintenance') ?? $this->confirm(trans('commands.make_node.maintenance_mode'));
$data['memory'] = $this->option('maxMemory') ?? $this->ask(trans('commands.make_node.memory'), '0');
$data['memory_overallocate'] = $this->option('overallocateMemory') ?? $this->ask(trans('commands.make_node.memory_overallocate'), '-1');
$data['disk'] = $this->option('maxDisk') ?? $this->ask(trans('commands.make_node.disk'), '0');
$data['disk_overallocate'] = $this->option('overallocateDisk') ?? $this->ask(trans('commands.make_node.disk_overallocate'), '-1');
$data['cpu'] = $this->option('maxCpu') ?? $this->ask(trans('commands.make_node.cpu'), '0');
$data['cpu_overallocate'] = $this->option('overallocateCpu') ?? $this->ask(trans('commands.make_node.cpu_overallocate'), '-1');
$data['upload_size'] = $this->option('uploadSize') ?? $this->ask(trans('commands.make_node.upload_size'), '256');
$data['daemon_listen'] = $this->option('daemonListeningPort') ?? $this->ask(trans('commands.make_node.daemonListen'), '8080');
$data['daemon_connect'] = $this->option('daemonConnectingPort') ?? $this->ask(trans('commands.make_node.daemonConnect'), '8080');
$data['daemon_sftp'] = $this->option('daemonSFTPPort') ?? $this->ask(trans('commands.make_node.daemonSFTP'), '2022');
$data['daemon_sftp_alias'] = $this->option('daemonSFTPAlias') ?? $this->ask(trans('commands.make_node.daemonSFTPAlias'), '');
$data['daemon_base'] = $this->option('daemonBase') ?? $this->ask(trans('commands.make_node.daemonBase'), '/var/lib/pelican/volumes');
$data['fqdn'] = $this->option('fqdn') ?? $this->ask(__('commands.make_node.fqdn'));
$data['public'] = $this->option('public') ?? $this->confirm(__('commands.make_node.public'), true);
$data['behind_proxy'] = $this->option('proxy') ?? $this->confirm(__('commands.make_node.behind_proxy'));
$data['maintenance_mode'] = $this->option('maintenance') ?? $this->confirm(__('commands.make_node.maintenance_mode'));
$data['memory'] = $this->option('maxMemory') ?? $this->ask(__('commands.make_node.memory'), '0');
$data['memory_overallocate'] = $this->option('overallocateMemory') ?? $this->ask(__('commands.make_node.memory_overallocate'), '-1');
$data['disk'] = $this->option('maxDisk') ?? $this->ask(__('commands.make_node.disk'), '0');
$data['disk_overallocate'] = $this->option('overallocateDisk') ?? $this->ask(__('commands.make_node.disk_overallocate'), '-1');
$data['cpu'] = $this->option('maxCpu') ?? $this->ask(__('commands.make_node.cpu'), '0');
$data['cpu_overallocate'] = $this->option('overallocateCpu') ?? $this->ask(__('commands.make_node.cpu_overallocate'), '-1');
$data['upload_size'] = $this->option('uploadSize') ?? $this->ask(__('commands.make_node.upload_size'), '256');
$data['daemon_listen'] = $this->option('daemonListeningPort') ?? $this->ask(__('commands.make_node.daemonListen'), '8080');
$data['daemon_sftp'] = $this->option('daemonSFTPPort') ?? $this->ask(__('commands.make_node.daemonSFTP'), '2022');
$data['daemon_sftp_alias'] = $this->option('daemonSFTPAlias') ?? $this->ask(__('commands.make_node.daemonSFTPAlias'), '');
$data['daemon_base'] = $this->option('daemonBase') ?? $this->ask(__('commands.make_node.daemonBase'), '/var/lib/pelican/volumes');
$node = Node::create($data);
$this->line(trans('commands.make_node.success', ['name' => $data['name'], 'id' => $node->id]));
$node = $this->creationService->handle($data);
$this->line(__('commands.make_node.success', ['name' => $data['name'], 'id' => $node->id]));
}
}

View File

@ -19,14 +19,14 @@ class NodeConfigurationCommand extends Command
/** @var \App\Models\Node $node */
$node = Node::query()->where($column, $this->argument('node'))->firstOr(function () {
$this->error(trans('commands.node_config.error_not_exist'));
$this->error(__('commands.node_config.error_not_exist'));
exit(1);
});
$format = $this->option('format');
if (!in_array($format, ['yaml', 'yml', 'json'])) {
$this->error(trans('commands.node_config.error_invalid_format'));
$this->error(__('commands.node_config.error_invalid_format'));
return 1;
}

View File

@ -13,12 +13,12 @@ class KeyGenerateCommand extends BaseKeyGenerateCommand
public function handle(): void
{
if (!empty(config('app.key')) && $this->input->isInteractive()) {
$this->output->warning(trans('commands.key_generate.error_already_exist'));
if (!$this->confirm(trans('commands.key_generate.understand'))) {
$this->output->warning(__('commands.key_generate.error_already_exist'));
if (!$this->confirm(__('commands.key_generate.understand'))) {
return;
}
if (!$this->confirm(trans('commands.key_generate.continue'))) {
if (!$this->confirm(__('commands.key_generate.continue'))) {
return;
}
}

View File

@ -6,7 +6,6 @@ use Illuminate\Console\Command;
use App\Models\Schedule;
use Illuminate\Database\Eloquent\Builder;
use App\Services\Schedules\ProcessScheduleService;
use Throwable;
class ProcessRunnableCommand extends Command
{
@ -14,7 +13,10 @@ class ProcessRunnableCommand extends Command
protected $description = 'Process schedules in the database and determine which are ready to run.';
public function handle(ProcessScheduleService $processScheduleService): int
/**
* Handle command execution.
*/
public function handle(): int
{
$schedules = Schedule::query()
->with('tasks')
@ -25,7 +27,7 @@ class ProcessRunnableCommand extends Command
->get();
if ($schedules->count() < 1) {
$this->line(trans('commands.schedule.process.no_tasks'));
$this->line(__('commands.schedule.process.no_tasks'));
return 0;
}
@ -33,7 +35,7 @@ class ProcessRunnableCommand extends Command
$bar = $this->output->createProgressBar(count($schedules));
foreach ($schedules as $schedule) {
$bar->clear();
$this->processSchedule($processScheduleService, $schedule);
$this->processSchedule($schedule);
$bar->advance();
$bar->display();
}
@ -48,23 +50,23 @@ 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(ProcessScheduleService $processScheduleService, Schedule $schedule): void
protected function processSchedule(Schedule $schedule): void
{
if ($schedule->tasks->isEmpty()) {
return;
}
try {
$processScheduleService->handle($schedule);
$this->getLaravel()->make(ProcessScheduleService::class)->handle($schedule);
$this->line(trans('command/messages.schedule.output_line', [
'schedule' => $schedule->name,
'id' => $schedule->id,
]));
} catch (Throwable $exception) {
} catch (\Throwable|\Exception $exception) {
logger()->error($exception, ['schedule_id' => $schedule->id]);
$this->error(trans('commands.schedule.process.no_tasks') . " #$schedule->id: " . $exception->getMessage());
$this->error(__('commands.schedule.process.no_tasks') . " #$schedule->id: " . $exception->getMessage());
}
}
}

View File

@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Builder;
use Illuminate\Validation\ValidationException;
use Illuminate\Validation\Factory as ValidatorFactory;
use App\Repositories\Daemon\DaemonPowerRepository;
use Exception;
use App\Exceptions\Http\Connection\DaemonConnectionException;
class BulkPowerActionCommand extends Command
{
@ -19,13 +19,26 @@ class BulkPowerActionCommand extends Command
protected $description = 'Perform bulk power management on large groupings of servers or nodes at once.';
public function handle(DaemonPowerRepository $powerRepository, ValidatorFactory $validator): void
/**
* BulkPowerActionCommand constructor.
*/
public function __construct(private DaemonPowerRepository $powerRepository, private ValidatorFactory $validator)
{
parent::__construct();
}
/**
* Handle the bulk power request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function handle(): void
{
$action = $this->argument('action');
$nodes = empty($this->option('nodes')) ? [] : explode(',', $this->option('nodes'));
$servers = empty($this->option('servers')) ? [] : explode(',', $this->option('servers'));
$validator = $validator->make([
$validator = $this->validator->make([
'action' => $action,
'nodes' => $nodes,
'servers' => $servers,
@ -51,17 +64,13 @@ class BulkPowerActionCommand extends Command
}
$bar = $this->output->createProgressBar($count);
$this->getQueryBuilder($servers, $nodes)->get()->each(function ($server, int $index) use ($action, $powerRepository, &$bar): mixed {
$powerRepository = $this->powerRepository;
$this->getQueryBuilder($servers, $nodes)->each(function (Server $server) use ($action, $powerRepository, &$bar) {
$bar->clear();
if (!$server instanceof Server) {
return null;
}
try {
$powerRepository->setServer($server)->send($action);
} catch (Exception $exception) {
} catch (DaemonConnectionException $exception) {
$this->output->error(trans('command/messages.server.power.action_failed', [
'name' => $server->name,
'id' => $server->id,
@ -72,8 +81,6 @@ class BulkPowerActionCommand extends Command
$bar->advance();
$bar->display();
return null;
});
$this->line('');
@ -81,9 +88,6 @@ class BulkPowerActionCommand extends Command
/**
* Returns the query builder instance that will return the servers that should be affected.
*
* @param string[]|int[] $servers
* @param string[]|int[] $nodes
*/
protected function getQueryBuilder(array $servers, array $nodes): Builder
{

View File

@ -34,26 +34,30 @@ class UpgradeCommand extends Command
{
$skipDownload = $this->option('skip-download');
if (!$skipDownload) {
$this->output->warning(trans('commands.upgrade.integrity'));
$this->output->comment(trans('commands.upgrade.source_url'));
$this->output->warning(__('commands.upgrade.integrity'));
$this->output->comment(__('commands.upgrade.source_url'));
$this->line($this->getUrl());
}
if (version_compare(PHP_VERSION, '7.4.0') < 0) {
$this->error(__('commands.upgrade.php_version') . ' [' . PHP_VERSION . '].');
}
$user = 'www-data';
$group = 'www-data';
if ($this->input->isInteractive()) {
if (!$skipDownload) {
$skipDownload = !$this->confirm(trans('commands.upgrade.skipDownload'), true);
$skipDownload = !$this->confirm(__('commands.upgrade.skipDownload'), true);
}
if (is_null($this->option('user'))) {
$userDetails = function_exists('posix_getpwuid') ? posix_getpwuid(fileowner('public')) : [];
$user = $userDetails['name'] ?? 'www-data';
$message = trans('commands.upgrade.webserver_user', ['user' => $user]);
$message = __('commands.upgrade.webserver_user', ['user' => $user]);
if (!$this->confirm($message, true)) {
$user = $this->anticipate(
trans('commands.upgrade.name_webserver'),
__('commands.upgrade.name_webserver'),
[
'www-data',
'nginx',
@ -67,10 +71,10 @@ class UpgradeCommand extends Command
$groupDetails = function_exists('posix_getgrgid') ? posix_getgrgid(filegroup('public')) : [];
$group = $groupDetails['name'] ?? 'www-data';
$message = trans('commands.upgrade.group_webserver', ['group' => $user]);
$message = __('commands.upgrade.group_webserver', ['group' => $user]);
if (!$this->confirm($message, true)) {
$group = $this->anticipate(
trans('commands.upgrade.group_webserver_question'),
__('commands.upgrade.group_webserver_question'),
[
'www-data',
'nginx',
@ -80,8 +84,8 @@ class UpgradeCommand extends Command
}
}
if (!$this->confirm(trans('commands.upgrade.are_your_sure'))) {
$this->warn(trans('commands.upgrade.terminated'));
if (!$this->confirm(__('commands.upgrade.are_your_sure'))) {
$this->warn(__('commands.upgrade.terminated'));
return;
}
@ -171,7 +175,7 @@ class UpgradeCommand extends Command
});
$this->newLine(2);
$this->info(trans('commands.upgrade.success'));
$this->info(__('commands.upgrade.success'));
}
protected function withProgress(ProgressBar $bar, \Closure $callback): void

View File

@ -19,7 +19,7 @@ class DisableTwoFactorCommand extends Command
public function handle(): void
{
if ($this->input->isInteractive()) {
$this->output->warning(trans('command/messages.user.2fa_help_text.0') . trans('command/messages.user.2fa_help_text.1'));
$this->output->warning(trans('command/messages.user.2fa_help_text'));
}
$email = $this->option('email') ?? $this->ask(trans('command/messages.user.ask_email'));

View File

@ -7,13 +7,11 @@ 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 Spatie\Health\Commands\RunHealthChecksCommand;
use Spatie\Health\Commands\ScheduleCheckHeartbeatCommand;
class Kernel extends ConsoleKernel
{
@ -30,11 +28,8 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule): void
{
if (config('cache.default') === 'redis') {
// https://laravel.com/docs/10.x/upgrade#redis-cache-tags
// This only needs to run when using redis. anything else throws an error.
$schedule->command('cache:prune-stale-tags')->hourly();
}
// https://laravel.com/docs/10.x/upgrade#redis-cache-tags
$schedule->command('cache:prune-stale-tags')->hourly();
// Execute scheduled commands for servers every minute, as if there was a normal cron running.
$schedule->command(ProcessRunnableCommand::class)->everyMinute()->withoutOverlapping();
@ -43,6 +38,8 @@ class Kernel extends ConsoleKernel
$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.
$schedule->command(PruneOrphanedBackupsCommand::class)->everyThirtyMinutes();
@ -51,12 +48,5 @@ class Kernel extends ConsoleKernel
if (config('activity.prune_days')) {
$schedule->command(PruneCommand::class, ['--model' => [ActivityLog::class]])->daily();
}
if (config('panel.webhook.prune_days')) {
$schedule->command(PruneCommand::class, ['--model' => [Webhook::class]])->daily();
}
$schedule->command(ScheduleCheckHeartbeatCommand::class)->everyMinute();
$schedule->command(RunHealthChecksCommand::class)->everyFiveMinutes();
}
}

View File

@ -1,22 +0,0 @@
<?php
namespace App\Contracts;
use Illuminate\Validation\Validator;
interface Validatable
{
public function getValidator(): Validator;
/**
* @return array<string, mixed>
*/
public static function getRules(): array;
/**
* @return array<string, array<string, mixed>>
*/
public static function getRulesForField(string $field): array;
public function validate(): void;
}

View File

@ -1,24 +0,0 @@
<?php
namespace App\Eloquent;
use Illuminate\Database\Eloquent\Builder;
/**
* @template TModel of \Illuminate\Database\Eloquent\Model
*
* @extends Builder<TModel>
*/
class BackupQueryBuilder extends Builder
{
public function nonFailed(): self
{
$this->where(function (Builder $query) {
$query
->whereNull('completed_at')
->orWhere('is_successful', true);
});
return $this;
}
}

View File

@ -1,37 +0,0 @@
<?php
namespace App\Enums;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
enum BackupStatus: string implements HasColor, HasIcon, HasLabel
{
case InProgress = 'in_progress';
case Successful = 'successful';
case Failed = 'failed';
public function getIcon(): string
{
return match ($this) {
self::InProgress => 'tabler-circle-dashed',
self::Successful => 'tabler-circle-check',
self::Failed => 'tabler-circle-x',
};
}
public function getColor(): string
{
return match ($this) {
self::InProgress => 'primary',
self::Successful => 'success',
self::Failed => 'danger',
};
}
public function getLabel(): string
{
return str($this->value)->headline();
}
}

View File

@ -1,11 +0,0 @@
<?php
namespace App\Enums;
enum ConsoleWidgetPosition: string
{
case Top = 'top';
case AboveConsole = 'above_console';
case BelowConsole = 'below_console';
case Bottom = 'bottom';
}

View File

@ -2,11 +2,7 @@
namespace App\Enums;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
enum ContainerStatus: string
{
// Docker Based
case Created = 'created';
@ -23,7 +19,7 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
// HTTP Based
case Missing = 'missing';
public function getIcon(): string
public function icon(): string
{
return match ($this) {
@ -40,17 +36,8 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
};
}
public function getColor(bool $hex = false): string
public function color(): string
{
if ($hex) {
return match ($this) {
self::Created, self::Restarting => '#2563EB',
self::Starting, self::Paused, self::Removing, self::Stopping => '#D97706',
self::Running => '#22C55E',
self::Exited, self::Missing, self::Dead, self::Offline => '#EF4444',
};
}
return match ($this) {
self::Created => 'primary',
self::Starting => 'warning',
@ -62,51 +49,7 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
self::Removing => 'warning',
self::Missing => 'danger',
self::Stopping => 'warning',
self::Offline => 'danger',
self::Offline => 'gray',
};
}
public function getLabel(): string
{
return str($this->value)->title();
}
public function isOffline(): bool
{
return in_array($this, [ContainerStatus::Offline, ContainerStatus::Missing]);
}
public function isStartingOrRunning(): bool
{
return in_array($this, [ContainerStatus::Starting, ContainerStatus::Running]);
}
public function isStartingOrStopping(): bool
{
return in_array($this, [ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting]);
}
public function isStartable(): bool
{
return !in_array($this, [ContainerStatus::Running, ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting, ContainerStatus::Missing]);
}
public function isRestartable(): bool
{
if ($this->isStartable()) {
return true;
}
return !in_array($this, [ContainerStatus::Offline, ContainerStatus::Missing]);
}
public function isStoppable(): bool
{
return !in_array($this, [ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting, ContainerStatus::Exited, ContainerStatus::Offline, ContainerStatus::Missing]);
}
public function isKillable(): bool
{
return !in_array($this, [ContainerStatus::Offline, ContainerStatus::Running, ContainerStatus::Exited, ContainerStatus::Missing]);
}
}

View File

@ -1,141 +0,0 @@
<?php
namespace App\Enums;
use Filament\Support\Contracts\HasLabel;
enum EditorLanguages: string implements HasLabel
{
case plaintext = 'plaintext';
case abap = 'abap';
case apex = 'apex';
case azcali = 'azcali';
case bat = 'bat';
case bicep = 'bicep';
case cameligo = 'cameligo';
case coljure = 'coljure';
case coffeescript = 'coffeescript';
case c = 'c';
case cpp = 'cpp';
case csharp = 'csharp';
case csp = 'csp';
case css = 'css';
case cypher = 'cypher';
case dart = 'dart';
case dockerfile = 'dockerfile';
case ecl = 'ecl';
case elixir = 'elixir';
case flow9 = 'flow9';
case fsharp = 'fsharp';
case go = 'go';
case graphql = 'graphql';
case handlebars = 'handlebars';
case hcl = 'hcl';
case html = 'html';
case ini = 'ini';
case java = 'java';
case javascript = 'javascript';
case julia = 'julia';
case json = 'json';
case kotlin = 'kotlin';
case less = 'less';
case lexon = 'lexon';
case lua = 'lua';
case liquid = 'liquid';
case m3 = 'm3';
case markdown = 'markdown';
case mdx = 'mdx';
case mips = 'mips';
case msdax = 'msdax';
case mysql = 'mysql';
case objectivec = 'objective-c';
case pascal = 'pascal';
case pascaligo = 'pascaligo';
case perl = 'perl';
case pgsql = 'pgsql';
case php = 'php';
case pla = 'pla';
case postiats = 'postiats';
case powerquery = 'powerquery';
case powershell = 'powershell';
case proto = 'proto';
case pug = 'pug';
case python = 'python';
case qsharp = 'qsharp';
case r = 'r';
case razor = 'razor';
case redis = 'redis';
case redshift = 'redshift';
case restructuredtext = 'restructuredtext';
case ruby = 'ruby';
case rust = 'rust';
case sb = 'sb';
case scala = 'scala';
case scheme = 'scheme';
case scss = 'scss';
case shell = 'shell';
case sol = 'sol';
case aes = 'aes';
case sparql = 'sparql';
case sql = 'sql';
case st = 'st';
case swift = 'swift';
case systemverilog = 'systemverilog';
case verilog = 'verilog';
case tcl = 'tcl';
case twig = 'twig';
case typescript = 'typescript';
case typespec = 'typespec';
case vb = 'vb';
case wgsl = 'wgsl';
case xml = 'xml';
case yaml = 'yaml';
public static function fromWithAlias(string $match): self
{
return match ($match) {
'h' => self::c,
'cc', 'hpp' => self::cpp,
'cs' => self::csharp,
'class' => self::java,
'htm' => self::html,
'js', 'mjs', 'cjs' => self::javascript,
'kt', 'kts' => self::kotlin,
'md' => self::markdown,
'm' => self::objectivec,
'pl', 'pm' => self::perl,
'php3', 'php4', 'php5', 'phtml' => self::php,
'py', 'pyc', 'pyo', 'pyi' => self::python,
'rdata', 'rds' => self::r,
'rb', 'erb' => self::ruby,
'sc' => self::scala,
'sh', 'zsh' => self::shell,
'ts', 'tsx' => self::typescript,
'yml' => self::yaml,
default => self::tryFrom($match) ?? self::plaintext,
};
}
public function getLabel(): string
{
return $this->name;
}
}

View File

@ -13,25 +13,4 @@ enum RolePermissionModels: string
case Role = 'role';
case Server = 'server';
case User = 'user';
case Webhook = 'webhook';
public function viewAny(): string
{
return RolePermissionPrefixes::ViewAny->value . ' ' . $this->value;
}
public function view(): string
{
return RolePermissionPrefixes::View->value . ' ' . $this->value;
}
public function create(): string
{
return RolePermissionPrefixes::Create->value . ' ' . $this->value;
}
public function update(): string
{
return RolePermissionPrefixes::Update->value . ' ' . $this->value;
}
}

View File

@ -1,10 +0,0 @@
<?php
namespace App\Enums;
enum ServerResourceType
{
case Unit;
case Percentage;
case Time;
}

View File

@ -2,11 +2,7 @@
namespace App\Enums;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
enum ServerState: string implements HasColor, HasIcon, HasLabel
enum ServerState: string
{
case Normal = 'normal';
case Installing = 'installing';
@ -15,7 +11,7 @@ enum ServerState: string implements HasColor, HasIcon, HasLabel
case Suspended = 'suspended';
case RestoringBackup = 'restoring_backup';
public function getIcon(): string
public function icon(): string
{
return match ($this) {
self::Normal => 'tabler-heart',
@ -27,16 +23,8 @@ enum ServerState: string implements HasColor, HasIcon, HasLabel
};
}
public function getColor(bool $hex = false): string
public function color(): string
{
if ($hex) {
return match ($this) {
self::Normal, self::Installing, self::RestoringBackup => '#2563EB',
self::Suspended => '#D97706',
self::InstallFailed, self::ReinstallFailed => '#EF4444',
};
}
return match ($this) {
self::Normal => 'primary',
self::Installing => 'primary',
@ -46,9 +34,4 @@ enum ServerState: string implements HasColor, HasIcon, HasLabel
self::RestoringBackup => 'primary',
};
}
public function getLabel(): string
{
return str($this->value)->headline();
}
}

View File

@ -1,9 +0,0 @@
<?php
namespace App\Enums;
enum SuspendAction: string
{
case Suspend = 'suspend';
case Unsuspend = 'unsuspend';
}

View File

@ -8,7 +8,9 @@ use Illuminate\Database\Eloquent\Model;
class ActivityLogged extends Event
{
public function __construct(public ActivityLog $model) {}
public function __construct(public ActivityLog $model)
{
}
public function is(string $event): bool
{

View File

@ -0,0 +1,13 @@
<?php
namespace App\Events\Auth;
use App\Models\User;
use App\Events\Event;
class DirectLogin extends Event
{
public function __construct(public User $user, public bool $remember)
{
}
}

View File

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

View File

@ -1,17 +1,18 @@
<?php
namespace App\Events\Server;
namespace App\Events\Auth;
use App\Events\Event;
use App\Models\Subuser;
use Illuminate\Queue\SerializesModels;
class SubUserAdded extends Event
class FailedPasswordReset extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public Subuser $subuser) {}
public function __construct(public string $ip, public string $email)
{
}
}

View File

@ -7,5 +7,7 @@ use App\Events\Event;
class ProvidedAuthenticationToken extends Event
{
public function __construct(public User $user, public bool $recovery = false) {}
public function __construct(public User $user, public bool $recovery = false)
{
}
}

View File

@ -2,4 +2,6 @@
namespace App\Events;
abstract class Event {}
abstract class Event
{
}

View File

@ -13,5 +13,7 @@ class Installed extends Event
/**
* Create a new event instance.
*/
public function __construct(public Server $server, public bool $successful, public bool $initialInstall) {}
public function __construct(public Server $server)
{
}
}

View File

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

View File

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

View File

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

View File

@ -10,11 +10,9 @@ 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;
/**
* @deprecated
*/
class DisplayException extends PanelException implements HttpExceptionInterface
{
public const LEVEL_DEBUG = 'debug';
@ -43,9 +41,6 @@ class DisplayException extends PanelException implements HttpExceptionInterface
return Response::HTTP_BAD_REQUEST;
}
/**
* @return array<string, string>
*/
public function getHeaders(): array
{
return [];
@ -72,6 +67,9 @@ class DisplayException extends PanelException implements HttpExceptionInterface
return response()->json(Handler::toArray($this), $this->getStatusCode(), $this->getHeaders());
}
// @phpstan-ignore-next-line
app(AlertsMessageBag::class)->danger($this->getMessage())->flash();
return redirect()->back()->withInput();
}

View File

@ -20,7 +20,6 @@ use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\Mailer\Exception\TransportException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Throwable;
class Handler extends ExceptionHandler
{
@ -46,8 +45,6 @@ class Handler extends ExceptionHandler
/**
* Maps exceptions to a specific response code. This handles special exception
* types that don't have a defined response code.
*
* @var array<class-string, int>
*/
protected static array $exceptionResponseCodes = [
AuthenticationException::class => 401,
@ -183,16 +180,9 @@ class Handler extends ExceptionHandler
}
/**
* @param array<string, mixed> $override
* @return array{errors: array{
* code: string,
* status: string,
* detail: string,
* source?: array{line: int, file: string},
* meta?: array{trace: string[], previous: string[]}
* }}|array{errors: array{non-empty-array<string, mixed>}}
* Return the exception as a JSONAPI representation for use on API requests.
*/
public static function exceptionToArray(Throwable $e, array $override = []): array
protected function convertExceptionToArray(\Throwable $e, array $override = []): array
{
$match = self::$exceptionResponseCodes[get_class($e)] ?? null;
@ -224,7 +214,7 @@ class Handler extends ExceptionHandler
'trace' => Collection::make($e->getTrace())
->map(fn ($trace) => Arr::except($trace, ['args']))
->all(),
'previous' => Collection::make(self::extractPrevious($e))
'previous' => Collection::make($this->extractPrevious($e))
->map(fn ($exception) => $exception->getTrace())
->map(fn ($trace) => Arr::except($trace, ['args']))
->all(),
@ -235,17 +225,6 @@ class Handler extends ExceptionHandler
return ['errors' => [array_merge($error, $override)]];
}
/**
* Return the exception as a JSONAPI representation for use on API requests.
*
* @param array{detail?: mixed, source?: mixed, meta?: mixed} $override
* @return array{errors?: array<mixed>}
*/
protected function convertExceptionToArray(Throwable $e, array $override = []): array
{
return self::exceptionToArray($e, $override);
}
/**
* Return an array of exceptions that should not be reported.
*/
@ -265,19 +244,22 @@ class Handler extends ExceptionHandler
return new JsonResponse($this->convertExceptionToArray($exception), JsonResponse::HTTP_UNAUTHORIZED);
}
return redirect()->guest(route('filament.app.auth.login'));
return redirect()->guest('/auth/login');
}
/**
* Extracts all the previous exceptions that lead to the one passed into this
* function being thrown.
*
* @return Throwable[]
* @return \Throwable[]
*/
public static function extractPrevious(Throwable $e): array
protected function extractPrevious(\Throwable $e): array
{
$previous = [];
while ($value = $e->getPrevious()) {
if (!$value instanceof \Throwable) {
break;
}
$previous[] = $value;
$e = $value;
}
@ -288,11 +270,10 @@ class Handler extends ExceptionHandler
/**
* Helper method to allow reaching into the handler to convert an exception
* into the expected array response type.
*
* @return array<mixed>
*/
public static function toArray(\Throwable $e): array
{
return self::exceptionToArray($e);
// @phpstan-ignore-next-line
return (new self(app()))->convertExceptionToArray($e);
}
}

View File

@ -4,4 +4,6 @@ namespace App\Exceptions\Http\Base;
use App\Exceptions\DisplayException;
class InvalidPasswordProvidedException extends DisplayException {}
class InvalidPasswordProvidedException extends DisplayException
{
}

View File

@ -0,0 +1,76 @@
<?php
namespace App\Exceptions\Http\Connection;
use Illuminate\Http\Response;
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;
/**
* Every request to the daemon instance will return a unique X-Request-Id header
* which allows for all errors to be efficiently tied to a specific request that
* triggered them, and gives users a more direct method of informing hosts when
* something goes wrong.
*/
private ?string $requestId;
/**
* Throw a displayable exception caused by a daemon connection error.
*/
public function __construct(GuzzleException $previous, bool $useStatusCode = true)
{
/** @var \GuzzleHttp\Psr7\Response|null $response */
$response = method_exists($previous, 'getResponse') ? $previous->getResponse() : null;
$this->requestId = $response?->getHeaderLine('X-Request-Id');
Context::add('request_id', $this->requestId);
if ($useStatusCode) {
$this->statusCode = is_null($response) ? $this->statusCode : $response->getStatusCode();
// There are rare conditions where daemon encounters a panic condition and crashes the
// request being made after content has already been sent over the wire. In these cases
// you can end up with a "successful" response code that is actual an error.
//
// Handle those better here since we shouldn't ever end up in this exception state and
// be returning a 2XX level response.
if ($this->statusCode < 400) {
$this->statusCode = Response::HTTP_BAD_GATEWAY;
}
}
if (is_null($response)) {
$message = 'Could not establish a connection to the machine running this server. Please try again.';
} else {
$message = sprintf('There was an error while communicating with the machine running this server. This error has been logged, please try again. (code: %s) (request_id: %s)', $response->getStatusCode(), $this->requestId ?? '<nil>');
}
// Attempt to pull the actual error message off the response and return that if it is not
// a 500 level error.
if ($this->statusCode < 500 && !is_null($response)) {
$body = json_decode($response->getBody()->__toString(), true);
$message = sprintf('An error occurred on the remote host: %s. (request id: %s)', $body['error'] ?? $message, $this->requestId ?? '<nil>');
}
$level = $this->statusCode >= 500 && $this->statusCode !== 504
? DisplayException::LEVEL_ERROR
: DisplayException::LEVEL_WARNING;
parent::__construct($message, $previous, $level);
}
/**
* Return the HTTP status code for this exception.
*/
public function getStatusCode(): int
{
return $this->statusCode;
}
}

View File

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

View File

@ -42,9 +42,6 @@ class DataValidationException extends PanelException implements HttpExceptionInt
return 500;
}
/**
* @return array<string, string>
*/
public function getHeaders(): array
{
return [];

View File

@ -2,4 +2,6 @@
namespace App\Exceptions;
class PanelException extends \Exception {}
class PanelException extends \Exception
{
}

View File

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

View File

@ -4,4 +4,6 @@ namespace App\Exceptions\Repository;
use App\Exceptions\DisplayException;
class DuplicateDatabaseNameException extends DisplayException {}
class DuplicateDatabaseNameException extends DisplayException
{
}

View File

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

View File

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

View File

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

View File

@ -4,4 +4,6 @@ namespace App\Exceptions\Service\Allocation;
use App\Exceptions\DisplayException;
class ServerUsingAllocationException extends DisplayException {}
class ServerUsingAllocationException extends DisplayException
{
}

View File

@ -4,4 +4,6 @@ namespace App\Exceptions\Service\Deployment;
use App\Exceptions\DisplayException;
class NoViableAllocationException extends DisplayException {}
class NoViableAllocationException extends DisplayException
{
}

View File

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

View File

@ -4,4 +4,6 @@ namespace App\Exceptions\Service\Egg;
use App\Exceptions\DisplayException;
class HasChildrenException extends DisplayException {}
class HasChildrenException extends DisplayException
{
}

View File

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

View File

@ -4,4 +4,6 @@ namespace App\Exceptions\Service\Egg\Variable;
use App\Exceptions\DisplayException;
class BadValidationRuleException extends DisplayException {}
class BadValidationRuleException extends DisplayException
{
}

View File

@ -4,4 +4,6 @@ namespace App\Exceptions\Service\Egg\Variable;
use App\Exceptions\DisplayException;
class ReservedVariableNameException extends DisplayException {}
class ReservedVariableNameException extends DisplayException
{
}

View File

@ -4,4 +4,6 @@ namespace App\Exceptions\Service;
use App\Exceptions\DisplayException;
class InvalidFileUploadException extends DisplayException {}
class InvalidFileUploadException extends DisplayException
{
}

View File

@ -4,4 +4,6 @@ namespace App\Exceptions\Service\Node;
use App\Exceptions\DisplayException;
class ConfigurationNotPersistedException extends DisplayException {}
class ConfigurationNotPersistedException extends DisplayException
{
}

View File

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

View File

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

View File

@ -4,4 +4,6 @@ namespace App\Exceptions\Service\Subuser;
use App\Exceptions\DisplayException;
class ServerSubuserExistsException extends DisplayException {}
class ServerSubuserExistsException extends DisplayException
{
}

View File

@ -4,4 +4,6 @@ namespace App\Exceptions\Service\Subuser;
use App\Exceptions\DisplayException;
class UserIsServerOwnerException extends DisplayException {}
class UserIsServerOwnerException extends DisplayException
{
}

View File

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

View File

@ -1,42 +0,0 @@
<?php
namespace App\Extensions\Avatar;
use App\Models\User;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
abstract class AvatarProvider
{
/**
* @var array<string, static>
*/
protected static array $providers = [];
public static function getProvider(string $id): ?self
{
return Arr::get(static::$providers, $id);
}
/**
* @return array<string, static>
*/
public static function getAll(): array
{
return static::$providers;
}
public function __construct()
{
static::$providers[$this->getId()] = $this;
}
abstract public function getId(): string;
abstract public function get(User $user): ?string;
public function getName(): string
{
return Str::title($this->getId());
}
}

View File

@ -1,24 +0,0 @@
<?php
namespace App\Extensions\Avatar\Providers;
use App\Extensions\Avatar\AvatarProvider;
use App\Models\User;
class GravatarProvider extends AvatarProvider
{
public function getId(): string
{
return 'gravatar';
}
public function get(User $user): string
{
return 'https://gravatar.com/avatar/' . md5($user->email);
}
public static function register(): self
{
return new self();
}
}

View File

@ -1,30 +0,0 @@
<?php
namespace App\Extensions\Avatar\Providers;
use App\Extensions\Avatar\AvatarProvider;
use App\Models\User;
class UiAvatarsProvider extends AvatarProvider
{
public function getId(): string
{
return 'uiavatars';
}
public function getName(): string
{
return 'UI Avatars';
}
public function get(User $user): ?string
{
// UI Avatars is the default of filament so just return null here
return null;
}
public static function register(): self
{
return new self();
}
}

View File

@ -16,19 +16,20 @@ class BackupManager
{
/**
* The array of resolved backup drivers.
*
* @var array<string, FilesystemAdapter>
*/
protected array $adapters = [];
/**
* The registered custom driver creators.
*
* @var array<string, callable>
*/
protected array $customCreators;
public function __construct(protected Application $app) {}
/**
* BackupManager constructor.
*/
public function __construct(protected Application $app)
{
}
/**
* Returns a backup adapter instance.
@ -87,8 +88,6 @@ class BackupManager
/**
* Calls a custom creator for a given adapter type.
*
* @param array{adapter: string} $config
*/
protected function callCustomCreator(array $config): mixed
{
@ -97,8 +96,6 @@ class BackupManager
/**
* Creates a new daemon adapter.
*
* @param array<string, string> $config
*/
public function createWingsAdapter(array $config): FilesystemAdapter
{
@ -107,8 +104,6 @@ class BackupManager
/**
* Creates a new S3 adapter.
*
* @param array<string, string> $config
*/
public function createS3Adapter(array $config): FilesystemAdapter
{
@ -125,8 +120,6 @@ class BackupManager
/**
* Returns the configuration associated with a given backup type.
*
* @return array<mixed>
*/
protected function getConfig(string $name): array
{
@ -156,9 +149,8 @@ class BackupManager
*/
public function forget(array|string $adapter): self
{
$adapters = &$this->adapters;
foreach ((array) $adapter as $adapterName) {
unset($adapters[$adapterName]);
unset($this->adapters[$adapterName]);
}
return $this;

View File

@ -1,118 +0,0 @@
<?php
namespace App\Extensions\Captcha\Providers;
use Filament\Forms\Components\Component;
use Filament\Forms\Components\TextInput;
use Illuminate\Foundation\Application;
use Illuminate\Support\Str;
abstract class CaptchaProvider
{
/**
* @var array<string, static>
*/
protected static array $providers = [];
/**
* @return self|static[]
*/
public static function get(?string $id = null): array|self
{
return $id ? static::$providers[$id] : static::$providers;
}
protected function __construct(protected Application $app)
{
if (array_key_exists($this->getId(), static::$providers)) {
if (!$this->app->runningUnitTests()) {
logger()->warning("Tried to create duplicate Captcha provider with id '{$this->getId()}'");
}
return;
}
config()->set('captcha.' . Str::lower($this->getId()), $this->getConfig());
static::$providers[$this->getId()] = $this;
}
abstract public function getId(): string;
abstract public function getComponent(): Component;
/**
* @return array<string, string|string[]|bool|null>
*/
public function getConfig(): array
{
$id = Str::upper($this->getId());
return [
'site_key' => env("CAPTCHA_{$id}_SITE_KEY"),
'secret_key' => env("CAPTCHA_{$id}_SECRET_KEY"),
];
}
/**
* @return Component[]
*/
public function getSettingsForm(): array
{
$id = Str::upper($this->getId());
return [
TextInput::make("CAPTCHA_{$id}_SITE_KEY")
->label('Site Key')
->placeholder('Site Key')
->columnSpan(2)
->required()
->password()
->revealable()
->autocomplete(false)
->default(env("CAPTCHA_{$id}_SITE_KEY")),
TextInput::make("CAPTCHA_{$id}_SECRET_KEY")
->label('Secret Key')
->placeholder('Secret Key')
->columnSpan(2)
->required()
->password()
->revealable()
->autocomplete(false)
->default(env("CAPTCHA_{$id}_SECRET_KEY")),
];
}
public function getName(): string
{
return Str::title($this->getId());
}
public function getIcon(): ?string
{
return null;
}
public function isEnabled(): bool
{
$id = Str::upper($this->getId());
return env("CAPTCHA_{$id}_ENABLED", false);
}
/**
* @return array<string, string|bool>
*/
public function validateResponse(?string $captchaResponse = null): array
{
return [
'success' => false,
'message' => 'validateResponse not defined',
];
}
public function verifyDomain(string $hostname, ?string $requestUrl = null): bool
{
return true;
}
}

View File

@ -1,106 +0,0 @@
<?php
namespace App\Extensions\Captcha\Providers;
use App\Filament\Components\Forms\Fields\TurnstileCaptcha;
use Exception;
use Filament\Forms\Components\Component;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Toggle;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\HtmlString;
class TurnstileProvider extends CaptchaProvider
{
public function getId(): string
{
return 'turnstile';
}
public function getComponent(): Component
{
return TurnstileCaptcha::make('turnstile');
}
/**
* @return array<string, string|string[]|bool|null>
*/
public function getConfig(): array
{
return array_merge(parent::getConfig(), [
'verify_domain' => env('CAPTCHA_TURNSTILE_VERIFY_DOMAIN'),
]);
}
/**
* @return Component[]
*/
public function getSettingsForm(): array
{
return array_merge(parent::getSettingsForm(), [
Toggle::make('CAPTCHA_TURNSTILE_VERIFY_DOMAIN')
->label(trans('admin/setting.captcha.verify'))
->columnSpan(2)
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->default(env('CAPTCHA_TURNSTILE_VERIFY_DOMAIN', true)),
Placeholder::make('info')
->label(trans('admin/setting.captcha.info_label'))
->columnSpan(2)
->content(new HtmlString(trans('admin/setting.captcha.info'))),
]);
}
public function getIcon(): string
{
return 'tabler-brand-cloudflare';
}
public static function register(Application $app): self
{
return new self($app);
}
/**
* @return array<string, string|bool>
*/
public function validateResponse(?string $captchaResponse = null): array
{
$captchaResponse ??= request()->get('cf-turnstile-response');
if (!$secret = env('CAPTCHA_TURNSTILE_SECRET_KEY')) {
throw new Exception('Turnstile secret key is not defined.');
}
$response = Http::asJson()
->timeout(15)
->connectTimeout(5)
->throw()
->post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [
'secret' => $secret,
'response' => $captchaResponse,
]);
return count($response->json()) ? $response->json() : [
'success' => false,
'message' => 'Unknown error occurred, please try again',
];
}
public function verifyDomain(string $hostname, ?string $requestUrl = null): bool
{
if (!env('CAPTCHA_TURNSTILE_VERIFY_DOMAIN', true)) {
return true;
}
$requestUrl ??= request()->url;
$requestUrl = parse_url($requestUrl);
return $hostname === array_get($requestUrl, 'host');
}
}

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