Compare commits

..

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

1635 changed files with 62669 additions and 28627 deletions

View File

@ -1,29 +1,10 @@
**.DS_Store
.env
.devcontainer
.dockerignore
.editorconfig
.git .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 node_modules
phpstan.neon vendor
phpunit.xml database/database.sqlite
readme.md
storage/debugbar/*.json storage/debugbar/*.json
storage/logs/*.log
storage/framework/cache/data/* storage/framework/cache/data/*
storage/framework/sessions/* storage/framework/sessions/*
storage/framework/testing storage/framework/testing
storage/framework/views/*.php storage/framework/views/*.php
storage/logs/*.log
vendor

View File

@ -3,4 +3,5 @@ APP_DEBUG=false
APP_KEY= APP_KEY=
APP_URL=http://panel.test APP_URL=http://panel.test
APP_INSTALLED=false APP_INSTALLED=false
APP_TIMEZONE=UTC
APP_LOCALE=en 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 #!/bin/ash -e
## check for .env file or symlink and generate app keys if missing #mkdir -p /var/log/supervisord/ /var/log/php8/ \
if [ -f /var/www/html/.env ]; then
## check for .env file and generate app keys if missing
if [ -f /pelican-data/.env ]; then
echo "external vars exist." echo "external vars exist."
rm -rf /var/www/html/.env
else else
echo "external vars don't exist." echo "external vars don't exist."
# webroot .env is symlinked to this path rm -rf /var/www/html/.env
touch /pelican-data/.env touch /pelican-data/.env
## manually generate a key because key generate --force fails ## manually generate a key because key generate --force fails
@ -23,7 +26,10 @@ else
echo -e "APP_INSTALLED=false" >> /pelican-data/.env echo -e "APP_INSTALLED=false" >> /pelican-data/.env
fi 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 if ! grep -q "APP_KEY=" .env || grep -q "APP_KEY=$" .env; then
echo "Generating APP_KEY..." echo "Generating APP_KEY..."
@ -39,6 +45,10 @@ php artisan migrate --force
echo -e "Optimizing Filament" echo -e "Optimizing Filament"
php artisan filament:optimize 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 export SUPERVISORD_CADDY=false
## disable caddy if SKIP_CADDY is set ## disable caddy if SKIP_CADDY is set
@ -49,5 +59,7 @@ else
export SUPERVISORD_CADDY=true export SUPERVISORD_CADDY=true
fi fi
chown -R www-data:www-data /pelican-data/.env /pelican-data/database
echo "Starting Supervisord" echo "Starting Supervisord"
exec "$@" exec "$@"

View File

@ -4,14 +4,16 @@ username=dummy
password=dummy password=dummy
[supervisord] [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_maxbytes=50MB ; maximum size of logfile before rotation
logfile_backups=2 ; number of backed up logfiles logfile_backups=2 ; number of backed up logfiles
loglevel=error ; info, debug, warn, trace loglevel=error ; info, debug, warn, trace
pidfile=/var/run/supervisord/supervisord.pid ; pidfile location pidfile=/var/run/supervisord.pid ; pidfile location
nodaemon=true ; run supervisord as a daemon nodaemon=false ; run supervisord as a daemon
minfds=1024 ; number of startup file descriptors minfds=1024 ; number of startup file descriptors
minprocs=200 ; number of process descriptors minprocs=200 ; number of process descriptors
user=root ; default user
childlogdir=/var/log/supervisord/ ; where child log files will live
[rpcinterface:supervisor] [rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
@ -28,6 +30,7 @@ autorestart=true
[program:queue-worker] [program:queue-worker]
command=/usr/local/bin/php /var/www/html/artisan queue:work --tries=3 command=/usr/local/bin/php /var/www/html/artisan queue:work --tries=3
user=www-data
autostart=true autostart=true
autorestart=true autorestart=true
@ -36,12 +39,5 @@ command=caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
autostart=%(ENV_SUPERVISORD_CADDY)s autostart=%(ENV_SUPERVISORD_CADDY)s
autorestart=%(ENV_SUPERVISORD_CADDY)s autorestart=%(ENV_SUPERVISORD_CADDY)s
priority=10 priority=10
stdout_logfile=/dev/fd/1 stdout_events_enabled=true
stdout_logfile_maxbytes=0 stderr_events_enabled=true
redirect_stderr=true
[program:supercronic]
command=supercronic -overlapping /etc/supercronic/crontab
autostart=true
autorestart=true
redirect_stderr=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: on:
push: push:
branches: branches:
- main - '**'
pull_request: pull_request:
branches:
- '**'
jobs: jobs:
ui: ui:
@ -18,25 +20,14 @@ jobs:
- name: Code Checkout - name: Code Checkout
uses: actions/checkout@v4 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 - name: Setup Node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: "yarn" cache: "yarn"
- name: Install JS dependencies - name: Install dependencies
run: yarn install --frozen-lockfile run: yarn install --frozen-lockfile
- name: Build - name: Build
run: yarn build run: yarn build:production

View File

@ -13,7 +13,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
php: [8.2, 8.3, 8.4] php: [8.2, 8.3]
database: ["mysql:8"] database: ["mysql:8"]
services: services:
database: database:
@ -66,16 +66,16 @@ jobs:
coverage: none coverage: none
- name: Install dependencies - 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 - name: Unit tests
run: vendor/bin/pest tests/Unit run: vendor/bin/phpunit tests/Unit
env: env:
DB_HOST: UNIT_NO_DB DB_HOST: UNIT_NO_DB
SKIP_MIGRATIONS: true SKIP_MIGRATIONS: true
- name: Integration tests - name: Integration tests
run: vendor/bin/pest tests/Integration run: vendor/bin/phpunit tests/Integration
env: env:
DB_PORT: ${{ job.services.database.ports[3306] }} DB_PORT: ${{ job.services.database.ports[3306] }}
DB_USERNAME: root DB_USERNAME: root
@ -86,7 +86,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
php: [8.2, 8.3, 8.4] php: [8.2, 8.3]
database: ["mariadb:10.6", "mariadb:10.11", "mariadb:11.4"] database: ["mariadb:10.6", "mariadb:10.11", "mariadb:11.4"]
services: services:
database: database:
@ -139,16 +139,16 @@ jobs:
coverage: none coverage: none
- name: Install dependencies - 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 - name: Unit tests
run: vendor/bin/pest tests/Unit run: vendor/bin/phpunit tests/Unit
env: env:
DB_HOST: UNIT_NO_DB DB_HOST: UNIT_NO_DB
SKIP_MIGRATIONS: true SKIP_MIGRATIONS: true
- name: Integration tests - name: Integration tests
run: vendor/bin/pest tests/Integration run: vendor/bin/phpunit tests/Integration
env: env:
DB_PORT: ${{ job.services.database.ports[3306] }} DB_PORT: ${{ job.services.database.ports[3306] }}
DB_USERNAME: root DB_USERNAME: root
@ -159,7 +159,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
php: [8.2, 8.3, 8.4] php: [8.2, 8.3]
env: env:
APP_ENV: testing APP_ENV: testing
APP_DEBUG: "false" APP_DEBUG: "false"
@ -200,92 +200,16 @@ jobs:
coverage: none coverage: none
- name: Install dependencies - 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 - name: Create SQLite file
run: touch database/testing.sqlite run: touch database/testing.sqlite
- name: Unit tests - name: Unit tests
run: vendor/bin/pest tests/Unit run: vendor/bin/phpunit tests/Unit
env: env:
DB_HOST: UNIT_NO_DB DB_HOST: UNIT_NO_DB
SKIP_MIGRATIONS: true SKIP_MIGRATIONS: true
- name: Integration tests - name: Integration tests
run: vendor/bin/pest tests/Integration run: vendor/bin/phpunit 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

View File

@ -1,5 +1,6 @@
name: Docker name: Docker
on: on:
push: push:
branches: branches:
@ -13,73 +14,18 @@ env:
IMAGE_NAME: ${{ github.repository }} IMAGE_NAME: ${{ github.repository }}
jobs: 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: build-and-push:
name: Build and Push ubuntu-24.04 name: Build and Push
runs-on: ubuntu-24.04 runs-on: ubuntu-latest
needs: build-php-base
permissions: permissions:
contents: read contents: read
packages: write 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. # 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'))" if: "!contains(github.ref, 'main') || (!contains(github.event.head_commit.message, 'skip docker') && !contains(github.event.head_commit.message, 'docker skip'))"
steps: steps:
- name: Code checkout - name: Code checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Docker metadata - name: Docker metadata
id: docker_meta id: docker_meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
@ -92,14 +38,11 @@ jobs:
type=ref,event=tag type=ref,event=tag
type=ref,event=branch type=ref,event=branch
- name: Set up QEMU - name: Setup QEMU
uses: docker/setup-qemu-action@v3 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 - name: Setup Docker buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
with:
driver-opts: network=host
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v3
@ -114,52 +57,30 @@ jobs:
echo "version_tag=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_OUTPUT echo "version_tag=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_OUTPUT
echo "short_sha=$(git rev-parse --short HEAD)" >> $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) - 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'" if: "github.event_name == 'release' && github.event.action == 'published'"
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
push: true push: true
platforms: 'linux/amd64,linux/arm64' platforms: linux/amd64,linux/arm64
build-args: | build-args: |
VERSION=${{ steps.build_info.outputs.version_tag }} VERSION=${{ steps.build_info.outputs.version_tag }}
labels: ${{ steps.docker_meta.outputs.labels }} labels: ${{ steps.docker_meta.outputs.labels }}
tags: ${{ steps.docker_meta.outputs.tags }} 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) - 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')" if: "github.event_name == 'push' && contains(github.ref, 'main')"
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}
platforms: 'linux/amd64,linux/arm64' platforms: linux/amd64,linux/arm64
build-args: | build-args: |
VERSION=dev-${{ steps.build_info.outputs.short_sha }} VERSION=dev-${{ steps.build_info.outputs.short_sha }}
labels: ${{ steps.docker_meta.outputs.labels }} labels: ${{ steps.docker_meta.outputs.labels }}
tags: ${{ steps.docker_meta.outputs.tags }} tags: ${{ steps.docker_meta.outputs.tags }}
cache-from: type=gha,scope=${{ matrix.os }} cache-from: type=gha
cache-to: type=gha,scope=${{ matrix.os }},mode=max cache-to: type=gha,mode=max

View File

@ -25,38 +25,21 @@ jobs:
run: cp .env.example .env run: cp .env.example .env
- name: Install dependencies - 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 - name: Pint
run: vendor/bin/pint --test run: vendor/bin/pint --test
phpstan: phpstan:
name: PHPStan name: PHPStan
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: [ 8.2, 8.3, 8.4 ]
steps: steps:
- name: Code Checkout - name: Code Checkout
uses: actions/checkout@v4 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 - name: Setup PHP
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2
with: with:
php-version: ${{ matrix.php }} php-version: "8.3"
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2 tools: composer:v2
coverage: none coverage: none
@ -65,7 +48,7 @@ jobs:
run: cp .env.example .env run: cp .env.example .env
- name: Install dependencies - 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 - 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 runs-on: ubuntu-22.04
permissions: permissions:
contents: write contents: write
steps: steps:
- name: Code checkout - name: Code checkout
uses: actions/checkout@v4 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 - name: Setup Node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
cache: "yarn" cache: "yarn"
- name: Install JS dependencies - name: Install dependencies
run: yarn install --frozen-lockfile run: yarn install --frozen-lockfile
- name: Build - name: Build
run: yarn build run: yarn build:production
- name: Create release branch and bump version - name: Create release branch and bump version
env: env:
@ -55,8 +44,8 @@ jobs:
- name: Create release archive - name: Create release archive
run: | run: |
rm -rf node_modules vendor tests CODE_OF_CONDUCT.md CONTRIBUTING.md phpunit.xml shell.nix rm -rf node_modules tests CODE_OF_CONDUCT.md CONTRIBUTING.md flake.lock flake.nix phpunit.xml shell.nix
tar -czf panel.tar.gz * .env.example tar -czf panel.tar.gz * .editorconfig .env.example .eslintignore .eslintrc.js .gitignore .prettierrc.json
- name: Create checksum - name: Create checksum
run: | run: |

5
.gitignore vendored
View File

@ -1,9 +1,9 @@
/.phpunit.cache /.phpunit.cache
/node_modules /node_modules
/public/build /public/build
/public/hot
/public/storage /public/storage
/storage/*.key /storage/*.key
/storage/pail
/storage/clockwork/* /storage/clockwork/*
/vendor /vendor
*.DS_Store* *.DS_Store*
@ -19,11 +19,10 @@ npm-debug.log
yarn-error.log yarn-error.log
/.fleet /.fleet
/.idea /.idea
/.nova
/.vscode /.vscode
public/assets/manifest.json public/assets/manifest.json
/database/*.sqlite* /database/*.sqlite
filament-monaco-editor/ filament-monaco-editor/
_ide_helper* _ide_helper*
/.phpstorm.meta.php /.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 # Pelican Production Dockerfile
## FROM node:20-alpine AS yarn
# If you want to build this locally you want to run `docker build -f Dockerfile.dev` #FROM --platform=$TARGETOS/$TARGETARCH node:20-alpine AS yarn
##
# ================================
# Stage 1-1: Composer Install
# ================================
FROM --platform=$TARGETOS/$TARGETARCH localhost:5000/base-php:$TARGETARCH AS composer
WORKDIR /build 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 --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 WORKDIR /var/www/html
# Install additional required libraries # Install dependencies
RUN apk update && apk add --no-cache \ 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 the Caddyfile to the container
COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public COPY Caddyfile /etc/caddy/Caddyfile
# Set permissions # Copy the application code to the container
# First ensure all files are owned by root and restrict www-data to read access COPY . .
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 --from=yarn /build/public/assets ./public/assets
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 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 \ HEALTHCHECK --interval=5m --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost/up || exit 1 CMD curl -f http://localhost/up || exit 1
@ -102,7 +55,5 @@ EXPOSE 80 443
VOLUME /pelican-data VOLUME /pelican-data
USER www-data ENTRYPOINT [ "/bin/ash", ".github/docker/entrypoint.sh" ]
ENTRYPOINT [ "/bin/ash", "docker/entrypoint.sh" ]
CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ] 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(); $eggs = Egg::all();
foreach ($eggs as $egg) { foreach ($eggs as $egg) {
try { 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) { } catch (Exception $exception) {
$this->error("{$egg->name}: Error ({$exception->getMessage()})"); $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; namespace App\Console\Commands\Environment;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
class AppSettingsCommand extends Command class AppSettingsCommand extends Command
{ {
@ -20,13 +21,9 @@ class AppSettingsCommand extends Command
if (!config('app.key')) { if (!config('app.key')) {
$this->comment('Generating app key'); $this->comment('Generating app key');
$this->call('key:generate'); Artisan::call('key:generate');
} }
$this->comment('Creating storage link'); Artisan::call('filament:optimize');
$this->call('storage:link');
$this->comment('Caching components & icons');
$this->call('filament:optimize');
} }
} }

View File

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

View File

@ -27,7 +27,6 @@ class DatabaseSettingsCommand extends Command
{--username= : Username to use when connecting to the MySQL/ MariaDB server.} {--username= : Username to use when connecting to the MySQL/ MariaDB server.}
{--password= : Password to use for the MySQL/ MariaDB database.}'; {--password= : Password to use for the MySQL/ MariaDB database.}';
/** @var array<array-key, mixed> */
protected array $variables = []; protected array $variables = [];
/** /**
@ -58,7 +57,7 @@ class DatabaseSettingsCommand extends Command
); );
if ($this->variables['DB_CONNECTION'] === 'mysql') { 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( $this->variables['DB_HOST'] = $this->option('host') ?? $this->ask(
'Database Host', 'Database Host',
config('database.connections.mysql.host', '127.0.0.1') config('database.connections.mysql.host', '127.0.0.1')
@ -74,7 +73,7 @@ class DatabaseSettingsCommand extends Command
config('database.connections.mysql.database', 'panel') 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( $this->variables['DB_USERNAME'] = $this->option('username') ?? $this->ask(
'Database Username', 'Database Username',
config('database.connections.mysql.username', 'pelican') config('database.connections.mysql.username', 'pelican')
@ -83,7 +82,7 @@ class DatabaseSettingsCommand extends Command
$askForMySQLPassword = true; $askForMySQLPassword = true;
if (!empty(config('database.connections.mysql.password')) && $this->input->isInteractive()) { if (!empty(config('database.connections.mysql.password')) && $this->input->isInteractive()) {
$this->variables['DB_PASSWORD'] = config('database.connections.mysql.password'); $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) { if ($askForMySQLPassword) {
@ -107,9 +106,9 @@ class DatabaseSettingsCommand extends Command
$this->database->connection('_panel_command_test')->getPdo(); $this->database->connection('_panel_command_test')->getPdo();
} catch (\PDOException $exception) { } 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(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'); $this->database->disconnect('_panel_command_test');
return $this->handle(); return $this->handle();
@ -118,7 +117,7 @@ class DatabaseSettingsCommand extends Command
return 1; return 1;
} }
} elseif ($this->variables['DB_CONNECTION'] === 'mariadb') { } 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( $this->variables['DB_HOST'] = $this->option('host') ?? $this->ask(
'Database Host', 'Database Host',
config('database.connections.mariadb.host', '127.0.0.1') config('database.connections.mariadb.host', '127.0.0.1')
@ -134,7 +133,7 @@ class DatabaseSettingsCommand extends Command
config('database.connections.mariadb.database', 'panel') 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( $this->variables['DB_USERNAME'] = $this->option('username') ?? $this->ask(
'Database Username', 'Database Username',
config('database.connections.mariadb.username', 'pelican') config('database.connections.mariadb.username', 'pelican')
@ -143,7 +142,7 @@ class DatabaseSettingsCommand extends Command
$askForMariaDBPassword = true; $askForMariaDBPassword = true;
if (!empty(config('database.connections.mariadb.password')) && $this->input->isInteractive()) { if (!empty(config('database.connections.mariadb.password')) && $this->input->isInteractive()) {
$this->variables['DB_PASSWORD'] = config('database.connections.mariadb.password'); $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) { if ($askForMariaDBPassword) {
@ -167,9 +166,9 @@ class DatabaseSettingsCommand extends Command
$this->database->connection('_panel_command_test')->getPdo(); $this->database->connection('_panel_command_test')->getPdo();
} catch (\PDOException $exception) { } 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(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'); $this->database->disconnect('_panel_command_test');
return $this->handle(); return $this->handle();
@ -180,7 +179,7 @@ class DatabaseSettingsCommand extends Command
} elseif ($this->variables['DB_CONNECTION'] === 'sqlite') { } elseif ($this->variables['DB_CONNECTION'] === 'sqlite') {
$this->variables['DB_DATABASE'] = $this->option('database') ?? $this->ask( $this->variables['DB_DATABASE'] = $this->option('database') ?? $this->ask(
'Database Path', 'Database Path',
(string) env('DB_DATABASE', 'database.sqlite') env('DB_DATABASE', 'database.sqlite')
); );
} }

View File

@ -22,7 +22,6 @@ class EmailSettingsCommand extends Command
{--username=} {--username=}
{--password=}'; {--password=}';
/** @var array<array-key, mixed> */
protected array $variables = []; protected array $variables = [];
/** /**
@ -92,7 +91,7 @@ class EmailSettingsCommand extends Command
trans('command/messages.environment.mail.ask_smtp_password') 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'), trans('command/messages.environment.mail.ask_encryption'),
['tls' => 'TLS', 'ssl' => 'SSL', '' => 'None'], ['tls' => 'TLS', 'ssl' => 'SSL', '' => 'None'],
config('mail.mailers.smtp.encryption', 'tls') 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-pass= : Password used to connect to redis.}
{--redis-port= : Port to connect to redis over.}'; {--redis-port= : Port to connect to redis over.}';
protected array $variables = [];
/** /**
* QueueSettingsCommand constructor. * QueueSettingsCommand constructor.
*/ */

View File

@ -18,21 +18,10 @@ class QueueWorkerServiceCommand extends Command
public function handle(): void 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'); $serviceName = $this->option('service-name') ?? $this->ask('Queue worker service name', 'pelican-queue');
$path = '/etc/systemd/system/' . $serviceName . '.service'; $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?')) { 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.'); $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-pass= : Password used to connect to redis.}
{--redis-port= : Port to connect to redis over.}'; {--redis-port= : Port to connect to redis over.}';
protected array $variables = [];
/** /**
* RedisSetupCommand constructor. * RedisSetupCommand constructor.
*/ */
@ -35,7 +37,7 @@ class RedisSetupCommand extends Command
{ {
$this->variables['CACHE_STORE'] = 'redis'; $this->variables['CACHE_STORE'] = 'redis';
$this->variables['QUEUE_CONNECTION'] = 'redis'; $this->variables['QUEUE_CONNECTION'] = 'redis';
$this->variables['SESSION_DRIVER'] = 'redis'; $this->variables['SESSION_DRIVERS'] = 'redis';
$this->requestRedisSettings(); $this->requestRedisSettings();

View File

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

View File

@ -3,6 +3,7 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use App\Services\Helpers\SoftwareVersionService;
class InfoCommand extends Command class InfoCommand extends Command
{ {
@ -10,8 +11,98 @@ class InfoCommand extends Command
protected $signature = 'p:info'; protected $signature = 'p:info';
/**
* InfoCommand constructor.
*/
public function __construct(private SoftwareVersionService $versionService)
{
parent::__construct();
}
/**
* Handle execution of command.
*/
public function handle(): void public function handle(): void
{ {
$this->call('about'); $this->output->title('Version Information');
$this->table([], [
['Panel Version', $this->versionService->currentPanelVersion()],
['Latest Version', $this->versionService->latestPanelVersion()],
['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\Console\Command;
use Illuminate\Contracts\Filesystem\Filesystem; use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory; use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory;
use SplFileInfo;
class CleanServiceBackupFilesCommand extends Command class CleanServiceBackupFilesCommand extends Command
{ {
@ -33,10 +32,9 @@ class CleanServiceBackupFilesCommand extends Command
*/ */
public function handle(): void public function handle(): void
{ {
/** @var SplFileInfo[] */
$files = $this->disk->files('services/.bak'); $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())); $lastModified = Carbon::createFromTimestamp($this->disk->lastModified($file->getPath()));
if ($lastModified->diffInMinutes(Carbon::now()) > self::BACKUP_THRESHOLD_MINUTES) { if ($lastModified->diffInMinutes(Carbon::now()) > self::BACKUP_THRESHOLD_MINUTES) {
$this->disk->delete($file->getPath()); $this->disk->delete($file->getPath());

View File

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

View File

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

View File

@ -6,7 +6,6 @@ use Illuminate\Console\Command;
use App\Models\Schedule; use App\Models\Schedule;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use App\Services\Schedules\ProcessScheduleService; use App\Services\Schedules\ProcessScheduleService;
use Throwable;
class ProcessRunnableCommand extends Command 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.'; 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() $schedules = Schedule::query()
->with('tasks') ->with('tasks')
@ -25,7 +27,7 @@ class ProcessRunnableCommand extends Command
->get(); ->get();
if ($schedules->count() < 1) { if ($schedules->count() < 1) {
$this->line(trans('commands.schedule.process.no_tasks')); $this->line(__('commands.schedule.process.no_tasks'));
return 0; return 0;
} }
@ -33,7 +35,7 @@ class ProcessRunnableCommand extends Command
$bar = $this->output->createProgressBar(count($schedules)); $bar = $this->output->createProgressBar(count($schedules));
foreach ($schedules as $schedule) { foreach ($schedules as $schedule) {
$bar->clear(); $bar->clear();
$this->processSchedule($processScheduleService, $schedule); $this->processSchedule($schedule);
$bar->advance(); $bar->advance();
$bar->display(); $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 * never throw an exception out, otherwise you'll end up killing the entire run group causing
* any other schedules to not process correctly. * 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()) { if ($schedule->tasks->isEmpty()) {
return; return;
} }
try { try {
$processScheduleService->handle($schedule); $this->getLaravel()->make(ProcessScheduleService::class)->handle($schedule);
$this->line(trans('command/messages.schedule.output_line', [ $this->line(trans('command/messages.schedule.output_line', [
'schedule' => $schedule->name, 'schedule' => $schedule->name,
'id' => $schedule->id, 'id' => $schedule->id,
])); ]));
} catch (Throwable $exception) { } catch (\Throwable|\Exception $exception) {
logger()->error($exception, ['schedule_id' => $schedule->id]); 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\ValidationException;
use Illuminate\Validation\Factory as ValidatorFactory; use Illuminate\Validation\Factory as ValidatorFactory;
use App\Repositories\Daemon\DaemonPowerRepository; use App\Repositories\Daemon\DaemonPowerRepository;
use Exception; use App\Exceptions\Http\Connection\DaemonConnectionException;
class BulkPowerActionCommand extends Command 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.'; 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'); $action = $this->argument('action');
$nodes = empty($this->option('nodes')) ? [] : explode(',', $this->option('nodes')); $nodes = empty($this->option('nodes')) ? [] : explode(',', $this->option('nodes'));
$servers = empty($this->option('servers')) ? [] : explode(',', $this->option('servers')); $servers = empty($this->option('servers')) ? [] : explode(',', $this->option('servers'));
$validator = $validator->make([ $validator = $this->validator->make([
'action' => $action, 'action' => $action,
'nodes' => $nodes, 'nodes' => $nodes,
'servers' => $servers, 'servers' => $servers,
@ -51,17 +64,13 @@ class BulkPowerActionCommand extends Command
} }
$bar = $this->output->createProgressBar($count); $bar = $this->output->createProgressBar($count);
$powerRepository = $this->powerRepository;
$this->getQueryBuilder($servers, $nodes)->get()->each(function ($server, int $index) use ($action, $powerRepository, &$bar): mixed { $this->getQueryBuilder($servers, $nodes)->each(function (Server $server) use ($action, $powerRepository, &$bar) {
$bar->clear(); $bar->clear();
if (!$server instanceof Server) {
return null;
}
try { try {
$powerRepository->setServer($server)->send($action); $powerRepository->setServer($server)->send($action);
} catch (Exception $exception) { } catch (DaemonConnectionException $exception) {
$this->output->error(trans('command/messages.server.power.action_failed', [ $this->output->error(trans('command/messages.server.power.action_failed', [
'name' => $server->name, 'name' => $server->name,
'id' => $server->id, 'id' => $server->id,
@ -72,8 +81,6 @@ class BulkPowerActionCommand extends Command
$bar->advance(); $bar->advance();
$bar->display(); $bar->display();
return null;
}); });
$this->line(''); $this->line('');
@ -81,9 +88,6 @@ class BulkPowerActionCommand extends Command
/** /**
* Returns the query builder instance that will return the servers that should be affected. * 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 protected function getQueryBuilder(array $servers, array $nodes): Builder
{ {

View File

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

View File

@ -19,7 +19,7 @@ class DisableTwoFactorCommand extends Command
public function handle(): void public function handle(): void
{ {
if ($this->input->isInteractive()) { 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')); $email = $this->option('email') ?? $this->ask(trans('command/messages.user.ask_email'));

View File

@ -7,13 +7,12 @@ use App\Console\Commands\Maintenance\CleanServiceBackupFilesCommand;
use App\Console\Commands\Maintenance\PruneImagesCommand; use App\Console\Commands\Maintenance\PruneImagesCommand;
use App\Console\Commands\Maintenance\PruneOrphanedBackupsCommand; use App\Console\Commands\Maintenance\PruneOrphanedBackupsCommand;
use App\Console\Commands\Schedule\ProcessRunnableCommand; use App\Console\Commands\Schedule\ProcessRunnableCommand;
use App\Jobs\NodeStatistics;
use App\Models\ActivityLog; use App\Models\ActivityLog;
use App\Models\Webhook; use App\Models\Webhook;
use Illuminate\Console\Scheduling\Schedule; use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Database\Console\PruneCommand; use Illuminate\Database\Console\PruneCommand;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel; use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use Spatie\Health\Commands\RunHealthChecksCommand;
use Spatie\Health\Commands\ScheduleCheckHeartbeatCommand;
class Kernel extends ConsoleKernel class Kernel extends ConsoleKernel
{ {
@ -30,11 +29,8 @@ class Kernel extends ConsoleKernel
*/ */
protected function schedule(Schedule $schedule): void protected function schedule(Schedule $schedule): void
{ {
if (config('cache.default') === 'redis') { // https://laravel.com/docs/10.x/upgrade#redis-cache-tags
// https://laravel.com/docs/10.x/upgrade#redis-cache-tags $schedule->command('cache:prune-stale-tags')->hourly();
// This only needs to run when using redis. anything else throws an error.
$schedule->command('cache:prune-stale-tags')->hourly();
}
// Execute scheduled commands for servers every minute, as if there was a normal cron running. // Execute scheduled commands for servers every minute, as if there was a normal cron running.
$schedule->command(ProcessRunnableCommand::class)->everyMinute()->withoutOverlapping(); $schedule->command(ProcessRunnableCommand::class)->everyMinute()->withoutOverlapping();
@ -43,6 +39,8 @@ class Kernel extends ConsoleKernel
$schedule->command(PruneImagesCommand::class)->daily(); $schedule->command(PruneImagesCommand::class)->daily();
$schedule->command(CheckEggUpdatesCommand::class)->hourly(); $schedule->command(CheckEggUpdatesCommand::class)->hourly();
$schedule->job(new NodeStatistics())->everyFiveSeconds()->withoutOverlapping();
if (config('backups.prune_age')) { if (config('backups.prune_age')) {
// Every 30 minutes, run the backup pruning command so that any abandoned backups can be deleted. // Every 30 minutes, run the backup pruning command so that any abandoned backups can be deleted.
$schedule->command(PruneOrphanedBackupsCommand::class)->everyThirtyMinutes(); $schedule->command(PruneOrphanedBackupsCommand::class)->everyThirtyMinutes();
@ -55,8 +53,5 @@ class Kernel extends ConsoleKernel
if (config('panel.webhook.prune_days')) { if (config('panel.webhook.prune_days')) {
$schedule->command(PruneCommand::class, ['--model' => [Webhook::class]])->daily(); $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; namespace App\Enums;
use Filament\Support\Contracts\HasColor; enum ContainerStatus: string
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
{ {
// Docker Based // Docker Based
case Created = 'created'; case Created = 'created';
@ -23,7 +19,7 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
// HTTP Based // HTTP Based
case Missing = 'missing'; case Missing = 'missing';
public function getIcon(): string public function icon(): string
{ {
return match ($this) { 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) { return match ($this) {
self::Created => 'primary', self::Created => 'primary',
self::Starting => 'warning', self::Starting => 'warning',
@ -62,51 +49,7 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
self::Removing => 'warning', self::Removing => 'warning',
self::Missing => 'danger', self::Missing => 'danger',
self::Stopping => 'warning', 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 Role = 'role';
case Server = 'server'; case Server = 'server';
case User = 'user'; 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; namespace App\Enums;
use Filament\Support\Contracts\HasColor; enum ServerState: string
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
enum ServerState: string implements HasColor, HasIcon, HasLabel
{ {
case Normal = 'normal'; case Normal = 'normal';
case Installing = 'installing'; case Installing = 'installing';
@ -15,7 +11,7 @@ enum ServerState: string implements HasColor, HasIcon, HasLabel
case Suspended = 'suspended'; case Suspended = 'suspended';
case RestoringBackup = 'restoring_backup'; case RestoringBackup = 'restoring_backup';
public function getIcon(): string public function icon(): string
{ {
return match ($this) { return match ($this) {
self::Normal => 'tabler-heart', 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) { return match ($this) {
self::Normal => 'primary', self::Normal => 'primary',
self::Installing => 'primary', self::Installing => 'primary',
@ -46,9 +34,4 @@ enum ServerState: string implements HasColor, HasIcon, HasLabel
self::RestoringBackup => 'primary', 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 class ActivityLogged extends Event
{ {
public function __construct(public ActivityLog $model) {} public function __construct(public ActivityLog $model)
{
}
public function is(string $event): bool 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. * Create a new event instance.
*/ */
public function __construct(public string $ip, public ?string $message) {} public function __construct(public string $ip, public ?string $message)
{
}
} }

View File

@ -1,17 +1,18 @@
<?php <?php
namespace App\Events\Server; namespace App\Events\Auth;
use App\Events\Event; use App\Events\Event;
use App\Models\Subuser;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
class SubUserAdded extends Event class FailedPasswordReset extends Event
{ {
use SerializesModels; use SerializesModels;
/** /**
* Create a new event instance. * 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 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; namespace App\Events;
abstract class Event {} abstract class Event
{
}

View File

@ -13,5 +13,7 @@ class Installed extends Event
/** /**
* Create a new event instance. * 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

@ -12,9 +12,6 @@ use Illuminate\Http\Response;
use Illuminate\Container\Container; use Illuminate\Container\Container;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
/**
* @deprecated
*/
class DisplayException extends PanelException implements HttpExceptionInterface class DisplayException extends PanelException implements HttpExceptionInterface
{ {
public const LEVEL_DEBUG = 'debug'; public const LEVEL_DEBUG = 'debug';
@ -43,9 +40,6 @@ class DisplayException extends PanelException implements HttpExceptionInterface
return Response::HTTP_BAD_REQUEST; return Response::HTTP_BAD_REQUEST;
} }
/**
* @return array<string, string>
*/
public function getHeaders(): array public function getHeaders(): array
{ {
return []; return [];

View File

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

View File

@ -0,0 +1,73 @@
<?php
namespace App\Exceptions\Http\Connection;
use Illuminate\Http\Response;
use GuzzleHttp\Exception\GuzzleException;
use App\Exceptions\DisplayException;
use Illuminate\Support\Facades\Context;
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

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

View File

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

View File

@ -4,4 +4,6 @@ namespace App\Exceptions\Repository;
use App\Exceptions\DisplayException; 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

@ -4,4 +4,6 @@ namespace App\Exceptions\Service\Allocation;
use App\Exceptions\DisplayException; 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; use App\Exceptions\DisplayException;
class NoViableAllocationException extends DisplayException {} class NoViableAllocationException extends DisplayException
{
}

View File

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

View File

@ -4,4 +4,6 @@ namespace App\Exceptions\Service\Egg\Variable;
use App\Exceptions\DisplayException; 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; 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; 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; use App\Exceptions\DisplayException;
class ConfigurationNotPersistedException extends DisplayException {} class ConfigurationNotPersistedException extends DisplayException
{
}

View File

@ -4,4 +4,6 @@ namespace App\Exceptions\Service\Subuser;
use App\Exceptions\DisplayException; 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; use App\Exceptions\DisplayException;
class UserIsServerOwnerException extends DisplayException {} class UserIsServerOwnerException extends DisplayException
{
}

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

View File

@ -0,0 +1,35 @@
<?php
namespace App\Extensions;
use App\Models\DatabaseHost;
class DynamicDatabaseConnection
{
public const DB_CHARSET = 'utf8';
public const DB_COLLATION = 'utf8_unicode_ci';
public const DB_DRIVER = 'mysql';
/**
* Adds a dynamic database connection entry to the runtime config.
*/
public function set(string $connection, DatabaseHost|int $host, string $database = 'mysql'): void
{
if (!$host instanceof DatabaseHost) {
$host = DatabaseHost::query()->findOrFail($host);
}
config()->set('database.connections.' . $connection, [
'driver' => self::DB_DRIVER,
'host' => $host->host,
'port' => $host->port,
'database' => $database,
'username' => $host->username,
'password' => $host->password,
'charset' => self::DB_CHARSET,
'collation' => self::DB_COLLATION,
]);
}
}

View File

@ -1,51 +0,0 @@
<?php
namespace App\Extensions\Features;
use Filament\Actions\Action;
use Illuminate\Foundation\Application;
abstract class FeatureProvider
{
/**
* @var array<string, static>
*/
protected static array $providers = [];
/**
* @param string[] $id
* @return self|static[]
*/
public static function getProviders(string|array|null $id = null): array|self
{
if (is_array($id)) {
return array_intersect_key(static::$providers, array_flip($id));
}
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 Feature provider with id '{$this->getId()}'");
}
return;
}
static::$providers[$this->getId()] = $this;
}
abstract public function getId(): string;
/**
* A matching subset string (case-insensitive) from the console output
*
* @return array<string>
*/
abstract public function getListeners(): array;
abstract public function getAction(): Action;
}

View File

@ -1,127 +0,0 @@
<?php
namespace App\Extensions\Features;
use App\Facades\Activity;
use App\Models\Permission;
use App\Models\Server;
use App\Models\ServerVariable;
use App\Repositories\Daemon\DaemonPowerRepository;
use Closure;
use Exception;
use Filament\Actions\Action;
use Filament\Facades\Filament;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\HtmlString;
class GSLToken extends FeatureProvider
{
public function __construct(protected Application $app)
{
parent::__construct($app);
}
/** @return array<string> */
public function getListeners(): array
{
return [
'(gsl token expired)',
'(account not found)',
];
}
public function getId(): string
{
return 'gsl_token';
}
public function getAction(): Action
{
/** @var Server $server */
$server = Filament::getTenant();
/** @var ServerVariable $serverVariable */
$serverVariable = $server->serverVariables()->whereHas('variable', function (Builder $query) {
$query->where('env_variable', 'STEAM_ACC');
})->first();
return Action::make($this->getId())
->requiresConfirmation()
->modalHeading('Invalid GSL token')
->modalDescription('It seems like your Gameserver Login Token (GSL token) is invalid or has expired.')
->modalSubmitActionLabel('Update GSL Token')
->disabledForm(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_UPDATE, $server))
->form([
Placeholder::make('info')
->label(new HtmlString(Blade::render('You can either <x-filament::link href="https://steamcommunity.com/dev/managegameservers" target="_blank">generate a new one</x-filament::link> and enter it below or leave the field blank to remove it completely.'))),
TextInput::make('gsltoken')
->label('GSL Token')
->rules([
fn (): Closure => function (string $attribute, $value, Closure $fail) use ($serverVariable) {
$validator = Validator::make(['validatorkey' => $value], [
'validatorkey' => $serverVariable->variable->rules,
]);
if ($validator->fails()) {
$message = str($validator->errors()->first())->replace('validatorkey', $serverVariable->variable->name);
$fail($message);
}
},
])
->hintIcon('tabler-code')
->label(fn () => $serverVariable->variable->name)
->hintIconTooltip(fn () => implode('|', $serverVariable->variable->rules))
->prefix(fn () => '{{' . $serverVariable->variable->env_variable . '}}')
->helperText(fn () => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description),
])
->action(function (array $data, DaemonPowerRepository $powerRepository) use ($server, $serverVariable) {
/** @var Server $server */
$server = Filament::getTenant();
try {
$new = $data['gsltoken'] ?? '';
$original = $serverVariable->variable_value;
$serverVariable->update([
'variable_value' => $new,
]);
if ($original !== $new) {
Activity::event('server:startup.edit')
->property([
'variable' => $serverVariable->variable->env_variable,
'old' => $original,
'new' => $new,
])
->log();
}
$powerRepository->setServer($server)->send('restart');
Notification::make()
->title('GSL Token updated')
->body('Server will restart now.')
->success()
->send();
} catch (Exception $exception) {
Notification::make()
->title('Could not update GSL Token')
->body($exception->getMessage())
->danger()
->send();
}
});
}
public static function register(Application $app): self
{
return new self($app);
}
}

View File

@ -1,100 +0,0 @@
<?php
namespace App\Extensions\Features;
use App\Facades\Activity;
use App\Models\Permission;
use App\Models\Server;
use App\Repositories\Daemon\DaemonPowerRepository;
use Exception;
use Filament\Actions\Action;
use Filament\Facades\Filament;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Select;
use Filament\Notifications\Notification;
use Illuminate\Foundation\Application;
class JavaVersion extends FeatureProvider
{
public function __construct(protected Application $app)
{
parent::__construct($app);
}
/** @return array<string> */
public function getListeners(): array
{
return [
'java.lang.UnsupportedClassVersionError',
'unsupported major.minor version',
'has been compiled by a more recent version of the java runtime',
'minecraft 1.17 requires running the server with java 16 or above',
'minecraft 1.18 requires running the server with java 17 or above',
'minecraft 1.19 requires running the server with java 17 or above',
];
}
public function getId(): string
{
return 'java_version';
}
public function getAction(): Action
{
/** @var Server $server */
$server = Filament::getTenant();
return Action::make($this->getId())
->requiresConfirmation()
->modalHeading('Unsupported Java Version')
->modalDescription('This server is currently running an unsupported version of Java and cannot be started.')
->modalSubmitActionLabel('Update Docker Image')
->disabledForm(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_DOCKER_IMAGE, $server))
->form([
Placeholder::make('java')
->label('Please select a supported version from the list below to continue starting the server.'),
Select::make('image')
->label('Docker Image')
->disabled(fn () => !in_array($server->image, $server->egg->docker_images))
->options(fn () => collect($server->egg->docker_images)->mapWithKeys(fn ($key, $value) => [$key => $value]))
->selectablePlaceholder(false)
->default(fn () => $server->image)
->notIn(fn () => $server->image)
->required()
->preload()
->native(false),
])
->action(function (array $data, DaemonPowerRepository $powerRepository) use ($server) {
try {
$new = $data['image'];
$original = $server->image;
$server->forceFill(['image' => $new])->saveOrFail();
if ($original !== $server->image) {
Activity::event('server:startup.image')
->property(['old' => $original, 'new' => $new])
->log();
}
$powerRepository->setServer($server)->send('restart');
Notification::make()
->title('Docker image updated')
->body('Server will restart now.')
->success()
->send();
} catch (Exception $exception) {
Notification::make()
->title('Could not update docker image')
->body($exception->getMessage())
->danger()
->send();
}
});
}
public static function register(Application $app): self
{
return new self($app);
}
}

View File

@ -1,71 +0,0 @@
<?php
namespace App\Extensions\Features;
use App\Models\Server;
use App\Repositories\Daemon\DaemonFileRepository;
use App\Repositories\Daemon\DaemonPowerRepository;
use Exception;
use Filament\Actions\Action;
use Filament\Facades\Filament;
use Filament\Notifications\Notification;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
class MinecraftEula extends FeatureProvider
{
public function __construct(protected Application $app)
{
parent::__construct($app);
}
/** @return array<string> */
public function getListeners(): array
{
return [
'you need to agree to the eula in order to run the server',
];
}
public function getId(): string
{
return 'eula';
}
public function getAction(): Action
{
return Action::make($this->getId())
->requiresConfirmation()
->modalHeading('Minecraft EULA')
->modalDescription(new HtmlString(Blade::render('By pressing "I Accept" below you are indicating your agreement to the <x-filament::link href="https://minecraft.net/eula" target="_blank">Minecraft EULA </x-filament::link>.')))
->modalSubmitActionLabel('I Accept')
->action(function (DaemonFileRepository $fileRepository, DaemonPowerRepository $powerRepository) {
try {
/** @var Server $server */
$server = Filament::getTenant();
$fileRepository->setServer($server)->putContent('eula.txt', 'eula=true');
$powerRepository->setServer($server)->send('restart');
Notification::make()
->title('Minecraft EULA accepted')
->body('Server will restart now.')
->success()
->send();
} catch (Exception $exception) {
Notification::make()
->title('Could not accept Minecraft EULA')
->body($exception->getMessage())
->danger()
->send();
}
});
}
public static function register(Application $app): self
{
return new self($app);
}
}

View File

@ -1,76 +0,0 @@
<?php
namespace App\Extensions\Features;
use Filament\Actions\Action;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
class PIDLimit extends FeatureProvider
{
public function __construct(protected Application $app)
{
parent::__construct($app);
}
/** @return array<string> */
public function getListeners(): array
{
return [
'pthread_create failed',
'failed to create thread',
'unable to create thread',
'unable to create native thread',
'unable to create new native thread',
'exception in thread "craft async scheduler management thread"',
];
}
public function getId(): string
{
return 'pid_limit';
}
public function getAction(): Action
{
return Action::make($this->getId())
->requiresConfirmation()
->icon('tabler-alert-triangle')
->modalHeading(fn () => auth()->user()->isAdmin() ? 'Memory or process limit reached...' : 'Possible resource limit reached...')
->modalDescription(new HtmlString(Blade::render(
auth()->user()->isAdmin() ? <<<'HTML'
<p>
This server has reached the maximum process or memory limit.
</p>
<p class="mt-4">
Increasing <code>container_pid_limit</code> in the wings
configuration, <code>config.yml</code>, might help resolve
this issue.
</p>
<p class="mt-4">
<b>Note: Wings must be restarted for the configuration file changes to take effect</b>
</p>
HTML
:
<<<'HTML'
<p>
This server is attempting to use more resources than allocated. Please contact the administrator
and give them the error below.
</p>
<p class="mt-4">
<code>
pthread_create failed, Possibly out of memory or process/resource limits reached
</code>
</p>
HTML
)))
->modalCancelActionLabel('Close')
->action(fn () => null);
}
public static function register(Application $app): self
{
return new self($app);
}
}

View File

@ -1,64 +0,0 @@
<?php
namespace App\Extensions\Features;
use Filament\Actions\Action;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
class SteamDiskSpace extends FeatureProvider
{
public function __construct(protected Application $app)
{
parent::__construct($app);
}
/** @return array<string> */
public function getListeners(): array
{
return [
'steamcmd needs 250mb of free disk space to update',
'0x202 after update job',
];
}
public function getId(): string
{
return 'steam_disk_space';
}
public function getAction(): Action
{
return Action::make($this->getId())
->requiresConfirmation()
->modalHeading('Out of available disk space...')
->modalDescription(new HtmlString(Blade::render(
auth()->user()->isAdmin() ? <<<'HTML'
<p>
This server has run out of available disk space and cannot complete the install or update
process.
</p>
<p class="mt-4">
Ensure the machine has enough disk space by typing{' '}
<code class="rounded py-1 px-2">df -h</code> on the machine hosting
this server. Delete files or increase the available disk space to resolve the issue.
</p>
HTML
:
<<<'HTML'
<p>
This server has run out of available disk space and cannot complete the install or update
process. Please get in touch with the administrator(s) and inform them of disk space issues.
</p>
HTML
)))
->modalCancelActionLabel('Close')
->action(fn () => null);
}
public static function register(Application $app): self
{
return new self($app);
}
}

View File

@ -1,5 +1,28 @@
<?php <?php
/* 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. */
namespace App\Extensions\Filesystem; namespace App\Extensions\Filesystem;
use Aws\S3\S3ClientInterface; use Aws\S3\S3ClientInterface;
@ -7,9 +30,6 @@ use League\Flysystem\AwsS3V3\AwsS3V3Adapter;
class S3Filesystem extends AwsS3V3Adapter class S3Filesystem extends AwsS3V3Adapter
{ {
/**
* @param array<mixed> $options
*/
public function __construct( public function __construct(
private S3ClientInterface $client, private S3ClientInterface $client,
private string $bucket, private string $bucket,

View File

@ -8,9 +8,6 @@ class PanelSerializer extends ArraySerializer
{ {
/** /**
* Serialize an item. * Serialize an item.
*
* @param array<mixed> $data
* @return array{object: ?string, attributes: array<mixed>}
*/ */
public function item(?string $resourceKey, array $data): array public function item(?string $resourceKey, array $data): array
{ {
@ -22,9 +19,6 @@ class PanelSerializer extends ArraySerializer
/** /**
* Serialize a collection. * Serialize a collection.
*
* @param array<mixed> $data
* @return array{object: 'list', data: array<mixed>}
*/ */
public function collection(?string $resourceKey, array $data): array public function collection(?string $resourceKey, array $data): array
{ {
@ -41,8 +35,6 @@ class PanelSerializer extends ArraySerializer
/** /**
* Serialize a null resource. * Serialize a null resource.
*
* @return ?array{object: ?string, attributes: null}
*/ */
public function null(): ?array public function null(): ?array
{ {
@ -54,10 +46,6 @@ class PanelSerializer extends ArraySerializer
/** /**
* Merge the included resources with the parent resource being serialized. * Merge the included resources with the parent resource being serialized.
*
* @param array{relationships: array{string, mixed}} $transformedData
* @param array{string, mixed} $includedData
* @return array{relationships: array{string, mixed}}
*/ */
public function mergeIncludes(array $transformedData, array $includedData): array public function mergeIncludes(array $transformedData, array $includedData): array
{ {

View File

@ -1,74 +0,0 @@
<?php
namespace App\Extensions\OAuth\Providers;
use Filament\Forms\Components\ColorPicker;
use Filament\Forms\Components\TextInput;
use Illuminate\Foundation\Application;
use SocialiteProviders\Authentik\Provider;
final class AuthentikProvider extends OAuthProvider
{
public function __construct(protected Application $app)
{
parent::__construct($app);
}
public function getId(): string
{
return 'authentik';
}
public function getProviderClass(): string
{
return Provider::class;
}
public function getServiceConfig(): array
{
return [
'base_url' => env('OAUTH_AUTHENTIK_BASE_URL'),
'client_id' => env('OAUTH_AUTHENTIK_CLIENT_ID'),
'client_secret' => env('OAUTH_AUTHENTIK_CLIENT_SECRET'),
];
}
public function getSettingsForm(): array
{
return array_merge(parent::getSettingsForm(), [
TextInput::make('OAUTH_AUTHENTIK_BASE_URL')
->label('Base URL')
->placeholder('Base URL')
->columnSpan(2)
->required()
->url()
->autocomplete(false)
->default(env('OAUTH_AUTHENTIK_BASE_URL')),
TextInput::make('OAUTH_AUTHENTIK_DISPLAY_NAME')
->label('Display Name')
->placeholder('Display Name')
->autocomplete(false)
->default(env('OAUTH_AUTHENTIK_DISPLAY_NAME', 'Authentik')),
ColorPicker::make('OAUTH_AUTHENTIK_DISPLAY_COLOR')
->label('Display Color')
->placeholder('#fd4b2d')
->default(env('OAUTH_AUTHENTIK_DISPLAY_COLOR', '#fd4b2d'))
->hex(),
]);
}
public function getName(): string
{
return env('OAUTH_AUTHENTIK_DISPLAY_NAME', 'Authentik');
}
public function getHexColor(): string
{
return env('OAUTH_AUTHENTIK_DISPLAY_COLOR', '#fd4b2d');
}
public static function register(Application $app): self
{
return new self($app);
}
}

View File

@ -1,38 +0,0 @@
<?php
namespace App\Extensions\OAuth\Providers;
use Illuminate\Foundation\Application;
final class CommonProvider extends OAuthProvider
{
protected function __construct(protected Application $app, private string $id, private ?string $providerClass, private ?string $icon, private ?string $hexColor)
{
parent::__construct($app);
}
public function getId(): string
{
return $this->id;
}
public function getProviderClass(): ?string
{
return $this->providerClass;
}
public function getIcon(): ?string
{
return $this->icon;
}
public function getHexColor(): ?string
{
return $this->hexColor;
}
public static function register(Application $app, string $id, ?string $providerClass = null, ?string $icon = null, ?string $hexColor = null): static
{
return new self($app, $id, $providerClass, $icon, $hexColor);
}
}

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