Compare commits

...

70 commits

Author SHA1 Message Date
esy
61faabcbfd feat: add license
Some checks failed
Docker / Push (push) Has been cancelled
Laravel / Static Analysis (push) Has been cancelled
Laravel / Lint (push) Has been cancelled
Laravel / Tests (mariadb:10.2, 8.1) (push) Has been cancelled
Laravel / Tests (mariadb:10.2, 8.2) (push) Has been cancelled
Laravel / Tests (mariadb:10.9, 8.1) (push) Has been cancelled
Laravel / Tests (mariadb:10.9, 8.2) (push) Has been cancelled
Laravel / Tests (mysql:8, 8.1) (push) Has been cancelled
Laravel / Tests (mysql:8, 8.2) (push) Has been cancelled
Laravel / Tests (postgres:13, 8.1) (push) Has been cancelled
Laravel / Tests (postgres:13, 8.2) (push) Has been cancelled
Laravel / Tests (postgres:14, 8.1) (push) Has been cancelled
Laravel / Tests (postgres:14, 8.2) (push) Has been cancelled
Laravel / Tests (postgres:15, 8.1) (push) Has been cancelled
Laravel / Tests (postgres:15, 8.2) (push) Has been cancelled
UI / Lint (push) Has been cancelled
UI / Tests (16) (push) Has been cancelled
UI / Tests (18) (push) Has been cancelled
2024-09-23 20:07:52 +00:00
Matthew Penner
a9bdf7a1ef
Merge branch '1.0-develop' into develop 2023-10-02 17:06:39 -06:00
Matthew Penner
7fa0c26d80
Update README.md 2023-10-02 17:06:05 -06:00
Matthew Penner
804a08bd2e
nix: update flake.lock 2023-10-02 16:59:23 -06:00
Matthew Penner
8ffe0f1ff0
chore: fix eslint 2023-10-02 16:56:48 -06:00
Matthew Penner
9b35a55eea
ui: make the Input component slightly shorter 2023-09-29 16:34:56 -06:00
Matthew Penner
35ded9def8
ui(admin): fix server egg select improperly switching 2023-09-29 16:33:15 -06:00
Matthew Penner
3721b2007b
ui(admin): fix egg creation 2023-09-28 13:48:21 -06:00
Matthew Penner
6f5fb09c13
Merge branch '1.0-develop' into develop 2023-08-22 19:22:50 -06:00
Boy132
5a417e9adb
app(setup): replace mail with sendmail driver (#4750) 2023-08-22 19:18:05 -06:00
Boy132
51cee7688a
app: update prune-backup command description (#4754) 2023-08-22 19:17:18 -06:00
Boy132
67b2d944a6
ui(client): allow MassActionBar to be clicked through (#4753) 2023-08-22 19:12:57 -06:00
Matthew Penner
57d27293d2
github(template): update paste domain (#4757)
Switch from `bin.ptdl.co` to `pteropaste.com`
2023-08-22 15:08:14 -10:00
Boy132
1af200c464 Replace bin.ptdl.co with pteropaste.com
ptdl.co always has cert issues. pteropaste is used anywhere else (e.g. in Discord bot commands)
Also increased the line number to 150.
2023-08-22 15:05:55 -10:00
Matthew Penner
97049f48c3
ui(server): hide sensitive information in copy-on-click notifications (#4761) 2023-08-22 15:04:35 -10:00
Vadym
2d4071ca25 do not show strings with password in notification text 2023-08-22 15:03:47 -10:00
Matthew Penner
5cd2697be3
api(client): allow setting empty server description
Closes https://github.com/pterodactyl/panel/issues/4752
2023-08-22 15:02:56 -10:00
Robert Nisipeanu
85f1259709 fix(4752): check if description field present on request 2023-08-22 15:01:49 -10:00
Matthew Penner
bf1768406b
ui(server): fix permissions check on file manager (#4793) 2023-08-22 15:01:14 -10:00
Boy132
a83058668f only files check for "read-content", check folders for "read"
fixes #4792
2023-08-22 15:00:25 -10:00
Matthew Penner
987440c8ca
app: fix formatting 2023-08-22 18:59:34 -06:00
Matthew Penner
341fa0a52d
docker: fix log directory (#4839) 2023-08-22 14:59:12 -10:00
Michael Parker
aa2f797f6f fix panel log folder
resolves #4838

Fixes an issue with the log symlink as well
2023-08-22 14:57:45 -10:00
Matthew Penner
04d83edd36
app: fix getMySQLTimezoneOffset() truncating seconds
Previously the `getMySQLTimezoneOffset()` function would truncate the
seconds part of a time offset (returning `+9:00` instead of `+9:30`) for
example. This only affects timezones with offsets that contain minutes.

Closes https://github.com/pterodactyl/panel/issues/4821
Superseeds https://github.com/pterodactyl/panel/pull/4827

Co-authored-by: danny6167 <daniel@barteck.com.au>
2023-08-22 18:54:59 -06:00
Matthew Penner
15860613b6
Update README.md 2023-08-22 18:50:11 -06:00
Matthew Penner
3cd15d6f21
ci(docker): skip login on pull requests 2023-07-11 20:52:09 -06:00
Matthew Penner
d3c6568522
Merge branch '1.0-develop' into develop 2023-07-11 20:40:41 -06:00
Matthew Penner
29783ed240
egg(rust): add support for Carbon
Rebased version of #4734 to 1.0-develop
2023-07-11 20:37:14 -06:00
Matthew Penner
b7b128efc5
Merge branch '1.0-develop' into develop 2023-06-28 20:24:29 -06:00
Matthew Penner
7c8bdfc4d8
Update README.md 2023-06-28 20:23:53 -06:00
Matthew Penner
eaf46429f2
Merge branch '1.0-develop' into develop 2023-05-12 23:12:06 -06:00
Matthew Penner
bad765039b
Update README.md 2023-05-12 23:10:17 -06:00
Matthew Penner
b23f3114e4
Update README.md 2023-05-12 23:08:43 -06:00
Matthew Penner
dc6fadd1c5
Merge branch '1.0-develop' into develop 2023-03-11 14:12:23 -07:00
Matthew Penner
8bfcffc477
Update README.md 2023-03-11 14:11:34 -07:00
Matthew Penner
f5ce1a240c
ci: install pnpm 2023-02-23 14:39:45 -07:00
Matthew Penner
8320c64918
switch from yarn to pnpm 2023-02-23 14:37:12 -07:00
Matthew Penner
0d225b0e8f
ci(docker): revert workflow changes 2023-02-23 14:35:07 -07:00
Matthew Penner
8ae40a7c71
containerfile: remove platform arg 2023-02-23 14:22:31 -07:00
Matthew Penner
7241829da6
containerfile: reinstall tzdata
ref; https://bugzilla.redhat.com/show_bug.cgi?id=1870814
2023-02-23 14:09:24 -07:00
Matthew Penner
05d8de0671
ci(container): tweak options 2023-02-23 13:47:39 -07:00
Matthew Penner
f98aa11a3a
ci(container): update to ubuntu-22.04 2023-02-23 13:43:40 -07:00
Matthew Penner
ab4b4e6ada
ci(container): replace old docker workflow 2023-02-23 13:40:04 -07:00
Matthew Penner
673f7282e0
ui: fix prettier 2023-02-23 13:15:06 -07:00
Matthew Penner
2195aa6447
ci(docker): upgrade to php 8.2 2023-02-23 13:08:49 -07:00
Matthew Penner
187c7f74cd
yarn: upgrade dependencies 2023-02-23 13:08:40 -07:00
Matthew Penner
0af39492e3
app(services): fix DeployServerDatabaseService phpstan 2023-02-23 12:49:07 -07:00
Matthew Penner
733771ae75
nix: update flake lock 2023-02-23 12:48:53 -07:00
Matthew Penner
85df1a4ec7
ci(laravel): add PHP 8.2 to test matrix 2023-02-23 12:48:28 -07:00
Matthew Penner
a1e5afccb4
composer: update lock 2023-02-23 12:37:27 -07:00
Matthew Penner
18f6611a2d
Merge branch '1.0-develop' into develop 2023-02-23 12:36:22 -07:00
Matthew Penner
1d38b4f0e2
Laravel 10 (#4706) 2023-02-23 12:30:16 -07:00
Matthew Penner
ad4ddc6300
nix: update flake 2023-02-23 12:24:47 -07:00
Devonte W
b746c3ead1
fix(api/client): add validation for backup request body (#4704) 2023-02-23 12:23:12 -07:00
Devonte W
aea5c474db
fix(resources/api): allow svg xml format (#4705) 2023-02-23 12:19:58 -07:00
Matthew Penner
582c50c6d4
Merge branch '1.0-develop' into develop 2023-02-06 10:22:50 -07:00
Matthew Penner
2a7833ca17
Update README.md 2023-02-06 10:22:21 -07:00
Alex
9b47403e00
egg(rust): add server.queryport option (#4681) 2023-02-04 14:42:04 -07:00
Matthew Penner
3bd8164415
Merge branch '1.0-develop' into develop 2023-01-27 13:09:22 -07:00
Matthew Penner
43f7c10617
Update CHANGELOG.md 2023-01-27 12:24:55 -07:00
Matthew Penner
c5be3ad04b
nix: update flake lock 2023-01-25 13:34:47 -07:00
Matthew Penner
d8b7747828
ui(server): lazy load server console to reduce main bundle size 2023-01-25 11:39:07 -07:00
Matthew Penner
7c67ff3711
yarn: upgrade dependencies 2023-01-25 11:36:42 -07:00
Matthew Penner
d09e61b4f4
Update Containerfile to node 18, install composer through rpms 2023-01-25 11:36:28 -07:00
Matthew Penner
4b82ca1042
ui(admin): fix users list 2023-01-25 11:14:39 -07:00
Matthew Penner
5063db7d95
Merge branch '1.0-develop' into develop 2023-01-25 10:47:17 -07:00
Matthew Penner
866b6df4b0
api(task): ensure sequence_id always starts at 1 2023-01-24 16:19:34 -07:00
Matthew Penner
2b14e46eec
api: fix sequence_id being ignored in server task API
Closes #4434
2023-01-24 15:57:24 -07:00
Matthew Penner
20f23a0b27
db: add uuid column to failed_jobs table
Refer to
<https://laravel.com/docs/8.x/upgrade#failed-jobs-table-batch-support>
for more information regarding this change.

Closes #4652
2023-01-24 14:02:41 -07:00
Matthew Penner
a27ea3d1bc
config(queue): default to redis driver
Updates the default `QUEUE_CONNECTION` value to be `redis`
instead of `sync`.  This can cause problems if users skip
the initial setup or select the prefilled options rather
than the recommended ones.

Closes #4660
2023-01-24 13:48:34 -07:00
107 changed files with 7962 additions and 7245 deletions

View file

@ -22,7 +22,7 @@ REDIS_PASSWORD=null
REDIS_PORT=6379 REDIS_PORT=6379
CACHE_DRIVER=file CACHE_DRIVER=file
QUEUE_CONNECTION=sync QUEUE_CONNECTION=redis
SESSION_DRIVER=file SESSION_DRIVER=file
HASHIDS_SALT= HASHIDS_SALT=

View file

@ -68,8 +68,8 @@ body:
Run the following command to collect logs on your system. Run the following command to collect logs on your system.
Wings: `sudo wings diagnostics` Wings: `sudo wings diagnostics`
Panel: `tail -n 100 /var/www/pterodactyl/storage/logs/laravel-$(date +%F).log | nc bin.ptdl.co 99` Panel: `tail -n 150 /var/www/pterodactyl/storage/logs/laravel-$(date +%F).log | nc pteropaste.com 99`
placeholder: "https://bin.ptdl.co/a1h6z" placeholder: "https://pteropaste.com/a1h6z"
render: bash render: bash
validations: validations:
required: false required: false

View file

@ -16,14 +16,13 @@ on:
jobs: jobs:
push: push:
name: Push name: Push
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
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: Fetch metadata
id: docker_meta id: metadata
uses: docker/metadata-action@v4 uses: docker/metadata-action@v4
with: with:
images: ghcr.io/pterodactyl/panel images: ghcr.io/pterodactyl/panel
@ -42,6 +41,7 @@ jobs:
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v2 uses: docker/login-action@v2
if: "github.event_name != 'pull_request'"
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@ -55,13 +55,13 @@ jobs:
sed -i "s/ 'version' => 'canary',/ 'version' => '${REF:1}',/" config/app.php sed -i "s/ 'version' => 'canary',/ 'version' => '${REF:1}',/" config/app.php
- name: Build and Push - name: Build and Push
uses: docker/build-push-action@v3 uses: docker/build-push-action@v4
with: with:
context: . context: .
file: ./Containerfile file: ./Containerfile
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64 #,linux/arm64 platforms: linux/amd64,linux/arm64
labels: ${{ steps.docker_meta.outputs.labels }} labels: ${{ steps.metadata.outputs.labels }}
tags: ${{ steps.docker_meta.outputs.tags }} tags: ${{ steps.metadata.outputs.tags }}
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max

View file

@ -13,7 +13,7 @@ on:
jobs: jobs:
analysis: analysis:
name: Static Analysis name: Static Analysis
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
env: env:
APP_ENV: testing APP_ENV: testing
APP_DEBUG: "true" APP_DEBUG: "true"
@ -42,7 +42,7 @@ jobs:
lint: lint:
name: Lint name: Lint
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
steps: steps:
- name: Code checkout - name: Code checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
@ -50,6 +50,7 @@ jobs:
- name: Setup PHP - name: Setup PHP
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2
with: with:
# TODO: Update to 8.2 once php-cs-fixer supports it
php-version: 8.1 php-version: 8.1
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2 tools: composer:v2
@ -63,11 +64,11 @@ jobs:
mysql: mysql:
name: Tests name: Tests
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
php: [8.1] php: [8.1, 8.2]
database: ["mariadb:10.2", "mariadb:10.9", "mysql:8"] database: ["mariadb:10.2", "mariadb:10.9", "mysql:8"]
services: services:
database: database:
@ -135,12 +136,12 @@ jobs:
postgres: postgres:
name: Tests name: Tests
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
if: "!contains(github.event.head_commit.message, 'skip ci') && !contains(github.event.head_commit.message, 'ci skip')" if: "!contains(github.event.head_commit.message, 'skip ci') && !contains(github.event.head_commit.message, 'ci skip')"
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
php: [8.1] php: [8.1, 8.2]
database: ["postgres:13", "postgres:14", "postgres:15"] database: ["postgres:13", "postgres:14", "postgres:15"]
services: services:
database: database:

View file

@ -8,22 +8,25 @@ on:
jobs: jobs:
release: release:
name: Release name: Release
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
steps: steps:
- name: Code checkout - name: Code checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Install pnpm
uses: pnpm/action-setup@v2
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 18
cache: yarn cache: pnpm
- name: Install dependencies - name: Install dependencies
run: yarn install --frozen-lockfile run: pnpm install
- name: Build - name: Build
run: yarn build run: pnpm run build
- name: Create release branch and bump version - name: Create release branch and bump version
env: env:

View file

@ -13,26 +13,29 @@ on:
jobs: jobs:
lint: lint:
name: Lint name: Lint
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
steps: steps:
- name: Code checkout - name: Code checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Install pnpm
uses: pnpm/action-setup@v2
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 18
cache: yarn cache: pnpm
- name: Install dependencies - name: Install dependencies
run: yarn install --frozen-lockfile run: pnpm install
- name: Lint - name: Lint
run: yarn run lint run: pnpm run lint
tests: tests:
name: Tests name: Tests
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -41,17 +44,20 @@ jobs:
- name: Code checkout - name: Code checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Install pnpm
uses: pnpm/action-setup@v2
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node }} node-version: ${{ matrix.node }}
cache: yarn cache: pnpm
- name: Install dependencies - name: Install dependencies
run: yarn install --frozen-lockfile run: pnpm install
- name: Build - name: Build
run: yarn run build run: pnpm run build
- name: Tests - name: Tests
run: yarn run test run: pnpm run test

1
.npmrc Normal file
View file

@ -0,0 +1 @@
shamefully-hoist=true

View file

@ -1,63 +0,0 @@
# Local Development
Pterodactyl is now powered by React, Typescript, and Tailwindcss using webpack at its core to generate compiled assets.
Release versions of Pterodactyl will include pre-compiled, minified, and hashed assets ready-to-go.
However, if you are interested in running custom themes or making modifications to the React files you'll need a build
system in place to generate these compiled assets. To get your environment setup you'll need at minimum:
* [Node.js](https://nodejs.org/en/) v14.x.x
* [Yarn](https://classic.yarnpkg.com/lang/en/) v1.x.x
* [Go](https://golang.org/) 1.17.x
### Install Dependencies
```bash
yarn install
```
The command above will download all of the dependencies necessary to get Pterodactyl assets building. After that, its as
simple as running the command below to generate assets while you're developing. Until you've run this command at least
once you'll likely see a 500 error on your Panel about a missing `manifest.json` file. This is generated by the commands
below.
```bash
# Build the compiled set of assets for development.
yarn run build
# Build the assets automatically as they are changed. This allows you to refresh
# the page and see the changes immediately.
yarn run watch
```
### Hot Module Reloading
For more advanced users, we also support 'Hot Module Reloading', allowing you to quickly see changes you're making
to the Vue template files without having to reload the page you're on. To Get started with this, you just need
to run the command below.
```bash
PUBLIC_PATH=http://192.168.1.1:8080 yarn run serve --host 192.168.1.1
```
There are two _very important_ parts of this command to take note of and change for your specific environment. The first
is the `--host` flag, which is required and should point to the machine where the `webpack-serve` server will be running.
The second is the `PUBLIC_PATH` environment variable which is the URL pointing to the HMR server and is appended to all of
the asset URLs used in Pterodactyl.
#### Development Environment
If you're using the [`pterodactyl/development`](https://github.com/pterodactyl/development) environments, which are
highly recommended, you can just run `yarn run serve` to run the HMR server, no additional configuration is necessary.
### Building for Production
Once you have your files squared away and ready for the live server, you'll be needing to generate compiled, minified,
and hashed assets to push live. To do so, run the command below:
```bash
yarn run build:production
```
This will generate a production JS bundle and associated assets, all located in `public/assets/` which will need to
be uploaded to your server or CDN for clients to use.
### Running Wings
To run `wings` in development all you need to do is set up the configuration file as normal when adding a new node, and
then you can build and run a local version of Wings by executing `make debug` in the Wings code directory. This must
be run on a Linux VM of some sort, you cannot run this locally on macOS or Windows.

View file

@ -3,6 +3,17 @@ 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.
## v1.11.3
### Changed
* When updating a server's description through the client API, if no value is specified, the description will now remain unchanged.
* When installing the Panel for the first time, the queue driver will now all default to `redis` instead of `sync`.
### Fixed
* `php artisan p:environment:mail` not correctly setting the right variable for `MAIL_FROM_ADDRESS`.
* Fixed the conflict state rendering on the UI for a server showing `reinstall_failed` as `restoring_backup`.
* Fixed the unknown column `uuid` error when jobs fail, causing them not to get stored correctly.
* Fixed the server task endpoints in the client API not allowing `sequence_id` and `continue_on_failure` to be set.
## v1.11.2 ## v1.11.2
### Changed ### Changed
* Telemetry no longer sends a map of Egg and Nest UUIDs to the number of servers using them. * Telemetry no longer sends a map of Egg and Nest UUIDs to the number of servers using them.

View file

@ -1,20 +1,20 @@
# Stage 0 - Caddy
FROM --platform=$TARGETOS/$TARGETARCH docker.io/library/caddy:latest AS caddy
# Stage 1 - Builder # Stage 1 - Builder
FROM --platform=$TARGETOS/$TARGETARCH registry.access.redhat.com/ubi9/nodejs-16-minimal AS builder FROM --platform=$TARGETOS/$TARGETARCH registry.access.redhat.com/ubi9/nodejs-18-minimal AS builder
RUN npm install -g yarn USER 0
RUN npm install -g pnpm
WORKDIR /var/www/pterodactyl WORKDIR /var/www/pterodactyl
COPY --chown=1001:0 public ./public COPY --chown=1001:0 public ./public
COPY --chown=1001:0 resources/scripts ./resources/scripts 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 . COPY --chown=1001:0 .eslintignore .eslintrc.js .npmrc .prettierrc.json package.json pnpm-lock.yaml tailwind.config.js tsconfig.json vite.config.ts .
RUN /opt/app-root/src/.npm-global/bin/yarn install --frozen-lockfile \ RUN /opt/app-root/src/.npm-global/bin/pnpm install \
&& /opt/app-root/src/.npm-global/bin/yarn build \ && /opt/app-root/src/.npm-global/bin/pnpm build \
&& rm -rf resources/scripts .eslintignore .eslintrc.yml .yarnrc.yml package.json tailwind.config.js tsconfig.json vite.config.ts yarn.lock node_modules && rm -rf resources/scripts .eslintignore .eslintrc.yml .npmrc package.json pnpm-lock.yaml tailwind.config.js tsconfig.json vite.config.ts node_modules
USER 1001
COPY --chown=1001:0 app ./app COPY --chown=1001:0 app ./app
COPY --chown=1001:0 bootstrap ./bootstrap COPY --chown=1001:0 bootstrap ./bootstrap
@ -34,10 +34,11 @@ RUN microdnf update -y \
&& rpm --install https://rpms.remirepo.net/enterprise/remi-release-9.rpm \ && rpm --install https://rpms.remirepo.net/enterprise/remi-release-9.rpm \
&& microdnf update -y \ && microdnf update -y \
&& microdnf install -y ca-certificates shadow-utils tar tzdata unzip wget \ && microdnf install -y ca-certificates shadow-utils tar tzdata unzip wget \
# ref; https://bugzilla.redhat.com/show_bug.cgi?id=1870814
&& microdnf reinstall -y tzdata \
&& microdnf module -y reset php \ && microdnf module -y reset php \
&& microdnf module -y enable php:remi-8.1 \ && microdnf module -y enable php:remi-8.2 \
&& 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 \ && microdnf install -y composer 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 \ && rm /etc/php-fpm.d/www.conf \
&& useradd --home-dir /var/lib/caddy --create-home caddy \ && useradd --home-dir /var/lib/caddy --create-home caddy \
&& mkdir /etc/caddy \ && mkdir /etc/caddy \
@ -66,7 +67,7 @@ RUN composer install --no-dev --optimize-autoloader \
&& rm -rf bootstrap/cache/*.php \ && rm -rf bootstrap/cache/*.php \
&& rm -rf .env storage/logs/*.log && rm -rf .env storage/logs/*.log
COPY --from=caddy /usr/bin/caddy /usr/local/bin/caddy COPY --from=docker.io/library/caddy:latest /usr/bin/caddy /usr/local/bin/caddy
COPY .github/docker/Caddyfile /etc/caddy/Caddyfile COPY .github/docker/Caddyfile /etc/caddy/Caddyfile
COPY .github/docker/php-fpm.conf /etc/php-fpm.conf COPY .github/docker/php-fpm.conf /etc/php-fpm.conf
COPY .github/docker/supervisord.conf /etc/supervisord.conf COPY .github/docker/supervisord.conf /etc/supervisord.conf

9
LICENSE Normal file
View file

@ -0,0 +1,9 @@
MIT License
Copyright (c) 2024 Skynet
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -24,21 +24,20 @@ Stop settling for less. Make game servers a first class citizen on your platform
## Sponsors ## Sponsors
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 development.
[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. |
| [**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. |
| [**WemX**](https://wemx.net/) | WemX helps automate your hosting company or SaaS business by automating billing, user management, authentication, and much more. |
| [**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! |
| [**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. | | [**DutchIS**](https://dutchis.net?ref=pterodactyl) | DutchIS provides instant infrastructure such as pay per use VPS hosting. Start your game hosting journey on DutchIS. |
| [**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. | | [**Skoali**](https://skoali.com/) | Skoali is a French company that hosts game servers and other types of services (VPS, WEB, Dedicated servers, ...). We also have a free plan for Minecraft and Garry's Mod. |
| [**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. | | | [**Rabbit Computing**](https://www.rabbitcomputing.com/link.php?id=5) | Rabbit Computing offers powerful VPS servers, highly available game hosting, and fully unlimited web hosting. Use code README for 20% off your first three months! |
### Supported Games ### Supported Games

View file

@ -44,7 +44,7 @@ class EmailSettingsCommand extends Command
trans('command/messages.environment.mail.ask_driver'), trans('command/messages.environment.mail.ask_driver'),
[ [
'smtp' => 'SMTP Server', 'smtp' => 'SMTP Server',
'mail' => 'PHP\'s Internal Mail Function', 'sendmail' => 'sendmail Binary',
'mailgun' => 'Mailgun Transactional Email', 'mailgun' => 'Mailgun Transactional Email',
'mandrill' => 'Mandrill Transactional Email', 'mandrill' => 'Mandrill Transactional Email',
'postmark' => 'Postmark Transactional Email', 'postmark' => 'Postmark Transactional Email',

View file

@ -10,7 +10,7 @@ class PruneOrphanedBackupsCommand extends Command
{ {
protected $signature = 'p:maintenance:prune-backups {--prune-age=}'; protected $signature = 'p:maintenance:prune-backups {--prune-age=}';
protected $description = 'Marks all backups that have not completed in the last "n" minutes as being failed.'; protected $description = 'Marks all backups older than "n" minutes that have not yet completed as being failed.';
/** /**
* PruneOrphanedBackupsCommand constructor. * PruneOrphanedBackupsCommand constructor.
@ -38,7 +38,7 @@ class PruneOrphanedBackupsCommand extends Command
return; return;
} }
$this->warn("Marking $count backups that have not been marked as completed in the last $since minutes as failed."); $this->warn("Marking $count uncompleted backups that are older than $since minutes as failed.");
$query->update([ $query->update([
'is_successful' => false, 'is_successful' => false,

View file

@ -18,7 +18,7 @@ class Kernel extends ConsoleKernel
/** /**
* Register the commands for the application. * Register the commands for the application.
*/ */
protected function commands() protected function commands(): void
{ {
$this->load(__DIR__ . '/Commands'); $this->load(__DIR__ . '/Commands');
} }
@ -26,8 +26,11 @@ class Kernel extends ConsoleKernel
/** /**
* Define the application's command schedule. * Define the application's command schedule.
*/ */
protected function schedule(Schedule $schedule) protected function schedule(Schedule $schedule): void
{ {
// https://laravel.com/docs/10.x/upgrade#redis-cache-tags
$schedule->command('cache:prune-stale-tags')->hourly();
// Execute scheduled commands for servers every minute, as if there was a normal cron running. // Execute scheduled commands for servers every minute, as if there was a normal cron running.
$schedule->command(ProcessRunnableCommand::class)->everyMinute()->withoutOverlapping(); $schedule->command(ProcessRunnableCommand::class)->everyMinute()->withoutOverlapping();
$schedule->command(CleanServiceBackupFilesCommand::class)->daily(); $schedule->command(CleanServiceBackupFilesCommand::class)->daily();

View file

@ -73,7 +73,7 @@ final class Handler extends ExceptionHandler
* *
* @noinspection PhpUnusedLocalVariableInspection * @noinspection PhpUnusedLocalVariableInspection
*/ */
public function register() public function register(): void
{ {
if (config('app.exceptions.report_all', false)) { if (config('app.exceptions.report_all', false)) {
$this->dontReport = []; $this->dontReport = [];

View file

@ -15,8 +15,6 @@ final class Time
*/ */
public static function getMySQLTimezoneOffset(string $timezone): string public static function getMySQLTimezoneOffset(string $timezone): string
{ {
$offset = round(CarbonImmutable::now($timezone)->getTimezone()->getOffset(CarbonImmutable::now('UTC')) / 3600); return CarbonImmutable::now($timezone)->getTimezone()->toOffsetName();
return sprintf('%s%s:00', $offset > 0 ? '+' : '-', str_pad((string) abs($offset), 2, '0', STR_PAD_LEFT));
} }
} }

View file

@ -27,8 +27,6 @@ class EggController extends ApplicationApiController
public function __construct(private EggExporterService $eggExporterService) public function __construct(private EggExporterService $eggExporterService)
{ {
parent::__construct(); parent::__construct();
$this->eggExporterService = $eggExporterService;
} }
/** /**

View file

@ -18,6 +18,7 @@ use Pterodactyl\Transformers\Api\Client\BackupTransformer;
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController; use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Pterodactyl\Http\Requests\Api\Client\Servers\Backups\StoreBackupRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Backups\StoreBackupRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Backups\RestoreBackupRequest;
class BackupController extends ClientApiController class BackupController extends ClientApiController
{ {
@ -188,12 +189,8 @@ class BackupController extends ClientApiController
* *
* @throws \Throwable * @throws \Throwable
*/ */
public function restore(Request $request, Server $server, Backup $backup): JsonResponse public function restore(RestoreBackupRequest $request, Server $server, Backup $backup): JsonResponse
{ {
if (!$request->user()->can(Permission::ACTION_BACKUP_RESTORE, $server)) {
throw new AuthorizationException();
}
// Cannot restore a backup unless a server is fully installed and not currently // Cannot restore a backup unless a server is fully installed and not currently
// processing a different backup restoration request. // processing a different backup restoration request.
if (!is_null($server->status)) { if (!is_null($server->status)) {

View file

@ -9,6 +9,7 @@ use Pterodactyl\Models\Schedule;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Pterodactyl\Facades\Activity; use Pterodactyl\Facades\Activity;
use Pterodactyl\Models\Permission; use Pterodactyl\Models\Permission;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Repositories\Eloquent\TaskRepository; use Pterodactyl\Repositories\Eloquent\TaskRepository;
use Pterodactyl\Exceptions\Http\HttpForbiddenException; use Pterodactyl\Exceptions\Http\HttpForbiddenException;
use Pterodactyl\Transformers\Api\Client\TaskTransformer; use Pterodactyl\Transformers\Api\Client\TaskTransformer;
@ -23,8 +24,10 @@ class ScheduleTaskController extends ClientApiController
/** /**
* ScheduleTaskController constructor. * ScheduleTaskController constructor.
*/ */
public function __construct(private TaskRepository $repository) public function __construct(
{ private ConnectionInterface $connection,
private TaskRepository $repository
) {
parent::__construct(); parent::__construct();
} }
@ -49,14 +52,35 @@ class ScheduleTaskController extends ClientApiController
$lastTask = $schedule->tasks()->orderByDesc('sequence_id')->first(); $lastTask = $schedule->tasks()->orderByDesc('sequence_id')->first();
/** @var \Pterodactyl\Models\Task $task */ /** @var \Pterodactyl\Models\Task $task */
$task = $this->repository->create([ $task = $this->connection->transaction(function () use ($request, $schedule, $lastTask) {
$sequenceId = ($lastTask->sequence_id ?? 0) + 1;
$requestSequenceId = $request->integer('sequence_id', $sequenceId);
// Ensure that the sequence id is at least 1.
if ($requestSequenceId < 1) {
$requestSequenceId = 1;
}
// If the sequence id from the request is greater than or equal to the next available
// sequence id, we don't need to do anything special. Otherwise, we need to update
// the sequence id of all tasks that are greater than or equal to the request sequence
// id to be one greater than the current value.
if ($requestSequenceId < $sequenceId) {
$schedule->tasks()
->where('sequence_id', '>=', $requestSequenceId)
->increment('sequence_id');
$sequenceId = $requestSequenceId;
}
return $this->repository->create([
'schedule_id' => $schedule->id, 'schedule_id' => $schedule->id,
'sequence_id' => ($lastTask->sequence_id ?? 0) + 1, 'sequence_id' => $sequenceId,
'action' => $request->input('action'), 'action' => $request->input('action'),
'payload' => $request->input('payload') ?? '', 'payload' => $request->input('payload') ?? '',
'time_offset' => $request->input('time_offset'), 'time_offset' => $request->input('time_offset'),
'continue_on_failure' => (bool) $request->input('continue_on_failure'), 'continue_on_failure' => $request->boolean('continue_on_failure'),
]); ]);
});
Activity::event('server:task.create') Activity::event('server:task.create')
->subject($schedule, $task) ->subject($schedule, $task)
@ -84,12 +108,34 @@ class ScheduleTaskController extends ClientApiController
throw new HttpForbiddenException("A backup task cannot be created when the server's backup limit is set to 0."); throw new HttpForbiddenException("A backup task cannot be created when the server's backup limit is set to 0.");
} }
$this->connection->transaction(function () use ($request, $schedule, $task) {
$sequenceId = $request->integer('sequence_id', $task->sequence_id);
// Ensure that the sequence id is at least 1.
if ($sequenceId < 1) {
$sequenceId = 1;
}
// Shift all other tasks in the schedule up or down to make room for the new task.
if ($sequenceId < $task->sequence_id) {
$schedule->tasks()
->where('sequence_id', '>=', $sequenceId)
->where('sequence_id', '<', $task->sequence_id)
->increment('sequence_id');
} elseif ($sequenceId > $task->sequence_id) {
$schedule->tasks()
->where('sequence_id', '>', $task->sequence_id)
->where('sequence_id', '<=', $sequenceId)
->decrement('sequence_id');
}
$this->repository->update($task->id, [ $this->repository->update($task->id, [
'sequence_id' => $sequenceId,
'action' => $request->input('action'), 'action' => $request->input('action'),
'payload' => $request->input('payload') ?? '', 'payload' => $request->input('payload') ?? '',
'time_offset' => $request->input('time_offset'), 'time_offset' => $request->input('time_offset'),
'continue_on_failure' => (bool) $request->input('continue_on_failure'), 'continue_on_failure' => $request->boolean('continue_on_failure'),
]); ]);
});
Activity::event('server:task.update') Activity::event('server:task.update')
->subject($schedule, $task) ->subject($schedule, $task)
@ -117,10 +163,9 @@ class ScheduleTaskController extends ClientApiController
throw new HttpForbiddenException('You do not have permission to perform this action.'); throw new HttpForbiddenException('You do not have permission to perform this action.');
} }
$schedule->tasks()->where('sequence_id', '>', $task->sequence_id)->update([ $schedule->tasks()
'sequence_id' => $schedule->tasks()->getConnection()->raw('(sequence_id - 1)'), ->where('sequence_id', '>', $task->sequence_id)
]); ->decrement('sequence_id');
$task->delete(); $task->delete();
Activity::event('server:task.delete')->subject($schedule, $task)->property('name', $schedule->name)->log(); Activity::event('server:task.delete')->subject($schedule, $task)->property('name', $schedule->name)->log();

View file

@ -35,7 +35,7 @@ class SettingsController extends ClientApiController
public function rename(RenameServerRequest $request, Server $server): JsonResponse public function rename(RenameServerRequest $request, Server $server): JsonResponse
{ {
$name = $request->input('name'); $name = $request->input('name');
$description = $request->input('description') ?? $server->description; $description = $request->has('description') ? (string) $request->input('description') : $server->description;
$this->repository->update($server->id, [ $this->repository->update($server->id, [
'name' => $name, 'name' => $name,
'description' => $description, 'description' => $description,

View file

@ -86,7 +86,7 @@ class Kernel extends HttpKernel
/** /**
* The application's route middleware. * The application's route middleware.
*/ */
protected $routeMiddleware = [ protected $middlewareAliases = [
'auth' => Authenticate::class, 'auth' => Authenticate::class,
'auth.basic' => AuthenticateWithBasicAuth::class, 'auth.basic' => AuthenticateWithBasicAuth::class,
'auth.session' => AuthenticateSession::class, 'auth.session' => AuthenticateSession::class,

View file

@ -0,0 +1,19 @@
<?php
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Backups;
use Pterodactyl\Models\Permission;
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
class RestoreBackupRequest extends ClientApiRequest
{
public function permission(): string
{
return Permission::ACTION_BACKUP_RESTORE;
}
public function rules(): array
{
return ['truncate' => 'required|boolean'];
}
}

View file

@ -23,6 +23,7 @@ class StoreTaskRequest extends ViewScheduleRequest
'payload' => 'required_unless:action,backup|string|nullable', 'payload' => 'required_unless:action,backup|string|nullable',
'time_offset' => 'required|numeric|min:0|max:900', 'time_offset' => 'required|numeric|min:0|max:900',
'sequence_id' => 'sometimes|required|numeric|min:1', 'sequence_id' => 'sometimes|required|numeric|min:1',
'continue_on_failure' => 'sometimes|required|boolean',
]; ];
} }
} }

View file

@ -18,6 +18,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property array|null $allowed_ips * @property array|null $allowed_ips
* @property string|null $memo * @property string|null $memo
* @property \Illuminate\Support\Carbon|null $last_used_at * @property \Illuminate\Support\Carbon|null $last_used_at
* @property \Illuminate\Support\Carbon|null $expires_at
* @property \Illuminate\Support\Carbon|null $created_at * @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at * @property \Illuminate\Support\Carbon|null $updated_at
* @property int $r_servers * @property int $r_servers
@ -97,6 +98,10 @@ class ApiKey extends Model
protected $casts = [ protected $casts = [
'allowed_ips' => 'array', 'allowed_ips' => 'array',
'user_id' => 'int', 'user_id' => 'int',
'last_used_at' => 'datetime',
'expires_at' => 'datetime',
self::CREATED_AT => 'datetime',
self::UPDATED_AT => 'datetime',
'r_' . AdminAcl::RESOURCE_USERS => 'int', 'r_' . AdminAcl::RESOURCE_USERS => 'int',
'r_' . AdminAcl::RESOURCE_ALLOCATIONS => 'int', 'r_' . AdminAcl::RESOURCE_ALLOCATIONS => 'int',
'r_' . AdminAcl::RESOURCE_DATABASE_HOSTS => 'int', 'r_' . AdminAcl::RESOURCE_DATABASE_HOSTS => 'int',
@ -117,6 +122,7 @@ class ApiKey extends Model
'allowed_ips', 'allowed_ips',
'memo', 'memo',
'last_used_at', 'last_used_at',
'expires_at',
]; ];
/** /**
@ -137,6 +143,7 @@ class ApiKey extends Model
'allowed_ips' => 'nullable|array', 'allowed_ips' => 'nullable|array',
'allowed_ips.*' => 'string', 'allowed_ips.*' => 'string',
'last_used_at' => 'nullable|date', 'last_used_at' => 'nullable|date',
'expires_at' => 'nullable|date',
'r_' . AdminAcl::RESOURCE_USERS => 'integer|min:0|max:3', 'r_' . AdminAcl::RESOURCE_USERS => 'integer|min:0|max:3',
'r_' . AdminAcl::RESOURCE_ALLOCATIONS => 'integer|min:0|max:3', 'r_' . AdminAcl::RESOURCE_ALLOCATIONS => 'integer|min:0|max:3',
'r_' . AdminAcl::RESOURCE_DATABASE_HOSTS => 'integer|min:0|max:3', 'r_' . AdminAcl::RESOURCE_DATABASE_HOSTS => 'integer|min:0|max:3',
@ -148,12 +155,6 @@ class ApiKey extends Model
'r_' . AdminAcl::RESOURCE_SERVERS => 'integer|min:0|max:3', 'r_' . AdminAcl::RESOURCE_SERVERS => 'integer|min:0|max:3',
]; ];
protected $dates = [
self::CREATED_AT,
self::UPDATED_AT,
'last_used_at',
];
/** /**
* Returns the user this token is assigned to. * Returns the user this token is assigned to.
*/ */

View file

@ -42,10 +42,7 @@ class Backup extends Model
'is_locked' => 'bool', 'is_locked' => 'bool',
'ignored_files' => 'array', 'ignored_files' => 'array',
'bytes' => 'int', 'bytes' => 'int',
]; 'completed_at' => 'datetime',
protected $dates = [
'completed_at',
]; ];
protected $attributes = [ protected $attributes = [

View file

@ -78,6 +78,9 @@ class Egg extends Model
* Fields that are not mass assignable. * Fields that are not mass assignable.
*/ */
protected $fillable = [ protected $fillable = [
'nest_id',
'author',
'uuid',
'name', 'name',
'description', 'description',
'features', 'features',

View file

@ -71,14 +71,8 @@ class Schedule extends Model
'is_active' => 'boolean', 'is_active' => 'boolean',
'is_processing' => 'boolean', 'is_processing' => 'boolean',
'only_when_online' => 'boolean', 'only_when_online' => 'boolean',
]; 'last_run_at' => 'datetime',
'next_run_at' => 'datetime',
/**
* Columns to mutate to a date.
*/
protected $dates = [
'last_run_at',
'next_run_at',
]; ];
protected $attributes = [ protected $attributes = [

View file

@ -140,11 +140,6 @@ class Server extends Model
*/ */
protected $with = ['allocation']; protected $with = ['allocation'];
/**
* The attributes that should be mutated to dates.
*/
protected $dates = [self::CREATED_AT, self::UPDATED_AT, 'deleted_at', 'installed_at'];
/** /**
* Fields that are not mass assignable. * Fields that are not mass assignable.
*/ */
@ -167,7 +162,7 @@ class Server extends Model
'allocation_id' => 'required|bail|unique:servers|exists:allocations,id', 'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',
'nest_id' => 'required|exists:nests,id', 'nest_id' => 'required|exists:nests,id',
'egg_id' => 'required|exists:eggs,id', 'egg_id' => 'required|exists:eggs,id',
'startup' => 'required|string', 'startup' => 'nullable|string',
'skip_scripts' => 'sometimes|boolean', 'skip_scripts' => 'sometimes|boolean',
'image' => 'required|string|max:191', 'image' => 'required|string|max:191',
'database_limit' => 'present|nullable|integer|min:0', 'database_limit' => 'present|nullable|integer|min:0',
@ -194,6 +189,10 @@ class Server extends Model
'database_limit' => 'integer', 'database_limit' => 'integer',
'allocation_limit' => 'integer', 'allocation_limit' => 'integer',
'backup_limit' => 'integer', 'backup_limit' => 'integer',
self::CREATED_AT => 'datetime',
self::UPDATED_AT => 'datetime',
'deleted_at' => 'datetime',
'installed_at' => 'datetime',
]; ];
/** /**

View file

@ -23,10 +23,8 @@ class TaskLog extends Model
'id' => 'integer', 'id' => 'integer',
'task_id' => 'integer', 'task_id' => 'integer',
'run_status' => 'integer', 'run_status' => 'integer',
'run_time' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime',
]; ];
/**
* The attributes that should be mutated to dates.
*/
protected $dates = ['run_time', 'created_at', 'updated_at'];
} }

View file

@ -138,10 +138,9 @@ class User extends Model implements
'root_admin' => 'boolean', 'root_admin' => 'boolean',
'use_totp' => 'boolean', 'use_totp' => 'boolean',
'gravatar' => 'boolean', 'gravatar' => 'boolean',
'totp_authenticated_at' => 'datetime',
]; ];
protected $dates = ['totp_authenticated_at'];
/** /**
* The attributes excluded from the model's JSON form. * The attributes excluded from the model's JSON form.
*/ */

View file

@ -15,7 +15,7 @@ class AppServiceProvider extends ServiceProvider
/** /**
* Bootstrap any application services. * Bootstrap any application services.
*/ */
public function boot() public function boot(): void
{ {
Schema::defaultStringLength(191); Schema::defaultStringLength(191);
@ -48,7 +48,7 @@ class AppServiceProvider extends ServiceProvider
/** /**
* Register application service providers. * Register application service providers.
*/ */
public function register() public function register(): void
{ {
// Only load the settings service provider if the environment // Only load the settings service provider if the environment
// is configured to allow it. // is configured to allow it.

View file

@ -17,14 +17,12 @@ class AuthServiceProvider extends ServiceProvider
Server::class => ServerPolicy::class, Server::class => ServerPolicy::class,
]; ];
public function boot() public function boot(): void
{ {
Sanctum::usePersonalAccessTokenModel(ApiKey::class); Sanctum::usePersonalAccessTokenModel(ApiKey::class);
$this->registerPolicies();
} }
public function register() public function register(): void
{ {
Sanctum::ignoreMigrations(); Sanctum::ignoreMigrations();
} }

View file

@ -11,7 +11,7 @@ class BackupsServiceProvider extends ServiceProvider implements DeferrableProvid
/** /**
* Register the S3 backup disk. * Register the S3 backup disk.
*/ */
public function register() public function register(): void
{ {
$this->app->singleton(BackupManager::class, function ($app) { $this->app->singleton(BackupManager::class, function ($app) {
return new BackupManager($app); return new BackupManager($app);

View file

@ -9,7 +9,7 @@ class BladeServiceProvider extends ServiceProvider
/** /**
* Perform post-registration booting of services. * Perform post-registration booting of services.
*/ */
public function boot() public function boot(): void
{ {
$this->app->make('blade.compiler') $this->app->make('blade.compiler')
->directive('datetimeHuman', function ($expression) { ->directive('datetimeHuman', function ($expression) {

View file

@ -10,7 +10,7 @@ class BroadcastServiceProvider extends ServiceProvider
/** /**
* Bootstrap any application services. * Bootstrap any application services.
*/ */
public function boot() public function boot(): void
{ {
Broadcast::routes(); Broadcast::routes();

View file

@ -11,7 +11,7 @@ class HashidsServiceProvider extends ServiceProvider
/** /**
* Register the ability to use Hashids. * Register the ability to use Hashids.
*/ */
public function register() public function register(): void
{ {
$this->app->singleton(HashidsInterface::class, function () { $this->app->singleton(HashidsInterface::class, function () {
return new Hashids( return new Hashids(

View file

@ -43,7 +43,7 @@ class RepositoryServiceProvider extends ServiceProvider
/** /**
* Register all the repository bindings. * Register all the repository bindings.
*/ */
public function register() public function register(): void
{ {
// Eloquent Repositories // Eloquent Repositories
$this->app->bind(AllocationRepositoryInterface::class, AllocationRepository::class); $this->app->bind(AllocationRepositoryInterface::class, AllocationRepository::class);

View file

@ -19,7 +19,7 @@ class RouteServiceProvider extends ServiceProvider
/** /**
* Define your route model bindings, pattern filters, etc. * Define your route model bindings, pattern filters, etc.
*/ */
public function boot() public function boot(): void
{ {
$this->configureRateLimiting(); $this->configureRateLimiting();
@ -68,7 +68,7 @@ class RouteServiceProvider extends ServiceProvider
/** /**
* Configure the rate limiters for the application. * Configure the rate limiters for the application.
*/ */
protected function configureRateLimiting() protected function configureRateLimiting(): void
{ {
// Authentication rate limiting. For login and checkpoint endpoints we'll apply // Authentication rate limiting. For login and checkpoint endpoints we'll apply
// a limit of 10 requests per minute, for the forgot password endpoint apply a // a limit of 10 requests per minute, for the forgot password endpoint apply a

View file

@ -57,7 +57,7 @@ class SettingsServiceProvider extends ServiceProvider
/** /**
* Boot the service provider. * Boot the service provider.
*/ */
public function boot(ConfigRepository $config, Encrypter $encrypter, Log $log, SettingsRepositoryInterface $settings) public function boot(ConfigRepository $config, Encrypter $encrypter, Log $log, SettingsRepositoryInterface $settings): void
{ {
// Only set the email driver settings from the database if we // Only set the email driver settings from the database if we
// are configured using SMTP as the driver. // are configured using SMTP as the driver.

View file

@ -10,7 +10,7 @@ class ViewComposerServiceProvider extends ServiceProvider
/** /**
* Register bindings in the container. * Register bindings in the container.
*/ */
public function boot() public function boot(): void
{ {
$this->app->make('view')->composer('*', AssetComposer::class); $this->app->make('view')->composer('*', AssetComposer::class);
} }

View file

@ -38,7 +38,9 @@ class DeployServerDatabaseService
throw new NoSuitableDatabaseHostException(); throw new NoSuitableDatabaseHostException();
} }
$databaseHostId = $hosts->random()->id; /** @var \Pterodactyl\Models\DatabaseHost $databaseHost */
$databaseHost = $hosts->random();
$databaseHostId = $databaseHost->id;
} }
return $this->managementService->create($server, [ return $this->managementService->create($server, [

View file

@ -17,54 +17,54 @@
} }
], ],
"require": { "require": {
"php": "^8.0.2 || ^8.1 || ^8.2", "php": "^8.1 || ^8.2",
"ext-json": "*", "ext-json": "*",
"ext-mbstring": "*", "ext-mbstring": "*",
"ext-pdo": "*", "ext-pdo": "*",
"ext-pdo_mysql": "*", "ext-pdo_mysql": "*",
"ext-posix": "*", "ext-posix": "*",
"ext-zip": "*", "ext-zip": "*",
"aws/aws-sdk-php": "~3.253", "aws/aws-sdk-php": "~3.260.1",
"doctrine/dbal": "~3.5", "doctrine/dbal": "~3.6.0",
"guzzlehttp/guzzle": "~7.5", "guzzlehttp/guzzle": "~7.5.0",
"hashids/hashids": "~4.1", "hashids/hashids": "~5.0.0",
"laracasts/utilities": "~3.2", "laracasts/utilities": "~3.2.2",
"laravel/framework": "~9.43", "laravel/framework": "~10.1.3",
"laravel/helpers": "~1.5", "laravel/helpers": "~1.6.0",
"laravel/sanctum": "~3.0", "laravel/sanctum": "~3.2.1",
"laravel/tinker": "~2.7", "laravel/tinker": "~2.8.1",
"laravel/ui": "~4.1", "laravel/ui": "~4.2.1",
"lcobucci/jwt": "~4.2", "lcobucci/jwt": "~4.3.0",
"league/flysystem-aws-s3-v3": "~3.10", "league/flysystem-aws-s3-v3": "~3.12.2",
"league/flysystem-memory": "~3.10", "league/flysystem-memory": "~3.10.3",
"matriphe/iso-639": "~1.2", "matriphe/iso-639": "~1.2",
"phpseclib/phpseclib": "~3.0", "phpseclib/phpseclib": "~3.0.18",
"pragmarx/google2fa": "~8.0", "pragmarx/google2fa": "~8.0.0",
"predis/predis": "~2.0", "predis/predis": "~2.1.1",
"psr/cache": "~3.0", "prologue/alerts": "~1.1.0",
"s1lentium/iptools": "~1.1", "psr/cache": "~3.0.0",
"spatie/laravel-fractal": "~6.0", "s1lentium/iptools": "~1.2.0",
"spatie/laravel-query-builder": "~5.1", "spatie/laravel-fractal": "~6.0.3",
"staudenmeir/belongs-to-through": "~2.12", "spatie/laravel-query-builder": "~5.1.2",
"symfony/http-client": "~6.0", "staudenmeir/belongs-to-through": "~2.13",
"symfony/mailgun-mailer": "~6.0", "symfony/http-client": "~6.2.6",
"symfony/postmark-mailer": "~6.0", "symfony/mailgun-mailer": "~6.2.5",
"symfony/yaml": "~6.0", "symfony/postmark-mailer": "~6.2.5",
"webmozart/assert": "~1.11" "symfony/yaml": "~6.2.5",
"webmozart/assert": "~1.11.0"
}, },
"require-dev": { "require-dev": {
"barryvdh/laravel-ide-helper": "~2.12.3", "barryvdh/laravel-ide-helper": "~2.13.0",
"fakerphp/faker": "~1.20", "fakerphp/faker": "~1.21.0",
"friendsofphp/php-cs-fixer": "~3.11", "friendsofphp/php-cs-fixer": "~3.14.4",
"itsgoingd/clockwork": "~5.1", "itsgoingd/clockwork": "~5.1.12",
"laravel/sail": "~1.16", "laravel/sail": "~1.21.0",
"mockery/mockery": "~1.5", "mockery/mockery": "~1.5.1",
"nunomaduro/collision": "~6.3", "nunomaduro/collision": "~7.0.5",
"nunomaduro/larastan": "^2.0", "nunomaduro/larastan": "~2.4.1",
"phpstan/phpstan": "~1.9", "phpstan/phpstan": "~1.10.1",
"php-mock/php-mock-phpunit": "~2.6", "phpunit/phpunit": "~10.0.11",
"phpunit/phpunit": "~9.5", "spatie/laravel-ignition": "~2.0.0"
"spatie/laravel-ignition": "~1.5"
}, },
"autoload": { "autoload": {
"files": [ "files": [
@ -84,24 +84,25 @@
"scripts": { "scripts": {
"cs:fix": "php-cs-fixer fix", "cs:fix": "php-cs-fixer fix",
"cs:check": "php-cs-fixer fix --dry-run --diff --verbose", "cs:check": "php-cs-fixer fix --dry-run --diff --verbose",
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi || true"
],
"post-root-package-install": [ "post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
], ],
"post-create-project-cmd": [ "post-create-project-cmd": [
"@php artisan key:generate" "@php artisan key:generate --ansi"
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover || true"
] ]
}, },
"prefer-stable": true,
"config": { "config": {
"optimize-autoloader": true, "optimize-autoloader": true,
"preferred-install": "dist", "preferred-install": "dist",
"sort-packages": true, "sort-packages": true,
"platform": { "platform": {
"php": "8.0.2" "php": "8.1.0"
}
} }
},
"minimum-stability": "stable",
"prefer-stable": true
} }

2734
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -27,7 +27,7 @@ return [
| sending an e-mail. You will specify which one you are using for your | sending an e-mail. You will specify which one you are using for your
| mailers below. You are free to add additional mailers as required. | mailers below. You are free to add additional mailers as required.
| |
| Supported: "smtp", "sendmail", "mailgun", "ses", | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
| "postmark", "log", "array", "failover" | "postmark", "log", "array", "failover"
| |
*/ */

View file

@ -34,7 +34,7 @@ class EggVariableFactory extends Factory
/** /**
* Indicate that the egg variable is viewable. * Indicate that the egg variable is viewable.
*/ */
public function viewable(): Factory public function viewable(): static
{ {
return $this->state(function (array $attributes) { return $this->state(function (array $attributes) {
return [ return [
@ -46,7 +46,7 @@ class EggVariableFactory extends Factory
/** /**
* Indicate that the egg variable is editable. * Indicate that the egg variable is editable.
*/ */
public function editable(): Factory public function editable(): static
{ {
return $this->state(function (array $attributes) { return $this->state(function (array $attributes) {
return [ return [

View file

@ -41,7 +41,7 @@ class UserFactory extends Factory
/** /**
* Indicate that the user is an admin. * Indicate that the user is an admin.
*/ */
public function admin(): Factory public function admin(): static
{ {
return $this->state(['root_admin' => true]); return $this->state(['root_admin' => true]);
} }

View file

@ -1,21 +1,21 @@
{ {
"_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO", "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO",
"meta": { "meta": {
"version": "PTDL_v1", "version": "PTDL_v2",
"update_url": null "update_url": null
}, },
"exported_at": "2022-01-18T11:44:55-05:00", "exported_at": "2023-03-25T13:37:00+00:00",
"name": "Rust", "name": "Rust",
"author": "support@pterodactyl.io", "author": "support@pterodactyl.io",
"description": "The only aim in Rust is to survive. To do this you will need to overcome struggles such as hunger, thirst and cold. Build a fire. Build a shelter. Kill animals for meat. Protect yourself from other players, and kill them for meat. Create alliances with other players and form a town. Do whatever it takes to survive.", "description": "The only aim in Rust is to survive. To do this you will need to overcome struggles such as hunger, thirst and cold. Build a fire. Build a shelter. Kill animals for meat. Protect yourself from other players, and kill them for meat. Create alliances with other players and form a town. Do whatever it takes to survive.",
"features": [ "features": [
"steam_disk_space" "steam_disk_space"
], ],
"images": [ "docker_images": {
"quay.io\/pterodactyl\/core:rust" "quay.io\/pterodactyl\/core:rust": "quay.io\/pterodactyl\/core:rust"
], },
"file_denylist": [], "file_denylist": [],
"startup": ".\/RustDedicated -batchmode +server.port {{SERVER_PORT}} +server.identity \"rust\" +rcon.port {{RCON_PORT}} +rcon.web true +server.hostname \\\"{{HOSTNAME}}\\\" +server.level \\\"{{LEVEL}}\\\" +server.description \\\"{{DESCRIPTION}}\\\" +server.url \\\"{{SERVER_URL}}\\\" +server.headerimage \\\"{{SERVER_IMG}}\\\" +server.logoimage \\\"{{SERVER_LOGO}}\\\" +server.maxplayers {{MAX_PLAYERS}} +rcon.password \\\"{{RCON_PASS}}\\\" +server.saveinterval {{SAVEINTERVAL}} +app.port {{APP_PORT}} $( [ -z ${MAP_URL} ] && printf %s \"+server.worldsize \\\"{{WORLD_SIZE}}\\\" +server.seed \\\"{{WORLD_SEED}}\\\"\" || printf %s \"+server.levelurl {{MAP_URL}}\" ) {{ADDITIONAL_ARGS}}", "startup": ".\/RustDedicated -batchmode +server.port {{SERVER_PORT}} +server.queryport {{QUERY_PORT}} +server.identity \"rust\" +rcon.port {{RCON_PORT}} +rcon.web true +server.hostname \\\"{{HOSTNAME}}\\\" +server.level \\\"{{LEVEL}}\\\" +server.description \\\"{{DESCRIPTION}}\\\" +server.url \\\"{{SERVER_URL}}\\\" +server.headerimage \\\"{{SERVER_IMG}}\\\" +server.logoimage \\\"{{SERVER_LOGO}}\\\" +server.maxplayers {{MAX_PLAYERS}} +rcon.password \\\"{{RCON_PASS}}\\\" +server.saveinterval {{SAVEINTERVAL}} +app.port {{APP_PORT}} $( [ -z ${MAP_URL} ] && printf %s \"+server.worldsize \\\"{{WORLD_SIZE}}\\\" +server.seed \\\"{{WORLD_SEED}}\\\"\" || printf %s \"+server.levelurl {{MAP_URL}}\" ) {{ADDITIONAL_ARGS}}",
"config": { "config": {
"files": "{}", "files": "{}",
"startup": "{\r\n \"done\": \"Server startup complete\"\r\n}", "startup": "{\r\n \"done\": \"Server startup complete\"\r\n}",
@ -37,16 +37,18 @@
"default_value": "A Rust Server", "default_value": "A Rust Server",
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|string|max:60" "rules": "required|string|max:60",
"field_type": "text"
}, },
{ {
"name": "OxideMod", "name": "Modding Framework",
"description": "Set whether you want the server to use and auto update OxideMod or not. Valid options are \"1\" for true and \"0\" for false.", "description": "The modding framework to be used: carbon, oxide, vanilla.\r\nDefaults to \"vanilla\" for a non-modded server installation.",
"env_variable": "OXIDE", "env_variable": "FRAMEWORK",
"default_value": "0", "default_value": "vanilla",
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|boolean" "rules": "required|in:carbon,oxide,vanilla",
"field_type": "text"
}, },
{ {
"name": "Level", "name": "Level",
@ -55,7 +57,8 @@
"default_value": "Procedural Map", "default_value": "Procedural Map",
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|string|max:20" "rules": "required|string|max:20",
"field_type": "text"
}, },
{ {
"name": "Description", "name": "Description",
@ -64,7 +67,8 @@
"default_value": "Powered by Pterodactyl", "default_value": "Powered by Pterodactyl",
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|string" "rules": "required|string",
"field_type": "text"
}, },
{ {
"name": "URL", "name": "URL",
@ -73,7 +77,8 @@
"default_value": "http:\/\/pterodactyl.io", "default_value": "http:\/\/pterodactyl.io",
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "nullable|url" "rules": "nullable|url",
"field_type": "text"
}, },
{ {
"name": "World Size", "name": "World Size",
@ -82,7 +87,8 @@
"default_value": "3000", "default_value": "3000",
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|integer" "rules": "required|integer",
"field_type": "text"
}, },
{ {
"name": "World Seed", "name": "World Seed",
@ -91,7 +97,8 @@
"default_value": "", "default_value": "",
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "nullable|string" "rules": "nullable|string",
"field_type": "text"
}, },
{ {
"name": "Max Players", "name": "Max Players",
@ -100,7 +107,8 @@
"default_value": "40", "default_value": "40",
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|integer" "rules": "required|integer",
"field_type": "text"
}, },
{ {
"name": "Server Image", "name": "Server Image",
@ -109,7 +117,18 @@
"default_value": "", "default_value": "",
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "nullable|url" "rules": "nullable|url",
"field_type": "text"
},
{
"name": "Query Port",
"description": "Server Query Port. Can't be the same as Game's primary port.",
"env_variable": "QUERY_PORT",
"default_value": "27017",
"user_viewable": true,
"user_editable": false,
"rules": "required|integer",
"field_type": "text"
}, },
{ {
"name": "RCON Port", "name": "RCON Port",
@ -118,16 +137,18 @@
"default_value": "28016", "default_value": "28016",
"user_viewable": true, "user_viewable": true,
"user_editable": false, "user_editable": false,
"rules": "required|integer" "rules": "required|integer",
"field_type": "text"
}, },
{ {
"name": "RCON Password", "name": "RCON Password",
"description": "RCON access password.", "description": "RCON access password.",
"env_variable": "RCON_PASS", "env_variable": "RCON_PASS",
"default_value": "CHANGEME", "default_value": "",
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|regex:\/^[\\w.-]*$\/|max:64" "rules": "required|regex:\/^[\\w.-]*$\/|max:64",
"field_type": "text"
}, },
{ {
"name": "Save Interval", "name": "Save Interval",
@ -136,7 +157,8 @@
"default_value": "60", "default_value": "60",
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "required|integer" "rules": "required|integer",
"field_type": "text"
}, },
{ {
"name": "Additional Arguments", "name": "Additional Arguments",
@ -145,7 +167,8 @@
"default_value": "", "default_value": "",
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "nullable|string" "rules": "nullable|string",
"field_type": "text"
}, },
{ {
"name": "App Port", "name": "App Port",
@ -154,7 +177,8 @@
"default_value": "28082", "default_value": "28082",
"user_viewable": true, "user_viewable": true,
"user_editable": false, "user_editable": false,
"rules": "required|integer" "rules": "required|integer",
"field_type": "text"
}, },
{ {
"name": "Server Logo", "name": "Server Logo",
@ -163,7 +187,8 @@
"default_value": "", "default_value": "",
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "nullable|url" "rules": "nullable|url",
"field_type": "text"
}, },
{ {
"name": "Custom Map URL", "name": "Custom Map URL",
@ -172,7 +197,8 @@
"default_value": "", "default_value": "",
"user_viewable": true, "user_viewable": true,
"user_editable": true, "user_editable": true,
"rules": "nullable|url" "rules": "nullable|url",
"field_type": "text"
} }
] ]
} }

View file

@ -0,0 +1,34 @@
<?php
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
return new class () extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('failed_jobs', function (Blueprint $table) {
$table->string('uuid')->after('id')->nullable()->unique();
});
DB::table('failed_jobs')->whereNull('uuid')->cursor()->each(function ($job) {
DB::table('failed_jobs')
->where('id', $job->id)
->update(['uuid' => (string) Illuminate\Support\Str::uuid()]);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('failed_jobs', function (Blueprint $table) {
$table->dropColumn('uuid');
});
}
};

View file

@ -0,0 +1,27 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
return new class () extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('api_keys', function (Blueprint $table) {
$table->timestamp('expires_at')->nullable()->after('last_used_at');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('api_keys', function (Blueprint $table) {
$table->dropColumn('expires_at');
});
}
};

View file

@ -1,61 +1,5 @@
{ {
"nodes": { "nodes": {
"alejandra": {
"inputs": {
"fenix": "fenix",
"flakeCompat": "flakeCompat",
"nixpkgs": [
"dream2nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1658427149,
"narHash": "sha256-ToD/1z/q5VHsLMrS2h96vjJoLho59eNRtknOUd19ey8=",
"owner": "kamadorueda",
"repo": "alejandra",
"rev": "f5a22afd2adfb249b4e68e0b33aa1f0fb73fb1be",
"type": "github"
},
"original": {
"owner": "kamadorueda",
"repo": "alejandra",
"type": "github"
}
},
"all-cabal-json": {
"flake": false,
"locked": {
"lastModified": 1665552503,
"narHash": "sha256-r14RmRSwzv5c+bWKUDaze6pXM7nOsiz1H8nvFHJvufc=",
"owner": "nix-community",
"repo": "all-cabal-json",
"rev": "d7c0434eebffb305071404edcf9d5cd99703878e",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "hackage",
"repo": "all-cabal-json",
"type": "github"
}
},
"crane": {
"flake": false,
"locked": {
"lastModified": 1661875961,
"narHash": "sha256-f1h/2c6Teeu1ofAHWzrS8TwBPcnN+EEu+z1sRVmMQTk=",
"owner": "ipetkov",
"repo": "crane",
"rev": "d9f394e4e20e97c2a60c3ad82c2b6ef99be19e24",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"devshell": { "devshell": {
"flake": false, "flake": false,
"locked": { "locked": {
@ -74,28 +18,21 @@
}, },
"dream2nix": { "dream2nix": {
"inputs": { "inputs": {
"alejandra": "alejandra",
"all-cabal-json": "all-cabal-json",
"crane": "crane",
"devshell": "devshell", "devshell": "devshell",
"flake-compat": "flake-compat",
"flake-parts": "flake-parts", "flake-parts": "flake-parts",
"flake-utils-pre-commit": "flake-utils-pre-commit", "nix-unit": "nix-unit",
"ghc-utils": "ghc-utils",
"gomod2nix": "gomod2nix",
"mach-nix": "mach-nix",
"nix-pypi-fetcher": "nix-pypi-fetcher",
"nixpkgs": [ "nixpkgs": [
"nixpkgs" "nixpkgs"
], ],
"poetry2nix": "poetry2nix",
"pre-commit-hooks": "pre-commit-hooks" "pre-commit-hooks": "pre-commit-hooks"
}, },
"locked": { "locked": {
"lastModified": 1669743839, "lastModified": 1695717405,
"narHash": "sha256-zxnaRaWfCJxy0JlORD4Kmtzd0pfpcGLnyaCIJY8OlIo=", "narHash": "sha256-MvHrU3h0Bw57s2p+wCUnSZliR4wvvPi3xkW+MRWB5HU=",
"owner": "nix-community", "owner": "nix-community",
"repo": "dream2nix", "repo": "dream2nix",
"rev": "b6af93946130748f72671dfd2ab84a5aeaf1f191", "rev": "6dbd59e4a47bd916a655c4425a3e730c6aeae033",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -104,39 +41,35 @@
"type": "github" "type": "github"
} }
}, },
"fenix": { "flake-compat": {
"inputs": { "flake": false,
"nixpkgs": [
"dream2nix",
"alejandra",
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": { "locked": {
"lastModified": 1657607339, "lastModified": 1673956053,
"narHash": "sha256-HaqoAwlbVVZH2n4P3jN2FFPMpVuhxDy1poNOR7kzODc=", "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
"owner": "nix-community", "owner": "edolstra",
"repo": "fenix", "repo": "flake-compat",
"rev": "b814c83d9e6aa5a28d0cf356ecfdafb2505ad37d", "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "nix-community", "owner": "edolstra",
"repo": "fenix", "repo": "flake-compat",
"type": "github" "type": "github"
} }
}, },
"flake-parts": { "flake-parts": {
"inputs": { "inputs": {
"nixpkgs-lib": "nixpkgs-lib" "nixpkgs-lib": [
"dream2nix",
"nixpkgs"
]
}, },
"locked": { "locked": {
"lastModified": 1668450977, "lastModified": 1675933616,
"narHash": "sha256-cfLhMhnvXn6x1vPm+Jow3RiFAUSCw/l1utktCw5rVA4=", "narHash": "sha256-/rczJkJHtx16IFxMmAWu5nNYcSXNg1YYXTHoGjLrLUA=",
"owner": "hercules-ci", "owner": "hercules-ci",
"repo": "flake-parts", "repo": "flake-parts",
"rev": "d591857e9d7dd9ddbfba0ea02b43b927c3c0f1fa", "rev": "47478a4a003e745402acf63be7f9a092d51b83d7",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -146,12 +79,15 @@
} }
}, },
"flake-utils": { "flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": { "locked": {
"lastModified": 1667395993, "lastModified": 1689068808,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", "narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", "rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -160,13 +96,16 @@
"type": "github" "type": "github"
} }
}, },
"flake-utils-pre-commit": { "flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": { "locked": {
"lastModified": 1644229661, "lastModified": 1694529238,
"narHash": "sha256-1YdnJAsNy69bpcjuoKdOYQX0YxZBiCYZo4Twxerqv7k=", "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "3cecb5b042f7f209c56ffd8371b2711a290ec797", "rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -175,69 +114,6 @@
"type": "github" "type": "github"
} }
}, },
"flakeCompat": {
"flake": false,
"locked": {
"lastModified": 1650374568,
"narHash": "sha256-Z+s0J8/r907g149rllvwhb4pKi8Wam5ij0st8PwAh+E=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "b4a34015c698c7793d592d66adbab377907a2be8",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"ghc-utils": {
"flake": false,
"locked": {
"lastModified": 1662774800,
"narHash": "sha256-1Rd2eohGUw/s1tfvkepeYpg8kCEXiIot0RijapUjAkE=",
"ref": "refs/heads/master",
"rev": "bb3a2d3dc52ff0253fb9c2812bd7aa2da03e0fea",
"revCount": 1072,
"type": "git",
"url": "https://gitlab.haskell.org/bgamari/ghc-utils"
},
"original": {
"type": "git",
"url": "https://gitlab.haskell.org/bgamari/ghc-utils"
}
},
"gomod2nix": {
"flake": false,
"locked": {
"lastModified": 1627572165,
"narHash": "sha256-MFpwnkvQpauj799b4QTBJQFEddbD02+Ln5k92QyHOSk=",
"owner": "tweag",
"repo": "gomod2nix",
"rev": "67f22dd738d092c6ba88e420350ada0ed4992ae8",
"type": "github"
},
"original": {
"owner": "tweag",
"repo": "gomod2nix",
"type": "github"
}
},
"mach-nix": {
"flake": false,
"locked": {
"lastModified": 1634711045,
"narHash": "sha256-m5A2Ty88NChLyFhXucECj6+AuiMZPHXNbw+9Kcs7F6Y=",
"owner": "DavHau",
"repo": "mach-nix",
"rev": "4433f74a97b94b596fa6cd9b9c0402104aceef5d",
"type": "github"
},
"original": {
"id": "mach-nix",
"type": "indirect"
}
},
"mk-node-package": { "mk-node-package": {
"inputs": { "inputs": {
"flake-utils": [ "flake-utils": [
@ -263,29 +139,62 @@
"type": "github" "type": "github"
} }
}, },
"nix-pypi-fetcher": { "nix-github-actions": {
"flake": false, "inputs": {
"nixpkgs": [
"dream2nix",
"nix-unit",
"nixpkgs"
]
},
"locked": { "locked": {
"lastModified": 1669065297, "lastModified": 1688870561,
"narHash": "sha256-UStjXjNIuIm7SzMOWvuYWIHBkPUKQ8Id63BMJjnIDoA=", "narHash": "sha256-4UYkifnPEw1nAzqqPOTL2MvWtm3sNGw1UTYTalkTcGY=",
"owner": "DavHau", "owner": "nix-community",
"repo": "nix-pypi-fetcher", "repo": "nix-github-actions",
"rev": "a9885ac6a091576b5195d547ac743d45a2a615ac", "rev": "165b1650b753316aa7f1787f3005a8d2da0f5301",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "DavHau", "owner": "nix-community",
"repo": "nix-pypi-fetcher", "repo": "nix-github-actions",
"type": "github"
}
},
"nix-unit": {
"inputs": {
"flake-parts": [
"dream2nix",
"flake-parts"
],
"nix-github-actions": "nix-github-actions",
"nixpkgs": [
"dream2nix",
"nixpkgs"
],
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1690289081,
"narHash": "sha256-PCXQAQt8+i2pkUym9P1JY4JGoeZJLzzxWBhprHDdItM=",
"owner": "adisbladis",
"repo": "nix-unit",
"rev": "a9d6f33e50d4dcd9cfc0c92253340437bbae282b",
"type": "github"
},
"original": {
"owner": "adisbladis",
"repo": "nix-unit",
"type": "github" "type": "github"
} }
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1669542132, "lastModified": 1695644571,
"narHash": "sha256-DRlg++NJAwPh8io3ExBJdNW7Djs3plVI5jgYQ+iXAZQ=", "narHash": "sha256-asS9dCCdlt1lPq0DLwkVBbVoEKuEuz+Zi3DG7pR/RxA=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "a115bb9bd56831941be3776c8a94005867f316a7", "rev": "6500b4580c2a1f3d0f980d32d285739d8e156d92",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -295,24 +204,6 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs-lib": {
"locked": {
"dir": "lib",
"lastModified": 1665349835,
"narHash": "sha256-UK4urM3iN80UXQ7EaOappDzcisYIuEURFRoGQ/yPkug=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "34c5293a71ffdb2fe054eb5288adc1882c1eb0b1",
"type": "github"
},
"original": {
"dir": "lib",
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"npmlock2nix": { "npmlock2nix": {
"flake": false, "flake": false,
"locked": { "locked": {
@ -347,29 +238,9 @@
"type": "github" "type": "github"
} }
}, },
"poetry2nix": {
"flake": false,
"locked": {
"lastModified": 1666918719,
"narHash": "sha256-BkK42fjAku+2WgCOv2/1NrPa754eQPV7gPBmoKQBWlc=",
"owner": "nix-community",
"repo": "poetry2nix",
"rev": "289efb187123656a116b915206e66852f038720e",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "1.36.0",
"repo": "poetry2nix",
"type": "github"
}
},
"pre-commit-hooks": { "pre-commit-hooks": {
"inputs": { "inputs": {
"flake-utils": [ "flake-utils": "flake-utils",
"dream2nix",
"flake-utils-pre-commit"
],
"nixpkgs": [ "nixpkgs": [
"dream2nix", "dream2nix",
"nixpkgs" "nixpkgs"
@ -392,25 +263,60 @@
"root": { "root": {
"inputs": { "inputs": {
"dream2nix": "dream2nix", "dream2nix": "dream2nix",
"flake-utils": "flake-utils", "flake-utils": "flake-utils_2",
"mk-node-package": "mk-node-package", "mk-node-package": "mk-node-package",
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"
} }
}, },
"rust-analyzer-src": { "systems": {
"flake": false,
"locked": { "locked": {
"lastModified": 1657557289, "lastModified": 1681028828,
"narHash": "sha256-PRW+nUwuqNTRAEa83SfX+7g+g8nQ+2MMbasQ9nt6+UM=", "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "rust-lang", "owner": "nix-systems",
"repo": "rust-analyzer", "repo": "default",
"rev": "caf23f29144b371035b864a1017dbc32573ad56d", "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "rust-lang", "owner": "nix-systems",
"ref": "nightly", "repo": "default",
"repo": "rust-analyzer", "type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"dream2nix",
"nix-unit",
"nixpkgs"
]
},
"locked": {
"lastModified": 1689620039,
"narHash": "sha256-BtNwghr05z7k5YMdq+6nbue+nEalvDepuA7qdQMAKoQ=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "719c2977f958c41fa60a928e2fbc50af14844114",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github" "type": "github"
} }
} }

View file

@ -159,6 +159,7 @@
} }
]; ];
systems = [system]; systems = [system];
autoProjects = true;
}) })
.packages .packages
."${system}" ."${system}"
@ -171,7 +172,7 @@
buildInputs = []; buildInputs = [];
buildPhase = '' buildPhase = ''
yarn run build pnpm run build
''; '';
installPhase = '' installPhase = ''
@ -213,6 +214,7 @@
copyToRoot = pkgs.buildEnv { copyToRoot = pkgs.buildEnv {
name = "image-root"; name = "image-root";
paths = [ paths = [
bash
dockerTools.fakeNss dockerTools.fakeNss
caCertificates caCertificates
caddy caddy
@ -221,11 +223,9 @@
coreutils coreutils
mysql80 mysql80
nodejs-18_x nodejs-18_x
nodePackages.npm
nodePackages.pnpm nodePackages.pnpm
nodePackages.yarn
php81WithExtensions php81WithExtensions
postgresql_14 postgresql_15
]; ];
pathsToLink = ["/bin" "/etc"]; pathsToLink = ["/bin" "/etc"];
}; };

View file

@ -1,9 +1,11 @@
{ {
"name": "@pterodactyl/panel", "name": "@pterodactyl/panel",
"version": "1.0.0",
"license": "MIT", "license": "MIT",
"private": true, "private": true,
"packageManager": "pnpm@8.7.6",
"engines": { "engines": {
"node": ">=16.0" "node": ">=16.13"
}, },
"scripts": { "scripts": {
"build": "vite build", "build": "vite build",
@ -39,15 +41,15 @@
"@codemirror/view": "^6.0.0", "@codemirror/view": "^6.0.0",
"@floating-ui/react-dom-interactions": "0.13.3", "@floating-ui/react-dom-interactions": "0.13.3",
"@flyyer/use-fit-text": "3.0.1", "@flyyer/use-fit-text": "3.0.1",
"@fortawesome/fontawesome-svg-core": "6.2.1", "@fortawesome/fontawesome-svg-core": "6.3.0",
"@fortawesome/free-brands-svg-icons": "6.2.1", "@fortawesome/free-brands-svg-icons": "6.3.0",
"@fortawesome/free-solid-svg-icons": "6.2.1", "@fortawesome/free-solid-svg-icons": "6.3.0",
"@fortawesome/react-fontawesome": "0.2.0", "@fortawesome/react-fontawesome": "0.2.0",
"@headlessui/react": "1.7.5", "@headlessui/react": "1.7.11",
"@heroicons/react": "1.0.6", "@heroicons/react": "1.0.6",
"@lezer/common": "1.0.2", "@lezer/common": "1.0.2",
"@lezer/highlight": "1.1.3", "@lezer/highlight": "1.1.3",
"@preact/signals-react": "1.2.1", "@preact/signals-react": "1.2.2",
"axios": "0.27.2", "axios": "0.27.2",
"boring-avatars": "1.7.0", "boring-avatars": "1.7.0",
"chart.js": "3.9.1", "chart.js": "3.9.1",
@ -55,75 +57,76 @@
"copy-to-clipboard": "3.3.3", "copy-to-clipboard": "3.3.3",
"date-fns": "2.29.3", "date-fns": "2.29.3",
"debounce": "1.2.1", "debounce": "1.2.1",
"deepmerge-ts": "4.2.2", "deepmerge-ts": "4.3.0",
"easy-peasy": "5.2.0", "easy-peasy": "5.2.0",
"events": "3.3.0", "events": "3.3.0",
"formik": "2.2.9", "formik": "2.2.9",
"framer-motion": "7.7.2", "framer-motion": "9.1.6",
"i18next": "22.4.3", "i18next": "22.4.10",
"i18next-http-backend": "2.1.0", "i18next-http-backend": "2.1.1",
"i18next-multiload-backend-adapter": "2.2.0", "i18next-multiload-backend-adapter": "2.2.0",
"nanoid": "4.0.0", "nanoid": "4.0.1",
"qrcode.react": "3.1.0", "qrcode.react": "3.1.0",
"react": "18.2.0", "react": "18.2.0",
"react-chartjs-2": "4.3.1", "react-chartjs-2": "4.3.1",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-fast-compare": "3.2.0", "react-fast-compare": "3.2.0",
"react-i18next": "12.1.1", "react-i18next": "12.2.0",
"react-router-dom": "6.4.5", "react-router-dom": "6.8.1",
"react-select": "5.7.0", "react-select": "5.7.0",
"reaptcha": "1.12.1", "reaptcha": "1.12.1",
"sockette": "2.0.6", "sockette": "2.0.6",
"styled-components": "5.3.6", "styled-components": "5.3.6",
"styled-components-breakpoint": "3.0.0-preview.20", "styled-components-breakpoint": "3.0.0-preview.20",
"swr": "1.3.0", "swr": "2.0.3",
"xterm": "5.0.0", "xterm": "5.1.0",
"xterm-addon-fit": "0.6.0", "xterm-addon-fit": "0.7.0",
"xterm-addon-search": "0.10.0", "xterm-addon-search": "0.11.0",
"xterm-addon-search-bar": "0.2.0", "xterm-addon-search-bar": "0.2.0",
"xterm-addon-web-links": "0.7.0", "xterm-addon-web-links": "0.8.0",
"yup": "0.32.11" "yup": "1.0.0"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/forms": "0.5.3", "@tailwindcss/forms": "0.5.3",
"@tailwindcss/line-clamp": "0.4.2", "@tailwindcss/line-clamp": "0.4.2",
"@testing-library/dom": "8.19.0", "@testing-library/dom": "9.0.0",
"@testing-library/react": "13.4.0", "@testing-library/react": "14.0.0",
"@testing-library/user-event": "14.4.3", "@testing-library/user-event": "14.4.3",
"@types/debounce": "1.2.1", "@types/debounce": "1.2.1",
"@types/events": "3.0.0", "@types/events": "3.0.0",
"@types/node": "18.11.13", "@types/node": "18.14.1",
"@types/react": "18.0.26", "@types/react": "18.0.28",
"@types/react-dom": "18.0.9", "@types/react-dom": "18.0.11",
"@types/styled-components": "5.1.26", "@types/styled-components": "5.1.26",
"@typescript-eslint/eslint-plugin": "5.46.1", "@typescript-eslint/eslint-plugin": "5.53.0",
"@typescript-eslint/parser": "5.46.1", "@typescript-eslint/parser": "5.53.0",
"@vitejs/plugin-react": "3.0.0", "@vitejs/plugin-react": "3.1.0",
"autoprefixer": "10.4.13", "autoprefixer": "10.4.13",
"babel-plugin-styled-components": "2.0.7", "babel-plugin-styled-components": "2.0.7",
"babel-plugin-twin": "1.1.0", "babel-plugin-twin": "1.1.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint": "8.29.0", "eslint": "8.34.0",
"eslint-config-prettier": "8.5.0", "eslint-config-prettier": "8.6.0",
"eslint-plugin-node": "11.1.0", "eslint-plugin-node": "11.1.0",
"eslint-plugin-prettier": "4.2.1", "eslint-plugin-prettier": "4.2.1",
"eslint-plugin-react": "7.31.11", "eslint-plugin-react": "7.32.2",
"eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-react-hooks": "4.6.0",
"happy-dom": "8.1.1", "happy-dom": "8.7.2",
"laravel-vite-plugin": "0.7.3", "laravel-vite-plugin": "0.7.4",
"pathe": "1.0.0", "pathe": "1.1.0",
"postcss": "8.4.20", "postcss": "8.4.21",
"postcss-nesting": "10.2.0", "postcss-import": "15.1.0",
"postcss-preset-env": "7.8.3", "postcss-nesting": "11.2.1",
"prettier": "2.8.1", "postcss-preset-env": "8.0.1",
"prettier-plugin-tailwindcss": "0.2.1", "prettier": "2.8.4",
"prettier-plugin-tailwindcss": "0.2.3",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"tailwindcss": "3.2.4", "tailwindcss": "3.2.7",
"ts-essentials": "9.3.0", "ts-essentials": "9.3.0",
"twin.macro": "2.8.2", "twin.macro": "2.8.2",
"typescript": "4.9.4", "typescript": "4.9.5",
"vite": "4.0.3", "vite": "4.1.4",
"vitest": "0.26.2" "vitest": "0.28.5"
}, },
"browserslist": [ "browserslist": [
"> 0.5%", "> 0.5%",

View file

@ -4,9 +4,8 @@
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd" xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="bootstrap/tests.php" bootstrap="bootstrap/tests.php"
colors="true" colors="true"
printerClass="NunoMaduro\Collision\Adapters\Phpunit\Printer"
> >
<coverage processUncoveredFiles="true"> <coverage>
<include> <include>
<directory suffix=".php">./app</directory> <directory suffix=".php">./app</directory>
</include> </include>

5803
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

View file

@ -56,7 +56,7 @@ export interface EggVariable extends Model {
* A standard API response with the minimum viable details for the frontend * A standard API response with the minimum viable details for the frontend
* to correctly render a egg. * to correctly render a egg.
*/ */
type LoadedEgg = WithRelationships<Egg, 'nest' | 'variables'>; export type LoadedEgg = WithRelationships<Egg, 'nest' | 'variables'>;
/** /**
* Gets a single egg from the database and returns it. * Gets a single egg from the database and returns it.

View file

@ -14,13 +14,13 @@ export default (egg: Partial<Egg2>): Promise<Egg> => {
config_files: egg.configFiles, config_files: egg.configFiles,
config_startup: egg.configStartup, config_startup: egg.configStartup,
config_stop: egg.configStop, config_stop: egg.configStop,
config_from: egg.configFrom,
startup: egg.startup, startup: egg.startup,
script_container: egg.scriptContainer, script_container: egg.scriptContainer,
copy_script_from: egg.copyScriptFrom,
script_entry: egg.scriptEntry, script_entry: egg.scriptEntry,
script_is_privileged: egg.scriptIsPrivileged,
script_install: egg.scriptInstall, script_install: egg.scriptInstall,
// config_from: egg.configFrom,
// copy_script_from: egg.copyScriptFrom,
// script_is_privileged: egg.scriptIsPrivileged,
}) })
.then(({ data }) => resolve(rawDataToEgg(data))) .then(({ data }) => resolve(rawDataToEgg(data)))
.catch(reject); .catch(reject);

View file

@ -43,15 +43,15 @@ export interface Egg {
configFiles: Record<string, any> | null; configFiles: Record<string, any> | null;
configStartup: Record<string, any> | null; configStartup: Record<string, any> | null;
configStop: string | null; configStop: string | null;
configFrom: number | null;
startup: string; startup: string;
scriptContainer: string; scriptContainer: string;
copyScriptFrom: number | null;
scriptEntry: string; scriptEntry: string;
scriptIsPrivileged: boolean;
scriptInstall: string | null; scriptInstall: string | null;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
// configFrom: number | null;
// copyScriptFrom: number | null;
// scriptIsPrivileged: boolean;
relations: { relations: {
nest?: Nest; nest?: Nest;

View file

@ -17,7 +17,7 @@ export interface UpdateUserValues {
} }
const filters = ['id', 'uuid', 'external_id', 'username', 'email'] as const; const filters = ['id', 'uuid', 'external_id', 'username', 'email'] as const;
type Filters = typeof filters[number]; type Filters = (typeof filters)[number];
const useGetUsers = ( const useGetUsers = (
params?: QueryBuilderParams<Filters>, params?: QueryBuilderParams<Filters>,

View file

@ -47,7 +47,7 @@ export const rawDataToFileObject = (data: FractalResponseData): FileObject => ({
isEditable: function () { isEditable: function () {
if (this.isArchiveType() || !this.isFile) return false; if (this.isArchiveType() || !this.isFile) return false;
const matches = ['application/jar', 'application/octet-stream', 'inode/directory', /^image\//]; const matches = ['application/jar', 'application/octet-stream', 'inode/directory', /^image\/(?!svg\+xml)/];
return matches.every(m => !this.mimetype.match(m)); return matches.every(m => !this.mimetype.match(m));
}, },

View file

@ -11,16 +11,16 @@ import Select from '@/components/elements/Select';
interface Props { interface Props {
nestId?: number; nestId?: number;
selectedEggId?: number; selectedEggId?: number;
onEggSelect: (egg: Egg | null) => void; onEggSelect: (egg: WithRelationships<Egg, 'variables'> | undefined) => void;
} }
export default ({ nestId, selectedEggId, onEggSelect }: Props) => { export default ({ nestId, selectedEggId, onEggSelect }: Props) => {
const [, , { setValue, setTouched }] = useField<Record<string, string | undefined>>('environment'); const [, , { setValue, setTouched }] = useField<Record<string, string | undefined>>('environment');
const [eggs, setEggs] = useState<WithRelationships<Egg, 'variables'>[] | null>(null); const [eggs, setEggs] = useState<WithRelationships<Egg, 'variables'>[] | undefined>(undefined);
const selectEgg = (egg: Egg | null) => { const selectEgg = (egg: WithRelationships<Egg, 'variables'> | undefined) => {
if (egg === null) { if (egg === undefined) {
onEggSelect(null); onEggSelect(undefined);
return; return;
} }
@ -40,26 +40,29 @@ export default ({ nestId, selectedEggId, onEggSelect }: Props) => {
useEffect(() => { useEffect(() => {
if (!nestId) { if (!nestId) {
setEggs(null); setEggs(undefined);
return; return;
} }
searchEggs(nestId, {}) searchEggs(nestId, {})
.then(eggs => { .then(_eggs => {
setEggs(eggs); setEggs(_eggs);
selectEgg(eggs[0] || null);
// If the currently selected egg is in the selected nest, use it instead of picking the first egg on the nest.
const egg = _eggs.find(egg => egg.id === selectedEggId) ?? _eggs[0];
selectEgg(egg);
}) })
.catch(error => console.error(error)); .catch(error => console.error(error));
}, [nestId]); }, [nestId]);
const onSelectChange = (e: ChangeEvent<HTMLSelectElement>) => { const onSelectChange = (event: ChangeEvent<HTMLSelectElement>) => {
selectEgg(eggs?.find(egg => egg.id.toString() === e.currentTarget.value) || null); selectEgg(eggs?.find(egg => egg.id.toString() === event.currentTarget.value) ?? undefined);
}; };
return ( return (
<> <>
<Label>Egg</Label> <Label>Egg</Label>
<Select id={'eggId'} name={'eggId'} defaultValue={selectedEggId} onChange={onSelectChange}> <Select id={'eggId'} name={'eggId'} value={selectedEggId} onChange={onSelectChange}>
{!eggs ? ( {!eggs ? (
<option disabled>Loading...</option> <option disabled>Loading...</option>
) : ( ) : (

View file

@ -30,6 +30,7 @@ import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import FlashMessageRender from '@/components/FlashMessageRender'; import FlashMessageRender from '@/components/FlashMessageRender';
import useFlash from '@/plugins/useFlash'; import useFlash from '@/plugins/useFlash';
import AdminContentBlock from '@/components/admin/AdminContentBlock'; import AdminContentBlock from '@/components/admin/AdminContentBlock';
import { WithRelationships } from '@/api/admin';
function InternalForm() { function InternalForm() {
const { const {
@ -39,12 +40,12 @@ function InternalForm() {
values: { environment }, values: { environment },
} = useFormikContext<CreateServerRequest>(); } = useFormikContext<CreateServerRequest>();
const [egg, setEgg] = useState<Egg | null>(null); const [egg, setEgg] = useState<WithRelationships<Egg, 'variables'> | undefined>(undefined);
const [node, setNode] = useState<Node | null>(null); const [node, setNode] = useState<Node | undefined>(undefined);
const [allocations, setAllocations] = useState<Allocation[] | null>(null); const [allocations, setAllocations] = useState<Allocation[] | undefined>(undefined);
useEffect(() => { useEffect(() => {
if (egg === null) { if (egg === undefined) {
return; return;
} }
@ -54,7 +55,7 @@ function InternalForm() {
}, [egg]); }, [egg]);
useEffect(() => { useEffect(() => {
if (node === null) { if (node === undefined) {
return; return;
} }
@ -64,11 +65,11 @@ function InternalForm() {
return ( return (
<Form> <Form>
<div css={tw`grid grid-cols-2 gap-y-6 gap-x-8 mb-16`}> <div className="grid grid-cols-2 gap-y-6 gap-x-8 mb-16">
<div css={tw`grid grid-cols-1 gap-y-6 col-span-2 md:col-span-1`}> <div className="grid grid-cols-1 gap-y-6 col-span-2 md:col-span-1">
<BaseSettingsBox> <BaseSettingsBox>
<NodeSelect node={node} setNode={setNode} /> <NodeSelect node={node} setNode={setNode} />
<div css={tw`xl:col-span-2 bg-neutral-800 border border-neutral-900 shadow-inner p-4 rounded`}> <div className="xl:col-span-2 bg-neutral-800 border border-neutral-900 shadow-inner p-4 rounded">
<FormikSwitch <FormikSwitch
name={'startOnCompletion'} name={'startOnCompletion'}
label={'Start after installation'} label={'Start after installation'}
@ -77,20 +78,20 @@ function InternalForm() {
</div> </div>
</BaseSettingsBox> </BaseSettingsBox>
<FeatureLimitsBox /> <FeatureLimitsBox />
<ServerServiceContainer egg={egg} setEgg={setEgg} nestId={0} /> <ServerServiceContainer selectedEggId={egg?.id} setEgg={setEgg} nestId={0} />
</div> </div>
<div css={tw`grid grid-cols-1 gap-y-6 col-span-2 md:col-span-1`}> <div className="grid grid-cols-1 gap-y-6 col-span-2 md:col-span-1">
<AdminBox icon={faNetworkWired} title={'Networking'} isLoading={isSubmitting}> <AdminBox icon={faNetworkWired} title="Networking" isLoading={isSubmitting}>
<div css={tw`grid grid-cols-1 gap-4 lg:gap-6`}> <div className="grid grid-cols-1 gap-4 lg:gap-6">
<div> <div>
<Label htmlFor={'allocation.default'}>Primary Allocation</Label> <Label htmlFor={'allocation.default'}>Primary Allocation</Label>
<Select <Select
id={'allocation.default'} id={'allocation.default'}
name={'allocation.default'} name={'allocation.default'}
disabled={node === null} disabled={node === undefined}
onChange={e => setFieldValue('allocation.default', Number(e.currentTarget.value))} onChange={e => setFieldValue('allocation.default', Number(e.currentTarget.value))}
> >
{node === null ? ( {node === undefined ? (
<option value="">Select a node...</option> <option value="">Select a node...</option>
) : ( ) : (
<option value="">Select an allocation...</option> <option value="">Select an allocation...</option>
@ -116,7 +117,7 @@ function InternalForm() {
<ServerImageContainer /> <ServerImageContainer />
</div> </div>
<AdminBox title={'Startup Command'} css={tw`relative w-full col-span-2`}> <AdminBox title={'Startup Command'} className="relative w-full col-span-2">
<SpinnerOverlay visible={isSubmitting} /> <SpinnerOverlay visible={isSubmitting} />
<Field <Field
@ -131,7 +132,7 @@ function InternalForm() {
/> />
</AdminBox> </AdminBox>
<div css={tw`col-span-2 grid grid-cols-1 md:grid-cols-2 gap-y-6 gap-x-8`}> <div className="col-span-2 grid grid-cols-1 md:grid-cols-2 gap-y-6 gap-x-8">
{/* This ensures that no variables are rendered unless the environment has a value for the variable. */} {/* This ensures that no variables are rendered unless the environment has a value for the variable. */}
{egg?.relationships.variables {egg?.relationships.variables
?.filter(v => Object.keys(environment).find(e => e === v.environmentVariable) !== undefined) ?.filter(v => Object.keys(environment).find(e => e === v.environmentVariable) !== undefined)
@ -140,9 +141,9 @@ function InternalForm() {
))} ))}
</div> </div>
<div css={tw`bg-neutral-700 rounded shadow-md px-4 py-3 col-span-2`}> <div className="bg-neutral-700 rounded shadow-md px-4 py-3 col-span-2">
<div css={tw`flex flex-row`}> <div className="flex flex-row">
<Button type="submit" size="small" css={tw`ml-auto`} disabled={isSubmitting || !isValid}> <Button type="submit" size="small" className="ml-auto" disabled={isSubmitting || !isValid}>
Create Server Create Server
</Button> </Button>
</div> </div>

View file

@ -3,11 +3,9 @@ import { useStoreActions } from 'easy-peasy';
import type { FormikHelpers } from 'formik'; import type { FormikHelpers } from 'formik';
import { Form, Formik, useField, useFormikContext } from 'formik'; import { Form, Formik, useField, useFormikContext } from 'formik';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import tw from 'twin.macro';
import { object } from 'yup'; import { object } from 'yup';
import type { InferModel } from '@/api/admin'; import type { Egg, EggVariable, LoadedEgg } from '@/api/admin/egg';
import type { Egg, EggVariable } from '@/api/admin/egg';
import { getEgg } from '@/api/admin/egg'; import { getEgg } from '@/api/admin/egg';
import type { Server } from '@/api/admin/server'; import type { Server } from '@/api/admin/server';
import { useServerFromRoute } from '@/api/admin/server'; import { useServerFromRoute } from '@/api/admin/server';
@ -23,12 +21,13 @@ import Field from '@/components/elements/Field';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import Label from '@/components/elements/Label'; import Label from '@/components/elements/Label';
import type { ApplicationStore } from '@/state'; import type { ApplicationStore } from '@/state';
import { WithRelationships } from '@/api/admin';
function ServerStartupLineContainer({ egg, server }: { egg: Egg | null; server: Server }) { function ServerStartupLineContainer({ egg, server }: { egg?: Egg; server: Server }) {
const { isSubmitting, setFieldValue } = useFormikContext(); const { isSubmitting, setFieldValue } = useFormikContext();
useEffect(() => { useEffect(() => {
if (egg === null) { if (egg === undefined) {
return; return;
} }
@ -44,10 +43,10 @@ function ServerStartupLineContainer({ egg, server }: { egg: Egg | null; server:
}, [egg]); }, [egg]);
return ( return (
<AdminBox title={'Startup Command'} css={tw`relative w-full`}> <AdminBox title={'Startup Command'} className="relative w-full">
<SpinnerOverlay visible={isSubmitting} /> <SpinnerOverlay visible={isSubmitting} />
<div css={tw`mb-6`}> <div className="mb-6">
<Field <Field
id={'startup'} id={'startup'}
name={'startup'} name={'startup'}
@ -69,12 +68,12 @@ function ServerStartupLineContainer({ egg, server }: { egg: Egg | null; server:
} }
export function ServerServiceContainer({ export function ServerServiceContainer({
egg, selectedEggId,
setEgg, setEgg,
nestId: _nestId, nestId: _nestId,
}: { }: {
egg: Egg | null; selectedEggId?: number;
setEgg: (value: Egg | null) => void; setEgg: (value: WithRelationships<Egg, 'variables'> | undefined) => void;
nestId: number; nestId: number;
}) { }) {
const { isSubmitting } = useFormikContext(); const { isSubmitting } = useFormikContext();
@ -82,14 +81,14 @@ export function ServerServiceContainer({
const [nestId, setNestId] = useState<number>(_nestId); const [nestId, setNestId] = useState<number>(_nestId);
return ( return (
<AdminBox title={'Service Configuration'} isLoading={isSubmitting} css={tw`w-full`}> <AdminBox title={'Service Configuration'} isLoading={isSubmitting} className="w-full">
<div css={tw`mb-6`}> <div className="mb-6">
<NestSelector selectedNestId={nestId} onNestSelect={setNestId} /> <NestSelector selectedNestId={nestId} onNestSelect={setNestId} />
</div> </div>
<div css={tw`mb-6`}> <div className="mb-6">
<EggSelect nestId={nestId} selectedEggId={egg?.id} onEggSelect={setEgg} /> <EggSelect nestId={nestId} selectedEggId={selectedEggId} onEggSelect={setEgg} />
</div> </div>
<div css={tw`bg-neutral-800 border border-neutral-900 shadow-inner p-4 rounded`}> <div className="bg-neutral-800 border border-neutral-900 shadow-inner p-4 rounded">
<FormikSwitch name={'skipScripts'} label={'Skip Egg Install Script'} description={'Soon™'} /> <FormikSwitch name={'skipScripts'} label={'Skip Egg Install Script'} description={'Soon™'} />
</div> </div>
</AdminBox> </AdminBox>
@ -100,10 +99,10 @@ export function ServerImageContainer() {
const { isSubmitting } = useFormikContext(); const { isSubmitting } = useFormikContext();
return ( return (
<AdminBox title={'Image Configuration'} css={tw`relative w-full`}> <AdminBox title={'Image Configuration'} className="relative w-full">
<SpinnerOverlay visible={isSubmitting} /> <SpinnerOverlay visible={isSubmitting} />
<div css={tw`md:w-full md:flex md:flex-col`}> <div className="md:w-full md:flex md:flex-col">
<div> <div>
{/* TODO: make this a proper select but allow a custom image to be specified if needed. */} {/* TODO: make this a proper select but allow a custom image to be specified if needed. */}
<Field id={'image'} name={'image'} label={'Docker Image'} type={'text'} /> <Field id={'image'} name={'image'} label={'Docker Image'} type={'text'} />
@ -130,7 +129,7 @@ export function ServerVariableContainer({ variable, value }: { variable: EggVari
}, [value]); }, [value]);
return ( return (
<AdminBox css={tw`relative w-full`} title={<p css={tw`text-sm uppercase`}>{variable.name}</p>}> <AdminBox className="relative w-full" title={<p className="text-sm uppercase">{variable.name}</p>}>
<SpinnerOverlay visible={isSubmitting} /> <SpinnerOverlay visible={isSubmitting} />
<Field <Field
@ -145,12 +144,14 @@ export function ServerVariableContainer({ variable, value }: { variable: EggVari
} }
function ServerStartupForm({ function ServerStartupForm({
selectedEggId,
egg, egg,
setEgg, setEgg,
server, server,
}: { }: {
egg: Egg | null; selectedEggId?: number;
setEgg: (value: Egg | null) => void; egg?: LoadedEgg;
setEgg: (value: LoadedEgg | undefined) => void;
server: Server; server: Server;
}) { }) {
const { const {
@ -161,22 +162,22 @@ function ServerStartupForm({
return ( return (
<Form> <Form>
<div css={tw`flex flex-col mb-16`}> <div className="flex flex-col mb-16">
<div css={tw`flex flex-row mb-6`}> <div className="flex flex-row mb-6">
<ServerStartupLineContainer egg={egg} server={server} /> <ServerStartupLineContainer egg={egg} server={server} />
</div> </div>
<div css={tw`grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6 mb-6`}> <div className="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6 mb-6">
<div css={tw`flex`}> <div className="flex">
<ServerServiceContainer egg={egg} setEgg={setEgg} nestId={server.nestId} /> <ServerServiceContainer selectedEggId={selectedEggId} setEgg={setEgg} nestId={server.nestId} />
</div> </div>
<div css={tw`flex`}> <div className="flex">
<ServerImageContainer /> <ServerImageContainer />
</div> </div>
</div> </div>
<div css={tw`grid grid-cols-1 md:grid-cols-2 gap-y-6 gap-x-8`}> <div className="grid grid-cols-1 md:grid-cols-2 gap-y-6 gap-x-8">
{/* This ensures that no variables are rendered unless the environment has a value for the variable. */} {/* This ensures that no variables are rendered unless the environment has a value for the variable. */}
{egg?.relationships.variables {egg?.relationships.variables
?.filter(v => Object.keys(environment).find(e => e === v.environmentVariable) !== undefined) ?.filter(v => Object.keys(environment).find(e => e === v.environmentVariable) !== undefined)
@ -193,9 +194,9 @@ function ServerStartupForm({
))} ))}
</div> </div>
<div css={tw`bg-neutral-700 rounded shadow-md py-2 pr-6 mt-6`}> <div className="bg-neutral-700 rounded shadow-md py-2 pr-6 mt-6">
<div css={tw`flex flex-row`}> <div className="flex flex-row">
<Button type="submit" size="small" css={tw`ml-auto`} disabled={isSubmitting || !isValid}> <Button type="submit" size="small" className="ml-auto" disabled={isSubmitting || !isValid}>
Save Changes Save Changes
</Button> </Button>
</div> </div>
@ -210,10 +211,12 @@ export default () => {
const { clearFlashes, clearAndAddHttpError } = useStoreActions( const { clearFlashes, clearAndAddHttpError } = useStoreActions(
(actions: Actions<ApplicationStore>) => actions.flashes, (actions: Actions<ApplicationStore>) => actions.flashes,
); );
const [egg, setEgg] = useState<InferModel<typeof getEgg> | null>(null); const [egg, setEgg] = useState<LoadedEgg | undefined>(undefined);
useEffect(() => { useEffect(() => {
if (!server) return; if (!server) {
return;
}
getEgg(server.eggId) getEgg(server.eggId)
.then(egg => setEgg(egg)) .then(egg => setEgg(egg))
@ -249,10 +252,10 @@ export default () => {
validationSchema={object().shape({})} validationSchema={object().shape({})}
> >
<ServerStartupForm <ServerStartupForm
selectedEggId={egg?.id ?? server.eggId}
egg={egg} egg={egg}
// @ts-expect-error fix this
setEgg={setEgg} setEgg={setEgg}
server={server} server={server as Server}
/> />
</Formik> </Formik>
); );

View file

@ -4,7 +4,7 @@ import { useState } from 'react';
import Checkbox from '@/components/elements/inputs/Checkbox'; import Checkbox from '@/components/elements/inputs/Checkbox';
import { Dropdown } from '@/components/elements/dropdown'; import { Dropdown } from '@/components/elements/dropdown';
import { Dialog } from '@/components/elements/dialog'; import { Dialog } from '@/components/elements/dialog';
import { User } from '@definitions/admin'; import type { User } from '@definitions/admin';
interface Props { interface Props {
user: User; user: User;
@ -12,7 +12,7 @@ interface Props {
onRowChange: (user: User, selected: boolean) => void; onRowChange: (user: User, selected: boolean) => void;
} }
const UserTableRow = ({ user, selected, onRowChange }: Props) => { function UserTableRow({ user, selected, onRowChange }: Props) {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
return ( return (
@ -56,12 +56,14 @@ const UserTableRow = ({ user, selected, onRowChange }: Props) => {
</span> </span>
)} )}
</td> </td>
<td className={'whitespace-nowrap px-6 py-4'}> <td className="whitespace-nowrap px-6 py-4">
<Dropdown> <Dropdown>
<Dropdown.Button className={'px-2'}> <Dropdown.Button className="px-2">
<DotsVerticalIcon /> <DotsVerticalIcon />
</Dropdown.Button> </Dropdown.Button>
<Dropdown.Item icon={<PencilIcon />}>Edit</Dropdown.Item> <Dropdown.Item to={`/admin/users/${user.id}`} icon={<PencilIcon />}>
Edit
</Dropdown.Item>
<Dropdown.Item icon={<SupportIcon />}>Reset Password</Dropdown.Item> <Dropdown.Item icon={<SupportIcon />}>Reset Password</Dropdown.Item>
<Dropdown.Item icon={<LockOpenIcon />} disabled={!user.isUsingTwoFactor}> <Dropdown.Item icon={<LockOpenIcon />} disabled={!user.isUsingTwoFactor}>
Disable 2-FA Disable 2-FA
@ -76,6 +78,6 @@ const UserTableRow = ({ user, selected, onRowChange }: Props) => {
</tr> </tr>
</> </>
); );
}; }
export default UserTableRow; export default UserTableRow;

View file

@ -1,5 +1,6 @@
import { LockOpenIcon, PlusIcon, SupportIcon, TrashIcon } from '@heroicons/react/solid'; import { LockOpenIcon, PlusIcon, SupportIcon, TrashIcon } from '@heroicons/react/solid';
import { Fragment, useEffect, useState } from 'react'; import { Fragment, useEffect, useState } from 'react';
import { NavLink } from 'react-router-dom';
import { useGetUsers } from '@/api/admin/users'; import { useGetUsers } from '@/api/admin/users';
import type { UUID } from '@/api/definitions'; import type { UUID } from '@/api/definitions';
@ -16,7 +17,7 @@ import { Shape } from '@/components/elements/button/types';
const filters = ['id', 'uuid', 'external_id', 'username', 'email'] as const; const filters = ['id', 'uuid', 'external_id', 'username', 'email'] as const;
const UsersContainer = () => { function UsersContainer() {
const [search, setSearch] = useDebouncedState('', 500); const [search, setSearch] = useDebouncedState('', 500);
const [selected, setSelected] = useState<UUID[]>([]); const [selected, setSelected] = useState<UUID[]>([]);
const { data: users } = useGetUsers( const { data: users } = useGetUsers(
@ -42,13 +43,16 @@ const UsersContainer = () => {
return ( return (
<div> <div>
<div className={'mb-4 flex justify-end'}> <div className="mb-4 flex justify-end">
<Button className={'shadow focus:ring-offset-2 focus:ring-offset-neutral-800'}> <NavLink to="/admin/users/new">
Add User <PlusIcon className={'ml-2 h-5 w-5'} /> <Button className="shadow focus:ring-offset-2 focus:ring-offset-neutral-800">
Add User <PlusIcon className="ml-2 h-5 w-5" />
</Button> </Button>
</NavLink>
</div> </div>
<div className={'relative flex items-center rounded-t bg-neutral-700 px-4 py-2'}>
<div className={'mr-6'}> <div className="relative flex items-center rounded-t bg-neutral-700 px-4 py-2">
<div className="mr-6">
<Checkbox <Checkbox
checked={selectAllChecked} checked={selectAllChecked}
disabled={!users?.items.length} disabled={!users?.items.length}
@ -56,22 +60,18 @@ const UsersContainer = () => {
onChange={onSelectAll} onChange={onSelectAll}
/> />
</div> </div>
<div className={'flex-1'}> <div className="flex-1">
<InputField <InputField
type={'text'} type="text"
name={'filter'} name="filter"
placeholder={'Begin typing to filter...'} placeholder="Begin typing to filter..."
className={'w-56 focus:w-96'} className="w-56 focus:w-96"
onChange={e => setSearch(e.currentTarget.value)} onChange={e => setSearch(e.currentTarget.value)}
/> />
</div> </div>
<Transition.Fade as={Fragment} show={selected.length > 0} duration={'duration-75'}> <Transition.Fade as={Fragment} show={selected.length > 0} duration="duration-75">
<div <div className="absolute top-0 left-0 flex h-full w-full items-center justify-end space-x-4 rounded-t bg-neutral-700 px-4">
className={ <div className="flex-1">
'absolute top-0 left-0 flex h-full w-full items-center justify-end space-x-4 rounded-t bg-neutral-700 px-4'
}
>
<div className={'flex-1'}>
<Checkbox <Checkbox
checked={selectAllChecked} checked={selectAllChecked}
indeterminate={selected.length !== users?.items.length} indeterminate={selected.length !== users?.items.length}
@ -79,26 +79,26 @@ const UsersContainer = () => {
/> />
</div> </div>
<Button.Text shape={Shape.IconSquare}> <Button.Text shape={Shape.IconSquare}>
<SupportIcon className={'h-4 w-4'} /> <SupportIcon className="h-4 w-4" />
</Button.Text> </Button.Text>
<Button.Text shape={Shape.IconSquare}> <Button.Text shape={Shape.IconSquare}>
<LockOpenIcon className={'h-4 w-4'} /> <LockOpenIcon className="h-4 w-4" />
</Button.Text> </Button.Text>
<Button.Text shape={Shape.IconSquare}> <Button.Text shape={Shape.IconSquare}>
<TrashIcon className={'h-4 w-4'} /> <TrashIcon className="h-4 w-4" />
</Button.Text> </Button.Text>
</div> </div>
</Transition.Fade> </Transition.Fade>
</div> </div>
<table className={'min-w-full rounded bg-neutral-700'}> <table className="min-w-full rounded bg-neutral-700">
<thead className={'bg-neutral-900'}> <thead className="bg-neutral-900">
<tr> <tr>
<th scope={'col'} className={'w-8'} /> <th scope="col" className="w-8" />
<th scope={'col'} className={'w-full px-6 py-2 text-left'}> <th scope="col" className="w-full px-6 py-2 text-left">
Email Email
</th> </th>
<th scope={'col'} /> <th scope="col" />
<th scope={'col'} /> <th scope="col" />
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -111,10 +111,10 @@ const UsersContainer = () => {
/> />
))} ))}
</tbody> </tbody>
{users && <TFootPaginated span={4} pagination={users.pagination} />} {users ? <TFootPaginated span={4} pagination={users.pagination} /> : null}
</table> </table>
</div> </div>
); );
}; }
export default UsersContainer; export default UsersContainer;

View file

@ -40,7 +40,7 @@ const inputStyle = css<Props>`
// Reset to normal styling. // Reset to normal styling.
resize: none; resize: none;
${tw`appearance-none outline-none w-full min-w-0`}; ${tw`appearance-none outline-none w-full min-w-0`};
${tw`p-3 border-2 rounded text-sm transition-all duration-150`}; ${tw`py-2.5 px-3 border-2 rounded text-sm transition-all duration-150`};
${tw`bg-neutral-600 border-neutral-500 hover:border-neutral-400 text-neutral-200 shadow-none focus:ring-0`}; ${tw`bg-neutral-600 border-neutral-500 hover:border-neutral-400 text-neutral-200 shadow-none focus:ring-0`};
& + .input-help { & + .input-help {

View file

@ -1,27 +1,29 @@
import { ElementType, forwardRef, useMemo } from 'react';
import * as React from 'react';
import { Menu, Transition } from '@headlessui/react'; import { Menu, Transition } from '@headlessui/react';
import styles from './style.module.css';
import classNames from 'classnames'; import classNames from 'classnames';
import DropdownItem from '@/components/elements/dropdown/DropdownItem'; import type { ElementType, ReactNode } from 'react';
import DropdownButton from '@/components/elements/dropdown/DropdownButton'; import { Children as ReactChildren } from 'react';
import { forwardRef, useMemo } from 'react';
import { DropdownButton } from '@/components/elements/dropdown/DropdownButton';
import { DropdownItem } from '@/components/elements/dropdown/DropdownItem';
import styles from './style.module.css';
interface Props { interface Props {
as?: ElementType; as?: ElementType;
children: React.ReactNode; children: ReactNode;
} }
const DropdownGap = ({ invisible }: { invisible?: boolean }) => ( const DropdownGap = ({ invisible }: { invisible?: boolean }) => (
<div className={classNames('m-2 border', { 'border-neutral-700': !invisible, 'border-transparent': invisible })} /> <div className={classNames('m-2 border', { 'border-neutral-700': !invisible, 'border-transparent': invisible })} />
); );
type TypedChild = (React.ReactChild | React.ReactFragment | React.ReactPortal) & { type TypedChild = ReactNode & {
type?: JSX.Element; type?: JSX.Element;
}; };
const Dropdown = forwardRef<typeof Menu, Props>(({ as, children }, ref) => { const Dropdown = forwardRef<typeof Menu, Props>(({ as, children }, ref) => {
const [Button, items] = useMemo(() => { const [Button, items] = useMemo(() => {
const list = React.Children.toArray(children) as unknown as TypedChild[]; const list = ReactChildren.toArray(children) as unknown as TypedChild[];
return [ return [
list.filter(child => child.type === DropdownButton), list.filter(child => child.type === DropdownButton),
@ -34,18 +36,18 @@ const Dropdown = forwardRef<typeof Menu, Props>(({ as, children }, ref) => {
} }
return ( return (
<Menu as={as || 'div'} className={styles.menu} ref={ref}> <Menu as={as ?? 'div'} className={styles.menu} ref={ref}>
{Button} {Button}
<Transition <Transition
enter={'transition duration-100 ease-out'} enter="transition duration-100 ease-out"
enterFrom={'transition scale-95 opacity-0'} enterFrom="transition scale-95 opacity-0"
enterTo={'transform scale-100 opacity-100'} enterTo="transform scale-100 opacity-100"
leave={'transition duration-75 ease-out'} leave="transition duration-75 ease-out"
leaveFrom={'transform scale-100 opacity-100'} leaveFrom="transform scale-100 opacity-100"
leaveTo={'transform scale-95 opacity-0'} leaveTo="transform scale-95 opacity-0"
> >
<Menu.Items className={classNames(styles.items_container, 'w-56')}> <Menu.Items className={classNames(styles.items_container, 'w-56')}>
<div className={'px-1 py-1'}>{items}</div> <div className="px-1 py-1">{items}</div>
</Menu.Items> </Menu.Items>
</Transition> </Transition>
</Menu> </Menu>

View file

@ -1,24 +1,29 @@
import classNames from 'classnames';
import styles from '@/components/elements/dropdown/style.module.css';
import { ChevronDownIcon } from '@heroicons/react/solid';
import { Menu } from '@headlessui/react'; import { Menu } from '@headlessui/react';
import * as React from 'react'; import { ChevronDownIcon } from '@heroicons/react/solid';
import classNames from 'classnames';
import type { ReactNode } from 'react';
import styles from './style.module.css';
interface Props { interface Props {
className?: string; className?: string;
animate?: boolean; animate?: boolean;
children: React.ReactNode; children: ReactNode;
} }
export default ({ className, animate = true, children }: Props) => ( function DropdownButton({ className, animate = true, children }: Props) {
<Menu.Button className={classNames(styles.button, className || 'px-4')}> return (
<Menu.Button className={classNames(styles.button, className ?? 'px-4')}>
{typeof children === 'string' ? ( {typeof children === 'string' ? (
<> <>
<span className={'mr-2'}>{children}</span> <span className="mr-2">{children}</span>
<ChevronDownIcon aria-hidden={'true'} data-animated={animate.toString()} /> <ChevronDownIcon aria-hidden="true" data-animated={animate.toString()} />
</> </>
) : ( ) : (
children children
)} )}
</Menu.Button> </Menu.Button>
); );
}
export { DropdownButton };

View file

@ -1,26 +1,32 @@
import { forwardRef } from 'react';
import * as React from 'react';
import { Menu } from '@headlessui/react'; import { Menu } from '@headlessui/react';
import styles from './style.module.css';
import classNames from 'classnames'; import classNames from 'classnames';
import type { MouseEvent, ReactNode, Ref } from 'react';
import { forwardRef } from 'react';
import type { NavLinkProps } from 'react-router-dom';
import { NavLink } from 'react-router-dom';
import styles from './style.module.css';
interface Props { interface Props {
children: React.ReactNode | ((opts: { active: boolean; disabled: boolean }) => JSX.Element); children: ReactNode | ((opts: { active: boolean; disabled: boolean }) => JSX.Element);
danger?: boolean; danger?: boolean;
disabled?: boolean; disabled?: boolean;
className?: string; className?: string;
icon?: JSX.Element; icon?: JSX.Element;
onClick?: (e: React.MouseEvent) => void; onClick?: (e: MouseEvent) => void;
} }
const DropdownItem = forwardRef<HTMLAnchorElement, Props>( const DropdownItem = forwardRef<HTMLAnchorElement | HTMLButtonElement, Props & Partial<Omit<NavLinkProps, 'children'>>>(
({ disabled, danger, className, onClick, children, icon: IconComponent }, ref) => { ({ disabled, danger, className, onClick, children, icon: IconComponent, ...props }, ref) => {
return ( return (
<Menu.Item disabled={disabled}> <Menu.Item disabled={disabled}>
{({ disabled, active }) => ( {({ disabled, active }) => (
<a <>
ref={ref} {'to' in props && props.to !== undefined ? (
href={'#'} <NavLink
{...props}
to={props.to}
ref={ref as unknown as Ref<HTMLAnchorElement>}
className={classNames( className={classNames(
styles.menu_item, styles.menu_item,
{ {
@ -33,11 +39,30 @@ const DropdownItem = forwardRef<HTMLAnchorElement, Props>(
> >
{IconComponent} {IconComponent}
{typeof children === 'function' ? children({ disabled, active }) : children} {typeof children === 'function' ? children({ disabled, active }) : children}
</a> </NavLink>
) : (
<button
type="button"
ref={ref as unknown as Ref<HTMLButtonElement>}
className={classNames(
styles.menu_item,
{
[styles.danger]: danger,
[styles.disabled]: disabled,
},
className,
)}
onClick={onClick}
>
{IconComponent}
{typeof children === 'function' ? children({ disabled, active }) : children}
</button>
)}
</>
)} )}
</Menu.Item> </Menu.Item>
); );
}, },
); );
export default DropdownItem; export { DropdownItem };

View file

@ -119,14 +119,14 @@ export default ({ database, className }: Props) => {
<Can action={'database.view_password'}> <Can action={'database.view_password'}>
<div css={tw`mt-6`}> <div css={tw`mt-6`}>
<Label>Password</Label> <Label>Password</Label>
<CopyOnClick text={database.password}> <CopyOnClick text={database.password} showInNotification={false}>
<Input type={'text'} readOnly value={database.password} /> <Input type={'text'} readOnly value={database.password} />
</CopyOnClick> </CopyOnClick>
</div> </div>
</Can> </Can>
<div css={tw`mt-6`}> <div css={tw`mt-6`}>
<Label>JDBC Connection String</Label> <Label>JDBC Connection String</Label>
<CopyOnClick text={jdbcConnectionString}> <CopyOnClick text={jdbcConnectionString} showInNotification={false}>
<Input type={'text'} readOnly value={jdbcConnectionString} /> <Input type={'text'} readOnly value={jdbcConnectionString} />
</CopyOnClick> </CopyOnClick>
</div> </div>

View file

@ -18,11 +18,12 @@ import { ServerContext } from '@/state/server';
import styles from './style.module.css'; import styles from './style.module.css';
function Clickable({ file, children }: { file: FileObject; children: ReactNode }) { function Clickable({ file, children }: { file: FileObject; children: ReactNode }) {
const [canRead] = usePermissions(['file.read']);
const [canReadContents] = usePermissions(['file.read-content']); const [canReadContents] = usePermissions(['file.read-content']);
const id = ServerContext.useStoreState(state => state.server.data!.id); const id = ServerContext.useStoreState(state => state.server.data!.id);
const directory = ServerContext.useStoreState(state => state.files.directory); const directory = ServerContext.useStoreState(state => state.files.directory);
return !canReadContents || (file.isFile && !file.isEditable()) ? ( return (file.isFile && (!file.isEditable() || !canReadContents)) || (!file.isFile && !canRead) ? (
<div className={styles.details}>{children}</div> <div className={styles.details}>{children}</div>
) : ( ) : (
<NavLink <NavLink

View file

@ -93,7 +93,7 @@ const MassActionsBar = () => {
/> />
)} )}
<Portal> <Portal>
<div className="fixed bottom-0 z-50 mb-6 flex w-full justify-center"> <div className="pointer-events-none fixed bottom-0 z-50 mb-6 flex w-full justify-center">
<FadeTransition duration="duration-75" show={selectedFiles.length > 0} appear unmount> <FadeTransition duration="duration-75" show={selectedFiles.length > 0} appear unmount>
<div className="pointer-events-auto flex items-center space-x-4 rounded bg-black/50 p-4"> <div className="pointer-events-auto flex items-center space-x-4 rounded bg-black/50 p-4">
<Button onClick={() => setShowMove(true)}>Move</Button> <Button onClick={() => setShowMove(true)}>Move</Button>

View file

@ -1,7 +1,6 @@
import type { ComponentType } from 'react'; import type { ComponentType } from 'react';
import { lazy } from 'react'; import { lazy } from 'react';
import ServerConsole from '@/components/server/console/ServerConsoleContainer';
import DatabasesContainer from '@/components/server/databases/DatabasesContainer'; import DatabasesContainer from '@/components/server/databases/DatabasesContainer';
import ScheduleContainer from '@/components/server/schedules/ScheduleContainer'; import ScheduleContainer from '@/components/server/schedules/ScheduleContainer';
import UsersContainer from '@/components/server/users/UsersContainer'; import UsersContainer from '@/components/server/users/UsersContainer';
@ -21,6 +20,7 @@ import ServerActivityLogContainer from '@/components/server/ServerActivityLogCon
// //
// These specific lazy loaded routes are to avoid loading in heavy screens // These specific lazy loaded routes are to avoid loading in heavy screens
// for the server dashboard when they're only needed for specific instances. // for the server dashboard when they're only needed for specific instances.
const ServerConsoleContainer = lazy(() => import('@/components/server/console/ServerConsoleContainer'));
const FileEditContainer = lazy(() => import('@/components/server/files/FileEditContainer')); const FileEditContainer = lazy(() => import('@/components/server/files/FileEditContainer'));
const ScheduleEditContainer = lazy(() => import('@/components/server/schedules/ScheduleEditContainer')); const ScheduleEditContainer = lazy(() => import('@/components/server/schedules/ScheduleEditContainer'));
@ -86,7 +86,7 @@ export default {
path: '', path: '',
permission: null, permission: null,
name: 'Console', name: 'Console',
component: ServerConsole, component: ServerConsoleContainer,
end: true, end: true,
}, },
{ {

View file

@ -9,7 +9,13 @@ with pkgs;
alejandra alejandra
composer composer
nodejs-18_x nodejs-18_x
nodePackages.yarn nodePackages.pnpm
php81WithExtensions php81WithExtensions
docker-compose
]; ];
shellHook = ''
PATH="$PATH:${pkgs.docker-compose}/libexec/docker/cli-plugins"
'';
} }

View file

@ -255,7 +255,7 @@ class UserControllerTest extends ApplicationApiIntegrationTestCase
* Endpoints that should return a 403 error when the key does not have write * Endpoints that should return a 403 error when the key does not have write
* permissions for user management. * permissions for user management.
*/ */
public function userWriteEndpointsDataProvider(): array public static function userWriteEndpointsDataProvider(): array
{ {
return [ return [
['postJson', '/api/application/users'], ['postJson', '/api/application/users'],

View file

@ -241,7 +241,7 @@ class ApiKeyControllerTest extends ClientApiIntegrationTestCase
* Provides some different IP address combinations that can be used when * Provides some different IP address combinations that can be used when
* testing that we accept the expected IP values. * testing that we accept the expected IP values.
*/ */
public function validIPAddressDataProvider(): array public static function validIPAddressDataProvider(): array
{ {
return [ return [
[[]], [[]],

View file

@ -331,7 +331,7 @@ class ClientControllerTest extends ClientApiIntegrationTestCase
$response->assertJsonPath('data.0.attributes.relationships.allocations.data.0.attributes.notes', null); $response->assertJsonPath('data.0.attributes.relationships.allocations.data.0.attributes.notes', null);
} }
public function filterTypeDataProvider(): array public static function filterTypeDataProvider(): array
{ {
return [['admin'], ['admin-all']]; return [['admin'], ['admin-all']];
} }

View file

@ -46,7 +46,7 @@ class AllocationAuthorizationTest extends ClientApiIntegrationTestCase
$this->actingAs($user)->json($method, $this->link($server3, '/network/allocations/' . $allocation3->id . $endpoint))->assertNotFound(); $this->actingAs($user)->json($method, $this->link($server3, '/network/allocations/' . $allocation3->id . $endpoint))->assertNotFound();
} }
public function methodDataProvider(): array public static function methodDataProvider(): array
{ {
return [ return [
['POST', ''], ['POST', ''],

View file

@ -86,7 +86,7 @@ class CreateNewAllocationTest extends ClientApiIntegrationTestCase
->assertJsonPath('errors.0.detail', 'Cannot assign additional allocations to this server: limit has been reached.'); ->assertJsonPath('errors.0.detail', 'Cannot assign additional allocations to this server: limit has been reached.');
} }
public function permissionDataProvider(): array public static function permissionDataProvider(): array
{ {
return [[[Permission::ACTION_ALLOCATION_CREATE]], [[]]]; return [[[Permission::ACTION_ALLOCATION_CREATE]], [[]]];
} }

View file

@ -98,10 +98,7 @@ class DeleteAllocationTest extends ClientApiIntegrationTestCase
$this->actingAs($user)->deleteJson($this->link($server, "/network/allocations/{$server2->allocation_id}"))->assertNotFound(); $this->actingAs($user)->deleteJson($this->link($server, "/network/allocations/{$server2->allocation_id}"))->assertNotFound();
} }
/** public static function permissionDataProvider(): array
* @return array
*/
public function permissionDataProvider()
{ {
return [[[Permission::ACTION_ALLOCATION_DELETE]], [[]]]; return [[[Permission::ACTION_ALLOCATION_DELETE]], [[]]];
} }

View file

@ -54,7 +54,7 @@ class BackupAuthorizationTest extends ClientApiIntegrationTestCase
$this->actingAs($user)->json($method, $this->link($server3, '/backups/' . $backup3->uuid . $endpoint))->assertNotFound(); $this->actingAs($user)->json($method, $this->link($server3, '/backups/' . $backup3->uuid . $endpoint))->assertNotFound();
} }
public function methodDataProvider(): array public static function methodDataProvider(): array
{ {
return [ return [
['GET', ''], ['GET', ''],

View file

@ -58,7 +58,7 @@ class DatabaseAuthorizationTest extends ClientApiIntegrationTestCase
$this->actingAs($user)->json($method, $this->link($server3, '/databases/' . $hashids->encode($database3->id) . $endpoint))->assertNotFound(); $this->actingAs($user)->json($method, $this->link($server3, '/databases/' . $hashids->encode($database3->id) . $endpoint))->assertNotFound();
} }
public function methodDataProvider(): array public static function methodDataProvider(): array
{ {
return [ return [
['POST', '/rotate-password'], ['POST', '/rotate-password'],

View file

@ -133,7 +133,7 @@ class NetworkAllocationControllerTest extends ClientApiIntegrationTestCase
->assertForbidden(); ->assertForbidden();
} }
public function updatePermissionsDataProvider(): array public static function updatePermissionsDataProvider(): array
{ {
return [[[]], [[Permission::ACTION_ALLOCATION_UPDATE]]]; return [[[]], [[Permission::ACTION_ALLOCATION_UPDATE]]];
} }

View file

@ -72,7 +72,7 @@ class PowerControllerTest extends ClientApiIntegrationTestCase
/** /**
* Returns invalid permission combinations for a given power action. * Returns invalid permission combinations for a given power action.
*/ */
public function invalidPermissionDataProvider(): array public static function invalidPermissionDataProvider(): array
{ {
return [ return [
['start', [Permission::ACTION_CONTROL_STOP, Permission::ACTION_CONTROL_RESTART]], ['start', [Permission::ACTION_CONTROL_STOP, Permission::ACTION_CONTROL_RESTART]],
@ -83,7 +83,7 @@ class PowerControllerTest extends ClientApiIntegrationTestCase
]; ];
} }
public function validPowerActionDataProvider(): array public static function validPowerActionDataProvider(): array
{ {
return [ return [
['start', Permission::ACTION_CONTROL_START], ['start', Permission::ACTION_CONTROL_START],

View file

@ -89,7 +89,7 @@ class CreateServerScheduleTest extends ClientApiIntegrationTestCase
->assertForbidden(); ->assertForbidden();
} }
public function permissionsDataProvider(): array public static function permissionsDataProvider(): array
{ {
return [[[]], [[Permission::ACTION_SCHEDULE_CREATE]]]; return [[[]], [[Permission::ACTION_SCHEDULE_CREATE]]];
} }

View file

@ -77,7 +77,7 @@ class DeleteServerScheduleTest extends ClientApiIntegrationTestCase
$this->assertDatabaseHas('schedules', ['id' => $schedule->id]); $this->assertDatabaseHas('schedules', ['id' => $schedule->id]);
} }
public function permissionsDataProvider(): array public static function permissionsDataProvider(): array
{ {
return [[[]], [[Permission::ACTION_SCHEDULE_DELETE]]]; return [[[]], [[Permission::ACTION_SCHEDULE_DELETE]]];
} }

View file

@ -64,7 +64,7 @@ class ExecuteScheduleTest extends ClientApiIntegrationTestCase
$this->actingAs($user)->postJson($this->link($schedule, '/execute'))->assertForbidden(); $this->actingAs($user)->postJson($this->link($schedule, '/execute'))->assertForbidden();
} }
public function permissionsDataProvider(): array public static function permissionsDataProvider(): array
{ {
return [[[]], [[Permission::ACTION_SCHEDULE_UPDATE]]]; return [[[]], [[Permission::ACTION_SCHEDULE_UPDATE]]];
} }

View file

@ -89,7 +89,7 @@ class GetServerSchedulesTest extends ClientApiIntegrationTestCase
->assertForbidden(); ->assertForbidden();
} }
public function permissionsDataProvider(): array public static function permissionsDataProvider(): array
{ {
return [ return [
[[], false], [[], false],

View file

@ -54,7 +54,7 @@ class ScheduleAuthorizationTest extends ClientApiIntegrationTestCase
$this->actingAs($user)->json($method, $this->link($server3, '/schedules/' . $schedule3->id . $endpoint))->assertNotFound(); $this->actingAs($user)->json($method, $this->link($server3, '/schedules/' . $schedule3->id . $endpoint))->assertNotFound();
} }
public function methodDataProvider(): array public static function methodDataProvider(): array
{ {
return [ return [
['GET', ''], ['GET', ''],

View file

@ -109,7 +109,7 @@ class UpdateServerScheduleTest extends ClientApiIntegrationTestCase
$this->assertFalse($schedule->is_processing); $this->assertFalse($schedule->is_processing);
} }
public function permissionsDataProvider(): array public static function permissionsDataProvider(): array
{ {
return [[[]], [[Permission::ACTION_SCHEDULE_UPDATE]]]; return [[[]], [[Permission::ACTION_SCHEDULE_UPDATE]]];
} }

View file

@ -170,7 +170,7 @@ class CreateServerScheduleTaskTest extends ClientApiIntegrationTestCase
->assertForbidden(); ->assertForbidden();
} }
public function permissionsDataProvider(): array public static function permissionsDataProvider(): array
{ {
return [[[]], [[Permission::ACTION_SCHEDULE_UPDATE]]]; return [[[]], [[Permission::ACTION_SCHEDULE_UPDATE]]];
} }

View file

@ -112,12 +112,12 @@ class SettingsControllerTest extends ClientApiIntegrationTestCase
$this->assertTrue($server->isInstalled()); $this->assertTrue($server->isInstalled());
} }
public function renamePermissionsDataProvider(): array public static function renamePermissionsDataProvider(): array
{ {
return [[[]], [[Permission::ACTION_SETTINGS_RENAME]]]; return [[[]], [[Permission::ACTION_SETTINGS_RENAME]]];
} }
public function reinstallPermissionsDataProvider(): array public static function reinstallPermissionsDataProvider(): array
{ {
return [[[]], [[Permission::ACTION_SETTINGS_REINSTALL]]]; return [[[]], [[Permission::ACTION_SETTINGS_REINSTALL]]];
} }

View file

@ -58,7 +58,7 @@ class GetStartupAndVariablesTest extends ClientApiIntegrationTestCase
$this->actingAs($user2)->getJson($this->link($server) . '/startup')->assertNotFound(); $this->actingAs($user2)->getJson($this->link($server) . '/startup')->assertNotFound();
} }
public function permissionsDataProvider(): array public static function permissionsDataProvider(): array
{ {
return [[[]], [[Permission::ACTION_STARTUP_READ]]]; return [[[]], [[Permission::ACTION_STARTUP_READ]]];
} }

View file

@ -149,7 +149,7 @@ class UpdateStartupVariableTest extends ClientApiIntegrationTestCase
$this->actingAs($user2)->putJson($this->link($server) . '/startup/variable')->assertNotFound(); $this->actingAs($user2)->putJson($this->link($server) . '/startup/variable')->assertNotFound();
} }
public function permissionsDataProvider(): array public static function permissionsDataProvider(): array
{ {
return [[[]], [[Permission::ACTION_STARTUP_UPDATE]]]; return [[[]], [[Permission::ACTION_STARTUP_UPDATE]]];
} }

View file

@ -155,7 +155,7 @@ class CreateServerSubuserTest extends ClientApiIntegrationTestCase
$response->assertJsonPath('errors.0.detail', 'A user with that email address is already assigned as a subuser for this server.'); $response->assertJsonPath('errors.0.detail', 'A user with that email address is already assigned as a subuser for this server.');
} }
public function permissionsDataProvider(): array public static function permissionsDataProvider(): array
{ {
return [[[]], [[Permission::ACTION_USER_CREATE]]]; return [[[]], [[Permission::ACTION_USER_CREATE]]];
} }

View file

@ -49,7 +49,7 @@ class SubuserAuthorizationTest extends ClientApiIntegrationTestCase
$this->actingAs($user)->json($method, $this->link($server3, '/users/' . $internal->uuid))->assertNotFound(); $this->actingAs($user)->json($method, $this->link($server3, '/users/' . $internal->uuid))->assertNotFound();
} }
public function methodDataProvider(): array public static function methodDataProvider(): array
{ {
return [['GET'], ['POST'], ['DELETE']]; return [['GET'], ['POST'], ['DELETE']];
} }

View file

@ -213,7 +213,7 @@ class SftpAuthenticationControllerTest extends IntegrationTestCase
$this->post('/api/remote/sftp/auth', $data)->assertForbidden(); $this->post('/api/remote/sftp/auth', $data)->assertForbidden();
} }
public function authorizationTypeDataProvider(): array public static function authorizationTypeDataProvider(): array
{ {
return [ return [
'password auth' => ['password'], 'password auth' => ['password'],
@ -221,7 +221,7 @@ class SftpAuthenticationControllerTest extends IntegrationTestCase
]; ];
} }
public function serverStateDataProvider(): array public static function serverStateDataProvider(): array
{ {
return [ return [
'installing' => [Server::STATUS_INSTALLING], 'installing' => [Server::STATUS_INSTALLING],

View file

@ -37,7 +37,7 @@ class RunTaskJobTest extends IntegrationTestCase
$job = new RunTaskJob($task); $job = new RunTaskJob($task);
Bus::dispatchNow($job); Bus::dispatchSync($job);
$task->refresh(); $task->refresh();
$schedule->refresh(); $schedule->refresh();
@ -61,7 +61,7 @@ class RunTaskJobTest extends IntegrationTestCase
$this->expectException(\InvalidArgumentException::class); $this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid task action provided: foobar'); $this->expectExceptionMessage('Invalid task action provided: foobar');
Bus::dispatchNow($job); Bus::dispatchSync($job);
} }
/** /**
@ -95,7 +95,7 @@ class RunTaskJobTest extends IntegrationTestCase
}))->andReturnSelf(); }))->andReturnSelf();
$mock->expects('send')->with('start')->andReturn(new Response()); $mock->expects('send')->with('start')->andReturn(new Response());
Bus::dispatchNow(new RunTaskJob($task, $isManualRun)); Bus::dispatchSync(new RunTaskJob($task, $isManualRun));
$task->refresh(); $task->refresh();
$schedule->refresh(); $schedule->refresh();
@ -133,7 +133,7 @@ class RunTaskJobTest extends IntegrationTestCase
$this->expectException(DaemonConnectionException::class); $this->expectException(DaemonConnectionException::class);
} }
Bus::dispatchNow(new RunTaskJob($task)); Bus::dispatchSync(new RunTaskJob($task));
if ($continueOnFailure) { if ($continueOnFailure) {
$task->refresh(); $task->refresh();
@ -165,7 +165,7 @@ class RunTaskJobTest extends IntegrationTestCase
'payload' => 'start', 'payload' => 'start',
]); ]);
Bus::dispatchNow(new RunTaskJob($task)); Bus::dispatchSync(new RunTaskJob($task));
$task->refresh(); $task->refresh();
$schedule->refresh(); $schedule->refresh();
@ -175,7 +175,7 @@ class RunTaskJobTest extends IntegrationTestCase
$this->assertTrue(Carbon::now()->isSameAs(\DateTimeInterface::ATOM, $schedule->last_run_at)); $this->assertTrue(Carbon::now()->isSameAs(\DateTimeInterface::ATOM, $schedule->last_run_at));
} }
public function isManualRunDataProvider(): array public static function isManualRunDataProvider(): array
{ {
return [[true], [false]]; return [[true], [false]];
} }

View file

@ -194,7 +194,7 @@ class DatabaseManagementServiceTest extends IntegrationTestCase
$this->assertDatabaseMissing('databases', ['server_id' => $server->id]); $this->assertDatabaseMissing('databases', ['server_id' => $server->id]);
} }
public function invalidDataDataProvider(): array public static function invalidDataDataProvider(): array
{ {
return [ return [
[[]], [[]],

View file

@ -141,7 +141,7 @@ class DeployServerDatabaseServiceTest extends IntegrationTestCase
$this->assertInstanceOf(Database::class, $response); $this->assertInstanceOf(Database::class, $response);
} }
public function invalidDataProvider(): array public static function invalidDataProvider(): array
{ {
return [ return [
[['remote' => '%']], [['remote' => '%']],

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