Merge branch 'develop' into matthewpi/security-keys-backport

This commit is contained in:
Matthew Penner 2023-01-17 15:33:53 -07:00
commit f631ac1946
No known key found for this signature in database
1153 changed files with 25099 additions and 37002 deletions

20
.env.ci
View file

@ -1,20 +0,0 @@
APP_ENV=testing
APP_DEBUG=true
APP_KEY=SomeRandomString3232RandomString
APP_THEME=pterodactyl
APP_TIMEZONE=UTC
APP_URL=http://localhost/
APP_ENVIRONMENT_ONLY=true
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_DATABASE=testing
DB_USERNAME=root
DB_PASSWORD=
CACHE_DRIVER=array
SESSION_DRIVER=array
MAIL_DRIVER=array
QUEUE_DRIVER=sync
HASHIDS_SALT=test123

View file

@ -1,7 +1,6 @@
APP_ENV=production APP_ENV=production
APP_DEBUG=false APP_DEBUG=false
APP_KEY= APP_KEY=
APP_THEME=pterodactyl
APP_TIMEZONE=UTC APP_TIMEZONE=UTC
APP_URL=http://panel.example.com APP_URL=http://panel.example.com
APP_LOCALE=en APP_LOCALE=en
@ -41,4 +40,4 @@ MAIL_FROM_NAME="Pterodactyl Panel"
# mail servers such as Gmail to reject your mail. # mail servers such as Gmail to reject your mail.
# #
# @see: https://github.com/pterodactyl/panel/pull/3110 # @see: https://github.com/pterodactyl/panel/pull/3110
# SERVER_NAME=panel.example.com # MAIL_EHLO_DOMAIN=panel.example.com

View file

@ -1,9 +1,3 @@
const prettier = {
singleQuote: true,
jsxSingleQuote: true,
printWidth: 120,
};
/** @type {import('eslint').Linter.Config} */ /** @type {import('eslint').Linter.Config} */
module.exports = { module.exports = {
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
@ -21,33 +15,29 @@ module.exports = {
version: 'detect', version: 'detect',
}, },
linkComponents: [ linkComponents: [
{name: 'Link', linkAttribute: 'to'}, { name: 'Link', linkAttribute: 'to' },
{name: 'NavLink', linkAttribute: 'to'}, { name: 'NavLink', linkAttribute: 'to' },
], ],
}, },
env: { env: {
browser: true, browser: true,
es6: true, es6: true,
}, },
plugins: [ plugins: ['react', 'react-hooks', 'prettier', '@typescript-eslint'],
'react',
'react-hooks',
'prettier',
'@typescript-eslint',
],
extends: [ extends: [
// 'standard', // 'standard',
'eslint:recommended', 'eslint:recommended',
'plugin:react/recommended', 'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended',
'plugin:jest-dom/recommended',
], ],
rules: { rules: {
eqeqeq: 'error', eqeqeq: 'error',
'prettier/prettier': ['error', prettier], 'prettier/prettier': ['error', {}, { usePrettierrc: true }],
// TypeScript can infer this significantly better than eslint ever can. // TypeScript can infer this significantly better than eslint ever can.
'react/prop-types': 0, 'react/prop-types': 0,
'react/display-name': 0, 'react/display-name': 0,
'react/no-unknown-property': ['error', {ignore: ['css']}],
'@typescript-eslint/no-explicit-any': 0, '@typescript-eslint/no-explicit-any': 0,
'@typescript-eslint/no-non-null-assertion': 0, '@typescript-eslint/no-non-null-assertion': 0,
// This setup is required to avoid a spam of errors when running eslint about React being // This setup is required to avoid a spam of errors when running eslint about React being
@ -56,7 +46,7 @@ module.exports = {
// @see https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-use-before-define.md#how-to-use // @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, 'no-use-before-define': 0,
'@typescript-eslint/no-use-before-define': 'warn', '@typescript-eslint/no-use-before-define': 'warn',
'@typescript-eslint/no-unused-vars': ['warn', {argsIgnorePattern: '^_', varsIgnorePattern: '^_'}], '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
'@typescript-eslint/ban-ts-comment': ['error', {'ts-expect-error': 'allow-with-description'}], '@typescript-eslint/ban-ts-comment': ['error', { 'ts-expect-error': 'allow-with-description' }],
} },
}; };

View file

@ -1,4 +1,4 @@
blank_issues_enabled: false blank_issues_enabled: true
contact_links: contact_links:
- name: Installation Help - name: Installation Help
url: https://discord.gg/pterodactyl url: https://discord.gg/pterodactyl

21
.github/docker/Caddyfile vendored Normal file
View file

@ -0,0 +1,21 @@
:8080 {
root * /var/www/pterodactyl/public/
file_server
header {
-Server
-X-Powered-By
Referrer-Policy "same-origin"
X-Frame-Options "deny"
X-XSS-Protection "1; mode=block"
X-Content-Type-Options "nosniff"
}
encode gzip zstd
php_fastcgi 127.0.0.1:9000 {
trusted_proxies 172.20.0.0/16
}
try_files {path} {path}/ /index.php?{query}
}

View file

@ -1,76 +0,0 @@
# Pterodactyl 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/pterodactyl/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/pterodactyl/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_DRIVER` | 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` |

View file

@ -1,51 +0,0 @@
# 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/
#
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/pterodactyl.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;
}
}

View file

@ -1,70 +0,0 @@
# 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/pterodactyl.app-access.log;
error_log /var/log/nginx/pterodactyl.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,81 +0,0 @@
#!/bin/ash -e
cd /app
mkdir -p /var/log/panel/logs/ /var/log/supervisord/ /var/log/nginx/ /var/log/php7/ \
&& chmod 777 /var/log/panel/logs/ \
&& ln -s /var/log/panel/logs/ /app/storage/logs/
## check for .env file and generate app keys if missing
if [ -f /app/var/.env ]; then
echo "external vars exist."
rm -rf /app/.env
ln -s /app/var/.env /app/
else
echo "external vars don't exist."
rm -rf /app/.env
touch /app/var/.env
## manually generate a key because key generate --force fails
if [ -z $APP_KEY ]; then
echo -e "Generating key."
APP_KEY=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
echo -e "Generated app key: $APP_KEY"
echo -e "APP_KEY=$APP_KEY" > /app/var/.env
else
echo -e "APP_KEY exists in environment, using that."
echo -e "APP_KEY=$APP_KEY" > /app/var/.env
fi
ln -s /app/var/.env /app/
fi
echo "Checking if https is required."
if [ -f /etc/nginx/http.d/panel.conf ]; then
echo "Using nginx config already in place."
if [ $LE_EMAIL ]; then
echo "Checking for cert update"
certbot certonly -d $(echo $APP_URL | sed 's~http[s]*://~~g') --standalone -m $LE_EMAIL --agree-tos -n
else
echo "No letsencrypt email is set"
fi
else
echo "Checking if letsencrypt email is set."
if [ -z $LE_EMAIL ]; then
echo "No letsencrypt email is set using http config."
cp .github/docker/default.conf /etc/nginx/http.d/panel.conf
else
echo "writing ssl config"
cp .github/docker/default_ssl.conf /etc/nginx/http.d/panel.conf
echo "updating ssl config for domain"
sed -i "s|<domain>|$(echo $APP_URL | sed 's~http[s]*://~~g')|g" /etc/nginx/http.d/panel.conf
echo "generating certs"
certbot certonly -d $(echo $APP_URL | sed 's~http[s]*://~~g') --standalone -m $LE_EMAIL --agree-tos -n
fi
echo "Removing the default nginx config"
rm -rf /etc/nginx/http.d/default.conf
fi
if [[ -z $DB_PORT ]]; then
echo -e "DB_PORT not specified, defaulting to 3306"
DB_PORT=3306
fi
## check for DB up before starting the panel
echo "Checking database status."
until nc -z -v -w30 $DB_HOST $DB_PORT
do
echo "Waiting for database connection..."
# wait for 1 seconds before check again
sleep 1
done
## make sure the db is set up
echo -e "Migrating and Seeding D.B"
php artisan migrate --seed --force
## start cronjobs for the queue
echo -e "Starting cron jobs."
crond -L /var/log/crond -l 5
echo -e "Starting supervisord."
exec "$@"

21
.github/docker/php-fpm.conf vendored Normal file
View file

@ -0,0 +1,21 @@
[global]
error_log = /dev/stderr
daemonize = no
[www]
user = nobody
group = nobody
listen = 127.0.0.1:9000
pm = dynamic
pm.start_servers = 4
pm.min_spare_servers = 4
pm.max_spare_servers = 16
pm.max_children = 64
pm.max_requests = 256
clear_env = no
catch_workers_output = yes
decorate_workers_output = no

View file

@ -1,39 +1,57 @@
[unix_http_server]
file=/tmp/supervisor.sock ; path to your socket file
[supervisord] [supervisord]
logfile=/var/log/supervisord/supervisord.log ; supervisord log file logfile=/dev/stdout
logfile_maxbytes=50MB ; maximum size of logfile before rotation logfile_maxbytes=0
logfile_backups=2 ; number of backed up logfiles loglevel=info
loglevel=error ; info, debug, warn, trace minfds=1024
pidfile=/var/run/supervisord.pid ; pidfile location minprocs=200
nodaemon=false ; run supervisord as a daemon nodaemon=true
minfds=1024 ; number of startup file descriptors pidfile=/dev/null
minprocs=200 ; number of process descriptors
user=root ; default user
childlogdir=/var/log/supervisord/ ; where child log files will live
[rpcinterface:supervisor] [unix_http_server]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface file=/tmp/supervisor.sock
[supervisorctl] [supervisorctl]
serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL for a unix socket serverurl=unix:///tmp/supervisor.sock
[program:php-fpm] [rpcinterface:supervisor]
command=/usr/local/sbin/php-fpm -F supervisor.rpcinterface_factory=supervisor.rpcinterface:make_main_rpcinterface
autostart=true
autorestart=true
[program:queue-worker] [program:caddy]
command=/usr/local/bin/php /app/artisan queue:work --queue=high,standard,low --sleep=3 --tries=3 command=/usr/local/bin/caddy run --config /etc/caddy/Caddyfile
user=nginx
autostart=true
autorestart=true
[program:nginx]
command=/usr/sbin/nginx -g 'daemon off;'
autostart=true autostart=true
autorestart=true autorestart=true
priority=10 priority=10
stdout_events_enabled=true stdout_logfile=/dev/stdout
stderr_events_enabled=true stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:php-fpm]
command=/usr/sbin/php-fpm --nodaemonize
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
environment=LOG_CHANNEL="stderr"
[program:queue-worker]
command=/usr/bin/php /var/www/pterodactyl/artisan queue:work --queue=standard --sleep=3 --tries=3
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
environment=LOG_CHANNEL="stderr"
[program:yacron]
command=/usr/local/bin/yacron -c /etc/yacron.yaml
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
environment=LOG_CHANNEL="stderr"

View file

@ -1,16 +0,0 @@
[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

8
.github/docker/yacron.yaml vendored Normal file
View file

@ -0,0 +1,8 @@
jobs:
- name: scheduler
command:
- /usr/bin/php
- /var/www/pterodactyl/artisan
- schedule:run
schedule: "* * * * *"
utc: true

View file

@ -1,35 +0,0 @@
name: Build
on:
push:
branches:
- "develop"
- "1.0-develop"
pull_request:
branches:
- "develop"
- "1.0-develop"
jobs:
ui:
name: UI
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
node-version: [16]
steps:
- name: Code Checkout
uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: "yarn"
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Build
run: yarn build:production

View file

@ -1,72 +0,0 @@
name: Tests
on:
push:
branches:
- "develop"
- "1.0-develop"
pull_request:
branches:
- "develop"
- "1.0-develop"
jobs:
tests:
name: Tests
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
php: [8.1]
database: ["mariadb:10.2", "mysql:8"]
services:
database:
image: ${{ matrix.database }}
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
MYSQL_DATABASE: testing
ports:
- 3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- name: Code Checkout
uses: actions/checkout@v3
- name: Get cache directory
id: composer-cache
run: |
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-${{ matrix.php }}-
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: bcmath, cli, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2
coverage: none
- name: Setup .env
run: cp .env.ci .env
- name: Install dependencies
run: composer install --no-interaction --no-progress --no-suggest --prefer-dist
- name: Unit tests
run: vendor/bin/phpunit --bootstrap vendor/autoload.php tests/Unit
if: ${{ always() }}
env:
DB_HOST: UNIT_NO_DB
- name: Integration tests
run: vendor/bin/phpunit tests/Integration
env:
DB_PORT: ${{ job.services.database.ports[3306] }}
DB_USERNAME: root

View file

@ -1,61 +1,67 @@
name: Publish Docker Image name: Docker
on: on:
push: push:
branches: branches:
- "develop" - develop
- "release/v*" - 1.0-develop
pull_request:
branches:
- develop
- 1.0-develop
release:
types:
- published
jobs: jobs:
push: push:
name: Push Image to GitHub Packages name: Push
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
# Always run against a tag, even if the commit into the tag has [docker skip]
# within the commit message.
if: "!contains(github.ref, 'develop') || (!contains(github.event.head_commit.message, 'skip docker') && !contains(github.event.head_commit.message, 'docker skip'))" if: "!contains(github.ref, 'develop') || (!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@v3 uses: actions/checkout@v3
- name: Docker Metadata - name: Docker metadata
uses: docker/metadata-action@v4
id: docker_meta id: docker_meta
uses: docker/metadata-action@v4
with: with:
images: ghcr.io/pterodactyl/panel images: ghcr.io/pterodactyl/panel
flavor: |
latest=false
tags: |
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.action == 'published' && github.event.release.prerelease == false }}
type=ref,event=tag
type=ref,event=branch
- name: Setup QEMU - name: Setup QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v2
- name: Setup Docker Buildx - name: Setup Docker buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
- name: Docker Login - name: Login to GitHub Container Registry
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.REGISTRY_TOKEN }} password: ${{ secrets.REGISTRY_TOKEN }}
- name: Release production build - name: Update version
uses: docker/build-push-action@v2 if: "github.event_name == 'release' && github.event.action == 'published'"
if: "contains(github.ref, 'release/v')" env:
with: REF: ${{ github.event.release.tag_name }}
context: . run: |
file: ./Dockerfile sed -i "s/ 'version' => 'canary',/ 'version' => '${REF:1}',/" config/app.php
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.docker_meta.outputs.labels }}
- name: Release development build - name: Build and Push
uses: docker/build-push-action@v2 uses: docker/build-push-action@v3
if: "contains(github.ref, 'develop')"
with: with:
context: . context: .
file: ./Dockerfile file: ./Containerfile
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64,linux/arm64 platforms: linux/amd64 #,linux/arm64
tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.docker_meta.outputs.labels }} labels: ${{ steps.docker_meta.outputs.labels }}
tags: ${{ steps.docker_meta.outputs.tags }}
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max

209
.github/workflows/laravel.yaml vendored Normal file
View file

@ -0,0 +1,209 @@
name: Laravel
on:
push:
branches:
- "develop"
- "1.0-develop"
pull_request:
branches:
- "develop"
- "1.0-develop"
jobs:
analysis:
name: Static Analysis
runs-on: ubuntu-20.04
env:
APP_ENV: testing
APP_DEBUG: "true"
APP_KEY: SomeRandomString3232RandomString
CACHE_DRIVER: array
MAIL_MAILER: array
SESSION_DRIVER: array
QUEUE_CONNECTION: sync
steps:
- name: Code checkout
uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.1
extensions: bcmath, cli, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2
coverage: none
- name: Install dependencies
run: composer install --no-interaction --no-progress --no-suggest --prefer-dist
- name: Analyze
run: vendor/bin/phpstan analyse
lint:
name: Lint
runs-on: ubuntu-20.04
steps:
- name: Code checkout
uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.1
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-progress --no-suggest --prefer-dist
- name: PHP CS Fixer
run: vendor/bin/php-cs-fixer fix --dry-run --diff
mysql:
name: Tests
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
php: [8.1]
database: ["mariadb:10.2", "mariadb:10.9", "mysql:8"]
services:
database:
image: docker.io/library/${{ matrix.database }}
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
MYSQL_DATABASE: testing
ports:
- 3306/tcp
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
env:
APP_ENV: testing
APP_DEBUG: "true"
APP_KEY: SomeRandomString3232RandomString
APP_THEME: pterodactyl
APP_TIMEZONE: UTC
APP_URL: http://localhost/
APP_ENVIRONMENT_ONLY: "true"
CACHE_DRIVER: array
MAIL_MAILER: array
SESSION_DRIVER: array
QUEUE_CONNECTION: sync
HASHIDS_SALT: test123
DB_CONNECTION: mysql
DB_HOST: 127.0.0.1
DB_DATABASE: testing
DB_USERNAME: root
steps:
- name: Code checkout
uses: actions/checkout@v3
- name: Get cache directory
id: composer-cache
run: |
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-${{ matrix.php }}-
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: bcmath, cli, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2
coverage: none
- name: Install dependencies
run: composer install --no-interaction --no-progress --no-suggest --prefer-dist
- name: Unit tests
run: vendor/bin/phpunit --bootstrap vendor/autoload.php tests/Unit
env:
DB_HOST: UNIT_NO_DB
- name: Integration tests
run: vendor/bin/phpunit tests/Integration
env:
DB_PORT: ${{ job.services.database.ports[3306] }}
postgres:
name: Tests
runs-on: ubuntu-20.04
if: "!contains(github.event.head_commit.message, 'skip ci') && !contains(github.event.head_commit.message, 'ci skip')"
strategy:
fail-fast: false
matrix:
php: [8.1]
database: ["postgres:13", "postgres:14", "postgres:15"]
services:
database:
image: docker.io/library/${{ matrix.database }}
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: testing
ports:
- 5432/tcp
options: --health-cmd=pg_isready --health-interval=10s --health-timeout=5s --health-retries=3
env:
APP_ENV: testing
APP_DEBUG: "true"
APP_KEY: SomeRandomString3232RandomString
APP_THEME: pterodactyl
APP_TIMEZONE: UTC
APP_URL: http://localhost/
APP_ENVIRONMENT_ONLY: "true"
CACHE_DRIVER: array
MAIL_MAILER: array
SESSION_DRIVER: array
QUEUE_CONNECTION: sync
HASHIDS_SALT: test123
DB_CONNECTION: pgsql
DB_HOST: 127.0.0.1
DB_DATABASE: testing
DB_USERNAME: postgres
DB_PASSWORD: postgres
steps:
- name: Code checkout
uses: actions/checkout@v3
- name: Get cache directory
id: composer-cache
run: |
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-${{ matrix.php }}-
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: bcmath, cli, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2
coverage: none
- name: Install dependencies
run: composer install --no-interaction --no-progress --no-suggest --prefer-dist
- name: Unit tests
run: vendor/bin/phpunit --bootstrap vendor/autoload.php tests/Unit
env:
DB_HOST: UNIT_NO_DB
- name: Integration tests
run: vendor/bin/phpunit tests/Integration
env:
DB_PORT: ${{ job.services.database.ports[5432] }}

View file

@ -1,36 +0,0 @@
name: Lint
on:
push:
branches:
- "develop"
- "1.0-develop"
pull_request:
branches:
- "develop"
- "1.0-develop"
jobs:
lint:
name: Lint
runs-on: ubuntu-20.04
steps:
- name: Code Checkout
uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: "8.1"
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2
coverage: none
- name: Setup .env
run: cp .env.ci .env
- name: Install dependencies
run: composer install --no-interaction --no-progress --no-suggest --prefer-dist
- name: PHP CS Fixer
run: vendor/bin/php-cs-fixer fix --dry-run --diff

View file

@ -10,20 +10,20 @@ jobs:
name: Release name: Release
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- name: Code Checkout - name: Code checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 16 node-version: 18
cache: "yarn" cache: yarn
- name: Install dependencies - name: Install dependencies
run: yarn install --frozen-lockfile run: yarn install --frozen-lockfile
- name: Build - name: Build
run: yarn build:production run: yarn build
- name: Create release branch and bump version - name: Create release branch and bump version
env: env:
@ -36,21 +36,19 @@ jobs:
git push -u origin $BRANCH git push -u origin $BRANCH
sed -i "s/ 'version' => 'canary',/ 'version' => '${REF:11}',/" config/app.php sed -i "s/ 'version' => 'canary',/ 'version' => '${REF:11}',/" config/app.php
git add config/app.php git add config/app.php
git commit -m "bump version for release" git commit -m "ci(release): bump version"
git push git push
- name: Create release archive - name: Create release archive
run: | run: |
rm -rf node_modules/ test/ codecov.yml CODE_OF_CONDUCT.md CONTRIBUTING.md phpunit.xml Vagrantfile rm -rf node_modules tests CODE_OF_CONDUCT.md CONTRIBUTING.md flake.lock flake.nix phpstan.neon phpunit.xml shell.nix
tar -czf panel.tar.gz * .env.example .eslintignore .eslintrc.js tar -czf panel.tar.gz * .editorconfig .env.example .eslintignore .eslintrc.js .gitignore .prettierrc.json
- name: Extract changelog - name: Extract changelog
id: extract_changelog
env: env:
REF: ${{ github.ref }} REF: ${{ github.ref }}
run: | run: |
sed -n "/^## ${REF:10}/,/^## /{/^## /b;p}" CHANGELOG.md > ./RELEASE_CHANGELOG sed -n "/^## ${REF:10}/,/^## /{/^## /b;p}" CHANGELOG.md > ./RELEASE_CHANGELOG
echo ::set-output name=version_name::`sed -nr "s/^## (${REF:10} .*)$/\1/p" CHANGELOG.md`
- name: Create checksum and add to changelog - name: Create checksum and add to changelog
run: | run: |
@ -58,17 +56,15 @@ jobs:
echo -e "\n#### SHA256 Checksum\n\n\`\`\`\n$SUM\n\`\`\`\n" >> ./RELEASE_CHANGELOG echo -e "\n#### SHA256 Checksum\n\n\`\`\`\n$SUM\n\`\`\`\n" >> ./RELEASE_CHANGELOG
echo $SUM > checksum.txt echo $SUM > checksum.txt
- name: Create Release - name: Create release
id: create_release id: create_release
uses: actions/create-release@v1 uses: softprops/action-gh-release@v1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
tag_name: ${{ github.ref }}
release_name: ${{ steps.extract_changelog.outputs.version_name }}
body_path: ./RELEASE_CHANGELOG
draft: true draft: true
prerelease: ${{ contains(github.ref, 'beta') || contains(github.ref, 'alpha') }} prerelease: ${{ contains(github.ref, 'rc') || contains(github.ref, 'beta') || contains(github.ref, 'alpha') }}
body_path: ./RELEASE_CHANGELOG
- name: Upload release archive - name: Upload release archive
id: upload-release-archive id: upload-release-archive

57
.github/workflows/ui.yaml vendored Normal file
View file

@ -0,0 +1,57 @@
name: UI
on:
push:
branches:
- "develop"
- "1.0-develop"
pull_request:
branches:
- "develop"
- "1.0-develop"
jobs:
lint:
name: Lint
runs-on: ubuntu-20.04
steps:
- name: Code checkout
uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 18
cache: yarn
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Lint
run: yarn run lint
tests:
name: Tests
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
node: [16, 18]
steps:
- name: Code checkout
uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
cache: yarn
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Build
run: yarn run build
- name: Tests
run: yarn run test

5
.gitignore vendored
View file

@ -29,3 +29,8 @@ misc
coverage.xml coverage.xml
resources/lang/locales.js resources/lang/locales.js
.phpunit.result.cache .phpunit.result.cache
/public/build
/public/hot
result
docker-compose.yaml

4
.prettierignore Normal file
View file

@ -0,0 +1,4 @@
.github
public
node_modules
resources/views

22
.prettierrc.json Normal file
View file

@ -0,0 +1,22 @@
{
"printWidth": 120,
"tabWidth": 4,
"useTabs": false,
"semi": true,
"singleQuote": true,
"jsxSingleQuote": false,
"trailingComma": "all",
"bracketSpacing": true,
"arrowParens": "avoid",
"endOfLine": "lf",
"overrides": [
{
"files": ["**/*.md"],
"options": {
"tabWidth": 2,
"useTabs": false,
"singleQuote": false
}
}
]
}

View file

@ -3,10 +3,67 @@ This file is a running track of new features and fixes to each version of the pa
This project follows [Semantic Versioning](http://semver.org) guidelines. This project follows [Semantic Versioning](http://semver.org) guidelines.
## [Unreleased] ## v1.11.2
### Changed ### Changed
* Changed minimum PHP version is now 8.0 instead of `7.4`. * Telemetry no longer sends a map of Egg and Nest UUIDs to the number of servers using them.
* Increased the timeout for the decompress files endpoint in the client API from 15 seconds to 15 minutes.
### Fixed
* Fixed Panel Docker image having a `v` prefix in the version displayed in the admin area.
* Fixed emails using the wrong queue name, causing them to not be sent.
* Fixed the settings keys used for configuring SMTP settings, causing settings to not save properly.
* Fixed the `MAIL_EHLO_DOMAIN` environment variable not being properly backwards compatible with the old `SERVER_NAME` variable.
## v1.11.1
### Fixed
* Fixed Panel Docker image showing `canary` as it's version.
## v1.11.0
### Changed (since 1.10.4)
* Changed minimum PHP version requirement from `7.4` to `8.0`.
* Upgraded from Laravel 8 to Laravel 9. * Upgraded from Laravel 8 to Laravel 9.
* This release requires Wings v1.11.x in order for Server Transfers to work.
* `MB` byte suffixes are now displayed as `MiB` to more accurately reflect the actual value.
* Server re-installation failures are tracked independently of the initial installation process.
### Fixed (since 1.10.4)
* Node maintenance mode now properly blocks access to servers.
* Fixed the length validation on the Minecraft Forge egg.
* Fixed the password in the JDBC string not being properly URL encoded.
* Fixed an issue where Wings would throw a validation error while attempting to upload activity logs.
* Properly handle a missing `Content-Length` header in the response from the daemon.
* Ensure activity log properties are always returned as an object instead of an empty array.
### Added (since 1.10.4)
* Added the `server:settings.description` activity log event for when a server description is changed.
* Added the ability to cancel file uploads in the file manager for a server.
* Added a telemetry service to collect anonymous metrics from the panel, this feature is *enabled* by default and can be toggled using the `PTERODACTYL_TELEMETRY_ENABLED` environment variable.
## v1.11.0-rc.2
### Changed
* `MB` byte suffixes are now displayed as `MiB` to more accurately reflect the actual value.
* Server re-installation failures are tracked independently of the initial installation process.
### Fixed
* Properly handle a missing `Content-Length` header in the response from the daemon.
* Ensure activity log properties are always returned as an object instead of an empty array.
### Added
* Added the `server:settings.description` activity log event for when a server description is changed.
* Added the ability to cancel file uploads in the file manager for a server.
* Added a telemetry service to collect anonymous metrics from the panel, this feature is disabled by default and can be toggled using the `PTERODACTYL_TELEMETRY_ENABLED` environment variable.
## v1.11.0-rc.1
### Changed
* Changed minimum PHP version requirement from `7.4` to `8.0`.
* Upgraded from Laravel 8 to Laravel 9.
* This release requires Wings v1.11.x in order for Server Transfers to work.
### Fixed
* Node maintenance mode now properly blocks access to servers.
* Fixed the length validation on the Minecraft Forge egg.
* Fixed the password in the JDBC string not being properly URL encoded.
* Fixed an issue where Wings would throw a validation error while attempting to upload activity logs.
## v1.10.4 ## v1.10.4
### Fixed ### Fixed

76
Containerfile Normal file
View file

@ -0,0 +1,76 @@
# Stage 0 - Caddy
FROM --platform=$TARGETOS/$TARGETARCH docker.io/library/caddy:latest AS caddy
# Stage 1 - Builder
FROM --platform=$TARGETOS/$TARGETARCH registry.access.redhat.com/ubi9/nodejs-16-minimal AS builder
RUN npm install -g yarn
WORKDIR /var/www/pterodactyl
COPY --chown=1001:0 public ./public
COPY --chown=1001:0 resources/scripts ./resources/scripts
COPY --chown=1001:0 .eslintignore .eslintrc.js .prettierrc.json package.json tailwind.config.js tsconfig.json vite.config.ts yarn.lock .
RUN /opt/app-root/src/.npm-global/bin/yarn install --frozen-lockfile \
&& /opt/app-root/src/.npm-global/bin/yarn build \
&& rm -rf resources/scripts .eslintignore .eslintrc.yml .yarnrc.yml package.json tailwind.config.js tsconfig.json vite.config.ts yarn.lock node_modules
COPY --chown=1001:0 app ./app
COPY --chown=1001:0 bootstrap ./bootstrap
COPY --chown=1001:0 config ./config
COPY --chown=1001:0 database ./database
COPY --chown=1001:0 resources/lang ./resources/lang
COPY --chown=1001:0 resources/views ./resources/views
COPY --chown=1001:0 routes ./routes
COPY --chown=1001:0 .env.example ./.env
COPY --chown=1001:0 artisan CHANGELOG.md composer.json composer.lock LICENSE.md README.md SECURITY.md .
# Stage 2 - Final
FROM --platform=$TARGETOS/$TARGETARCH registry.access.redhat.com/ubi9/ubi-minimal
RUN microdnf update -y \
&& rpm --install https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm \
&& rpm --install https://rpms.remirepo.net/enterprise/remi-release-9.rpm \
&& microdnf update -y \
&& microdnf install -y ca-certificates shadow-utils tar tzdata unzip wget \
&& microdnf module -y reset php \
&& microdnf module -y enable php:remi-8.1 \
&& microdnf install -y cronie php-{bcmath,cli,common,fpm,gd,gmp,intl,json,mbstring,mysqlnd,opcache,pdo,pecl-redis5,pecl-zip,phpiredis,pgsql,process,sodium,xml,zstd} supervisor \
&& curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \
&& rm /etc/php-fpm.d/www.conf \
&& useradd --home-dir /var/lib/caddy --create-home caddy \
&& mkdir /etc/caddy \
&& wget -O /usr/local/bin/yacron https://github.com/gjcarneiro/yacron/releases/download/0.17.0/yacron-0.17.0-x86_64-unknown-linux-gnu \
&& chmod 755 /usr/local/bin/yacron \
&& microdnf remove -y tar wget \
&& microdnf clean all
COPY --chown=caddy:caddy --from=builder /var/www/pterodactyl /var/www/pterodactyl
WORKDIR /var/www/pterodactyl
RUN mkdir -p /tmp/pterodactyl/cache /tmp/pterodactyl/framework/{cache,sessions,views} storage/framework \
&& rm -rf bootstrap/cache storage/framework/sessions storage/framework/views storage/framework/cache \
&& ln -s /tmp/pterodactyl/cache /var/www/pterodactyl/bootstrap/cache \
&& ln -s /tmp/pterodactyl/framework/cache /var/www/pterodactyl/storage/framework/cache \
&& ln -s /tmp/pterodactyl/framework/sessions /var/www/pterodactyl/storage/framework/sessions \
&& ln -s /tmp/pterodactyl/framework/views /var/www/pterodactyl/storage/framework/views \
&& chmod -R 755 /var/www/pterodactyl/storage/* /tmp/pterodactyl/cache \
&& chown -R caddy:caddy /var/www/pterodactyl /tmp/pterodactyl/{cache,framework}
USER caddy
ENV USER=caddy
RUN composer install --no-dev --optimize-autoloader \
&& rm -rf bootstrap/cache/*.php \
&& rm -rf .env storage/logs/*.log
COPY --from=caddy /usr/bin/caddy /usr/local/bin/caddy
COPY .github/docker/Caddyfile /etc/caddy/Caddyfile
COPY .github/docker/php-fpm.conf /etc/php-fpm.conf
COPY .github/docker/supervisord.conf /etc/supervisord.conf
COPY .github/docker/yacron.yaml /etc/yacron.yaml
EXPOSE 8080
CMD ["/usr/bin/supervisord", "--configuration=/etc/supervisord.conf"]

View file

@ -1,40 +0,0 @@
# Stage 0:
# Build the assets that are needed for the frontend. This build stage is then discarded
# since we won't need NodeJS anymore in the future. This Docker image ships a final production
# level distribution of Pterodactyl.
FROM --platform=$TARGETOS/$TARGETARCH mhart/alpine-node:14
WORKDIR /app
COPY . ./
RUN yarn install --frozen-lockfile \
&& yarn run build:production
# Stage 1:
# Build the actual container with all of the needed PHP dependencies that will run the application.
FROM --platform=$TARGETOS/$TARGETARCH php:8.1-fpm-alpine
WORKDIR /app
COPY . ./
COPY --from=0 /app/public/assets ./public/assets
RUN apk add --no-cache --update ca-certificates dcron curl git supervisor tar unzip nginx libpng-dev libxml2-dev libzip-dev certbot certbot-nginx \
&& docker-php-ext-configure zip \
&& docker-php-ext-install bcmath gd pdo_mysql zip \
&& curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \
&& cp .env.example .env \
&& mkdir -p bootstrap/cache/ storage/logs storage/framework/sessions storage/framework/views storage/framework/cache \
&& chmod 777 -R bootstrap storage \
&& composer install --no-dev --optimize-autoloader \
&& rm -rf .env bootstrap/cache/*.php \
&& chown -R nginx:nginx .
RUN rm /usr/local/etc/php-fpm.conf \
&& echo "* * * * * /usr/local/bin/php /app/artisan schedule:run >> /dev/null 2>&1" >> /var/spool/cron/crontabs/root \
&& echo "0 23 * * * certbot renew --nginx --quiet" >> /var/spool/cron/crontabs/root \
&& sed -i s/ssl_session_cache/#ssl_session_cache/g /etc/nginx/nginx.conf \
&& mkdir -p /var/run/php /var/run/nginx
COPY .github/docker/default.conf /etc/nginx/http.d/default.conf
COPY .github/docker/www.conf /usr/local/etc/php-fpm.conf
COPY .github/docker/supervisord.conf /etc/supervisord.conf
EXPOSE 80 443
ENTRYPOINT [ "/bin/ash", ".github/docker/entrypoint.sh" ]
CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ]

View file

@ -1,6 +1,6 @@
[![Logo Image](https://cdn.pterodactyl.io/logos/new/pterodactyl_logo.png)](https://pterodactyl.io) [![Logo Image](https://cdn.pterodactyl.io/logos/new/pterodactyl_logo.png)](https://pterodactyl.io)
![GitHub Workflow Status](https://img.shields.io/github/workflow/status/pterodactyl/panel/tests?label=Tests&style=for-the-badge) ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/pterodactyl/panel/laravel.yaml?label=Tests&style=for-the-badge&branch=develop)
![Discord](https://img.shields.io/discord/122900397965705216?label=Discord&logo=Discord&logoColor=white&style=for-the-badge) ![Discord](https://img.shields.io/discord/122900397965705216?label=Discord&logo=Discord&logoColor=white&style=for-the-badge)
![GitHub Releases](https://img.shields.io/github/downloads/pterodactyl/panel/latest/total?style=for-the-badge) ![GitHub Releases](https://img.shields.io/github/downloads/pterodactyl/panel/latest/total?style=for-the-badge)
![GitHub contributors](https://img.shields.io/github/contributors/pterodactyl/panel?style=for-the-badge) ![GitHub contributors](https://img.shields.io/github/contributors/pterodactyl/panel?style=for-the-badge)
@ -27,17 +27,18 @@ Stop settling for less. Make game servers a first class citizen on your platform
I would like to extend my sincere thanks to the following sponsors for helping fund Pterodactyl's developement. I would like to extend my sincere thanks to the following sponsors for helping fund Pterodactyl's developement.
[Interested in becoming a sponsor?](https://github.com/sponsors/matthewpi) [Interested in becoming a sponsor?](https://github.com/sponsors/matthewpi)
| Company | About | | Company | About |
|-----------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |-----------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [**WISP**](https://wisp.gg) | Extra features. | | [**WISP**](https://wisp.gg) | Extra features. |
| [**Fragnet**](https://fragnet.net) | Providing low latency, high-end game hosting solutions to gamers, game studios and eSports platforms. | | [**RocketNode**](https://rocketnode.com/) | Innovative game server hosting combined with a straightforward control panel, affordable prices, and Rocket-Fast support. |
| [**RocketNode**](https://rocketnode.com/) | Innovative game server hosting combined with a straightforward control panel, affordable prices, and Rocket-Fast support. | | [**Aussie Server Hosts**](https://aussieserverhosts.com/) | No frills Australian Owned and operated High Performance Server hosting for some of the most demanding games serving Australia and New Zealand. |
| [**Aussie Server Hosts**](https://aussieserverhosts.com/) | No frills Australian Owned and operated High Performance Server hosting for some of the most demanding games serving Australia and New Zealand. | | [**BisectHosting**](https://www.bisecthosting.com/) | BisectHosting provides Minecraft, Valheim and other server hosting services with the highest reliability and lightning fast support since 2012. |
| [**BisectHosting**](https://www.bisecthosting.com/) | BisectHosting provides Minecraft, Valheim and other server hosting services with the highest reliability and lightning fast support since 2012. | | [**MineStrator**](https://minestrator.com/) | Looking for the most highend French hosting company for your minecraft server? More than 24,000 members on our discord trust us. Give us a try! |
| [**MineStrator**](https://minestrator.com/) | Looking for the most highend French hosting company for your minecraft server? More than 24,000 members on our discord trust us. Give us a try! | | [**Skynode**](https://www.skynode.pro/) | Skynode provides blazing fast game servers along with a top-notch user experience. Whatever our clients are looking for, we're able to provide it! |
| [**Skynode**](https://www.skynode.pro/) | Skynode provides blazing fast game servers along with a top-notch user experience. Whatever our clients are looking for, we're able to provide it! | | [**VibeGAMES**](https://vibegames.net/) | VibeGAMES is a game server provider that specializes in DDOS protection for the games we offer. We have multiple locations in the US, Brazil, France, Germany, Singapore, Australia and South Africa. |
| [**VibeGAMES**](https://vibegames.net/) | VibeGAMES is a game server provider that specializes in DDOS protection for the games we offer. We have multiple locations in the US, Brazil, France, Germany, Singapore, Australia and South Africa. | | [**Pterodactyl Market**](https://pterodactylmarket.com/) | Pterodactyl Market is a one-and-stop shop for Pterodactyl. In our market, you can find Add-ons, Themes, Eggs, and more for Pterodactyl. |
| [**Pterodactyl Market**](https://pterodactylmarket.com/) | Pterodactyl Market is a one-and-stop shop for Pterodactyl. In our market, you can find Add-ons, Themes, Eggs, and more for Pterodactyl. | | [**UltraServers**](https://ultraservers.com/) | Deploy premium games hosting with the click of a button. Manage and swap games with ease and let us take care of the rest. We currently support Minecraft, Rust, ARK, 7 Days to Die, Garys MOD, CS:GO, Satisfactory and others. |
| [**Realms Hosting**](https://realmshosting.com/) | Want to build your Gaming Empire? Use Realms Hosting today to kick start your game server hosting with outstanding DDOS Protection, 24/7 Support, Cheap Prices and a Custom Control Panel. | |
### Supported Games ### Supported Games

View file

@ -1,11 +1,13 @@
# Security Policy # Security Policy
## Supported Versions ## Supported Versions
The following versions of Pterodactyl are receiving active support and maintenance. Any security vulnerabilities discovered must be reproducible in supported versions. The following versions of Pterodactyl are receiving active support and maintenance. Any security vulnerabilities discovered must be reproducible in supported versions.
| Panel | Daemon | Supported | | Panel | Daemon | Supported |
|--------|--------------|--------------------| |--------|--------------|--------------------|
| 1.10.x | wings@1.7.x | :white_check_mark: | | 1.10.x | wings@1.7.x | :white_check_mark: |
| 1.11.x | wings@1.11.x | :white_check_mark: |
| 0.7.x | daemon@0.6.x | :x: | | 0.7.x | daemon@0.6.x | :x: |

View file

@ -2,7 +2,6 @@
namespace Pterodactyl\Console\Commands\Environment; namespace Pterodactyl\Console\Commands\Environment;
use DateTimeZone;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Kernel; use Illuminate\Contracts\Console\Kernel;
use Pterodactyl\Traits\Commands\EnvironmentWriterTrait; use Pterodactyl\Traits\Commands\EnvironmentWriterTrait;
@ -44,7 +43,8 @@ class AppSettingsCommand extends Command
{--redis-host= : Redis host to use for connections.} {--redis-host= : Redis host to use for connections.}
{--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.}
{--settings-ui= : Enable or disable the settings UI.}'; {--settings-ui= : Enable or disable the settings UI.}
{--telemetry= : Enable or disable anonymous telemetry.}';
protected array $variables = []; protected array $variables = [];
@ -88,7 +88,7 @@ class AppSettingsCommand extends Command
$this->output->comment('The timezone should match one of PHP\'s supported timezones. If you are unsure, please reference https://php.net/manual/en/timezones.php.'); $this->output->comment('The timezone should match one of PHP\'s supported timezones. If you are unsure, please reference https://php.net/manual/en/timezones.php.');
$this->variables['APP_TIMEZONE'] = $this->option('timezone') ?? $this->anticipate( $this->variables['APP_TIMEZONE'] = $this->option('timezone') ?? $this->anticipate(
'Application Timezone', 'Application Timezone',
DateTimeZone::listIdentifiers(), \DateTimeZone::listIdentifiers(),
config('app.timezone') config('app.timezone')
); );
@ -119,6 +119,12 @@ class AppSettingsCommand extends Command
$this->variables['APP_ENVIRONMENT_ONLY'] = $this->confirm('Enable UI based settings editor?', true) ? 'false' : 'true'; $this->variables['APP_ENVIRONMENT_ONLY'] = $this->confirm('Enable UI based settings editor?', true) ? 'false' : 'true';
} }
$this->output->comment('Please reference https://pterodactyl.io/panel/1.0/additional_configuration.html#telemetry for more detailed information regarding telemetry data and collection.');
$this->variables['PTERODACTYL_TELEMETRY_ENABLED'] = $this->option('telemetry') ?? $this->confirm(
'Enable sending anonymous telemetry data?',
config('pterodactyl.telemetry.enabled', true)
) ? 'true' : 'false';
// Make sure session cookies are set as "secure" when using HTTPS // Make sure session cookies are set as "secure" when using HTTPS
if (str_starts_with($this->variables['APP_URL'], 'https://')) { if (str_starts_with($this->variables['APP_URL'], 'https://')) {
$this->variables['SESSION_SECURE_COOKIE'] = 'true'; $this->variables['SESSION_SECURE_COOKIE'] = 'true';

View file

@ -2,7 +2,6 @@
namespace Pterodactyl\Console\Commands\Environment; namespace Pterodactyl\Console\Commands\Environment;
use PDOException;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Kernel; use Illuminate\Contracts\Console\Kernel;
use Illuminate\Database\DatabaseManager; use Illuminate\Database\DatabaseManager;
@ -72,7 +71,7 @@ class DatabaseSettingsCommand extends Command
try { try {
$this->testMySQLConnection(); $this->testMySQLConnection();
} 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('Your connection credentials have NOT been saved. You will need to provide valid connection information before proceeding.'); $this->output->error('Your connection credentials have NOT been saved. You will need to provide valid connection information before proceeding.');

View file

@ -57,7 +57,7 @@ class EmailSettingsCommand extends Command
$this->{$method}(); $this->{$method}();
} }
$this->variables['MAIL_FROM'] = $this->option('email') ?? $this->ask( $this->variables['MAIL_FROM_ADDRESS'] = $this->option('email') ?? $this->ask(
trans('command/messages.environment.mail.ask_mail_from'), trans('command/messages.environment.mail.ask_mail_from'),
$this->config->get('mail.from.address') $this->config->get('mail.from.address')
); );
@ -67,12 +67,6 @@ class EmailSettingsCommand extends Command
$this->config->get('mail.from.name') $this->config->get('mail.from.name')
); );
$this->variables['MAIL_ENCRYPTION'] = $this->option('encryption') ?? $this->choice(
trans('command/messages.environment.mail.ask_encryption'),
['tls' => 'TLS', 'ssl' => 'SSL', '' => 'None'],
$this->config->get('mail.encryption', 'tls')
);
$this->writeToEnvironment($this->variables); $this->writeToEnvironment($this->variables);
$this->line('Updating stored environment configuration file.'); $this->line('Updating stored environment configuration file.');
@ -102,6 +96,12 @@ class EmailSettingsCommand extends Command
$this->variables['MAIL_PASSWORD'] = $this->option('password') ?? $this->secret( $this->variables['MAIL_PASSWORD'] = $this->option('password') ?? $this->secret(
trans('command/messages.environment.mail.ask_smtp_password') trans('command/messages.environment.mail.ask_smtp_password')
); );
$this->variables['MAIL_ENCRYPTION'] = $this->option('encryption') ?? $this->choice(
trans('command/messages.environment.mail.ask_encryption'),
['tls' => 'TLS', 'ssl' => 'SSL', '' => 'None'],
$this->config->get('mail.mailers.smtp.encryption', 'tls')
);
} }
/** /**

View file

@ -15,7 +15,7 @@ class InfoCommand extends Command
/** /**
* VersionCommand constructor. * VersionCommand constructor.
*/ */
public function __construct(private ConfigRepository $config, private SoftwareVersionService $versionService) public function __construct(private ConfigRepository $config, private SoftwareVersionService $softwareVersionService)
{ {
parent::__construct(); parent::__construct();
} }
@ -27,9 +27,9 @@ class InfoCommand extends Command
{ {
$this->output->title('Version Information'); $this->output->title('Version Information');
$this->table([], [ $this->table([], [
['Panel Version', $this->config->get('app.version')], ['Panel Version', $this->softwareVersionService->getCurrentVersion()],
['Latest Version', $this->versionService->getPanel()], ['Latest Version', $this->softwareVersionService->getLatestPanel()],
['Up-to-Date', $this->versionService->isLatestPanel() ? 'Yes' : $this->formatText('No', 'bg=red')], ['Up-to-Date', $this->softwareVersionService->isLatestPanel() ? 'Yes' : $this->formatText('No', 'bg=red')],
['Unique Identifier', $this->config->get('pterodactyl.service.author')], ['Unique Identifier', $this->config->get('pterodactyl.service.author')],
], 'compact'); ], 'compact');

View file

@ -2,7 +2,6 @@
namespace Pterodactyl\Console\Commands\Maintenance; namespace Pterodactyl\Console\Commands\Maintenance;
use SplFileInfo;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Contracts\Filesystem\Filesystem; use Illuminate\Contracts\Filesystem\Filesystem;
@ -35,7 +34,7 @@ class CleanServiceBackupFilesCommand extends Command
{ {
$files = $this->disk->files('services/.bak'); $files = $this->disk->files('services/.bak');
collect($files)->each(function (SplFileInfo $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

@ -3,7 +3,6 @@
namespace Pterodactyl\Console\Commands\Maintenance; namespace Pterodactyl\Console\Commands\Maintenance;
use Carbon\CarbonImmutable; use Carbon\CarbonImmutable;
use InvalidArgumentException;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Pterodactyl\Repositories\Eloquent\BackupRepository; use Pterodactyl\Repositories\Eloquent\BackupRepository;
@ -25,7 +24,7 @@ class PruneOrphanedBackupsCommand extends Command
{ {
$since = $this->option('prune-age') ?? config('backups.prune_age', 360); $since = $this->option('prune-age') ?? config('backups.prune_age', 360);
if (!$since || !is_digit($since)) { if (!$since || !is_digit($since)) {
throw new InvalidArgumentException('The "--prune-age" argument must be a value greater than 0.'); throw new \InvalidArgumentException('The "--prune-age" argument must be a value greater than 0.');
} }
$query = $this->backupRepository->getBuilder() $query = $this->backupRepository->getBuilder()

View file

@ -21,6 +21,6 @@ class UpCommand extends BaseUpCommand
return 1; return 1;
} }
return parent::handle() ?? 0; return parent::handle();
} }
} }

View file

@ -3,7 +3,6 @@
namespace Pterodactyl\Console\Commands\Schedule; namespace Pterodactyl\Console\Commands\Schedule;
use Exception; use Exception;
use Throwable;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Pterodactyl\Models\Schedule; use Pterodactyl\Models\Schedule;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
@ -68,7 +67,7 @@ class ProcessRunnableCommand extends Command
'schedule' => $schedule->name, 'schedule' => $schedule->name,
'hash' => $schedule->hashid, 'hash' => $schedule->hashid,
])); ]));
} catch (Throwable|Exception $exception) { } catch (\Throwable|\Exception $exception) {
Log::error($exception, ['schedule_id' => $schedule->id]); Log::error($exception, ['schedule_id' => $schedule->id]);
$this->error("An error was encountered while processing Schedule #$schedule->id: " . $exception->getMessage()); $this->error("An error was encountered while processing Schedule #$schedule->id: " . $exception->getMessage());

View file

@ -0,0 +1,34 @@
<?php
namespace Pterodactyl\Console\Commands;
use Illuminate\Console\Command;
use Symfony\Component\VarDumper\VarDumper;
use Pterodactyl\Services\Telemetry\TelemetryCollectionService;
class TelemetryCommand extends Command
{
protected $description = 'Displays all the data that would be sent to the Pterodactyl Telemetry Service if telemetry collection is enabled.';
protected $signature = 'p:telemetry';
/**
* TelemetryCommand constructor.
*/
public function __construct(private TelemetryCollectionService $telemetryCollectionService)
{
parent::__construct();
}
/**
* Handle execution of command.
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
*/
public function handle()
{
$this->output->info('Collecting telemetry data, this may take a while...');
VarDumper::dump($this->telemetryCollectionService->collect());
}
}

View file

@ -2,7 +2,6 @@
namespace Pterodactyl\Console\Commands; namespace Pterodactyl\Console\Commands;
use Closure;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Pterodactyl\Console\Kernel; use Pterodactyl\Console\Kernel;
use Symfony\Component\Process\Process; use Symfony\Component\Process\Process;
@ -177,7 +176,7 @@ class UpgradeCommand extends Command
$this->info('Panel has been successfully upgraded. Please ensure you also update any Wings instances: https://pterodactyl.io/wings/1.0/upgrading.html'); $this->info('Panel has been successfully upgraded. Please ensure you also update any Wings instances: https://pterodactyl.io/wings/1.0/upgrading.html');
} }
protected function withProgress(ProgressBar $bar, Closure $callback) protected function withProgress(ProgressBar $bar, \Closure $callback)
{ {
$bar->clear(); $bar->clear();
$callback(); $callback();

View file

@ -44,10 +44,10 @@ class DeleteUserCommand extends Command
if ($this->input->isInteractive()) { if ($this->input->isInteractive()) {
$tableValues = []; $tableValues = [];
foreach ($results as $user) { foreach ($results as $user) {
$tableValues[] = [$user->id, $user->email, $user->name]; $tableValues[] = [$user->id, $user->email, $user->username];
} }
$this->table(['User ID', 'Email', 'Name'], $tableValues); $this->table(['User ID', 'Email', 'Username'], $tableValues);
if (!$deleteUser = $this->ask(trans('command/messages.user.select_search_user'))) { if (!$deleteUser = $this->ask(trans('command/messages.user.select_search_user'))) {
return $this->handle(); return $this->handle();
} }

View file

@ -30,8 +30,6 @@ class MakeUserCommand extends Command
$root_admin = $this->option('admin') ?? $this->confirm(trans('command/messages.user.ask_admin')); $root_admin = $this->option('admin') ?? $this->confirm(trans('command/messages.user.ask_admin'));
$email = $this->option('email') ?? $this->ask(trans('command/messages.user.ask_email')); $email = $this->option('email') ?? $this->ask(trans('command/messages.user.ask_email'));
$username = $this->option('username') ?? $this->ask(trans('command/messages.user.ask_username')); $username = $this->option('username') ?? $this->ask(trans('command/messages.user.ask_username'));
$name_first = $this->option('name-first') ?? $this->ask(trans('command/messages.user.ask_name_first'));
$name_last = $this->option('name-last') ?? $this->ask(trans('command/messages.user.ask_name_last'));
if (is_null($password = $this->option('password')) && !$this->option('no-password')) { if (is_null($password = $this->option('password')) && !$this->option('no-password')) {
$this->warn(trans('command/messages.user.ask_password_help')); $this->warn(trans('command/messages.user.ask_password_help'));
@ -39,12 +37,11 @@ class MakeUserCommand extends Command
$password = $this->secret(trans('command/messages.user.ask_password')); $password = $this->secret(trans('command/messages.user.ask_password'));
} }
$user = $this->creationService->handle(compact('email', 'username', 'name_first', 'name_last', 'password', 'root_admin')); $user = $this->creationService->handle(compact('email', 'username', 'password', 'root_admin'));
$this->table(['Field', 'Value'], [ $this->table(['Field', 'Value'], [
['UUID', $user->uuid], ['UUID', $user->uuid],
['Email', $user->email], ['Email', $user->email],
['Username', $user->username], ['Username', $user->username],
['Name', $user->name],
['Admin', $user->root_admin ? 'Yes' : 'No'], ['Admin', $user->root_admin ? 'Yes' : 'No'],
]); ]);
} }

View file

@ -2,10 +2,13 @@
namespace Pterodactyl\Console; namespace Pterodactyl\Console;
use Ramsey\Uuid\Uuid;
use Pterodactyl\Models\ActivityLog; use Pterodactyl\Models\ActivityLog;
use Illuminate\Console\Scheduling\Schedule; use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Database\Console\PruneCommand; use Illuminate\Database\Console\PruneCommand;
use Pterodactyl\Repositories\Eloquent\SettingsRepository;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel; use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use Pterodactyl\Services\Telemetry\TelemetryCollectionService;
use Pterodactyl\Console\Commands\Schedule\ProcessRunnableCommand; use Pterodactyl\Console\Commands\Schedule\ProcessRunnableCommand;
use Pterodactyl\Console\Commands\Maintenance\PruneOrphanedBackupsCommand; use Pterodactyl\Console\Commands\Maintenance\PruneOrphanedBackupsCommand;
use Pterodactyl\Console\Commands\Maintenance\CleanServiceBackupFilesCommand; use Pterodactyl\Console\Commands\Maintenance\CleanServiceBackupFilesCommand;
@ -37,5 +40,34 @@ class Kernel extends ConsoleKernel
if (config('activity.prune_days')) { if (config('activity.prune_days')) {
$schedule->command(PruneCommand::class, ['--model' => [ActivityLog::class]])->daily(); $schedule->command(PruneCommand::class, ['--model' => [ActivityLog::class]])->daily();
} }
if (config('pterodactyl.telemetry.enabled')) {
$this->registerTelemetry($schedule);
}
}
/**
* I wonder what this does.
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
private function registerTelemetry(Schedule $schedule): void
{
$settingsRepository = app()->make(SettingsRepository::class);
$uuid = $settingsRepository->get('app:telemetry:uuid');
if (is_null($uuid)) {
$uuid = Uuid::uuid4()->toString();
$settingsRepository->set('app:telemetry:uuid', $uuid);
}
// Calculate a fixed time to run the data push at, this will be the same time every day.
$time = hexdec(str_replace('-', '', substr($uuid, 27))) % 1440;
$hour = floor($time / 60);
$minute = $time % 60;
// Run the telemetry collector.
$schedule->call(app()->make(TelemetryCollectionService::class))->description('Collect Telemetry')->dailyAt("$hour:$minute");
} }
} }

View file

@ -2,8 +2,6 @@
namespace Pterodactyl\Exceptions; namespace Pterodactyl\Exceptions;
use Exception; class AccountNotFoundException extends \Exception
class AccountNotFoundException extends Exception
{ {
} }

View file

@ -2,8 +2,6 @@
namespace Pterodactyl\Exceptions; namespace Pterodactyl\Exceptions;
use Exception; class AutoDeploymentException extends \Exception
class AutoDeploymentException extends Exception
{ {
} }

View file

@ -3,14 +3,12 @@
namespace Pterodactyl\Exceptions; namespace Pterodactyl\Exceptions;
use Exception; use Exception;
use Throwable;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Container\Container; use Illuminate\Container\Container;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
class DisplayException extends PterodactylException implements HttpExceptionInterface class DisplayException extends PterodactylException implements HttpExceptionInterface
@ -23,7 +21,7 @@ class DisplayException extends PterodactylException implements HttpExceptionInte
/** /**
* DisplayException constructor. * DisplayException constructor.
*/ */
public function __construct(string $message, ?Throwable $previous = null, protected string $level = self::LEVEL_ERROR, int $code = 0) public function __construct(string $message, ?\Throwable $previous = null, protected string $level = self::LEVEL_ERROR, int $code = 0)
{ {
parent::__construct($message, $code, $previous); parent::__construct($message, $code, $previous);
} }
@ -54,8 +52,6 @@ class DisplayException extends PterodactylException implements HttpExceptionInte
return response()->json(Handler::toArray($this), $this->getStatusCode(), $this->getHeaders()); return response()->json(Handler::toArray($this), $this->getStatusCode(), $this->getHeaders());
} }
app(AlertsMessageBag::class)->danger($this->getMessage())->flash();
return redirect()->back()->withInput(); return redirect()->back()->withInput();
} }
@ -67,7 +63,7 @@ class DisplayException extends PterodactylException implements HttpExceptionInte
*/ */
public function report() public function report()
{ {
if (!$this->getPrevious() instanceof Exception || !Handler::isReportable($this->getPrevious())) { if (!$this->getPrevious() instanceof \Exception || !Handler::isReportable($this->getPrevious())) {
return null; return null;
} }

View file

@ -3,8 +3,6 @@
namespace Pterodactyl\Exceptions; namespace Pterodactyl\Exceptions;
use Exception; use Exception;
use Throwable;
use PDOException;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
@ -25,7 +23,7 @@ use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
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;
class Handler extends ExceptionHandler final class Handler extends ExceptionHandler
{ {
/** /**
* The validation parser in Laravel formats custom rules using the class name * The validation parser in Laravel formats custom rules using the class name
@ -81,7 +79,7 @@ class Handler extends ExceptionHandler
$this->dontReport = []; $this->dontReport = [];
} }
$this->reportable(function (PDOException $ex) { $this->reportable(function (\PDOException $ex) {
$ex = $this->generateCleanedExceptionStack($ex); $ex = $this->generateCleanedExceptionStack($ex);
}); });
@ -90,7 +88,7 @@ class Handler extends ExceptionHandler
}); });
} }
private function generateCleanedExceptionStack(Throwable $exception): string private function generateCleanedExceptionStack(\Throwable $exception): string
{ {
$cleanedStack = ''; $cleanedStack = '';
foreach ($exception->getTrace() as $index => $item) { foreach ($exception->getTrace() as $index => $item) {
@ -123,7 +121,7 @@ class Handler extends ExceptionHandler
* *
* @throws \Throwable * @throws \Throwable
*/ */
public function render($request, Throwable $e): Response public function render($request, \Throwable $e): Response
{ {
$connections = $this->container->make(Connection::class); $connections = $this->container->make(Connection::class);
@ -189,7 +187,7 @@ class Handler extends ExceptionHandler
/** /**
* Return the exception as a JSONAPI representation for use on API requests. * Return the exception as a JSONAPI representation for use on API requests.
*/ */
protected function convertExceptionToArray(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;
@ -235,7 +233,7 @@ class Handler extends ExceptionHandler
/** /**
* Return an array of exceptions that should not be reported. * Return an array of exceptions that should not be reported.
*/ */
public static function isReportable(Exception $exception): bool public static function isReportable(\Exception $exception): bool
{ {
return (new static(Container::getInstance()))->shouldReport($exception); return (new static(Container::getInstance()))->shouldReport($exception);
} }
@ -260,11 +258,11 @@ class Handler extends ExceptionHandler
* *
* @return \Throwable[] * @return \Throwable[]
*/ */
protected 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) { if (!$value instanceof \Throwable) {
break; break;
} }
$previous[] = $value; $previous[] = $value;
@ -278,7 +276,7 @@ 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.
*/ */
public static function toArray(Throwable $e): array public static function toArray(\Throwable $e): array
{ {
return (new self(app()))->convertExceptionToArray($e); return (new self(app()))->convertExceptionToArray($e);
} }

View file

@ -0,0 +1,21 @@
<?php
namespace Pterodactyl\Exceptions\Http;
use Illuminate\Http\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;
class QueryValueOutOfRangeHttpException extends HttpException
{
/**
* QueryValueOutOfRangeHttpException constructor.
*/
public function __construct(string $name, int $min, int $max, \Throwable $previous = null)
{
parent::__construct(
Response::HTTP_BAD_REQUEST,
'\"' . $name . '\" query parameter must be between ' . $min . ' and ' . $max,
$previous,
);
}
}

View file

@ -2,7 +2,6 @@
namespace Pterodactyl\Exceptions\Http\Server; namespace Pterodactyl\Exceptions\Http\Server;
use Throwable;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException; use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
@ -12,11 +11,13 @@ class ServerStateConflictException extends ConflictHttpException
* Exception thrown when the server is in an unsupported state for API access or * Exception thrown when the server is in an unsupported state for API access or
* certain operations within the codebase. * certain operations within the codebase.
*/ */
public function __construct(Server $server, Throwable $previous = null) public function __construct(Server $server, \Throwable $previous = null)
{ {
$message = 'This server is currently in an unsupported state, please try again later.'; $message = 'This server is currently in an unsupported state, please try again later.';
if ($server->isSuspended()) { if ($server->isSuspended()) {
$message = 'This server is currently suspended and the functionality requested is unavailable.'; $message = 'This server is currently suspended and the functionality requested is unavailable.';
} elseif ($server->node->isUnderMaintenance()) {
$message = 'The node of this server is currently under maintenance and the functionality requested is unavailable.';
} elseif (!$server->isInstalled()) { } elseif (!$server->isInstalled()) {
$message = 'This server has not yet completed its installation process, please try again later.'; $message = 'This server has not yet completed its installation process, please try again later.';
} elseif ($server->status === Server::STATUS_RESTORING_BACKUP) { } elseif ($server->status === Server::STATUS_RESTORING_BACKUP) {

View file

@ -2,7 +2,6 @@
namespace Pterodactyl\Exceptions\Http; namespace Pterodactyl\Exceptions\Http;
use Throwable;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
@ -12,7 +11,7 @@ class TwoFactorAuthRequiredException extends HttpException implements HttpExcept
/** /**
* TwoFactorAuthRequiredException constructor. * TwoFactorAuthRequiredException constructor.
*/ */
public function __construct(Throwable $previous = null) public function __construct(\Throwable $previous = null)
{ {
parent::__construct(Response::HTTP_BAD_REQUEST, 'Two-factor authentication is required on this account in order to access this endpoint.', $previous); parent::__construct(Response::HTTP_BAD_REQUEST, 'Two-factor authentication is required on this account in order to access this endpoint.', $previous);
} }

View file

@ -1,15 +0,0 @@
<?php
namespace Pterodactyl\Exceptions;
use Exception;
use Spatie\Ignition\Contracts\Solution;
use Spatie\Ignition\Contracts\ProvidesSolution;
class ManifestDoesNotExistException extends Exception implements ProvidesSolution
{
public function getSolution(): Solution
{
return new Solutions\ManifestDoesNotExistSolution();
}
}

View file

@ -2,8 +2,6 @@
namespace Pterodactyl\Exceptions; namespace Pterodactyl\Exceptions;
use Exception; class PterodactylException extends \Exception
class PterodactylException extends Exception
{ {
} }

View file

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

View file

@ -2,8 +2,6 @@
namespace Pterodactyl\Exceptions\Service\Helper; namespace Pterodactyl\Exceptions\Service\Helper;
use Exception; class CdnVersionFetchingException extends \Exception
class CdnVersionFetchingException extends Exception
{ {
} }

View file

@ -2,7 +2,6 @@
namespace Pterodactyl\Exceptions\Service; namespace Pterodactyl\Exceptions\Service;
use Throwable;
use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\DisplayException;
class ServiceLimitExceededException extends DisplayException class ServiceLimitExceededException extends DisplayException
@ -11,7 +10,7 @@ class ServiceLimitExceededException extends DisplayException
* Exception thrown when something goes over a defined limit, such as allocated * Exception thrown when something goes over a defined limit, such as allocated
* ports, tasks, databases, etc. * ports, tasks, databases, etc.
*/ */
public function __construct(string $message, Throwable $previous = null) public function __construct(string $message, \Throwable $previous = null)
{ {
parent::__construct($message, $previous, self::LEVEL_WARNING); parent::__construct($message, $previous, self::LEVEL_WARNING);
} }

View file

@ -1,25 +0,0 @@
<?php
namespace Pterodactyl\Exceptions\Solutions;
use Spatie\Ignition\Contracts\Solution;
class ManifestDoesNotExistSolution implements Solution
{
public function getSolutionTitle(): string
{
return "The manifest.json file hasn't been generated yet";
}
public function getSolutionDescription(): string
{
return 'Run yarn run build:production to build the frontend first.';
}
public function getDocumentationLinks(): array
{
return [
'Docs' => 'https://github.com/pterodactyl/panel/blob/develop/package.json',
];
}
}

View file

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

View file

@ -2,12 +2,10 @@
namespace Pterodactyl\Extensions\Backups; namespace Pterodactyl\Extensions\Backups;
use Closure;
use Aws\S3\S3Client; use Aws\S3\S3Client;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert;
use InvalidArgumentException;
use Illuminate\Foundation\Application; use Illuminate\Foundation\Application;
use League\Flysystem\FilesystemAdapter; use League\Flysystem\FilesystemAdapter;
use Pterodactyl\Extensions\Filesystem\S3Filesystem; use Pterodactyl\Extensions\Filesystem\S3Filesystem;
@ -70,7 +68,7 @@ class BackupManager
$config = $this->getConfig($name); $config = $this->getConfig($name);
if (empty($config['adapter'])) { if (empty($config['adapter'])) {
throw new InvalidArgumentException("Backup disk [$name] does not have a configured adapter."); throw new \InvalidArgumentException("Backup disk [$name] does not have a configured adapter.");
} }
$adapter = $config['adapter']; $adapter = $config['adapter'];
@ -88,7 +86,7 @@ class BackupManager
return $instance; return $instance;
} }
throw new InvalidArgumentException("Adapter [$adapter] is not supported."); throw new \InvalidArgumentException("Adapter [$adapter] is not supported.");
} }
/** /**
@ -164,7 +162,7 @@ class BackupManager
/** /**
* Register a custom adapter creator closure. * Register a custom adapter creator closure.
*/ */
public function extend(string $adapter, Closure $callback): self public function extend(string $adapter, \Closure $callback): self
{ {
$this->customCreators[$adapter] = $callback; $this->customCreators[$adapter] = $callback;

View file

@ -1,13 +0,0 @@
<?php
namespace Pterodactyl\Extensions\Facades;
use Illuminate\Support\Facades\Facade;
class Theme extends Facade
{
protected static function getFacadeAccessor(): string
{
return 'extensions.themes';
}
}

View file

@ -2,7 +2,6 @@
namespace Pterodactyl\Extensions\Lcobucci\JWT\Encoding; namespace Pterodactyl\Extensions\Lcobucci\JWT\Encoding;
use DateTimeImmutable;
use Lcobucci\JWT\ClaimsFormatter; use Lcobucci\JWT\ClaimsFormatter;
use Lcobucci\JWT\Token\RegisteredClaims; use Lcobucci\JWT\Token\RegisteredClaims;
@ -21,7 +20,7 @@ final class TimestampDates implements ClaimsFormatter
continue; continue;
} }
assert($claims[$claim] instanceof DateTimeImmutable); assert($claims[$claim] instanceof \DateTimeImmutable);
$claims[$claim] = $claims[$claim]->getTimestamp(); $claims[$claim] = $claims[$claim]->getTimestamp();
} }

View file

@ -6,17 +6,6 @@ use League\Fractal\Serializer\ArraySerializer;
class PterodactylSerializer extends ArraySerializer class PterodactylSerializer extends ArraySerializer
{ {
/**
* Serialize an item.
*/
public function item(?string $resourceKey, array $data): array
{
return [
'object' => $resourceKey,
'attributes' => $data,
];
}
/** /**
* Serialize a collection. * Serialize a collection.
*/ */
@ -33,6 +22,17 @@ class PterodactylSerializer extends ArraySerializer
]; ];
} }
/**
* Serialize an item.
*/
public function item(?string $resourceKey, array $data): array
{
return [
'object' => $resourceKey,
'attributes' => $data,
];
}
/** /**
* Serialize a null resource. * Serialize a null resource.
*/ */

View file

@ -3,8 +3,8 @@
namespace Pterodactyl\Extensions\Spatie\Fractalistic; namespace Pterodactyl\Extensions\Spatie\Fractalistic;
use League\Fractal\Scope; use League\Fractal\Scope;
use League\Fractal\TransformerAbstract;
use Spatie\Fractal\Fractal as SpatieFractal; use Spatie\Fractal\Fractal as SpatieFractal;
use Pterodactyl\Transformers\Api\Transformer;
use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use League\Fractal\Pagination\IlluminatePaginatorAdapter; use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use Pterodactyl\Extensions\League\Fractal\Serializers\PterodactylSerializer; use Pterodactyl\Extensions\League\Fractal\Serializers\PterodactylSerializer;
@ -32,12 +32,9 @@ class Fractal extends SpatieFractal
// If the resource name is not set attempt to pull it off the transformer // If the resource name is not set attempt to pull it off the transformer
// itself and set it automatically. // itself and set it automatically.
if ( $class = is_string($this->transformer) ? new $this->transformer() : $this->transformer;
is_null($this->resourceName) if (is_null($this->resourceName) && $class instanceof Transformer) {
&& $this->transformer instanceof TransformerAbstract $this->resourceName = $class->getResourceName();
&& method_exists($this->transformer, 'getResourceName')
) {
$this->resourceName = $this->transformer->getResourceName();
} }
return parent::createData(); return parent::createData();

View file

@ -1,21 +0,0 @@
<?php
namespace Pterodactyl\Extensions\Themes;
class Theme
{
public function js($path): string
{
return sprintf('<script src="%s"></script>' . PHP_EOL, $this->getUrl($path));
}
public function css($path): string
{
return sprintf('<link media="all" type="text/css" rel="stylesheet" href="%s"/>' . PHP_EOL, $this->getUrl($path));
}
protected function getUrl($path): string
{
return '/themes/pterodactyl/' . ltrim($path, '/');
}
}

View file

@ -2,7 +2,6 @@
namespace Pterodactyl\Helpers; namespace Pterodactyl\Helpers;
use Exception;
use Carbon\Carbon; use Carbon\Carbon;
use Cron\CronExpression; use Cron\CronExpression;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
@ -25,7 +24,7 @@ class Utilities
$string = substr_replace($string, $character, random_int(0, $length - 1), 1); $string = substr_replace($string, $character, random_int(0, $length - 1), 1);
} }
} catch (Exception $exception) { } catch (\Exception $exception) {
// Just log the error and hope for the best at this point. // Just log the error and hope for the best at this point.
Log::error($exception); Log::error($exception);
} }

View file

@ -1,87 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Admin;
use Illuminate\View\View;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Pterodactyl\Models\ApiKey;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Pterodactyl\Services\Acl\Api\AdminAcl;
use Illuminate\View\Factory as ViewFactory;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Services\Api\KeyCreationService;
use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface;
use Pterodactyl\Http\Requests\Admin\Api\StoreApplicationApiKeyRequest;
class ApiController extends Controller
{
/**
* ApiController constructor.
*/
public function __construct(
private AlertsMessageBag $alert,
private ApiKeyRepositoryInterface $repository,
private KeyCreationService $keyCreationService,
private ViewFactory $view,
) {
}
/**
* Render view showing all of a user's application API keys.
*/
public function index(Request $request): View
{
return $this->view->make('admin.api.index', [
'keys' => $this->repository->getApplicationKeys($request->user()),
]);
}
/**
* Render view allowing an admin to create a new application API key.
*
* @throws \ReflectionException
*/
public function create(): View
{
$resources = AdminAcl::getResourceList();
sort($resources);
return $this->view->make('admin.api.new', [
'resources' => $resources,
'permissions' => [
'r' => AdminAcl::READ,
'rw' => AdminAcl::READ | AdminAcl::WRITE,
'n' => AdminAcl::NONE,
],
]);
}
/**
* Store the new key and redirect the user back to the application key listing.
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
*/
public function store(StoreApplicationApiKeyRequest $request): RedirectResponse
{
$this->keyCreationService->setKeyType(ApiKey::TYPE_APPLICATION)->handle([
'memo' => $request->input('memo'),
'user_id' => $request->user()->id,
], $request->getKeyPermissions());
$this->alert->success('A new application API key has been generated for your account.')->flash();
return redirect()->route('admin.api.index');
}
/**
* Delete an application API key from the database.
*/
public function delete(Request $request, string $identifier): Response
{
$this->repository->deleteApplicationKey($request->user(), $identifier);
return response('', 204);
}
}

View file

@ -3,24 +3,12 @@
namespace Pterodactyl\Http\Controllers\Admin; namespace Pterodactyl\Http\Controllers\Admin;
use Illuminate\View\View; use Illuminate\View\View;
use Illuminate\View\Factory as ViewFactory;
use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Services\Helpers\SoftwareVersionService;
class BaseController extends Controller class BaseController extends Controller
{ {
/**
* BaseController constructor.
*/
public function __construct(private SoftwareVersionService $version, private ViewFactory $view)
{
}
/**
* Return the admin index view.
*/
public function index(): View public function index(): View
{ {
return $this->view->make('admin.index', ['version' => $this->version]); return view('templates/base.core');
} }
} }

View file

@ -1,130 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Admin;
use Exception;
use PDOException;
use Illuminate\View\View;
use Pterodactyl\Models\DatabaseHost;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\View\Factory as ViewFactory;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Services\Databases\Hosts\HostUpdateService;
use Pterodactyl\Http\Requests\Admin\DatabaseHostFormRequest;
use Pterodactyl\Services\Databases\Hosts\HostCreationService;
use Pterodactyl\Services\Databases\Hosts\HostDeletionService;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
use Pterodactyl\Contracts\Repository\LocationRepositoryInterface;
use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface;
class DatabaseController extends Controller
{
/**
* DatabaseController constructor.
*/
public function __construct(
private AlertsMessageBag $alert,
private DatabaseHostRepositoryInterface $repository,
private DatabaseRepositoryInterface $databaseRepository,
private HostCreationService $creationService,
private HostDeletionService $deletionService,
private HostUpdateService $updateService,
private LocationRepositoryInterface $locationRepository,
private ViewFactory $view
) {
}
/**
* Display database host index.
*/
public function index(): View
{
return $this->view->make('admin.databases.index', [
'locations' => $this->locationRepository->getAllWithNodes(),
'hosts' => $this->repository->getWithViewDetails(),
]);
}
/**
* Display database host to user.
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function view(int $host): View
{
return $this->view->make('admin.databases.view', [
'locations' => $this->locationRepository->getAllWithNodes(),
'host' => $this->repository->find($host),
'databases' => $this->databaseRepository->getDatabasesForHost($host),
]);
}
/**
* Handle request to create a new database host.
*
* @throws \Throwable
*/
public function create(DatabaseHostFormRequest $request): RedirectResponse
{
try {
$host = $this->creationService->handle($request->normalize());
} catch (Exception $exception) {
if ($exception instanceof PDOException || $exception->getPrevious() instanceof PDOException) {
$this->alert->danger(
sprintf('There was an error while trying to connect to the host or while executing a query: "%s"', $exception->getMessage())
)->flash();
return redirect()->route('admin.databases')->withInput($request->validated());
} else {
throw $exception;
}
}
$this->alert->success('Successfully created a new database host on the system.')->flash();
return redirect()->route('admin.databases.view', $host->id);
}
/**
* Handle updating database host.
*
* @throws \Throwable
*/
public function update(DatabaseHostFormRequest $request, DatabaseHost $host): RedirectResponse
{
$redirect = redirect()->route('admin.databases.view', $host->id);
try {
$this->updateService->handle($host->id, $request->normalize());
$this->alert->success('Database host was updated successfully.')->flash();
} catch (Exception $exception) {
// Catch any SQL related exceptions and display them back to the user, otherwise just
// throw the exception like normal and move on with it.
if ($exception instanceof PDOException || $exception->getPrevious() instanceof PDOException) {
$this->alert->danger(
sprintf('There was an error while trying to connect to the host or while executing a query: "%s"', $exception->getMessage())
)->flash();
return $redirect->withInput($request->normalize());
} else {
throw $exception;
}
}
return $redirect;
}
/**
* Handle request to delete a database host.
*
* @throws \Pterodactyl\Exceptions\Service\HasActiveServersException
*/
public function delete(int $host): RedirectResponse
{
$this->deletionService->handle($host);
$this->alert->success('The requested database host has been deleted from the system.')->flash();
return redirect()->route('admin.databases');
}
}

View file

@ -1,103 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Admin;
use Illuminate\View\View;
use Pterodactyl\Models\Location;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\View\Factory as ViewFactory;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Http\Requests\Admin\LocationFormRequest;
use Pterodactyl\Services\Locations\LocationUpdateService;
use Pterodactyl\Services\Locations\LocationCreationService;
use Pterodactyl\Services\Locations\LocationDeletionService;
use Pterodactyl\Contracts\Repository\LocationRepositoryInterface;
class LocationController extends Controller
{
/**
* LocationController constructor.
*/
public function __construct(
protected AlertsMessageBag $alert,
protected LocationCreationService $creationService,
protected LocationDeletionService $deletionService,
protected LocationRepositoryInterface $repository,
protected LocationUpdateService $updateService,
protected ViewFactory $view
) {
}
/**
* Return the location overview page.
*/
public function index(): View
{
return $this->view->make('admin.locations.index', [
'locations' => $this->repository->getAllWithDetails(),
]);
}
/**
* Return the location view page.
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function view(int $id): View
{
return $this->view->make('admin.locations.view', [
'location' => $this->repository->getWithNodes($id),
]);
}
/**
* Handle request to create new location.
*
* @throws \Throwable
*/
public function create(LocationFormRequest $request): RedirectResponse
{
$location = $this->creationService->handle($request->normalize());
$this->alert->success('Location was created successfully.')->flash();
return redirect()->route('admin.locations.view', $location->id);
}
/**
* Handle request to update or delete location.
*
* @throws \Throwable
*/
public function update(LocationFormRequest $request, Location $location): RedirectResponse
{
if ($request->input('action') === 'delete') {
return $this->delete($location);
}
$this->updateService->handle($location->id, $request->normalize());
$this->alert->success('Location was updated successfully.')->flash();
return redirect()->route('admin.locations.view', $location->id);
}
/**
* Delete a location from the system.
*
* @throws \Exception
* @throws \Pterodactyl\Exceptions\DisplayException
*/
public function delete(Location $location): RedirectResponse
{
try {
$this->deletionService->handle($location->id);
return redirect()->route('admin.locations');
} catch (DisplayException $ex) {
$this->alert->danger($ex->getMessage())->flash();
}
return redirect()->route('admin.locations.view', $location->id);
}
}

View file

@ -1,165 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Admin;
use Ramsey\Uuid\Uuid;
use Illuminate\View\View;
use Illuminate\Http\Request;
use Pterodactyl\Models\Nest;
use Illuminate\Http\Response;
use Pterodactyl\Models\Mount;
use Pterodactyl\Models\Location;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\View\Factory as ViewFactory;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Http\Requests\Admin\MountFormRequest;
use Pterodactyl\Repositories\Eloquent\MountRepository;
use Pterodactyl\Contracts\Repository\NestRepositoryInterface;
use Pterodactyl\Contracts\Repository\LocationRepositoryInterface;
class MountController extends Controller
{
/**
* MountController constructor.
*/
public function __construct(
protected AlertsMessageBag $alert,
protected NestRepositoryInterface $nestRepository,
protected LocationRepositoryInterface $locationRepository,
protected MountRepository $repository,
protected ViewFactory $view
) {
}
/**
* Return the mount overview page.
*/
public function index(): View
{
return $this->view->make('admin.mounts.index', [
'mounts' => $this->repository->getAllWithDetails(),
]);
}
/**
* Return the mount view page.
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function view(string $id): View
{
$nests = Nest::query()->with('eggs')->get();
$locations = Location::query()->with('nodes')->get();
return $this->view->make('admin.mounts.view', [
'mount' => $this->repository->getWithRelations($id),
'nests' => $nests,
'locations' => $locations,
]);
}
/**
* Handle request to create new mount.
*
* @throws \Throwable
*/
public function create(MountFormRequest $request): RedirectResponse
{
$model = (new Mount())->fill($request->validated());
$model->forceFill(['uuid' => Uuid::uuid4()->toString()]);
$model->saveOrFail();
$mount = $model->fresh();
$this->alert->success('Mount was created successfully.')->flash();
return redirect()->route('admin.mounts.view', $mount->id);
}
/**
* Handle request to update or delete location.
*
* @throws \Throwable
*/
public function update(MountFormRequest $request, Mount $mount): RedirectResponse
{
if ($request->input('action') === 'delete') {
return $this->delete($mount);
}
$mount->forceFill($request->validated())->save();
$this->alert->success('Mount was updated successfully.')->flash();
return redirect()->route('admin.mounts.view', $mount->id);
}
/**
* Delete a location from the system.
*
* @throws \Exception
*/
public function delete(Mount $mount): RedirectResponse
{
$mount->delete();
return redirect()->route('admin.mounts');
}
/**
* Adds eggs to the mount's many-to-many relation.
*/
public function addEggs(Request $request, Mount $mount): RedirectResponse
{
$validatedData = $request->validate([
'eggs' => 'required|exists:eggs,id',
]);
$eggs = $validatedData['eggs'] ?? [];
if (count($eggs) > 0) {
$mount->eggs()->attach($eggs);
}
$this->alert->success('Mount was updated successfully.')->flash();
return redirect()->route('admin.mounts.view', $mount->id);
}
/**
* Adds nodes to the mount's many-to-many relation.
*/
public function addNodes(Request $request, Mount $mount): RedirectResponse
{
$data = $request->validate(['nodes' => 'required|exists:nodes,id']);
$nodes = $data['nodes'] ?? [];
if (count($nodes) > 0) {
$mount->nodes()->attach($nodes);
}
$this->alert->success('Mount was updated successfully.')->flash();
return redirect()->route('admin.mounts.view', $mount->id);
}
/**
* Deletes an egg from the mount's many-to-many relation.
*/
public function deleteEgg(Mount $mount, int $egg_id): Response
{
$mount->eggs()->detach($egg_id);
return response('', 204);
}
/**
* Deletes a node from the mount's many-to-many relation.
*/
public function deleteNode(Mount $mount, int $node_id): Response
{
$mount->nodes()->detach($node_id);
return response('', 204);
}
}

View file

@ -1,129 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Admin\Nests;
use JavaScript;
use Illuminate\View\View;
use Pterodactyl\Models\Egg;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\View\Factory as ViewFactory;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Services\Eggs\EggUpdateService;
use Pterodactyl\Services\Eggs\EggCreationService;
use Pterodactyl\Services\Eggs\EggDeletionService;
use Pterodactyl\Http\Requests\Admin\Egg\EggFormRequest;
use Pterodactyl\Contracts\Repository\EggRepositoryInterface;
use Pterodactyl\Contracts\Repository\NestRepositoryInterface;
class EggController extends Controller
{
/**
* EggController constructor.
*/
public function __construct(
protected AlertsMessageBag $alert,
protected EggCreationService $creationService,
protected EggDeletionService $deletionService,
protected EggRepositoryInterface $repository,
protected EggUpdateService $updateService,
protected NestRepositoryInterface $nestRepository,
protected ViewFactory $view
) {
}
/**
* Handle a request to display the Egg creation page.
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function create(): View
{
$nests = $this->nestRepository->getWithEggs();
JavaScript::put(['nests' => $nests->keyBy('id')]);
return $this->view->make('admin.eggs.new', ['nests' => $nests]);
}
/**
* Handle request to store a new Egg.
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Service\Egg\NoParentConfigurationFoundException
*/
public function store(EggFormRequest $request): RedirectResponse
{
$data = $request->validated();
$data['docker_images'] = $this->normalizeDockerImages($data['docker_images'] ?? null);
$egg = $this->creationService->handle($data);
$this->alert->success(trans('admin/nests.eggs.notices.egg_created'))->flash();
return redirect()->route('admin.nests.egg.view', $egg->id);
}
/**
* Handle request to view a single Egg.
*/
public function view(Egg $egg): View
{
return $this->view->make('admin.eggs.view', [
'egg' => $egg,
'images' => array_map(
fn ($key, $value) => $key === $value ? $value : "$key|$value",
array_keys($egg->docker_images),
$egg->docker_images,
),
]);
}
/**
* Handle request to update an Egg.
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\Egg\NoParentConfigurationFoundException
*/
public function update(EggFormRequest $request, Egg $egg): RedirectResponse
{
$data = $request->validated();
$data['docker_images'] = $this->normalizeDockerImages($data['docker_images'] ?? null);
$this->updateService->handle($egg, $data);
$this->alert->success(trans('admin/nests.eggs.notices.updated'))->flash();
return redirect()->route('admin.nests.egg.view', $egg->id);
}
/**
* Handle request to destroy an egg.
*
* @throws \Pterodactyl\Exceptions\Service\Egg\HasChildrenException
* @throws \Pterodactyl\Exceptions\Service\HasActiveServersException
*/
public function destroy(Egg $egg): RedirectResponse
{
$this->deletionService->handle($egg->id);
$this->alert->success(trans('admin/nests.eggs.notices.deleted'))->flash();
return redirect()->route('admin.nests.view', $egg->nest_id);
}
/**
* Normalizes a string of docker image data into the expected egg format.
*/
protected function normalizeDockerImages(string $input = null): array
{
$data = array_map(fn ($value) => trim($value), explode("\n", $input ?? ''));
$images = [];
// Iterate over the image data provided and convert it into a name => image
// pairing that is used to improve the display on the front-end.
foreach ($data as $value) {
$parts = explode('|', $value, 2);
$images[$parts[0]] = empty($parts[1]) ? $parts[0] : $parts[1];
}
return $images;
}
}

View file

@ -1,65 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Admin\Nests;
use Illuminate\View\View;
use Pterodactyl\Models\Egg;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\View\Factory as ViewFactory;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Services\Eggs\Scripts\InstallScriptService;
use Pterodactyl\Contracts\Repository\EggRepositoryInterface;
use Pterodactyl\Http\Requests\Admin\Egg\EggScriptFormRequest;
class EggScriptController extends Controller
{
/**
* EggScriptController constructor.
*/
public function __construct(
protected AlertsMessageBag $alert,
protected EggRepositoryInterface $repository,
protected InstallScriptService $installScriptService,
protected ViewFactory $view
) {
}
/**
* Handle requests to render installation script for an Egg.
*/
public function index(int $egg): View
{
$egg = $this->repository->getWithCopyAttributes($egg);
$copy = $this->repository->findWhere([
['copy_script_from', '=', null],
['nest_id', '=', $egg->nest_id],
['id', '!=', $egg],
]);
$rely = $this->repository->findWhere([
['copy_script_from', '=', $egg->id],
]);
return $this->view->make('admin.eggs.scripts', [
'copyFromOptions' => $copy,
'relyOnScript' => $rely,
'egg' => $egg,
]);
}
/**
* Handle a request to update the installation script for an Egg.
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\Egg\InvalidCopyFromException
*/
public function update(EggScriptFormRequest $request, Egg $egg): RedirectResponse
{
$this->installScriptService->handle($egg, $request->normalize());
$this->alert->success(trans('admin/nests.eggs.notices.script_updated'))->flash();
return redirect()->route('admin.nests.egg.scripts', $egg);
}
}

View file

@ -1,74 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Admin\Nests;
use Pterodactyl\Models\Egg;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Pterodactyl\Http\Controllers\Controller;
use Symfony\Component\HttpFoundation\Response;
use Pterodactyl\Services\Eggs\Sharing\EggExporterService;
use Pterodactyl\Services\Eggs\Sharing\EggImporterService;
use Pterodactyl\Http\Requests\Admin\Egg\EggImportFormRequest;
use Pterodactyl\Services\Eggs\Sharing\EggUpdateImporterService;
class EggShareController extends Controller
{
/**
* EggShareController constructor.
*/
public function __construct(
protected AlertsMessageBag $alert,
protected EggExporterService $exporterService,
protected EggImporterService $importerService,
protected EggUpdateImporterService $updateImporterService
) {
}
/**
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function export(Egg $egg): Response
{
$filename = trim(preg_replace('/\W/', '-', kebab_case($egg->name)), '-');
return response($this->exporterService->handle($egg->id), 200, [
'Content-Transfer-Encoding' => 'binary',
'Content-Description' => 'File Transfer',
'Content-Disposition' => 'attachment; filename=egg-' . $filename . '.json',
'Content-Type' => 'application/json',
]);
}
/**
* Import a new service option using an XML file.
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException
* @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException
*/
public function import(EggImportFormRequest $request): RedirectResponse
{
$egg = $this->importerService->handle($request->file('import_file'), $request->input('import_to_nest'));
$this->alert->success(trans('admin/nests.eggs.notices.imported'))->flash();
return redirect()->route('admin.nests.egg.view', ['egg' => $egg->id]);
}
/**
* Update an existing Egg using a new imported file.
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException
* @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException
*/
public function update(EggImportFormRequest $request, Egg $egg): RedirectResponse
{
$this->updateImporterService->handle($egg, $request->file('import_file'));
$this->alert->success(trans('admin/nests.eggs.notices.updated_via_import'))->flash();
return redirect()->route('admin.nests.egg.view', ['egg' => $egg]);
}
}

View file

@ -1,90 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Admin\Nests;
use Illuminate\View\View;
use Pterodactyl\Models\Egg;
use Pterodactyl\Models\EggVariable;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\View\Factory as ViewFactory;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Contracts\Repository\EggRepositoryInterface;
use Pterodactyl\Services\Eggs\Variables\VariableUpdateService;
use Pterodactyl\Http\Requests\Admin\Egg\EggVariableFormRequest;
use Pterodactyl\Services\Eggs\Variables\VariableCreationService;
use Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface;
class EggVariableController extends Controller
{
/**
* EggVariableController constructor.
*/
public function __construct(
protected AlertsMessageBag $alert,
protected VariableCreationService $creationService,
protected VariableUpdateService $updateService,
protected EggRepositoryInterface $repository,
protected EggVariableRepositoryInterface $variableRepository,
protected ViewFactory $view
) {
}
/**
* Handle request to view the variables attached to an Egg.
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function view(int $egg): View
{
$egg = $this->repository->getWithVariables($egg);
return $this->view->make('admin.eggs.variables', ['egg' => $egg]);
}
/**
* Handle a request to create a new Egg variable.
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Service\Egg\Variable\BadValidationRuleException
* @throws \Pterodactyl\Exceptions\Service\Egg\Variable\ReservedVariableNameException
*/
public function store(EggVariableFormRequest $request, Egg $egg): RedirectResponse
{
$this->creationService->handle($egg->id, $request->normalize());
$this->alert->success(trans('admin/nests.variables.notices.variable_created'))->flash();
return redirect()->route('admin.nests.egg.variables', $egg->id);
}
/**
* Handle a request to update an existing Egg variable.
*
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\Egg\Variable\ReservedVariableNameException
*/
public function update(EggVariableFormRequest $request, Egg $egg, EggVariable $variable): RedirectResponse
{
$this->updateService->handle($variable, $request->normalize());
$this->alert->success(trans('admin/nests.variables.notices.variable_updated', [
'variable' => $variable->name,
]))->flash();
return redirect()->route('admin.nests.egg.variables', $egg->id);
}
/**
* Handle a request to delete an existing Egg variable from the Panel.
*/
public function destroy(int $egg, EggVariable $variable): RedirectResponse
{
$this->variableRepository->delete($variable->id);
$this->alert->success(trans('admin/nests.variables.notices.variable_deleted', [
'variable' => $variable->name,
]))->flash();
return redirect()->route('admin.nests.egg.variables', $egg);
}
}

View file

@ -1,102 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Admin\Nests;
use Illuminate\View\View;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\View\Factory as ViewFactory;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Services\Nests\NestUpdateService;
use Pterodactyl\Services\Nests\NestCreationService;
use Pterodactyl\Services\Nests\NestDeletionService;
use Pterodactyl\Contracts\Repository\NestRepositoryInterface;
use Pterodactyl\Http\Requests\Admin\Nest\StoreNestFormRequest;
class NestController extends Controller
{
/**
* NestController constructor.
*/
public function __construct(
protected AlertsMessageBag $alert,
protected NestCreationService $nestCreationService,
protected NestDeletionService $nestDeletionService,
protected NestRepositoryInterface $repository,
protected NestUpdateService $nestUpdateService,
protected ViewFactory $view
) {
}
/**
* Render nest listing page.
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function index(): View
{
return $this->view->make('admin.nests.index', [
'nests' => $this->repository->getWithCounts(),
]);
}
/**
* Render nest creation page.
*/
public function create(): View
{
return $this->view->make('admin.nests.new');
}
/**
* Handle the storage of a new nest.
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
*/
public function store(StoreNestFormRequest $request): RedirectResponse
{
$nest = $this->nestCreationService->handle($request->normalize());
$this->alert->success(trans('admin/nests.notices.created', ['name' => $nest->name]))->flash();
return redirect()->route('admin.nests.view', $nest->id);
}
/**
* Return details about a nest including all the eggs and servers per egg.
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function view(int $nest): View
{
return $this->view->make('admin.nests.view', [
'nest' => $this->repository->getWithEggServers($nest),
]);
}
/**
* Handle request to update a nest.
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function update(StoreNestFormRequest $request, int $nest): RedirectResponse
{
$this->nestUpdateService->handle($nest, $request->normalize());
$this->alert->success(trans('admin/nests.notices.updated'))->flash();
return redirect()->route('admin.nests.view', $nest);
}
/**
* Handle request to delete a nest.
*
* @throws \Pterodactyl\Exceptions\Service\HasActiveServersException
*/
public function destroy(int $nest): RedirectResponse
{
$this->nestDeletionService->handle($nest);
$this->alert->success(trans('admin/nests.notices.deleted'))->flash();
return redirect()->route('admin.nests');
}
}

View file

@ -1,62 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Admin;
use Illuminate\Http\Request;
use Pterodactyl\Models\Node;
use Pterodactyl\Models\ApiKey;
use Illuminate\Http\JsonResponse;
use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Services\Api\KeyCreationService;
use Pterodactyl\Repositories\Eloquent\ApiKeyRepository;
class NodeAutoDeployController extends Controller
{
/**
* NodeAutoDeployController constructor.
*/
public function __construct(
private ApiKeyRepository $repository,
private Encrypter $encrypter,
private KeyCreationService $keyCreationService
) {
}
/**
* Generates a new API key for the logged-in user with only permission to read
* nodes, and returns that as the deployment key for a node.
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
*/
public function __invoke(Request $request, Node $node): JsonResponse
{
/** @var \Pterodactyl\Models\ApiKey|null $key */
$key = $this->repository->getApplicationKeys($request->user())
->filter(function (ApiKey $key) {
foreach ($key->getAttributes() as $permission => $value) {
if ($permission === 'r_nodes' && $value === 1) {
return true;
}
}
return false;
})
->first();
// We couldn't find a key that exists for this user with only permission for
// reading nodes. Go ahead and create it now.
if (!$key) {
$key = $this->keyCreationService->setKeyType(ApiKey::TYPE_APPLICATION)->handle([
'user_id' => $request->user()->id,
'memo' => 'Automatically generated node deployment key.',
'allowed_ips' => [],
], ['r_nodes' => 1]);
}
return new JsonResponse([
'node' => $node->id,
'token' => $key->identifier . $this->encrypter->decrypt($key->token),
]);
}
}

View file

@ -1,35 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Admin\Nodes;
use Illuminate\View\View;
use Illuminate\Http\Request;
use Pterodactyl\Models\Node;
use Spatie\QueryBuilder\QueryBuilder;
use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Contracts\View\Factory as ViewFactory;
class NodeController extends Controller
{
/**
* NodeController constructor.
*/
public function __construct(private ViewFactory $view)
{
}
/**
* Returns a listing of nodes on the system.
*/
public function index(Request $request): View
{
$nodes = QueryBuilder::for(
Node::query()->with('location')->withCount('servers')
)
->allowedFilters(['uuid', 'name'])
->allowedSorts(['id'])
->paginate(25);
return $this->view->make('admin.nodes.index', ['nodes' => $nodes]);
}
}

View file

@ -1,102 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Admin\Nodes;
use Illuminate\View\View;
use Illuminate\Http\Request;
use Pterodactyl\Models\Node;
use Illuminate\Support\Collection;
use Pterodactyl\Models\Allocation;
use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Contracts\View\Factory as ViewFactory;
use Pterodactyl\Repositories\Eloquent\NodeRepository;
use Pterodactyl\Repositories\Eloquent\ServerRepository;
use Pterodactyl\Traits\Controllers\JavascriptInjection;
use Pterodactyl\Services\Helpers\SoftwareVersionService;
use Pterodactyl\Repositories\Eloquent\LocationRepository;
use Pterodactyl\Repositories\Eloquent\AllocationRepository;
class NodeViewController extends Controller
{
use JavascriptInjection;
/**
* NodeViewController constructor.
*/
public function __construct(
private AllocationRepository $allocationRepository,
private LocationRepository $locationRepository,
private NodeRepository $repository,
private ServerRepository $serverRepository,
private SoftwareVersionService $versionService,
private ViewFactory $view
) {
}
/**
* Returns index view for a specific node on the system.
*/
public function index(Request $request, Node $node): View
{
$node = $this->repository->loadLocationAndServerCount($node);
return $this->view->make('admin.nodes.view.index', [
'node' => $node,
'stats' => $this->repository->getUsageStats($node),
'version' => $this->versionService,
]);
}
/**
* Returns the settings page for a specific node.
*/
public function settings(Request $request, Node $node): View
{
return $this->view->make('admin.nodes.view.settings', [
'node' => $node,
'locations' => $this->locationRepository->all(),
]);
}
/**
* Return the node configuration page for a specific node.
*/
public function configuration(Request $request, Node $node): View
{
return $this->view->make('admin.nodes.view.configuration', compact('node'));
}
/**
* Return the node allocation management page.
*/
public function allocations(Request $request, Node $node): View
{
$node = $this->repository->loadNodeAllocations($node);
$this->plainInject(['node' => Collection::wrap($node)->only(['id'])]);
return $this->view->make('admin.nodes.view.allocation', [
'node' => $node,
'allocations' => Allocation::query()->where('node_id', $node->id)
->groupBy('ip')
->orderByRaw('INET_ATON(ip) ASC')
->get(['ip']),
]);
}
/**
* Return a listing of servers that exist for this specific node.
*/
public function servers(Request $request, Node $node): View
{
$this->plainInject([
'node' => Collection::wrap($node->makeVisible(['daemon_token_id', 'daemon_token']))
->only(['scheme', 'fqdn', 'daemonListen', 'daemon_token_id', 'daemon_token']),
]);
return $this->view->make('admin.nodes.view.servers', [
'node' => $node,
'servers' => $this->serverRepository->loadAllServersForNode($node->id, 25),
]);
}
}

View file

@ -1,40 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Admin\Nodes;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use Pterodactyl\Models\Node;
use Illuminate\Http\JsonResponse;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Repositories\Wings\DaemonConfigurationRepository;
class SystemInformationController extends Controller
{
/**
* SystemInformationController constructor.
*/
public function __construct(private DaemonConfigurationRepository $repository)
{
}
/**
* Returns system information from the Daemon.
*
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function __invoke(Request $request, Node $node): JsonResponse
{
$data = $this->repository->setNode($node)->getSystemInformation();
return new JsonResponse([
'version' => $data['version'] ?? '',
'system' => [
'type' => Str::title($data['os'] ?? 'Unknown'),
'arch' => $data['architecture'] ?? '--',
'release' => $data['kernel_version'] ?? '--',
'cpus' => $data['cpu_count'] ?? 0,
],
]);
}
}

View file

@ -1,183 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Admin;
use Illuminate\View\View;
use Illuminate\Http\Request;
use Pterodactyl\Models\Node;
use Illuminate\Http\Response;
use Pterodactyl\Models\Allocation;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\View\Factory as ViewFactory;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Services\Nodes\NodeUpdateService;
use Illuminate\Cache\Repository as CacheRepository;
use Pterodactyl\Services\Nodes\NodeCreationService;
use Pterodactyl\Services\Nodes\NodeDeletionService;
use Pterodactyl\Services\Allocations\AssignmentService;
use Pterodactyl\Services\Helpers\SoftwareVersionService;
use Pterodactyl\Http\Requests\Admin\Node\NodeFormRequest;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Http\Requests\Admin\Node\AllocationFormRequest;
use Pterodactyl\Services\Allocations\AllocationDeletionService;
use Pterodactyl\Contracts\Repository\LocationRepositoryInterface;
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
use Pterodactyl\Http\Requests\Admin\Node\AllocationAliasFormRequest;
class NodesController extends Controller
{
/**
* NodesController constructor.
*/
public function __construct(
protected AlertsMessageBag $alert,
protected AllocationDeletionService $allocationDeletionService,
protected AllocationRepositoryInterface $allocationRepository,
protected AssignmentService $assignmentService,
protected CacheRepository $cache,
protected NodeCreationService $creationService,
protected NodeDeletionService $deletionService,
protected LocationRepositoryInterface $locationRepository,
protected NodeRepositoryInterface $repository,
protected ServerRepositoryInterface $serverRepository,
protected NodeUpdateService $updateService,
protected SoftwareVersionService $versionService,
protected ViewFactory $view
) {
}
/**
* Displays create new node page.
*/
public function create(): View|RedirectResponse
{
$locations = $this->locationRepository->all();
if (count($locations) < 1) {
$this->alert->warning(trans('admin/node.notices.location_required'))->flash();
return redirect()->route('admin.locations');
}
return $this->view->make('admin.nodes.new', ['locations' => $locations]);
}
/**
* Post controller to create a new node on the system.
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
*/
public function store(NodeFormRequest $request): RedirectResponse
{
$node = $this->creationService->handle($request->normalize());
$this->alert->info(trans('admin/node.notices.node_created'))->flash();
return redirect()->route('admin.nodes.view.allocation', $node->id);
}
/**
* Updates settings for a node.
*
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function updateSettings(NodeFormRequest $request, Node $node): RedirectResponse
{
$this->updateService->handle($node, $request->normalize(), $request->input('reset_secret') === 'on');
$this->alert->success(trans('admin/node.notices.node_updated'))->flash();
return redirect()->route('admin.nodes.view.settings', $node->id)->withInput();
}
/**
* Removes a single allocation from a node.
*
* @throws \Pterodactyl\Exceptions\Service\Allocation\ServerUsingAllocationException
*/
public function allocationRemoveSingle(int $node, Allocation $allocation): Response
{
$this->allocationDeletionService->handle($allocation);
return response('', 204);
}
/**
* Removes multiple individual allocations from a node.
*
* @throws \Pterodactyl\Exceptions\Service\Allocation\ServerUsingAllocationException
*/
public function allocationRemoveMultiple(Request $request, int $node): Response
{
$allocations = $request->input('allocations');
foreach ($allocations as $rawAllocation) {
$allocation = new Allocation();
$allocation->id = $rawAllocation['id'];
$this->allocationRemoveSingle($node, $allocation);
}
return response('', 204);
}
/**
* Remove all allocations for a specific IP at once on a node.
*/
public function allocationRemoveBlock(Request $request, int $node): RedirectResponse
{
$this->allocationRepository->deleteWhere([
['node_id', '=', $node],
['server_id', '=', null],
['ip', '=', $request->input('ip')],
]);
$this->alert->success(trans('admin/node.notices.unallocated_deleted', ['ip' => $request->input('ip')]))
->flash();
return redirect()->route('admin.nodes.view.allocation', $node);
}
/**
* Sets an alias for a specific allocation on a node.
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function allocationSetAlias(AllocationAliasFormRequest $request): \Symfony\Component\HttpFoundation\Response
{
$this->allocationRepository->update($request->input('allocation_id'), [
'ip_alias' => (empty($request->input('alias'))) ? null : $request->input('alias'),
]);
return response('', 204);
}
/**
* Creates new allocations on a node.
*
* @throws \Pterodactyl\Exceptions\Service\Allocation\CidrOutOfRangeException
* @throws \Pterodactyl\Exceptions\Service\Allocation\InvalidPortMappingException
* @throws \Pterodactyl\Exceptions\Service\Allocation\PortOutOfRangeException
* @throws \Pterodactyl\Exceptions\Service\Allocation\TooManyPortsInRangeException
*/
public function createAllocation(AllocationFormRequest $request, Node $node): RedirectResponse
{
$this->assignmentService->handle($node, $request->normalize());
$this->alert->success(trans('admin/node.notices.allocations_added'))->flash();
return redirect()->route('admin.nodes.view.allocation', $node->id);
}
/**
* Deletes a node from the system.
*
* @throws \Pterodactyl\Exceptions\DisplayException
*/
public function delete(int|Node $node): RedirectResponse
{
$this->deletionService->handle($node);
$this->alert->success(trans('admin/node.notices.node_deleted'))->flash();
return redirect()->route('admin.nodes');
}
}

View file

@ -1,86 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Admin\Servers;
use JavaScript;
use Illuminate\View\View;
use Pterodactyl\Models\Node;
use Pterodactyl\Models\Location;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\View\Factory as ViewFactory;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Repositories\Eloquent\NestRepository;
use Pterodactyl\Repositories\Eloquent\NodeRepository;
use Pterodactyl\Http\Requests\Admin\ServerFormRequest;
use Pterodactyl\Services\Servers\ServerCreationService;
class CreateServerController extends Controller
{
/**
* CreateServerController constructor.
*/
public function __construct(
private AlertsMessageBag $alert,
private NestRepository $nestRepository,
private NodeRepository $nodeRepository,
private ServerCreationService $creationService,
private ViewFactory $view
) {
}
/**
* Displays the create server page.
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function index(): View|RedirectResponse
{
$nodes = Node::all();
if (count($nodes) < 1) {
$this->alert->warning(trans('admin/server.alerts.node_required'))->flash();
return redirect()->route('admin.nodes');
}
$nests = $this->nestRepository->getWithEggs();
JavaScript::put([
'nodeData' => $this->nodeRepository->getNodesForServerCreation(),
'nests' => $nests->map(function ($item) {
return array_merge($item->toArray(), [
'eggs' => $item->eggs->keyBy('id')->toArray(),
]);
})->keyBy('id'),
]);
return $this->view->make('admin.servers.new', [
'locations' => Location::all(),
'nests' => $nests,
]);
}
/**
* Create a new server on the remote system.
*
* @throws \Illuminate\Validation\ValidationException
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableAllocationException
* @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException
* @throws \Throwable
*/
public function store(ServerFormRequest $request): RedirectResponse
{
$data = $request->except(['_token']);
if (!empty($data['custom_image'])) {
$data['image'] = $data['custom_image'];
unset($data['custom_image']);
}
$server = $this->creationService->handle($data);
$this->alert->success(trans('admin/server.alerts.server_created'))->flash();
return new RedirectResponse('/admin/servers/view/' . $server->id);
}
}

View file

@ -1,38 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Admin\Servers;
use Illuminate\View\View;
use Illuminate\Http\Request;
use Pterodactyl\Models\Server;
use Spatie\QueryBuilder\QueryBuilder;
use Spatie\QueryBuilder\AllowedFilter;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Models\Filters\AdminServerFilter;
use Illuminate\Contracts\View\Factory as ViewFactory;
class ServerController extends Controller
{
/**
* ServerController constructor.
*/
public function __construct(private ViewFactory $view)
{
}
/**
* Returns all the servers that exist on the system using a paginated result set. If
* a query is passed along in the request it is also passed to the repository function.
*/
public function index(Request $request): View
{
$servers = QueryBuilder::for(Server::query()->with('node', 'user', 'allocation'))
->allowedFilters([
AllowedFilter::exact('owner_id'),
AllowedFilter::custom('*', new AdminServerFilter()),
])
->paginate(config()->get('pterodactyl.paginate.admin.servers'));
return $this->view->make('admin.servers.index', ['servers' => $servers]);
}
}

View file

@ -1,105 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Admin\Servers;
use Illuminate\Http\Request;
use Pterodactyl\Models\Server;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Pterodactyl\Models\ServerTransfer;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Services\Servers\TransferService;
use Pterodactyl\Repositories\Eloquent\NodeRepository;
use Pterodactyl\Repositories\Wings\DaemonConfigurationRepository;
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
class ServerTransferController extends Controller
{
/**
* ServerTransferController constructor.
*/
public function __construct(
private AlertsMessageBag $alert,
private AllocationRepositoryInterface $allocationRepository,
private NodeRepository $nodeRepository,
private TransferService $transferService,
private DaemonConfigurationRepository $daemonConfigurationRepository
) {
}
/**
* Starts a transfer of a server to a new node.
*
* @throws \Throwable
*/
public function transfer(Request $request, Server $server): RedirectResponse
{
$validatedData = $request->validate([
'node_id' => 'required|exists:nodes,id',
'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',
'allocation_additional' => 'nullable',
]);
$node_id = $validatedData['node_id'];
$allocation_id = intval($validatedData['allocation_id']);
$additional_allocations = array_map('intval', $validatedData['allocation_additional'] ?? []);
// Check if the node is viable for the transfer.
$node = $this->nodeRepository->getNodeWithResourceUsage($node_id);
if ($node->isViable($server->memory, $server->disk)) {
// Check if the selected daemon is online.
$this->daemonConfigurationRepository->setNode($node)->getSystemInformation();
$server->validateTransferState();
// Create a new ServerTransfer entry.
$transfer = new ServerTransfer();
$transfer->server_id = $server->id;
$transfer->old_node = $server->node_id;
$transfer->new_node = $node_id;
$transfer->old_allocation = $server->allocation_id;
$transfer->new_allocation = $allocation_id;
$transfer->old_additional_allocations = $server->allocations->where('id', '!=', $server->allocation_id)->pluck('id');
$transfer->new_additional_allocations = $additional_allocations;
$transfer->save();
// Add the allocations to the server, so they cannot be automatically assigned while the transfer is in progress.
$this->assignAllocationsToServer($server, $node_id, $allocation_id, $additional_allocations);
// Request an archive from the server's current daemon. (this also checks if the daemon is online)
$this->transferService->requestArchive($server);
$this->alert->success(trans('admin/server.alerts.transfer_started'))->flash();
} else {
$this->alert->danger(trans('admin/server.alerts.transfer_not_viable'))->flash();
}
return redirect()->route('admin.servers.view.manage', $server->id);
}
/**
* Assigns the specified allocations to the specified server.
*/
private function assignAllocationsToServer(Server $server, int $node_id, int $allocation_id, array $additional_allocations)
{
$allocations = $additional_allocations;
$allocations[] = $allocation_id;
$unassigned = $this->allocationRepository->getUnassignedAllocationIds($node_id);
$updateIds = [];
foreach ($allocations as $allocation) {
if (!in_array($allocation, $unassigned)) {
continue;
}
$updateIds[] = $allocation;
}
if (!empty($updateIds)) {
$this->allocationRepository->updateWhereIn('id', $updateIds, ['server_id' => $server->id]);
}
}
}

View file

@ -1,155 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Admin\Servers;
use JavaScript;
use Illuminate\View\View;
use Illuminate\Http\Request;
use Pterodactyl\Models\Nest;
use Pterodactyl\Models\Server;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Services\Servers\EnvironmentService;
use Illuminate\Contracts\View\Factory as ViewFactory;
use Pterodactyl\Repositories\Eloquent\NestRepository;
use Pterodactyl\Repositories\Eloquent\NodeRepository;
use Pterodactyl\Repositories\Eloquent\MountRepository;
use Pterodactyl\Repositories\Eloquent\ServerRepository;
use Pterodactyl\Traits\Controllers\JavascriptInjection;
use Pterodactyl\Repositories\Eloquent\LocationRepository;
use Pterodactyl\Repositories\Eloquent\DatabaseHostRepository;
class ServerViewController extends Controller
{
use JavascriptInjection;
/**
* ServerViewController constructor.
*/
public function __construct(
private DatabaseHostRepository $databaseHostRepository,
private LocationRepository $locationRepository,
private MountRepository $mountRepository,
private NestRepository $nestRepository,
private NodeRepository $nodeRepository,
private ServerRepository $repository,
private EnvironmentService $environmentService,
private ViewFactory $view
) {
}
/**
* Returns the index view for a server.
*/
public function index(Request $request, Server $server): View
{
return $this->view->make('admin.servers.view.index', compact('server'));
}
/**
* Returns the server details page.
*/
public function details(Request $request, Server $server): View
{
return $this->view->make('admin.servers.view.details', compact('server'));
}
/**
* Returns a view of server build settings.
*/
public function build(Request $request, Server $server): View
{
$allocations = $server->node->allocations->toBase();
return $this->view->make('admin.servers.view.build', [
'server' => $server,
'assigned' => $allocations->where('server_id', $server->id)->sortBy('port')->sortBy('ip'),
'unassigned' => $allocations->where('server_id', null)->sortBy('port')->sortBy('ip'),
]);
}
/**
* Returns the server startup management page.
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function startup(Request $request, Server $server): View
{
$nests = $this->nestRepository->getWithEggs();
$variables = $this->environmentService->handle($server);
$this->plainInject([
'server' => $server,
'server_variables' => $variables,
'nests' => $nests->map(function (Nest $item) {
return array_merge($item->toArray(), [
'eggs' => $item->eggs->keyBy('id')->toArray(),
]);
})->keyBy('id'),
]);
return $this->view->make('admin.servers.view.startup', compact('server', 'nests'));
}
/**
* Returns all the databases that exist for the server.
*/
public function database(Request $request, Server $server): View
{
return $this->view->make('admin.servers.view.database', [
'hosts' => $this->databaseHostRepository->all(),
'server' => $server,
]);
}
/**
* Returns all the mounts that exist for the server.
*/
public function mounts(Request $request, Server $server): View
{
$server->load('mounts');
return $this->view->make('admin.servers.view.mounts', [
'mounts' => $this->mountRepository->getMountListForServer($server),
'server' => $server,
]);
}
/**
* Returns the base server management page, or an exception if the server
* is in a state that cannot be recovered from.
*
* @throws \Pterodactyl\Exceptions\DisplayException
*/
public function manage(Request $request, Server $server): View
{
if ($server->status === Server::STATUS_INSTALL_FAILED) {
throw new DisplayException('This server is in a failed install state and cannot be recovered. Please delete and re-create the server.');
}
// Check if the panel doesn't have at least 2 nodes configured.
$nodes = $this->nodeRepository->all();
$canTransfer = false;
if (count($nodes) >= 2) {
$canTransfer = true;
}
JavaScript::put([
'nodeData' => $this->nodeRepository->getNodesForServerCreation(),
]);
return $this->view->make('admin.servers.view.manage', [
'server' => $server,
'locations' => $this->locationRepository->all(),
'canTransfer' => $canTransfer,
]);
}
/**
* Returns the server deletion page.
*/
public function delete(Request $request, Server $server): View
{
return $this->view->make('admin.servers.view.delete', compact('server'));
}
}

View file

@ -1,273 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Admin;
use Illuminate\Http\Request;
use Pterodactyl\Models\User;
use Illuminate\Http\Response;
use Pterodactyl\Models\Mount;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Database;
use Pterodactyl\Models\MountServer;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Validation\ValidationException;
use Pterodactyl\Services\Servers\SuspensionService;
use Pterodactyl\Repositories\Eloquent\MountRepository;
use Pterodactyl\Services\Servers\ServerDeletionService;
use Pterodactyl\Services\Servers\ReinstallServerService;
use Pterodactyl\Exceptions\Model\DataValidationException;
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
use Pterodactyl\Services\Servers\BuildModificationService;
use Pterodactyl\Services\Databases\DatabasePasswordService;
use Pterodactyl\Services\Servers\DetailsModificationService;
use Pterodactyl\Services\Servers\StartupModificationService;
use Pterodactyl\Contracts\Repository\NestRepositoryInterface;
use Pterodactyl\Repositories\Eloquent\DatabaseHostRepository;
use Pterodactyl\Services\Databases\DatabaseManagementService;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
use Pterodactyl\Services\Servers\ServerConfigurationStructureService;
use Pterodactyl\Http\Requests\Admin\Servers\Databases\StoreServerDatabaseRequest;
class ServersController extends Controller
{
/**
* ServersController constructor.
*/
public function __construct(
protected AlertsMessageBag $alert,
protected AllocationRepositoryInterface $allocationRepository,
protected BuildModificationService $buildModificationService,
protected ConfigRepository $config,
protected DaemonServerRepository $daemonServerRepository,
protected DatabaseManagementService $databaseManagementService,
protected DatabasePasswordService $databasePasswordService,
protected DatabaseRepositoryInterface $databaseRepository,
protected DatabaseHostRepository $databaseHostRepository,
protected ServerDeletionService $deletionService,
protected DetailsModificationService $detailsModificationService,
protected ReinstallServerService $reinstallService,
protected ServerRepositoryInterface $repository,
protected MountRepository $mountRepository,
protected NestRepositoryInterface $nestRepository,
protected ServerConfigurationStructureService $serverConfigurationStructureService,
protected StartupModificationService $startupModificationService,
protected SuspensionService $suspensionService
) {
}
/**
* Update the details for a server.
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function setDetails(Request $request, Server $server): RedirectResponse
{
$this->detailsModificationService->handle($server, $request->only([
'owner_id', 'external_id', 'name', 'description',
]));
$this->alert->success(trans('admin/server.alerts.details_updated'))->flash();
return redirect()->route('admin.servers.view.details', $server->id);
}
/**
* Toggles the installation status for a server.
*
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function toggleInstall(Server $server): RedirectResponse
{
if ($server->status === Server::STATUS_INSTALL_FAILED) {
throw new DisplayException(trans('admin/server.exceptions.marked_as_failed'));
}
$this->repository->update($server->id, [
'status' => $server->isInstalled() ? Server::STATUS_INSTALLING : null,
], true, true);
$this->alert->success(trans('admin/server.alerts.install_toggled'))->flash();
return redirect()->route('admin.servers.view.manage', $server->id);
}
/**
* Reinstalls the server with the currently assigned service.
*
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function reinstallServer(Server $server): RedirectResponse
{
$this->reinstallService->handle($server);
$this->alert->success(trans('admin/server.alerts.server_reinstalled'))->flash();
return redirect()->route('admin.servers.view.manage', $server->id);
}
/**
* Manage the suspension status for a server.
*
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function manageSuspension(Request $request, Server $server): RedirectResponse
{
$this->suspensionService->toggle($server, $request->input('action'));
$this->alert->success(trans('admin/server.alerts.suspension_toggled', [
'status' => $request->input('action') . 'ed',
]))->flash();
return redirect()->route('admin.servers.view.manage', $server->id);
}
/**
* Update the build configuration for a server.
*
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Illuminate\Validation\ValidationException
*/
public function updateBuild(Request $request, Server $server): RedirectResponse
{
try {
$this->buildModificationService->handle($server, $request->only([
'allocation_id', 'add_allocations', 'remove_allocations',
'memory', 'swap', 'io', 'cpu', 'threads', 'disk',
'database_limit', 'allocation_limit', 'backup_limit', 'oom_disabled',
]));
} catch (DataValidationException $exception) {
throw new ValidationException($exception->getValidator());
}
$this->alert->success(trans('admin/server.alerts.build_updated'))->flash();
return redirect()->route('admin.servers.view.build', $server->id);
}
/**
* Start the server deletion process.
*
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Throwable
*/
public function delete(Request $request, Server $server): RedirectResponse
{
$this->deletionService->withForce($request->filled('force_delete'))->handle($server);
$this->alert->success(trans('admin/server.alerts.server_deleted'))->flash();
return redirect()->route('admin.servers');
}
/**
* Update the startup command as well as variables.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function saveStartup(Request $request, Server $server): RedirectResponse
{
$data = $request->except('_token');
if (!empty($data['custom_docker_image'])) {
$data['docker_image'] = $data['custom_docker_image'];
unset($data['custom_docker_image']);
}
try {
$this->startupModificationService
->setUserLevel(User::USER_LEVEL_ADMIN)
->handle($server, $data);
} catch (DataValidationException $exception) {
throw new ValidationException($exception->getValidator());
}
$this->alert->success(trans('admin/server.alerts.startup_changed'))->flash();
return redirect()->route('admin.servers.view.startup', $server->id);
}
/**
* Creates a new database assigned to a specific server.
*
* @throws \Throwable
*/
public function newDatabase(StoreServerDatabaseRequest $request, Server $server): RedirectResponse
{
$this->databaseManagementService->create($server, [
'database' => DatabaseManagementService::generateUniqueDatabaseName($request->input('database'), $server->id),
'remote' => $request->input('remote'),
'database_host_id' => $request->input('database_host_id'),
'max_connections' => $request->input('max_connections'),
]);
return redirect()->route('admin.servers.view.database', $server->id)->withInput();
}
/**
* Resets the database password for a specific database on this server.
*
* @throws \Throwable
*/
public function resetDatabasePassword(Request $request, Server $server): Response
{
/** @var \Pterodactyl\Models\Database $database */
$database = $server->databases()->findOrFail($request->input('database'));
$this->databasePasswordService->handle($database);
return response('', 204);
}
/**
* Deletes a database from a server.
*
* @throws \Exception
*/
public function deleteDatabase(Server $server, Database $database): Response
{
$this->databaseManagementService->delete($database);
return response('', 204);
}
/**
* Add a mount to a server.
*
* @throws \Throwable
*/
public function addMount(Request $request, Server $server): RedirectResponse
{
$mountServer = (new MountServer())->forceFill([
'mount_id' => $request->input('mount_id'),
'server_id' => $server->id,
]);
$mountServer->saveOrFail();
$this->alert->success('Mount was added successfully.')->flash();
return redirect()->route('admin.servers.view.mounts', $server->id);
}
/**
* Remove a mount from a server.
*/
public function deleteMount(Server $server, Mount $mount): RedirectResponse
{
MountServer::where('mount_id', $mount->id)->where('server_id', $server->id)->delete();
$this->alert->success('Mount was removed successfully.')->flash();
return redirect()->route('admin.servers.view.mounts', $server->id);
}
}

View file

@ -1,62 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Admin\Settings;
use Illuminate\View\View;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\View\Factory as ViewFactory;
use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Pterodactyl\Contracts\Repository\SettingsRepositoryInterface;
use Pterodactyl\Http\Requests\Admin\Settings\AdvancedSettingsFormRequest;
class AdvancedController extends Controller
{
/**
* AdvancedController constructor.
*/
public function __construct(
private AlertsMessageBag $alert,
private ConfigRepository $config,
private Kernel $kernel,
private SettingsRepositoryInterface $settings,
private ViewFactory $view
) {
}
/**
* Render advanced Panel settings UI.
*/
public function index(): View
{
$showRecaptchaWarning = false;
if (
$this->config->get('recaptcha._shipped_secret_key') === $this->config->get('recaptcha.secret_key')
|| $this->config->get('recaptcha._shipped_website_key') === $this->config->get('recaptcha.website_key')
) {
$showRecaptchaWarning = true;
}
return $this->view->make('admin.settings.advanced', [
'showRecaptchaWarning' => $showRecaptchaWarning,
]);
}
/**
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function update(AdvancedSettingsFormRequest $request): RedirectResponse
{
foreach ($request->normalize() as $key => $value) {
$this->settings->set('settings::' . $key, $value);
}
$this->kernel->call('queue:restart');
$this->alert->success('Advanced settings have been updated successfully and the queue worker was restarted to apply these changes.')->flash();
return redirect()->route('admin.settings.advanced');
}
}

View file

@ -1,60 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Admin\Settings;
use Illuminate\View\View;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\View\Factory as ViewFactory;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Traits\Helpers\AvailableLanguages;
use Pterodactyl\Services\Helpers\SoftwareVersionService;
use Pterodactyl\Contracts\Repository\SettingsRepositoryInterface;
use Pterodactyl\Http\Requests\Admin\Settings\BaseSettingsFormRequest;
class IndexController extends Controller
{
use AvailableLanguages;
/**
* IndexController constructor.
*/
public function __construct(
private AlertsMessageBag $alert,
private Kernel $kernel,
private SettingsRepositoryInterface $settings,
private SoftwareVersionService $versionService,
private ViewFactory $view
) {
}
/**
* Render the UI for basic Panel settings.
*/
public function index(): View
{
return $this->view->make('admin.settings.index', [
'version' => $this->versionService,
'languages' => $this->getAvailableLanguages(true),
]);
}
/**
* Handle settings update.
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function update(BaseSettingsFormRequest $request): RedirectResponse
{
foreach ($request->normalize() as $key => $value) {
$this->settings->set('settings::' . $key, $value);
}
$this->kernel->call('queue:restart');
$this->alert->success('Panel settings have been updated successfully and the queue worker was restarted to apply these changes.')->flash();
return redirect()->route('admin.settings');
}
}

View file

@ -1,91 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Admin\Settings;
use Exception;
use Illuminate\View\View;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Contracts\Console\Kernel;
use Pterodactyl\Notifications\MailTested;
use Illuminate\View\Factory as ViewFactory;
use Illuminate\Support\Facades\Notification;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Providers\SettingsServiceProvider;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Pterodactyl\Contracts\Repository\SettingsRepositoryInterface;
use Pterodactyl\Http\Requests\Admin\Settings\MailSettingsFormRequest;
class MailController extends Controller
{
/**
* MailController constructor.
*/
public function __construct(
private ConfigRepository $config,
private Encrypter $encrypter,
private Kernel $kernel,
private SettingsRepositoryInterface $settings,
private ViewFactory $view
) {
}
/**
* Render UI for editing mail settings. This UI should only display if
* the server is configured to send mail using SMTP.
*/
public function index(): View
{
return $this->view->make('admin.settings.mail', [
'disabled' => $this->config->get('mail.default') !== 'smtp',
]);
}
/**
* Handle request to update SMTP mail settings.
*
* @throws DisplayException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function update(MailSettingsFormRequest $request): Response
{
if ($this->config->get('mail.default') !== 'smtp') {
throw new DisplayException('This feature is only available if SMTP is the selected email driver for the Panel.');
}
$values = $request->normalize();
if (array_get($values, 'mail:password') === '!e') {
$values['mail:password'] = '';
}
foreach ($values as $key => $value) {
if (in_array($key, SettingsServiceProvider::getEncryptedKeys()) && !empty($value)) {
$value = $this->encrypter->encrypt($value);
}
$this->settings->set('settings::' . $key, $value);
}
$this->kernel->call('queue:restart');
return response('', 204);
}
/**
* Submit a request to send a test mail message.
*/
public function test(Request $request): Response
{
try {
Notification::route('mail', $request->user()->email)
->notify(new MailTested($request->user()));
} catch (Exception $exception) {
return response($exception->getMessage(), 500);
}
return response('', 204);
}
}

View file

@ -1,153 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Admin;
use Illuminate\View\View;
use Illuminate\Http\Request;
use Pterodactyl\Models\User;
use Pterodactyl\Models\Model;
use Illuminate\Support\Collection;
use Illuminate\Http\RedirectResponse;
use Prologue\Alerts\AlertsMessageBag;
use Spatie\QueryBuilder\QueryBuilder;
use Illuminate\View\Factory as ViewFactory;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Contracts\Translation\Translator;
use Pterodactyl\Services\Users\UserUpdateService;
use Pterodactyl\Traits\Helpers\AvailableLanguages;
use Pterodactyl\Services\Users\UserCreationService;
use Pterodactyl\Services\Users\UserDeletionService;
use Pterodactyl\Http\Requests\Admin\UserFormRequest;
use Pterodactyl\Http\Requests\Admin\NewUserFormRequest;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
class UserController extends Controller
{
use AvailableLanguages;
/**
* UserController constructor.
*/
public function __construct(
protected AlertsMessageBag $alert,
protected UserCreationService $creationService,
protected UserDeletionService $deletionService,
protected Translator $translator,
protected UserUpdateService $updateService,
protected UserRepositoryInterface $repository,
protected ViewFactory $view
) {
}
/**
* Display user index page.
*/
public function index(Request $request): View
{
$users = QueryBuilder::for(
User::query()->select('users.*')
->selectRaw('COUNT(DISTINCT(subusers.id)) as subuser_of_count')
->selectRaw('COUNT(DISTINCT(servers.id)) as servers_count')
->leftJoin('subusers', 'subusers.user_id', '=', 'users.id')
->leftJoin('servers', 'servers.owner_id', '=', 'users.id')
->groupBy('users.id')
)
->allowedFilters(['username', 'email', 'uuid'])
->allowedSorts(['id', 'uuid'])
->paginate(50);
return $this->view->make('admin.users.index', ['users' => $users]);
}
/**
* Display new user page.
*/
public function create(): View
{
return $this->view->make('admin.users.new', [
'languages' => $this->getAvailableLanguages(true),
]);
}
/**
* Display user view page.
*/
public function view(User $user): View
{
return $this->view->make('admin.users.view', [
'user' => $user,
'languages' => $this->getAvailableLanguages(true),
]);
}
/**
* Delete a user from the system.
*
* @throws \Exception
* @throws \Pterodactyl\Exceptions\DisplayException
*/
public function delete(Request $request, User $user): RedirectResponse
{
if ($request->user()->id === $user->id) {
throw new DisplayException($this->translator->get('admin/user.exceptions.user_has_servers'));
}
$this->deletionService->handle($user);
return redirect()->route('admin.users');
}
/**
* Create a user.
*
* @throws \Exception
* @throws \Throwable
*/
public function store(NewUserFormRequest $request): RedirectResponse
{
$user = $this->creationService->handle($request->normalize());
$this->alert->success($this->translator->get('admin/user.notices.account_created'))->flash();
return redirect()->route('admin.users.view', $user->id);
}
/**
* Update a user on the system.
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function update(UserFormRequest $request, User $user): RedirectResponse
{
$this->updateService
->setUserLevel(User::USER_LEVEL_ADMIN)
->handle($user, $request->normalize());
$this->alert->success(trans('admin/user.notices.account_updated'))->flash();
return redirect()->route('admin.users.view', $user->id);
}
/**
* Get a JSON response of users on the system.
*/
public function json(Request $request): Model|Collection
{
$users = QueryBuilder::for(User::query())->allowedFilters(['email'])->paginate(25);
// Handle single user requests.
if ($request->query('user_id')) {
$user = User::query()->findOrFail($request->input('user_id'));
$user->md5 = md5(strtolower($user->email));
return $user;
}
return $users->map(function ($item) {
$item->md5 = md5(strtolower($item->email));
return $item;
});
}
}

View file

@ -3,19 +3,16 @@
namespace Pterodactyl\Http\Controllers\Api\Application; namespace Pterodactyl\Http\Controllers\Api\Application;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Webmozart\Assert\Assert;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Container\Container; use Illuminate\Container\Container;
use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Extensions\Spatie\Fractalistic\Fractal; use Pterodactyl\Extensions\Spatie\Fractalistic\Fractal;
use Pterodactyl\Transformers\Api\Application\BaseTransformer;
abstract class ApplicationApiController extends Controller abstract class ApplicationApiController extends Controller
{ {
protected Request $request;
protected Fractal $fractal; protected Fractal $fractal;
protected Request $request;
/** /**
* ApplicationApiController constructor. * ApplicationApiController constructor.
@ -47,21 +44,11 @@ abstract class ApplicationApiController extends Controller
} }
/** /**
* Return an instance of an application transformer. * Return an HTTP/201 response for the API.
*
* @template T of \Pterodactyl\Transformers\Api\Application\BaseTransformer
*
* @param class-string<T> $abstract
*
* @return T
*
* @noinspection PhpDocSignatureInspection
*/ */
public function getTransformer(string $abstract) protected function returnAccepted(): Response
{ {
Assert::subclassOf($abstract, BaseTransformer::class); return new Response('', Response::HTTP_ACCEPTED);
return $abstract::fromRequest($this->request);
} }
/** /**

View file

@ -0,0 +1,99 @@
<?php
namespace Pterodactyl\Http\Controllers\Api\Application\Databases;
use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse;
use Pterodactyl\Models\DatabaseHost;
use Spatie\QueryBuilder\QueryBuilder;
use Pterodactyl\Services\Databases\Hosts\HostUpdateService;
use Pterodactyl\Services\Databases\Hosts\HostCreationService;
use Pterodactyl\Exceptions\Http\QueryValueOutOfRangeHttpException;
use Pterodactyl\Transformers\Api\Application\DatabaseHostTransformer;
use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController;
use Pterodactyl\Http\Requests\Api\Application\Databases\GetDatabaseRequest;
use Pterodactyl\Http\Requests\Api\Application\Databases\GetDatabasesRequest;
use Pterodactyl\Http\Requests\Api\Application\Databases\StoreDatabaseRequest;
use Pterodactyl\Http\Requests\Api\Application\Databases\DeleteDatabaseRequest;
use Pterodactyl\Http\Requests\Api\Application\Databases\UpdateDatabaseRequest;
class DatabaseController extends ApplicationApiController
{
/**
* DatabaseController constructor.
*/
public function __construct(private HostCreationService $creationService, private HostUpdateService $updateService)
{
parent::__construct();
}
/**
* Returns an array of all database hosts.
*/
public function index(GetDatabasesRequest $request): array
{
$perPage = (int) $request->query('per_page', '10');
if ($perPage < 1 || $perPage > 100) {
throw new QueryValueOutOfRangeHttpException('per_page', 1, 100);
}
$databases = QueryBuilder::for(DatabaseHost::query())
->allowedFilters(['name', 'host'])
->allowedSorts(['id', 'name', 'host'])
->paginate($perPage);
return $this->fractal->collection($databases)
->transformWith(DatabaseHostTransformer::class)
->toArray();
}
/**
* Returns a single database host.
*/
public function view(GetDatabaseRequest $request, DatabaseHost $databaseHost): array
{
return $this->fractal->item($databaseHost)
->transformWith(DatabaseHostTransformer::class)
->toArray();
}
/**
* Creates a new database host.
*
* @throws \Throwable
*/
public function store(StoreDatabaseRequest $request): JsonResponse
{
$databaseHost = $this->creationService->handle($request->validated());
return $this->fractal->item($databaseHost)
->transformWith(DatabaseHostTransformer::class)
->respond(JsonResponse::HTTP_CREATED);
}
/**
* Updates a database host.
*
* @throws \Throwable
*/
public function update(UpdateDatabaseRequest $request, DatabaseHost $databaseHost): array
{
$databaseHost = $this->updateService->handle($databaseHost->id, $request->validated());
return $this->fractal->item($databaseHost)
->transformWith(DatabaseHostTransformer::class)
->toArray();
}
/**
* Deletes a database host.
*
* @throws \Exception
*/
public function delete(DeleteDatabaseRequest $request, DatabaseHost $databaseHost): Response
{
$databaseHost->delete();
return $this->returnNoContent();
}
}

View file

@ -0,0 +1,120 @@
<?php
namespace Pterodactyl\Http\Controllers\Api\Application\Eggs;
use Ramsey\Uuid\Uuid;
use Pterodactyl\Models\Egg;
use Pterodactyl\Models\Nest;
use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse;
use Spatie\QueryBuilder\QueryBuilder;
use Pterodactyl\Services\Eggs\Sharing\EggExporterService;
use Pterodactyl\Transformers\Api\Application\EggTransformer;
use Pterodactyl\Http\Requests\Api\Application\Eggs\GetEggRequest;
use Pterodactyl\Exceptions\Http\QueryValueOutOfRangeHttpException;
use Pterodactyl\Http\Requests\Api\Application\Eggs\GetEggsRequest;
use Pterodactyl\Http\Requests\Api\Application\Eggs\StoreEggRequest;
use Pterodactyl\Http\Requests\Api\Application\Eggs\DeleteEggRequest;
use Pterodactyl\Http\Requests\Api\Application\Eggs\ExportEggRequest;
use Pterodactyl\Http\Requests\Api\Application\Eggs\UpdateEggRequest;
use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController;
class EggController extends ApplicationApiController
{
/**
* EggController constructor.
*/
public function __construct(private EggExporterService $eggExporterService)
{
parent::__construct();
$this->eggExporterService = $eggExporterService;
}
/**
* Return an array of all eggs on a given nest.
*/
public function index(GetEggsRequest $request, Nest $nest): array
{
$perPage = (int) $request->query('per_page', '10');
if ($perPage > 100) {
throw new QueryValueOutOfRangeHttpException('per_page', 1, 100);
}
// @phpstan-ignore-next-line
$eggs = QueryBuilder::for(Egg::query())
->where('nest_id', '=', $nest->id)
->allowedFilters(['id', 'name', 'author'])
->allowedSorts(['id', 'name', 'author']);
if ($perPage > 0) {
$eggs = $eggs->paginate($perPage);
}
return $this->fractal->collection($eggs)
->transformWith(EggTransformer::class)
->toArray();
}
/**
* Returns a single egg.
*/
public function view(GetEggRequest $request, Egg $egg): array
{
return $this->fractal->item($egg)
->transformWith(EggTransformer::class)
->toArray();
}
/**
* Creates a new egg.
*/
public function store(StoreEggRequest $request): JsonResponse
{
$validated = $request->validated();
$merged = array_merge($validated, [
'uuid' => Uuid::uuid4()->toString(),
// TODO: allow this to be set in the request, and default to config value if null or not present.
'author' => config('pterodactyl.service.author'),
]);
$egg = Egg::query()->create($merged);
return $this->fractal->item($egg)
->transformWith(EggTransformer::class)
->respond(Response::HTTP_CREATED);
}
/**
* Updates an egg.
*/
public function update(UpdateEggRequest $request, Egg $egg): array
{
$egg->update($request->validated());
return $this->fractal->item($egg)
->transformWith(EggTransformer::class)
->toArray();
}
/**
* Deletes an egg.
*
* @throws \Exception
*/
public function delete(DeleteEggRequest $request, Egg $egg): Response
{
$egg->delete();
return $this->returnNoContent();
}
/**
* Exports an egg.
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function export(ExportEggRequest $request, int $eggId): JsonResponse
{
return new JsonResponse($this->eggExporterService->handle($eggId));
}
}

View file

@ -0,0 +1,75 @@
<?php
namespace Pterodactyl\Http\Controllers\Api\Application\Eggs;
use Pterodactyl\Models\Egg;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Pterodactyl\Models\EggVariable;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Services\Eggs\Variables\VariableUpdateService;
use Pterodactyl\Services\Eggs\Variables\VariableCreationService;
use Pterodactyl\Transformers\Api\Application\EggVariableTransformer;
use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController;
use Pterodactyl\Http\Requests\Api\Application\Eggs\Variables\StoreEggVariableRequest;
use Pterodactyl\Http\Requests\Api\Application\Eggs\Variables\UpdateEggVariablesRequest;
class EggVariableController extends ApplicationApiController
{
public function __construct(
private ConnectionInterface $connection,
private VariableCreationService $variableCreationService,
private VariableUpdateService $variableUpdateService
) {
parent::__construct();
}
/**
* Creates a new egg variable.
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Service\Egg\Variable\BadValidationRuleException
* @throws \Pterodactyl\Exceptions\Service\Egg\Variable\ReservedVariableNameException
*/
public function store(StoreEggVariableRequest $request, Egg $egg): array
{
$variable = $this->variableCreationService->handle($egg->id, $request->validated());
return $this->fractal->item($variable)
->transformWith(EggVariableTransformer::class)
->toArray();
}
/**
* Updates multiple egg variables.
*
* @throws \Throwable
*/
public function update(UpdateEggVariablesRequest $request, Egg $egg): array
{
$validated = $request->validated();
$this->connection->transaction(function () use ($egg, $validated) {
foreach ($validated as $data) {
$this->variableUpdateService->handle($egg, $data);
}
});
return $this->fractal->collection($egg->refresh()->variables)
->transformWith(EggVariableTransformer::class)
->toArray();
}
/**
* Deletes a single egg variable.
*/
public function delete(Request $request, Egg $egg, EggVariable $eggVariable): Response
{
EggVariable::query()
->where('id', $eggVariable->id)
->where('egg_id', $egg->id)
->delete();
return $this->returnNoContent();
}
}

View file

@ -10,6 +10,7 @@ use Pterodactyl\Services\Locations\LocationUpdateService;
use Pterodactyl\Services\Locations\LocationCreationService; use Pterodactyl\Services\Locations\LocationCreationService;
use Pterodactyl\Services\Locations\LocationDeletionService; use Pterodactyl\Services\Locations\LocationDeletionService;
use Pterodactyl\Transformers\Api\Application\LocationTransformer; use Pterodactyl\Transformers\Api\Application\LocationTransformer;
use Pterodactyl\Exceptions\Http\QueryValueOutOfRangeHttpException;
use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController; use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController;
use Pterodactyl\Http\Requests\Api\Application\Locations\GetLocationRequest; use Pterodactyl\Http\Requests\Api\Application\Locations\GetLocationRequest;
use Pterodactyl\Http\Requests\Api\Application\Locations\GetLocationsRequest; use Pterodactyl\Http\Requests\Api\Application\Locations\GetLocationsRequest;
@ -35,13 +36,18 @@ class LocationController extends ApplicationApiController
*/ */
public function index(GetLocationsRequest $request): array public function index(GetLocationsRequest $request): array
{ {
$perPage = (int) $request->query('per_page', '10');
if ($perPage < 1 || $perPage > 100) {
throw new QueryValueOutOfRangeHttpException('per_page', 1, 100);
}
$locations = QueryBuilder::for(Location::query()) $locations = QueryBuilder::for(Location::query())
->allowedFilters(['short', 'long']) ->allowedFilters(['short', 'long'])
->allowedSorts(['id']) ->allowedSorts(['id', 'short', 'long'])
->paginate($request->query('per_page') ?? 50); ->paginate($perPage);
return $this->fractal->collection($locations) return $this->fractal->collection($locations)
->transformWith($this->getTransformer(LocationTransformer::class)) ->transformWith(LocationTransformer::class)
->toArray(); ->toArray();
} }
@ -51,7 +57,7 @@ class LocationController extends ApplicationApiController
public function view(GetLocationRequest $request, Location $location): array public function view(GetLocationRequest $request, Location $location): array
{ {
return $this->fractal->item($location) return $this->fractal->item($location)
->transformWith($this->getTransformer(LocationTransformer::class)) ->transformWith(LocationTransformer::class)
->toArray(); ->toArray();
} }
@ -66,12 +72,7 @@ class LocationController extends ApplicationApiController
$location = $this->creationService->handle($request->validated()); $location = $this->creationService->handle($request->validated());
return $this->fractal->item($location) return $this->fractal->item($location)
->transformWith($this->getTransformer(LocationTransformer::class)) ->transformWith(LocationTransformer::class)
->addMeta([
'resource' => route('api.application.locations.view', [
'location' => $location->id,
]),
])
->respond(201); ->respond(201);
} }
@ -86,7 +87,7 @@ class LocationController extends ApplicationApiController
$location = $this->updateService->handle($location, $request->validated()); $location = $this->updateService->handle($location, $request->validated());
return $this->fractal->item($location) return $this->fractal->item($location)
->transformWith($this->getTransformer(LocationTransformer::class)) ->transformWith(LocationTransformer::class)
->toArray(); ->toArray();
} }
@ -99,6 +100,6 @@ class LocationController extends ApplicationApiController
{ {
$this->deletionService->handle($location); $this->deletionService->handle($location);
return response('', 204); return $this->returnNoContent();
} }
} }

View file

@ -0,0 +1,163 @@
<?php
namespace Pterodactyl\Http\Controllers\Api\Application\Mounts;
use Illuminate\Http\Response;
use Pterodactyl\Models\Mount;
use Illuminate\Http\JsonResponse;
use Spatie\QueryBuilder\QueryBuilder;
use Pterodactyl\Transformers\Api\Application\MountTransformer;
use Pterodactyl\Exceptions\Http\QueryValueOutOfRangeHttpException;
use Pterodactyl\Http\Requests\Api\Application\Mounts\GetMountRequest;
use Pterodactyl\Http\Requests\Api\Application\Mounts\GetMountsRequest;
use Pterodactyl\Http\Requests\Api\Application\Mounts\MountEggsRequest;
use Pterodactyl\Http\Requests\Api\Application\Mounts\MountNodesRequest;
use Pterodactyl\Http\Requests\Api\Application\Mounts\StoreMountRequest;
use Pterodactyl\Http\Requests\Api\Application\Mounts\DeleteMountRequest;
use Pterodactyl\Http\Requests\Api\Application\Mounts\UpdateMountRequest;
use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController;
class MountController extends ApplicationApiController
{
/**
* MountController constructor.
*/
public function __construct()
{
parent::__construct();
}
/**
* Returns an array of all mount.
*/
public function index(GetMountsRequest $request): array
{
$perPage = (int) $request->query('per_page', '10');
if ($perPage < 1 || $perPage > 100) {
throw new QueryValueOutOfRangeHttpException('per_page', 1, 100);
}
$mounts = QueryBuilder::for(Mount::query())
->allowedFilters(['id', 'name', 'source', 'target'])
->allowedSorts(['id', 'name', 'source', 'target'])
->paginate($perPage);
return $this->fractal->collection($mounts)
->transformWith(MountTransformer::class)
->toArray();
}
/**
* Returns a single mount.
*/
public function view(GetMountRequest $request, Mount $mount): array
{
return $this->fractal->item($mount)
->transformWith(MountTransformer::class)
->toArray();
}
/**
* Creates a new mount.
*/
public function store(StoreMountRequest $request): JsonResponse
{
$mount = Mount::query()->create($request->validated());
return $this->fractal->item($mount)
->transformWith(MountTransformer::class)
->respond(JsonResponse::HTTP_CREATED);
}
/**
* Updates a mount.
*/
public function update(UpdateMountRequest $request, Mount $mount): array
{
$mount->update($request->validated());
return $this->fractal->item($mount)
->transformWith(MountTransformer::class)
->toArray();
}
/**
* Deletes a mount.
*
* @throws \Exception
*/
public function delete(DeleteMountRequest $request, Mount $mount): Response
{
$mount->delete();
return $this->returnNoContent();
}
/**
* Attaches eggs to a mount.
*/
public function addEggs(MountEggsRequest $request, Mount $mount): array
{
$data = $request->validated();
$eggs = $data['eggs'] ?? [];
if (count($eggs) > 0) {
$mount->eggs()->syncWithoutDetaching($eggs);
}
return $this->fractal->item($mount)
->transformWith(MountTransformer::class)
->toArray();
}
/**
* Attaches nodes to a mount.
*/
public function addNodes(MountNodesRequest $request, Mount $mount): array
{
$data = $request->validated();
$nodes = $data['nodes'] ?? [];
if (count($nodes) > 0) {
$mount->nodes()->syncWithoutDetaching($nodes);
}
return $this->fractal->item($mount)
->transformWith(MountTransformer::class)
->toArray();
}
/**
* Detaches eggs from a mount.
*/
public function deleteEggs(MountEggsRequest $request, Mount $mount): array
{
$data = $request->validated();
$eggs = $data['eggs'] ?? [];
if (count($eggs) > 0) {
$mount->eggs()->detach($eggs);
}
return $this->fractal->item($mount)
->transformWith(MountTransformer::class)
->toArray();
}
/**
* Detaches nodes from a mount.
*/
public function deleteNodes(MountNodesRequest $request, Mount $mount): array
{
$data = $request->validated();
$nodes = $data['nodes'] ?? [];
if (count($nodes) > 0) {
$mount->nodes()->detach($nodes);
}
return $this->fractal->item($mount)
->transformWith(MountTransformer::class)
->toArray();
}
}

View file

@ -1,33 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Api\Application\Nests;
use Pterodactyl\Models\Egg;
use Pterodactyl\Models\Nest;
use Pterodactyl\Transformers\Api\Application\EggTransformer;
use Pterodactyl\Http\Requests\Api\Application\Nests\Eggs\GetEggRequest;
use Pterodactyl\Http\Requests\Api\Application\Nests\Eggs\GetEggsRequest;
use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController;
class EggController extends ApplicationApiController
{
/**
* Return all eggs that exist for a given nest.
*/
public function index(GetEggsRequest $request, Nest $nest): array
{
return $this->fractal->collection($nest->eggs)
->transformWith($this->getTransformer(EggTransformer::class))
->toArray();
}
/**
* Return a single egg that exists on the specified nest.
*/
public function view(GetEggRequest $request, Nest $nest, Egg $egg): array
{
return $this->fractal->item($egg)
->transformWith($this->getTransformer(EggTransformer::class))
->toArray();
}
}

View file

@ -3,9 +3,21 @@
namespace Pterodactyl\Http\Controllers\Api\Application\Nests; namespace Pterodactyl\Http\Controllers\Api\Application\Nests;
use Pterodactyl\Models\Nest; use Pterodactyl\Models\Nest;
use Pterodactyl\Contracts\Repository\NestRepositoryInterface; use Illuminate\Http\Response;
use Spatie\QueryBuilder\QueryBuilder;
use Pterodactyl\Services\Nests\NestUpdateService;
use Pterodactyl\Services\Nests\NestCreationService;
use Pterodactyl\Services\Nests\NestDeletionService;
use Pterodactyl\Services\Eggs\Sharing\EggImporterService;
use Pterodactyl\Transformers\Api\Application\EggTransformer;
use Pterodactyl\Transformers\Api\Application\NestTransformer; use Pterodactyl\Transformers\Api\Application\NestTransformer;
use Pterodactyl\Exceptions\Http\QueryValueOutOfRangeHttpException;
use Pterodactyl\Http\Requests\Api\Application\Nests\GetNestRequest;
use Pterodactyl\Http\Requests\Api\Application\Eggs\ImportEggRequest;
use Pterodactyl\Http\Requests\Api\Application\Nests\GetNestsRequest; use Pterodactyl\Http\Requests\Api\Application\Nests\GetNestsRequest;
use Pterodactyl\Http\Requests\Api\Application\Nests\StoreNestRequest;
use Pterodactyl\Http\Requests\Api\Application\Nests\DeleteNestRequest;
use Pterodactyl\Http\Requests\Api\Application\Nests\UpdateNestRequest;
use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController; use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController;
class NestController extends ApplicationApiController class NestController extends ApplicationApiController
@ -13,8 +25,12 @@ class NestController extends ApplicationApiController
/** /**
* NestController constructor. * NestController constructor.
*/ */
public function __construct(private NestRepositoryInterface $repository) public function __construct(
{ private NestCreationService $nestCreationService,
private NestDeletionService $nestDeletionService,
private NestUpdateService $nestUpdateService,
private EggImporterService $eggImporterService
) {
parent::__construct(); parent::__construct();
} }
@ -23,20 +39,87 @@ class NestController extends ApplicationApiController
*/ */
public function index(GetNestsRequest $request): array public function index(GetNestsRequest $request): array
{ {
$nests = $this->repository->paginated($request->query('per_page') ?? 50); $perPage = (int) $request->query('per_page', '10');
if ($perPage > 100) {
throw new QueryValueOutOfRangeHttpException('per_page', 1, 100);
}
$nests = QueryBuilder::for(Nest::query())
->allowedFilters(['id', 'name', 'author'])
->allowedSorts(['id', 'name', 'author']);
if ($perPage > 0) {
$nests = $nests->paginate($perPage);
}
return $this->fractal->collection($nests) return $this->fractal->collection($nests)
->transformWith($this->getTransformer(NestTransformer::class)) ->transformWith(NestTransformer::class)
->toArray(); ->toArray();
} }
/** /**
* Return information about a single Nest model. * Return information about a single Nest model.
*/ */
public function view(GetNestsRequest $request, Nest $nest): array public function view(GetNestRequest $request, Nest $nest): array
{ {
return $this->fractal->item($nest) return $this->fractal->item($nest)
->transformWith($this->getTransformer(NestTransformer::class)) ->transformWith(NestTransformer::class)
->toArray(); ->toArray();
} }
/**
* Creates a new nest.
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
*/
public function store(StoreNestRequest $request): array
{
$nest = $this->nestCreationService->handle($request->validated());
return $this->fractal->item($nest)
->transformWith(NestTransformer::class)
->toArray();
}
/**
* Imports an egg.
*/
public function import(ImportEggRequest $request, Nest $nest): array
{
$egg = $this->eggImporterService->handleContent(
$nest->id,
$request->getContent(),
$request->headers->get('Content-Type'),
);
return $this->fractal->item($egg)
->transformWith(EggTransformer::class)
->toArray();
}
/**
* Updates an existing nest.
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function update(UpdateNestRequest $request, Nest $nest): array
{
$this->nestUpdateService->handle($nest->id, $request->validated());
return $this->fractal->item($nest)
->transformWith(NestTransformer::class)
->toArray();
}
/**
* Deletes an existing nest.
*
* @throws \Pterodactyl\Exceptions\Service\HasActiveServersException
*/
public function delete(DeleteNestRequest $request, Nest $nest): Response
{
$this->nestDeletionService->handle($nest->id);
return $this->returnNoContent();
}
} }

View file

@ -3,13 +3,14 @@
namespace Pterodactyl\Http\Controllers\Api\Application\Nodes; namespace Pterodactyl\Http\Controllers\Api\Application\Nodes;
use Pterodactyl\Models\Node; use Pterodactyl\Models\Node;
use Illuminate\Http\JsonResponse; use Illuminate\Http\Response;
use Pterodactyl\Models\Allocation; use Pterodactyl\Models\Allocation;
use Spatie\QueryBuilder\QueryBuilder; use Spatie\QueryBuilder\QueryBuilder;
use Spatie\QueryBuilder\AllowedFilter; use Spatie\QueryBuilder\AllowedFilter;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Pterodactyl\Services\Allocations\AssignmentService; use Pterodactyl\Services\Allocations\AssignmentService;
use Pterodactyl\Services\Allocations\AllocationDeletionService; use Pterodactyl\Services\Allocations\AllocationDeletionService;
use Pterodactyl\Exceptions\Http\QueryValueOutOfRangeHttpException;
use Pterodactyl\Transformers\Api\Application\AllocationTransformer; use Pterodactyl\Transformers\Api\Application\AllocationTransformer;
use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController; use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController;
use Pterodactyl\Http\Requests\Api\Application\Allocations\GetAllocationsRequest; use Pterodactyl\Http\Requests\Api\Application\Allocations\GetAllocationsRequest;
@ -33,23 +34,27 @@ class AllocationController extends ApplicationApiController
*/ */
public function index(GetAllocationsRequest $request, Node $node): array public function index(GetAllocationsRequest $request, Node $node): array
{ {
$allocations = QueryBuilder::for($node->allocations()) $perPage = (int) $request->query('per_page', '10');
->allowedFilters([ if ($perPage < 1 || $perPage > 100) {
AllowedFilter::exact('ip'), throw new QueryValueOutOfRangeHttpException('per_page', 1, 100);
AllowedFilter::exact('port'), }
'ip_alias',
AllowedFilter::callback('server_id', function (Builder $builder, $value) {
if (empty($value) || is_bool($value) || !ctype_digit((string) $value)) {
return $builder->whereNull('server_id');
}
return $builder->where('server_id', $value); $allocations = QueryBuilder::for(Allocation::query()->where('node_id', '=', $node->id))
->allowedFilters([
'id', 'ip', 'port', 'alias',
AllowedFilter::callback('server_id', function (Builder $query, $value) {
if ($value === '0') {
$query->whereNull('server_id');
} else {
$query->where('server_id', '=', $value);
}
}), }),
]) ])
->paginate($request->query('per_page') ?? 50); ->allowedSorts(['id', 'ip', 'port', 'server_id'])
->paginate($perPage);
return $this->fractal->collection($allocations) return $this->fractal->collection($allocations)
->transformWith($this->getTransformer(AllocationTransformer::class)) ->transformWith(AllocationTransformer::class)
->toArray(); ->toArray();
} }
@ -62,11 +67,11 @@ class AllocationController extends ApplicationApiController
* @throws \Pterodactyl\Exceptions\Service\Allocation\PortOutOfRangeException * @throws \Pterodactyl\Exceptions\Service\Allocation\PortOutOfRangeException
* @throws \Pterodactyl\Exceptions\Service\Allocation\TooManyPortsInRangeException * @throws \Pterodactyl\Exceptions\Service\Allocation\TooManyPortsInRangeException
*/ */
public function store(StoreAllocationRequest $request, Node $node): JsonResponse public function store(StoreAllocationRequest $request, Node $node): Response
{ {
$this->assignmentService->handle($node, $request->validated()); $this->assignmentService->handle($node, $request->validated());
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT); return $this->returnNoContent();
} }
/** /**
@ -74,10 +79,10 @@ class AllocationController extends ApplicationApiController
* *
* @throws \Pterodactyl\Exceptions\Service\Allocation\ServerUsingAllocationException * @throws \Pterodactyl\Exceptions\Service\Allocation\ServerUsingAllocationException
*/ */
public function delete(DeleteAllocationRequest $request, Node $node, Allocation $allocation): JsonResponse public function delete(DeleteAllocationRequest $request, Node $node, Allocation $allocation): Response
{ {
$this->deletionService->handle($allocation); $this->deletionService->handle($allocation);
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT); return $this->returnNoContent();
} }
} }

View file

@ -14,8 +14,12 @@ class NodeConfigurationController extends ApplicationApiController
* to remote machines so long as an API key is provided to the machine to make the request * to remote machines so long as an API key is provided to the machine to make the request
* with, and the node is known. * with, and the node is known.
*/ */
public function __invoke(GetNodeRequest $request, Node $node): JsonResponse public function __invoke(GetNodeRequest $request, Node $node): JsonResponse|string
{ {
if ($request->query('format') === 'yaml') {
return $node->getYamlConfiguration();
}
return new JsonResponse($node->getConfiguration()); return new JsonResponse($node->getConfiguration());
} }
} }

View file

@ -3,12 +3,14 @@
namespace Pterodactyl\Http\Controllers\Api\Application\Nodes; namespace Pterodactyl\Http\Controllers\Api\Application\Nodes;
use Pterodactyl\Models\Node; use Pterodactyl\Models\Node;
use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Spatie\QueryBuilder\QueryBuilder; use Spatie\QueryBuilder\QueryBuilder;
use Pterodactyl\Services\Nodes\NodeUpdateService; use Pterodactyl\Services\Nodes\NodeUpdateService;
use Pterodactyl\Services\Nodes\NodeCreationService; use Pterodactyl\Services\Nodes\NodeCreationService;
use Pterodactyl\Services\Nodes\NodeDeletionService; use Pterodactyl\Services\Nodes\NodeDeletionService;
use Pterodactyl\Transformers\Api\Application\NodeTransformer; use Pterodactyl\Transformers\Api\Application\NodeTransformer;
use Pterodactyl\Exceptions\Http\QueryValueOutOfRangeHttpException;
use Pterodactyl\Http\Requests\Api\Application\Nodes\GetNodeRequest; use Pterodactyl\Http\Requests\Api\Application\Nodes\GetNodeRequest;
use Pterodactyl\Http\Requests\Api\Application\Nodes\GetNodesRequest; use Pterodactyl\Http\Requests\Api\Application\Nodes\GetNodesRequest;
use Pterodactyl\Http\Requests\Api\Application\Nodes\StoreNodeRequest; use Pterodactyl\Http\Requests\Api\Application\Nodes\StoreNodeRequest;
@ -34,13 +36,18 @@ class NodeController extends ApplicationApiController
*/ */
public function index(GetNodesRequest $request): array public function index(GetNodesRequest $request): array
{ {
$perPage = (int) $request->query('per_page', '10');
if ($perPage < 1 || $perPage > 100) {
throw new QueryValueOutOfRangeHttpException('per_page', 1, 100);
}
$nodes = QueryBuilder::for(Node::query()) $nodes = QueryBuilder::for(Node::query())
->allowedFilters(['uuid', 'name', 'fqdn', 'daemon_token_id']) ->allowedFilters(['id', 'uuid', 'name', 'fqdn', 'daemon_token_id'])
->allowedSorts(['id', 'uuid', 'memory', 'disk']) ->allowedSorts(['id', 'uuid', 'name', 'location_id', 'fqdn', 'memory', 'disk'])
->paginate($request->query('per_page') ?? 50); ->paginate($perPage);
return $this->fractal->collection($nodes) return $this->fractal->collection($nodes)
->transformWith($this->getTransformer(NodeTransformer::class)) ->transformWith(NodeTransformer::class)
->toArray(); ->toArray();
} }
@ -50,7 +57,7 @@ class NodeController extends ApplicationApiController
public function view(GetNodeRequest $request, Node $node): array public function view(GetNodeRequest $request, Node $node): array
{ {
return $this->fractal->item($node) return $this->fractal->item($node)
->transformWith($this->getTransformer(NodeTransformer::class)) ->transformWith(NodeTransformer::class)
->toArray(); ->toArray();
} }
@ -65,12 +72,7 @@ class NodeController extends ApplicationApiController
$node = $this->creationService->handle($request->validated()); $node = $this->creationService->handle($request->validated());
return $this->fractal->item($node) return $this->fractal->item($node)
->transformWith($this->getTransformer(NodeTransformer::class)) ->transformWith(NodeTransformer::class)
->addMeta([
'resource' => route('api.application.nodes.view', [
'node' => $node->id,
]),
])
->respond(201); ->respond(201);
} }
@ -84,11 +86,10 @@ class NodeController extends ApplicationApiController
$node = $this->updateService->handle( $node = $this->updateService->handle(
$node, $node,
$request->validated(), $request->validated(),
$request->input('reset_secret') === true
); );
return $this->fractal->item($node) return $this->fractal->item($node)
->transformWith($this->getTransformer(NodeTransformer::class)) ->transformWith(NodeTransformer::class)
->toArray(); ->toArray();
} }
@ -98,10 +99,10 @@ class NodeController extends ApplicationApiController
* *
* @throws \Pterodactyl\Exceptions\Service\HasActiveServersException * @throws \Pterodactyl\Exceptions\Service\HasActiveServersException
*/ */
public function delete(DeleteNodeRequest $request, Node $node): JsonResponse public function delete(DeleteNodeRequest $request, Node $node): Response
{ {
$this->deletionService->handle($node); $this->deletionService->handle($node);
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT); return $this->returnNoContent();
} }
} }

View file

@ -33,7 +33,7 @@ class NodeDeploymentController extends ApplicationApiController
->handle($request->query('per_page'), $request->query('page')); ->handle($request->query('per_page'), $request->query('page'));
return $this->fractal->collection($nodes) return $this->fractal->collection($nodes)
->transformWith($this->getTransformer(NodeTransformer::class)) ->transformWith(NodeTransformer::class)
->toArray(); ->toArray();
} }
} }

View file

@ -0,0 +1,47 @@
<?php
namespace Pterodactyl\Http\Controllers\Api\Application\Nodes;
use Carbon\Carbon;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use Pterodactyl\Models\Node;
use Illuminate\Http\JsonResponse;
use Illuminate\Cache\Repository as CacheRepository;
use Pterodactyl\Repositories\Wings\DaemonConfigurationRepository;
use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController;
class NodeInformationController extends ApplicationApiController
{
/**
* NodeInformationController constructor.
*/
public function __construct(private CacheRepository $cache, private DaemonConfigurationRepository $repository)
{
parent::__construct();
}
/**
* Returns system information from the node.
*
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
*/
public function __invoke(Request $request, Node $node): JsonResponse
{
$data = $this->cache
->tags(['nodes'])
->remember($node->uuid, Carbon::now()->addSeconds(30), function () use ($node) {
return $this->repository->setNode($node)->getSystemInformation();
});
return new JsonResponse([
'version' => $data['version'] ?? null,
'system' => [
'type' => Str::title($data['os'] ?? 'Unknown'),
'arch' => $data['architecture'] ?? null,
'release' => $data['kernel_version'] ?? null,
'cpus' => $data['cpu_count'] ?? null,
],
]);
}
}

View file

@ -0,0 +1,96 @@
<?php
namespace Pterodactyl\Http\Controllers\Api\Application\Roles;
use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse;
use Pterodactyl\Models\AdminRole;
use Spatie\QueryBuilder\QueryBuilder;
use Pterodactyl\Exceptions\Http\QueryValueOutOfRangeHttpException;
use Pterodactyl\Transformers\Api\Application\AdminRoleTransformer;
use Pterodactyl\Http\Requests\Api\Application\Roles\GetRoleRequest;
use Pterodactyl\Http\Requests\Api\Application\Roles\GetRolesRequest;
use Pterodactyl\Http\Requests\Api\Application\Roles\StoreRoleRequest;
use Pterodactyl\Http\Requests\Api\Application\Roles\DeleteRoleRequest;
use Pterodactyl\Http\Requests\Api\Application\Roles\UpdateRoleRequest;
use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController;
class RoleController extends ApplicationApiController
{
/**
* RoleController constructor.
*/
public function __construct()
{
parent::__construct();
}
/**
* Returns an array of all roles.
*/
public function index(GetRolesRequest $request): array
{
$perPage = (int) $request->query('per_page', '10');
if ($perPage < 1 || $perPage > 100) {
throw new QueryValueOutOfRangeHttpException('per_page', 1, 100);
}
$roles = QueryBuilder::for(AdminRole::query())
->allowedFilters(['id', 'name'])
->allowedSorts(['id', 'name'])
->paginate($perPage);
return $this->fractal->collection($roles)
->transformWith(AdminRoleTransformer::class)
->toArray();
}
/**
* Returns a single role.
*/
public function view(GetRoleRequest $request, AdminRole $role): array
{
return $this->fractal->item($role)
->transformWith(AdminRoleTransformer::class)
->toArray();
}
/**
* Creates a new role.
*/
public function store(StoreRoleRequest $request): JsonResponse
{
$data = array_merge($request->validated(), [
'sort_id' => 99,
]);
$role = AdminRole::query()->create($data);
return $this->fractal->item($role)
->transformWith(AdminRoleTransformer::class)
->respond(JsonResponse::HTTP_CREATED);
}
/**
* Updates a role.
*/
public function update(UpdateRoleRequest $request, AdminRole $role): array
{
$role->update($request->validated());
return $this->fractal->item($role)
->transformWith(AdminRoleTransformer::class)
->toArray();
}
/**
* Deletes a role.
*
* @throws \Exception
*/
public function delete(DeleteRoleRequest $request, AdminRole $role): Response
{
$role->delete();
return $this->returnNoContent();
}
}

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