Compare commits
7 commits
develop
...
matthewpi/
Author | SHA1 | Date | |
---|---|---|---|
|
ba7ff571e5 | ||
|
f631ac1946 | ||
|
aa380d4c0d | ||
|
940c899eab | ||
|
ce7c913e18 | ||
|
d7d5da6beb | ||
|
06f692e649 |
131 changed files with 9048 additions and 7679 deletions
|
@ -22,7 +22,7 @@ REDIS_PASSWORD=null
|
|||
REDIS_PORT=6379
|
||||
|
||||
CACHE_DRIVER=file
|
||||
QUEUE_CONNECTION=redis
|
||||
QUEUE_CONNECTION=sync
|
||||
SESSION_DRIVER=file
|
||||
|
||||
HASHIDS_SALT=
|
||||
|
|
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
|
@ -68,8 +68,8 @@ body:
|
|||
Run the following command to collect logs on your system.
|
||||
|
||||
Wings: `sudo wings diagnostics`
|
||||
Panel: `tail -n 150 /var/www/pterodactyl/storage/logs/laravel-$(date +%F).log | nc pteropaste.com 99`
|
||||
placeholder: "https://pteropaste.com/a1h6z"
|
||||
Panel: `tail -n 100 /var/www/pterodactyl/storage/logs/laravel-$(date +%F).log | nc bin.ptdl.co 99`
|
||||
placeholder: "https://bin.ptdl.co/a1h6z"
|
||||
render: bash
|
||||
validations:
|
||||
required: false
|
||||
|
|
16
.github/workflows/docker.yaml
vendored
16
.github/workflows/docker.yaml
vendored
|
@ -16,13 +16,14 @@ on:
|
|||
jobs:
|
||||
push:
|
||||
name: Push
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-20.04
|
||||
if: "!contains(github.ref, 'develop') || (!contains(github.event.head_commit.message, 'skip docker') && !contains(github.event.head_commit.message, 'docker skip'))"
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Fetch metadata
|
||||
id: metadata
|
||||
- name: Docker metadata
|
||||
id: docker_meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ghcr.io/pterodactyl/panel
|
||||
|
@ -41,7 +42,6 @@ jobs:
|
|||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
if: "github.event_name != 'pull_request'"
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
|
@ -55,13 +55,13 @@ jobs:
|
|||
sed -i "s/ 'version' => 'canary',/ 'version' => '${REF:1}',/" config/app.php
|
||||
|
||||
- name: Build and Push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
file: ./Containerfile
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
labels: ${{ steps.metadata.outputs.labels }}
|
||||
tags: ${{ steps.metadata.outputs.tags }}
|
||||
platforms: linux/amd64 #,linux/arm64
|
||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
|
13
.github/workflows/laravel.yaml
vendored
13
.github/workflows/laravel.yaml
vendored
|
@ -13,7 +13,7 @@ on:
|
|||
jobs:
|
||||
analysis:
|
||||
name: Static Analysis
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-20.04
|
||||
env:
|
||||
APP_ENV: testing
|
||||
APP_DEBUG: "true"
|
||||
|
@ -42,7 +42,7 @@ jobs:
|
|||
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@v3
|
||||
|
@ -50,7 +50,6 @@ jobs:
|
|||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
# TODO: Update to 8.2 once php-cs-fixer supports it
|
||||
php-version: 8.1
|
||||
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
|
||||
tools: composer:v2
|
||||
|
@ -64,11 +63,11 @@ jobs:
|
|||
|
||||
mysql:
|
||||
name: Tests
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php: [8.1, 8.2]
|
||||
php: [8.1]
|
||||
database: ["mariadb:10.2", "mariadb:10.9", "mysql:8"]
|
||||
services:
|
||||
database:
|
||||
|
@ -136,12 +135,12 @@ jobs:
|
|||
|
||||
postgres:
|
||||
name: Tests
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-20.04
|
||||
if: "!contains(github.event.head_commit.message, 'skip ci') && !contains(github.event.head_commit.message, 'ci skip')"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php: [8.1, 8.2]
|
||||
php: [8.1]
|
||||
database: ["postgres:13", "postgres:14", "postgres:15"]
|
||||
services:
|
||||
database:
|
||||
|
|
11
.github/workflows/release.yaml
vendored
11
.github/workflows/release.yaml
vendored
|
@ -8,25 +8,22 @@ on:
|
|||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: pnpm
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
run: yarn build
|
||||
|
||||
- name: Create release branch and bump version
|
||||
env:
|
||||
|
|
24
.github/workflows/ui.yaml
vendored
24
.github/workflows/ui.yaml
vendored
|
@ -13,29 +13,26 @@ on:
|
|||
jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: pnpm
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Lint
|
||||
run: pnpm run lint
|
||||
run: yarn run lint
|
||||
|
||||
tests:
|
||||
name: Tests
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
@ -44,20 +41,17 @@ jobs:
|
|||
- name: Code checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: pnpm
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
run: yarn run build
|
||||
|
||||
- name: Tests
|
||||
run: pnpm run test
|
||||
run: yarn run test
|
||||
|
|
1
.npmrc
1
.npmrc
|
@ -1 +0,0 @@
|
|||
shamefully-hoist=true
|
63
BUILDING.md
Normal file
63
BUILDING.md
Normal file
|
@ -0,0 +1,63 @@
|
|||
# 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.
|
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -3,17 +3,6 @@ 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.
|
||||
|
||||
## 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
|
||||
### Changed
|
||||
* Telemetry no longer sends a map of Egg and Nest UUIDs to the number of servers using them.
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
# Stage 1 - Builder
|
||||
FROM --platform=$TARGETOS/$TARGETARCH registry.access.redhat.com/ubi9/nodejs-18-minimal AS builder
|
||||
# Stage 0 - Caddy
|
||||
FROM --platform=$TARGETOS/$TARGETARCH docker.io/library/caddy:latest AS caddy
|
||||
|
||||
USER 0
|
||||
RUN npm install -g pnpm
|
||||
# Stage 1 - Builder
|
||||
FROM --platform=$TARGETOS/$TARGETARCH registry.access.redhat.com/ubi9/nodejs-16-minimal AS builder
|
||||
|
||||
RUN npm install -g yarn
|
||||
|
||||
WORKDIR /var/www/pterodactyl
|
||||
|
||||
COPY --chown=1001:0 public ./public
|
||||
COPY --chown=1001:0 resources/scripts ./resources/scripts
|
||||
COPY --chown=1001:0 .eslintignore .eslintrc.js .npmrc .prettierrc.json package.json pnpm-lock.yaml tailwind.config.js tsconfig.json vite.config.ts .
|
||||
COPY --chown=1001:0 .eslintignore .eslintrc.js .prettierrc.json package.json tailwind.config.js tsconfig.json vite.config.ts yarn.lock .
|
||||
|
||||
RUN /opt/app-root/src/.npm-global/bin/pnpm install \
|
||||
&& /opt/app-root/src/.npm-global/bin/pnpm build \
|
||||
&& 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
|
||||
RUN /opt/app-root/src/.npm-global/bin/yarn install --frozen-lockfile \
|
||||
&& /opt/app-root/src/.npm-global/bin/yarn build \
|
||||
&& rm -rf resources/scripts .eslintignore .eslintrc.yml .yarnrc.yml package.json tailwind.config.js tsconfig.json vite.config.ts yarn.lock node_modules
|
||||
|
||||
COPY --chown=1001:0 app ./app
|
||||
COPY --chown=1001:0 bootstrap ./bootstrap
|
||||
|
@ -34,11 +34,10 @@ RUN microdnf update -y \
|
|||
&& rpm --install https://rpms.remirepo.net/enterprise/remi-release-9.rpm \
|
||||
&& microdnf update -y \
|
||||
&& microdnf install -y ca-certificates shadow-utils tar tzdata unzip wget \
|
||||
# ref; https://bugzilla.redhat.com/show_bug.cgi?id=1870814
|
||||
&& microdnf reinstall -y tzdata \
|
||||
&& microdnf module -y reset php \
|
||||
&& microdnf module -y enable php:remi-8.2 \
|
||||
&& 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 \
|
||||
&& microdnf module -y enable php:remi-8.1 \
|
||||
&& microdnf install -y cronie php-{bcmath,cli,common,fpm,gd,gmp,intl,json,mbstring,mysqlnd,opcache,pdo,pecl-redis5,pecl-zip,phpiredis,pgsql,process,sodium,xml,zstd} supervisor \
|
||||
&& curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \
|
||||
&& rm /etc/php-fpm.d/www.conf \
|
||||
&& useradd --home-dir /var/lib/caddy --create-home caddy \
|
||||
&& mkdir /etc/caddy \
|
||||
|
@ -67,7 +66,7 @@ RUN composer install --no-dev --optimize-autoloader \
|
|||
&& rm -rf bootstrap/cache/*.php \
|
||||
&& rm -rf .env storage/logs/*.log
|
||||
|
||||
COPY --from=docker.io/library/caddy:latest /usr/bin/caddy /usr/local/bin/caddy
|
||||
COPY --from=caddy /usr/bin/caddy /usr/local/bin/caddy
|
||||
COPY .github/docker/Caddyfile /etc/caddy/Caddyfile
|
||||
COPY .github/docker/php-fpm.conf /etc/php-fpm.conf
|
||||
COPY .github/docker/supervisord.conf /etc/supervisord.conf
|
||||
|
|
9
LICENSE
9
LICENSE
|
@ -1,9 +0,0 @@
|
|||
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.
|
13
README.md
13
README.md
|
@ -24,20 +24,21 @@ Stop settling for less. Make game servers a first class citizen on your platform
|
|||
|
||||
## Sponsors
|
||||
|
||||
I would like to extend my sincere thanks to the following sponsors for helping fund Pterodactyl's development.
|
||||
I would like to extend my sincere thanks to the following sponsors for helping fund Pterodactyl's developement.
|
||||
[Interested in becoming a sponsor?](https://github.com/sponsors/matthewpi)
|
||||
|
||||
| Company | About |
|
||||
|-----------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
|-----------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [**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. |
|
||||
| [**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. |
|
||||
| [**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. |
|
||||
| [**DutchIS**](https://dutchis.net?ref=pterodactyl) | DutchIS provides instant infrastructure such as pay per use VPS hosting. Start your game hosting journey on DutchIS. |
|
||||
| [**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. |
|
||||
| [**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! |
|
||||
| [**Pterodactyl Market**](https://pterodactylmarket.com/) | Pterodactyl Market is a one-and-stop shop for Pterodactyl. In our market, you can find Add-ons, Themes, Eggs, and more for Pterodactyl. |
|
||||
| [**UltraServers**](https://ultraservers.com/) | Deploy premium games hosting with the click of a button. Manage and swap games with ease and let us take care of the rest. We currently support Minecraft, Rust, ARK, 7 Days to Die, Garys MOD, CS:GO, Satisfactory and others. |
|
||||
| [**Realms Hosting**](https://realmshosting.com/) | Want to build your Gaming Empire? Use Realms Hosting today to kick start your game server hosting with outstanding DDOS Protection, 24/7 Support, Cheap Prices and a Custom Control Panel. | |
|
||||
|
||||
### Supported Games
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ class EmailSettingsCommand extends Command
|
|||
trans('command/messages.environment.mail.ask_driver'),
|
||||
[
|
||||
'smtp' => 'SMTP Server',
|
||||
'sendmail' => 'sendmail Binary',
|
||||
'mail' => 'PHP\'s Internal Mail Function',
|
||||
'mailgun' => 'Mailgun Transactional Email',
|
||||
'mandrill' => 'Mandrill Transactional Email',
|
||||
'postmark' => 'Postmark Transactional Email',
|
||||
|
|
|
@ -10,7 +10,7 @@ class PruneOrphanedBackupsCommand extends Command
|
|||
{
|
||||
protected $signature = 'p:maintenance:prune-backups {--prune-age=}';
|
||||
|
||||
protected $description = 'Marks all backups older than "n" minutes that have not yet completed as being failed.';
|
||||
protected $description = 'Marks all backups that have not completed in the last "n" minutes as being failed.';
|
||||
|
||||
/**
|
||||
* PruneOrphanedBackupsCommand constructor.
|
||||
|
@ -38,7 +38,7 @@ class PruneOrphanedBackupsCommand extends Command
|
|||
return;
|
||||
}
|
||||
|
||||
$this->warn("Marking $count uncompleted backups that are older than $since minutes as failed.");
|
||||
$this->warn("Marking $count backups that have not been marked as completed in the last $since minutes as failed.");
|
||||
|
||||
$query->update([
|
||||
'is_successful' => false,
|
||||
|
|
|
@ -18,7 +18,7 @@ class Kernel extends ConsoleKernel
|
|||
/**
|
||||
* Register the commands for the application.
|
||||
*/
|
||||
protected function commands(): void
|
||||
protected function commands()
|
||||
{
|
||||
$this->load(__DIR__ . '/Commands');
|
||||
}
|
||||
|
@ -26,11 +26,8 @@ class Kernel extends ConsoleKernel
|
|||
/**
|
||||
* Define the application's command schedule.
|
||||
*/
|
||||
protected function schedule(Schedule $schedule): void
|
||||
protected function schedule(Schedule $schedule)
|
||||
{
|
||||
// https://laravel.com/docs/10.x/upgrade#redis-cache-tags
|
||||
$schedule->command('cache:prune-stale-tags')->hourly();
|
||||
|
||||
// Execute scheduled commands for servers every minute, as if there was a normal cron running.
|
||||
$schedule->command(ProcessRunnableCommand::class)->everyMinute()->withoutOverlapping();
|
||||
$schedule->command(CleanServiceBackupFilesCommand::class)->daily();
|
||||
|
|
|
@ -73,7 +73,7 @@ final class Handler extends ExceptionHandler
|
|||
*
|
||||
* @noinspection PhpUnusedLocalVariableInspection
|
||||
*/
|
||||
public function register(): void
|
||||
public function register()
|
||||
{
|
||||
if (config('app.exceptions.report_all', false)) {
|
||||
$this->dontReport = [];
|
||||
|
|
|
@ -15,6 +15,8 @@ final class Time
|
|||
*/
|
||||
public static function getMySQLTimezoneOffset(string $timezone): string
|
||||
{
|
||||
return CarbonImmutable::now($timezone)->getTimezone()->toOffsetName();
|
||||
$offset = round(CarbonImmutable::now($timezone)->getTimezone()->getOffset(CarbonImmutable::now('UTC')) / 3600);
|
||||
|
||||
return sprintf('%s%s:00', $offset > 0 ? '+' : '-', str_pad((string) abs($offset), 2, '0', STR_PAD_LEFT));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,8 @@ class EggController extends ApplicationApiController
|
|||
public function __construct(private EggExporterService $eggExporterService)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->eggExporterService = $eggExporterService;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
105
app/Http/Controllers/Api/Client/SecurityKeyController.php
Normal file
105
app/Http/Controllers/Api/Client/SecurityKeyController.php
Normal file
|
@ -0,0 +1,105 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Api\Client;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Pterodactyl\Models\SecurityKey;
|
||||
use Pterodactyl\Exceptions\DisplayException;
|
||||
use Webauthn\PublicKeyCredentialCreationOptions;
|
||||
use Illuminate\Contracts\Cache\Repository as CacheRepository;
|
||||
use Pterodactyl\Transformers\Api\Client\SecurityKeyTransformer;
|
||||
use Pterodactyl\Repositories\SecurityKeys\WebauthnServerRepository;
|
||||
use Pterodactyl\Services\Users\SecurityKeys\StoreSecurityKeyService;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Account\RegisterSecurityKeyRequest;
|
||||
use Pterodactyl\Services\Users\SecurityKeys\CreatePublicKeyCredentialService;
|
||||
|
||||
class SecurityKeyController extends ClientApiController
|
||||
{
|
||||
public function __construct(
|
||||
protected CreatePublicKeyCredentialService $createPublicKeyCredentialService,
|
||||
protected CacheRepository $cache,
|
||||
protected WebauthnServerRepository $webauthnServerRepository,
|
||||
protected StoreSecurityKeyService $storeSecurityKeyService
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the hardware security keys (WebAuthn) that exists for a user.
|
||||
*/
|
||||
public function index(Request $request): array
|
||||
{
|
||||
return $this->fractal->collection($request->user()->securityKeys)
|
||||
->transformWith(SecurityKeyTransformer::class)
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data necessary for creating a new hardware security key for the
|
||||
* user.
|
||||
*
|
||||
* @throws \Webauthn\Exception\InvalidDataException
|
||||
*/
|
||||
public function create(Request $request): JsonResponse
|
||||
{
|
||||
$tokenId = Str::random(64);
|
||||
$credentials = $this->createPublicKeyCredentialService->handle($request->user());
|
||||
|
||||
// TODO: session
|
||||
$this->cache->put(
|
||||
"register-security-key:$tokenId",
|
||||
serialize($credentials),
|
||||
CarbonImmutable::now()->addMinutes(10)
|
||||
);
|
||||
|
||||
return new JsonResponse([
|
||||
'data' => [
|
||||
'token_id' => $tokenId,
|
||||
'credentials' => $credentials->jsonSerialize(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a new key for a user account.
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\DisplayException
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function store(RegisterSecurityKeyRequest $request): array
|
||||
{
|
||||
$credentials = unserialize(
|
||||
$this->cache->pull("register-security-key:{$request->input('token_id')}", serialize(null))
|
||||
);
|
||||
|
||||
if (
|
||||
!is_object($credentials) ||
|
||||
!$credentials instanceof PublicKeyCredentialCreationOptions ||
|
||||
$credentials->getUser()->getId() !== $request->user()->uuid
|
||||
) {
|
||||
throw new DisplayException('Could not register security key: invalid data present in session, please try again.');
|
||||
}
|
||||
|
||||
$key = $this->storeSecurityKeyService
|
||||
->setRequest(SecurityKey::getPsrRequestFactory($request))
|
||||
->setKeyName($request->input('name'))
|
||||
->handle($request->user(), $request->input('registration'), $credentials);
|
||||
|
||||
return $this->fractal->item($key)
|
||||
->transformWith(SecurityKeyTransformer::class)
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a WebAuthn key from a user's account.
|
||||
*/
|
||||
public function delete(Request $request, string $securityKey): JsonResponse
|
||||
{
|
||||
$request->user()->securityKeys()->where('uuid', $securityKey)->delete();
|
||||
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
}
|
|
@ -18,7 +18,6 @@ use Pterodactyl\Transformers\Api\Client\BackupTransformer;
|
|||
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Backups\StoreBackupRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Backups\RestoreBackupRequest;
|
||||
|
||||
class BackupController extends ClientApiController
|
||||
{
|
||||
|
@ -189,8 +188,12 @@ class BackupController extends ClientApiController
|
|||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function restore(RestoreBackupRequest $request, Server $server, Backup $backup): JsonResponse
|
||||
public function restore(Request $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
|
||||
// processing a different backup restoration request.
|
||||
if (!is_null($server->status)) {
|
||||
|
|
|
@ -9,7 +9,6 @@ use Pterodactyl\Models\Schedule;
|
|||
use Illuminate\Http\JsonResponse;
|
||||
use Pterodactyl\Facades\Activity;
|
||||
use Pterodactyl\Models\Permission;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Pterodactyl\Repositories\Eloquent\TaskRepository;
|
||||
use Pterodactyl\Exceptions\Http\HttpForbiddenException;
|
||||
use Pterodactyl\Transformers\Api\Client\TaskTransformer;
|
||||
|
@ -24,10 +23,8 @@ class ScheduleTaskController extends ClientApiController
|
|||
/**
|
||||
* ScheduleTaskController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private ConnectionInterface $connection,
|
||||
private TaskRepository $repository
|
||||
) {
|
||||
public function __construct(private TaskRepository $repository)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
|
@ -52,35 +49,14 @@ class ScheduleTaskController extends ClientApiController
|
|||
$lastTask = $schedule->tasks()->orderByDesc('sequence_id')->first();
|
||||
|
||||
/** @var \Pterodactyl\Models\Task $task */
|
||||
$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([
|
||||
$task = $this->repository->create([
|
||||
'schedule_id' => $schedule->id,
|
||||
'sequence_id' => $sequenceId,
|
||||
'sequence_id' => ($lastTask->sequence_id ?? 0) + 1,
|
||||
'action' => $request->input('action'),
|
||||
'payload' => $request->input('payload') ?? '',
|
||||
'time_offset' => $request->input('time_offset'),
|
||||
'continue_on_failure' => $request->boolean('continue_on_failure'),
|
||||
'continue_on_failure' => (bool) $request->input('continue_on_failure'),
|
||||
]);
|
||||
});
|
||||
|
||||
Activity::event('server:task.create')
|
||||
->subject($schedule, $task)
|
||||
|
@ -108,34 +84,12 @@ class ScheduleTaskController extends ClientApiController
|
|||
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, [
|
||||
'sequence_id' => $sequenceId,
|
||||
'action' => $request->input('action'),
|
||||
'payload' => $request->input('payload') ?? '',
|
||||
'time_offset' => $request->input('time_offset'),
|
||||
'continue_on_failure' => $request->boolean('continue_on_failure'),
|
||||
'continue_on_failure' => (bool) $request->input('continue_on_failure'),
|
||||
]);
|
||||
});
|
||||
|
||||
Activity::event('server:task.update')
|
||||
->subject($schedule, $task)
|
||||
|
@ -163,9 +117,10 @@ class ScheduleTaskController extends ClientApiController
|
|||
throw new HttpForbiddenException('You do not have permission to perform this action.');
|
||||
}
|
||||
|
||||
$schedule->tasks()
|
||||
->where('sequence_id', '>', $task->sequence_id)
|
||||
->decrement('sequence_id');
|
||||
$schedule->tasks()->where('sequence_id', '>', $task->sequence_id)->update([
|
||||
'sequence_id' => $schedule->tasks()->getConnection()->raw('(sequence_id - 1)'),
|
||||
]);
|
||||
|
||||
$task->delete();
|
||||
|
||||
Activity::event('server:task.delete')->subject($schedule, $task)->property('name', $schedule->name)->log();
|
||||
|
|
|
@ -35,7 +35,7 @@ class SettingsController extends ClientApiController
|
|||
public function rename(RenameServerRequest $request, Server $server): JsonResponse
|
||||
{
|
||||
$name = $request->input('name');
|
||||
$description = $request->has('description') ? (string) $request->input('description') : $server->description;
|
||||
$description = $request->input('description') ?? $server->description;
|
||||
$this->repository->update($server->id, [
|
||||
'name' => $name,
|
||||
'description' => $description,
|
||||
|
|
|
@ -8,7 +8,6 @@ use Illuminate\Auth\AuthManager;
|
|||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Auth\Events\Failed;
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Pterodactyl\Events\Auth\DirectLogin;
|
||||
use Pterodactyl\Exceptions\DisplayException;
|
||||
use Pterodactyl\Http\Controllers\Controller;
|
||||
|
@ -37,7 +36,7 @@ abstract class AbstractLoginController extends Controller
|
|||
protected string $redirectTo = '/';
|
||||
|
||||
/**
|
||||
* LoginController constructor.
|
||||
* AbstractLoginController constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
|
@ -60,7 +59,7 @@ abstract class AbstractLoginController extends Controller
|
|||
$this->getField($request->input('user')) => $request->input('user'),
|
||||
]);
|
||||
|
||||
if ($request->route()->named('auth.login-checkpoint')) {
|
||||
if ($request->route()->named('auth.checkpoint') || $request->route()->named('auth.checkpoint.key')) {
|
||||
throw new DisplayException($message ?? trans('auth.two_factor.checkpoint_failed'));
|
||||
}
|
||||
|
||||
|
@ -79,14 +78,13 @@ abstract class AbstractLoginController extends Controller
|
|||
|
||||
$this->auth->guard()->login($user, true);
|
||||
|
||||
Event::dispatch(new DirectLogin($user, true));
|
||||
event(new DirectLogin($user, true));
|
||||
|
||||
return new JsonResponse([
|
||||
'data' => [
|
||||
'complete' => true,
|
||||
'methods' => [],
|
||||
'intended' => $this->redirectPath(),
|
||||
'user' => $user->toReactObject(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -103,6 +101,6 @@ abstract class AbstractLoginController extends Controller
|
|||
*/
|
||||
protected function fireFailedLoginEvent(Authenticatable $user = null, array $credentials = [])
|
||||
{
|
||||
Event::dispatch(new Failed('auth', $user, $credentials));
|
||||
event(new Failed('auth', $user, $credentials));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,15 +4,18 @@ namespace Pterodactyl\Http\Controllers\Auth;
|
|||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Carbon\CarbonInterface;
|
||||
use Illuminate\Http\Request;
|
||||
use Pterodactyl\Models\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use PragmaRX\Google2FA\Google2FA;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Pterodactyl\Models\SecurityKey;
|
||||
use Illuminate\Contracts\Encryption\Encrypter;
|
||||
use Webauthn\PublicKeyCredentialRequestOptions;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Pterodactyl\Events\Auth\ProvidedAuthenticationToken;
|
||||
use Pterodactyl\Http\Requests\Auth\LoginCheckpointRequest;
|
||||
use Illuminate\Contracts\Validation\Factory as ValidationFactory;
|
||||
use Pterodactyl\Repositories\SecurityKeys\WebauthnServerRepository;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
|
||||
class LoginCheckpointController extends AbstractLoginController
|
||||
{
|
||||
|
@ -24,6 +27,7 @@ class LoginCheckpointController extends AbstractLoginController
|
|||
public function __construct(
|
||||
private Encrypter $encrypter,
|
||||
private Google2FA $google2FA,
|
||||
private WebauthnServerRepository $webauthnServerRepository,
|
||||
private ValidationFactory $validation
|
||||
) {
|
||||
parent::__construct();
|
||||
|
@ -34,13 +38,80 @@ class LoginCheckpointController extends AbstractLoginController
|
|||
* token. Once a user has reached this stage it is assumed that they have already
|
||||
* provided a valid username and password.
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse|void
|
||||
*
|
||||
* @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException
|
||||
* @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
|
||||
* @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException
|
||||
* @throws \Exception
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
* @throws \Pterodactyl\Exceptions\DisplayException
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __invoke(LoginCheckpointRequest $request): JsonResponse
|
||||
public function token(LoginCheckpointRequest $request)
|
||||
{
|
||||
$user = $this->extractUserFromRequest($request);
|
||||
|
||||
// Recovery tokens go through a slightly different pathway for usage.
|
||||
if (!is_null($recoveryToken = $request->input('recovery_token'))) {
|
||||
if ($this->isValidRecoveryToken($user, $recoveryToken)) {
|
||||
return $this->sendLoginResponse($user, $request);
|
||||
}
|
||||
} else {
|
||||
if (!$user->use_totp) {
|
||||
$this->sendFailedLoginResponse($request, $user);
|
||||
}
|
||||
|
||||
$decrypted = $this->encrypter->decrypt($user->totp_secret);
|
||||
|
||||
if ($this->google2FA->verifyKey($decrypted, $request->input('authentication_code') ?? '', config('pterodactyl.auth.2fa.window'))) {
|
||||
return $this->sendLoginResponse($user, $request);
|
||||
}
|
||||
}
|
||||
|
||||
$this->sendFailedLoginResponse($request, $user, !empty($recoveryToken) ? 'The recovery token provided is not valid.' : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates a login request using a security key for a user.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
* @throws \Pterodactyl\Exceptions\DisplayException
|
||||
*/
|
||||
public function key(Request $request): JsonResponse
|
||||
{
|
||||
$options = $request->session()->get(SecurityKey::PK_SESSION_NAME);
|
||||
if (!$options instanceof PublicKeyCredentialRequestOptions) {
|
||||
throw new BadRequestHttpException('No security keys configured in session.');
|
||||
}
|
||||
|
||||
$user = $this->extractUserFromRequest($request);
|
||||
|
||||
try {
|
||||
$source = $this->webauthnServerRepository->loadAndCheckAssertionResponse(
|
||||
$user,
|
||||
// TODO: we may have to `json_encode` this so it will be decoded properly.
|
||||
$request->input('data'),
|
||||
$options,
|
||||
SecurityKey::getPsrRequestFactory($request)
|
||||
);
|
||||
} catch (\Exception|\Throwable $e) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
if (hash_equals($user->uuid, $source->getUserHandle())) {
|
||||
return $this->sendLoginResponse($user, $request);
|
||||
}
|
||||
|
||||
throw new BadRequestHttpException('An unexpected error was encountered while validating that security key.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the user from the session data using the provided confirmation token.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
* @throws \Pterodactyl\Exceptions\DisplayException
|
||||
*/
|
||||
protected function extractUserFromRequest(Request $request): User
|
||||
{
|
||||
if ($this->hasTooManyLoginAttempts($request)) {
|
||||
$this->sendLockoutResponse($request);
|
||||
|
@ -62,24 +133,7 @@ class LoginCheckpointController extends AbstractLoginController
|
|||
$this->sendFailedLoginResponse($request, null, self::TOKEN_EXPIRED_MESSAGE);
|
||||
}
|
||||
|
||||
// Recovery tokens go through a slightly different pathway for usage.
|
||||
if (!is_null($recoveryToken = $request->input('recovery_token'))) {
|
||||
if ($this->isValidRecoveryToken($user, $recoveryToken)) {
|
||||
Event::dispatch(new ProvidedAuthenticationToken($user, true));
|
||||
|
||||
return $this->sendLoginResponse($user, $request);
|
||||
}
|
||||
} else {
|
||||
$decrypted = $this->encrypter->decrypt($user->totp_secret);
|
||||
|
||||
if ($this->google2FA->verifyKey($decrypted, $request->input('authentication_code') ?? '', config('pterodactyl.auth.2fa.window'))) {
|
||||
Event::dispatch(new ProvidedAuthenticationToken($user));
|
||||
|
||||
return $this->sendLoginResponse($user, $request);
|
||||
}
|
||||
}
|
||||
|
||||
$this->sendFailedLoginResponse($request, $user, !empty($recoveryToken) ? 'The recovery token provided is not valid.' : null);
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -101,14 +155,19 @@ class LoginCheckpointController extends AbstractLoginController
|
|||
return false;
|
||||
}
|
||||
|
||||
protected function hasValidSessionData(array $data): bool
|
||||
{
|
||||
return static::isValidSessionData($this->validation, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the data provided from the session is valid or not. This
|
||||
* will return false if the data is invalid, or if more time has passed than
|
||||
* was configured when the session was written.
|
||||
*/
|
||||
protected function hasValidSessionData(array $data): bool
|
||||
protected static function isValidSessionData(ValidationFactory $validation, array $data): bool
|
||||
{
|
||||
$validator = $this->validation->make($data, [
|
||||
$validator = $validation->make($data, [
|
||||
'user_id' => 'required|integer|min:1',
|
||||
'token_value' => 'required|string',
|
||||
'expires_at' => 'required',
|
||||
|
|
|
@ -9,10 +9,23 @@ use Pterodactyl\Models\User;
|
|||
use Illuminate\Http\JsonResponse;
|
||||
use Pterodactyl\Facades\Activity;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Pterodactyl\Models\SecurityKey;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Pterodactyl\Repositories\SecurityKeys\WebauthnServerRepository;
|
||||
|
||||
class LoginController extends AbstractLoginController
|
||||
{
|
||||
private const METHOD_TOTP = 'totp';
|
||||
private const METHOD_WEBAUTHN = 'webauthn';
|
||||
|
||||
/**
|
||||
* LoginController constructor.
|
||||
*/
|
||||
public function __construct(protected WebauthnServerRepository $webauthnServerRepository)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle all incoming requests for the authentication routes and render the
|
||||
* base authentication view component. React will take over at this point and
|
||||
|
@ -28,6 +41,7 @@ class LoginController extends AbstractLoginController
|
|||
*
|
||||
* @throws \Pterodactyl\Exceptions\DisplayException
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
* @throws \Webauthn\Exception\InvalidDataException
|
||||
*/
|
||||
public function login(Request $request): JsonResponse
|
||||
{
|
||||
|
@ -53,7 +67,9 @@ class LoginController extends AbstractLoginController
|
|||
$this->sendFailedLoginResponse($request, $user);
|
||||
}
|
||||
|
||||
if (!$user->use_totp) {
|
||||
// Return early if the user does not have 2FA enabled, otherwise we will require them
|
||||
// to complete a secondary challenge before they can log in.
|
||||
if (!$user->has2FAEnabled()) {
|
||||
return $this->sendLoginResponse($user, $request);
|
||||
}
|
||||
|
||||
|
@ -65,11 +81,23 @@ class LoginController extends AbstractLoginController
|
|||
'expires_at' => CarbonImmutable::now()->addMinutes(5),
|
||||
]);
|
||||
|
||||
return new JsonResponse([
|
||||
'data' => [
|
||||
$response = [
|
||||
'complete' => false,
|
||||
'confirmation_token' => $token,
|
||||
],
|
||||
]);
|
||||
'methods' => array_values(array_filter([
|
||||
$user->use_totp ? self::METHOD_TOTP : null,
|
||||
$user->securityKeys->isNotEmpty() ? self::METHOD_WEBAUTHN : null,
|
||||
])),
|
||||
'confirm_token' => $token,
|
||||
];
|
||||
|
||||
if ($user->securityKeys->isNotEmpty()) {
|
||||
$key = $this->webauthnServerRepository->generatePublicKeyCredentialRequestOptions($user);
|
||||
|
||||
$request->session()->put(SecurityKey::PK_SESSION_NAME, $key);
|
||||
|
||||
$request['webauthn'] = ['public_key' => $key];
|
||||
}
|
||||
|
||||
return new JsonResponse($response);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -86,7 +86,7 @@ class Kernel extends HttpKernel
|
|||
/**
|
||||
* The application's route middleware.
|
||||
*/
|
||||
protected $middlewareAliases = [
|
||||
protected $routeMiddleware = [
|
||||
'auth' => Authenticate::class,
|
||||
'auth.basic' => AuthenticateWithBasicAuth::class,
|
||||
'auth.session' => AuthenticateSession::class,
|
||||
|
|
|
@ -47,9 +47,7 @@ class RequireTwoFactorAuthentication
|
|||
// send them right through, nothing else needs to be checked.
|
||||
//
|
||||
// If the level is set as admin and the user is not an admin, pass them through as well.
|
||||
if ($level === self::LEVEL_NONE || $user->use_totp) {
|
||||
return $next($request);
|
||||
} elseif ($level === self::LEVEL_ADMIN && !$user->root_admin) {
|
||||
if ($level === self::LEVEL_NONE || $user->has2FAEnabled() || ($level === self::LEVEL_ADMIN && !$user->root_admin)) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Requests\Api\Client\Account;
|
||||
|
||||
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
|
||||
|
||||
class RegisterSecurityKeyRequest extends ClientApiRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['string', 'required'],
|
||||
'token_id' => ['required', 'string'],
|
||||
'registration' => ['required', 'array'],
|
||||
'registration.id' => ['required', 'string'],
|
||||
'registration.type' => ['required', 'in:public-key'],
|
||||
'registration.response.attestationObject' => ['required', 'string'],
|
||||
'registration.response.clientDataJSON' => ['required', 'string'],
|
||||
];
|
||||
}
|
||||
}
|
|
@ -23,7 +23,7 @@ class ClientApiRequest extends ApplicationApiRequest
|
|||
return $this->user()->can($this->permission(), $server);
|
||||
}
|
||||
|
||||
// If there is no server available on the reqest, trigger a failure since
|
||||
// If there is no server available on the request, trigger a failure since
|
||||
// we expect there to be one at this point.
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
<?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'];
|
||||
}
|
||||
}
|
|
@ -23,7 +23,6 @@ class StoreTaskRequest extends ViewScheduleRequest
|
|||
'payload' => 'required_unless:action,backup|string|nullable',
|
||||
'time_offset' => 'required|numeric|min:0|max:900',
|
||||
'sequence_id' => 'sometimes|required|numeric|min:1',
|
||||
'continue_on_failure' => 'sometimes|required|boolean',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||
* @property array|null $allowed_ips
|
||||
* @property string|null $memo
|
||||
* @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 $updated_at
|
||||
* @property int $r_servers
|
||||
|
@ -98,10 +97,6 @@ class ApiKey extends Model
|
|||
protected $casts = [
|
||||
'allowed_ips' => 'array',
|
||||
'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_ALLOCATIONS => 'int',
|
||||
'r_' . AdminAcl::RESOURCE_DATABASE_HOSTS => 'int',
|
||||
|
@ -122,7 +117,6 @@ class ApiKey extends Model
|
|||
'allowed_ips',
|
||||
'memo',
|
||||
'last_used_at',
|
||||
'expires_at',
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -143,7 +137,6 @@ class ApiKey extends Model
|
|||
'allowed_ips' => 'nullable|array',
|
||||
'allowed_ips.*' => 'string',
|
||||
'last_used_at' => 'nullable|date',
|
||||
'expires_at' => 'nullable|date',
|
||||
'r_' . AdminAcl::RESOURCE_USERS => 'integer|min:0|max:3',
|
||||
'r_' . AdminAcl::RESOURCE_ALLOCATIONS => 'integer|min:0|max:3',
|
||||
'r_' . AdminAcl::RESOURCE_DATABASE_HOSTS => 'integer|min:0|max:3',
|
||||
|
@ -155,6 +148,12 @@ class ApiKey extends Model
|
|||
'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.
|
||||
*/
|
||||
|
|
|
@ -42,7 +42,10 @@ class Backup extends Model
|
|||
'is_locked' => 'bool',
|
||||
'ignored_files' => 'array',
|
||||
'bytes' => 'int',
|
||||
'completed_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected $dates = [
|
||||
'completed_at',
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
|
|
|
@ -78,9 +78,6 @@ class Egg extends Model
|
|||
* Fields that are not mass assignable.
|
||||
*/
|
||||
protected $fillable = [
|
||||
'nest_id',
|
||||
'author',
|
||||
'uuid',
|
||||
'name',
|
||||
'description',
|
||||
'features',
|
||||
|
|
|
@ -71,8 +71,14 @@ class Schedule extends Model
|
|||
'is_active' => 'boolean',
|
||||
'is_processing' => '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 = [
|
||||
|
|
125
app/Models/SecurityKey.php
Normal file
125
app/Models/SecurityKey.php
Normal file
|
@ -0,0 +1,125 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Models;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
use Webauthn\TrustPath\TrustPath;
|
||||
use Symfony\Component\Uid\NilUuid;
|
||||
use Nyholm\Psr7\Factory\Psr17Factory;
|
||||
use Symfony\Component\Uid\AbstractUid;
|
||||
use Webauthn\PublicKeyCredentialSource;
|
||||
use Webauthn\TrustPath\TrustPathLoader;
|
||||
use Webauthn\PublicKeyCredentialDescriptor;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $uuid
|
||||
* @property int $user_id
|
||||
* @property string $name
|
||||
* @property string $public_key_id
|
||||
* @property string $public_key
|
||||
* @property AbstractUid $aaguid
|
||||
* @property string $type
|
||||
* @property string[] $transports
|
||||
* @property string $attestation_type
|
||||
* @property \Webauthn\TrustPath\TrustPath $trust_path
|
||||
* @property string $user_handle
|
||||
* @property int $counter
|
||||
* @property array<string, mixed>|null $other_ui
|
||||
*
|
||||
* @property \Carbon\CarbonImmutable $created_at
|
||||
* @property \Carbon\CarbonImmutable $updated_at
|
||||
*/
|
||||
class SecurityKey extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public const RESOURCE_NAME = 'security_key';
|
||||
public const PK_SESSION_NAME = 'security_key_pk_request';
|
||||
|
||||
protected $casts = [
|
||||
'user_id' => 'int',
|
||||
'transports' => 'array',
|
||||
'other_ui' => 'array',
|
||||
];
|
||||
|
||||
protected $guarded = [
|
||||
'uuid',
|
||||
'user_id',
|
||||
];
|
||||
|
||||
public function publicKey(): Attribute
|
||||
{
|
||||
return new Attribute(
|
||||
get: fn (string $value) => base64_decode($value),
|
||||
set: fn (string $value) => base64_encode($value),
|
||||
);
|
||||
}
|
||||
|
||||
public function publicKeyId(): Attribute
|
||||
{
|
||||
return new Attribute(
|
||||
get: fn (string $value) => base64_decode($value),
|
||||
set: fn (string $value) => base64_encode($value),
|
||||
);
|
||||
}
|
||||
|
||||
public function aaguid(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn (string|null $value): AbstractUid => is_null($value) ? new NilUuid() : Uuid::fromString($value),
|
||||
set: fn (AbstractUid|null $value): string|null => (is_null($value) || $value instanceof NilUuid) ? null : $value->__toString(),
|
||||
);
|
||||
}
|
||||
|
||||
public function trustPath(): Attribute
|
||||
{
|
||||
return new Attribute(
|
||||
get: fn (mixed $value) => is_null($value) ? null : TrustPathLoader::loadTrustPath(json_decode($value, true)),
|
||||
set: fn (TrustPath|null $value) => json_encode($value),
|
||||
);
|
||||
}
|
||||
|
||||
public function getPublicKeyCredentialDescriptor(): PublicKeyCredentialDescriptor
|
||||
{
|
||||
return new PublicKeyCredentialDescriptor($this->type, $this->public_key_id, $this->transports);
|
||||
}
|
||||
|
||||
public function getPublicKeyCredentialSource(): PublicKeyCredentialSource
|
||||
{
|
||||
return new PublicKeyCredentialSource(
|
||||
$this->public_key_id,
|
||||
$this->type,
|
||||
$this->transports,
|
||||
$this->attestation_type,
|
||||
$this->trust_path,
|
||||
$this->aaguid,
|
||||
$this->public_key,
|
||||
$this->user_handle,
|
||||
$this->counter
|
||||
);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a PSR17 Request factory to be used by different Webauthn tooling.
|
||||
*/
|
||||
public static function getPsrRequestFactory(Request $request): ServerRequestInterface
|
||||
{
|
||||
$factory = new Psr17Factory();
|
||||
|
||||
$httpFactory = new PsrHttpFactory($factory, $factory, $factory, $factory);
|
||||
|
||||
return $httpFactory->createRequest($request);
|
||||
}
|
||||
}
|
|
@ -140,6 +140,11 @@ class Server extends Model
|
|||
*/
|
||||
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.
|
||||
*/
|
||||
|
@ -162,7 +167,7 @@ class Server extends Model
|
|||
'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',
|
||||
'nest_id' => 'required|exists:nests,id',
|
||||
'egg_id' => 'required|exists:eggs,id',
|
||||
'startup' => 'nullable|string',
|
||||
'startup' => 'required|string',
|
||||
'skip_scripts' => 'sometimes|boolean',
|
||||
'image' => 'required|string|max:191',
|
||||
'database_limit' => 'present|nullable|integer|min:0',
|
||||
|
@ -189,10 +194,6 @@ class Server extends Model
|
|||
'database_limit' => 'integer',
|
||||
'allocation_limit' => 'integer',
|
||||
'backup_limit' => 'integer',
|
||||
self::CREATED_AT => 'datetime',
|
||||
self::UPDATED_AT => 'datetime',
|
||||
'deleted_at' => 'datetime',
|
||||
'installed_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
@ -23,8 +23,10 @@ class TaskLog extends Model
|
|||
'id' => 'integer',
|
||||
'task_id' => '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'];
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ use Illuminate\Validation\Rules\In;
|
|||
use Illuminate\Auth\Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Webauthn\PublicKeyCredentialUserEntity;
|
||||
use Pterodactyl\Models\Traits\HasAccessTokens;
|
||||
use Illuminate\Auth\Passwords\CanResetPassword;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
|
@ -51,6 +52,8 @@ use Pterodactyl\Notifications\SendPasswordReset as ResetPasswordNotification;
|
|||
* @property int|null $notifications_count
|
||||
* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\RecoveryToken[] $recoveryTokens
|
||||
* @property int|null $recovery_tokens_count
|
||||
* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\SecurityKey[] $securityKeys
|
||||
* @property int|null $security_keys_count
|
||||
* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\Server[] $servers
|
||||
* @property int|null $servers_count
|
||||
* @property \Illuminate\Database\Eloquent\Collection|\Pterodactyl\Models\UserSSHKey[] $sshKeys
|
||||
|
@ -138,9 +141,10 @@ class User extends Model implements
|
|||
'root_admin' => 'boolean',
|
||||
'use_totp' => 'boolean',
|
||||
'gravatar' => 'boolean',
|
||||
'totp_authenticated_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected $dates = ['totp_authenticated_at'];
|
||||
|
||||
/**
|
||||
* The attributes excluded from the model's JSON form.
|
||||
*/
|
||||
|
@ -275,6 +279,11 @@ class User extends Model implements
|
|||
return $this->hasMany(UserSSHKey::class);
|
||||
}
|
||||
|
||||
public function securityKeys(): HasMany
|
||||
{
|
||||
return $this->hasMany(SecurityKey::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the servers that a user can access by way of being the owner of the
|
||||
* server, or because they are assigned as a subuser for that server.
|
||||
|
@ -289,4 +298,17 @@ class User extends Model implements
|
|||
})
|
||||
->groupBy('servers.id');
|
||||
}
|
||||
|
||||
public function toPublicKeyCredentialEntity(): PublicKeyCredentialUserEntity
|
||||
{
|
||||
return PublicKeyCredentialUserEntity::create($this->username, $this->uuid, $this->email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the user has two-factor authentication enabled.
|
||||
*/
|
||||
public function has2FAEnabled(): bool
|
||||
{
|
||||
return $this->use_totp || $this->securityKeys->isNotEmpty();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ class AppServiceProvider extends ServiceProvider
|
|||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
public function boot()
|
||||
{
|
||||
Schema::defaultStringLength(191);
|
||||
|
||||
|
@ -48,7 +48,7 @@ class AppServiceProvider extends ServiceProvider
|
|||
/**
|
||||
* Register application service providers.
|
||||
*/
|
||||
public function register(): void
|
||||
public function register()
|
||||
{
|
||||
// Only load the settings service provider if the environment
|
||||
// is configured to allow it.
|
||||
|
|
|
@ -17,12 +17,14 @@ class AuthServiceProvider extends ServiceProvider
|
|||
Server::class => ServerPolicy::class,
|
||||
];
|
||||
|
||||
public function boot(): void
|
||||
public function boot()
|
||||
{
|
||||
Sanctum::usePersonalAccessTokenModel(ApiKey::class);
|
||||
|
||||
$this->registerPolicies();
|
||||
}
|
||||
|
||||
public function register(): void
|
||||
public function register()
|
||||
{
|
||||
Sanctum::ignoreMigrations();
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ class BackupsServiceProvider extends ServiceProvider implements DeferrableProvid
|
|||
/**
|
||||
* Register the S3 backup disk.
|
||||
*/
|
||||
public function register(): void
|
||||
public function register()
|
||||
{
|
||||
$this->app->singleton(BackupManager::class, function ($app) {
|
||||
return new BackupManager($app);
|
||||
|
|
|
@ -9,7 +9,7 @@ class BladeServiceProvider extends ServiceProvider
|
|||
/**
|
||||
* Perform post-registration booting of services.
|
||||
*/
|
||||
public function boot(): void
|
||||
public function boot()
|
||||
{
|
||||
$this->app->make('blade.compiler')
|
||||
->directive('datetimeHuman', function ($expression) {
|
||||
|
|
|
@ -10,7 +10,7 @@ class BroadcastServiceProvider extends ServiceProvider
|
|||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
public function boot()
|
||||
{
|
||||
Broadcast::routes();
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ class HashidsServiceProvider extends ServiceProvider
|
|||
/**
|
||||
* Register the ability to use Hashids.
|
||||
*/
|
||||
public function register(): void
|
||||
public function register()
|
||||
{
|
||||
$this->app->singleton(HashidsInterface::class, function () {
|
||||
return new Hashids(
|
||||
|
|
|
@ -43,7 +43,7 @@ class RepositoryServiceProvider extends ServiceProvider
|
|||
/**
|
||||
* Register all the repository bindings.
|
||||
*/
|
||||
public function register(): void
|
||||
public function register()
|
||||
{
|
||||
// Eloquent Repositories
|
||||
$this->app->bind(AllocationRepositoryInterface::class, AllocationRepository::class);
|
||||
|
|
|
@ -19,7 +19,7 @@ class RouteServiceProvider extends ServiceProvider
|
|||
/**
|
||||
* Define your route model bindings, pattern filters, etc.
|
||||
*/
|
||||
public function boot(): void
|
||||
public function boot()
|
||||
{
|
||||
$this->configureRateLimiting();
|
||||
|
||||
|
@ -68,7 +68,7 @@ class RouteServiceProvider extends ServiceProvider
|
|||
/**
|
||||
* Configure the rate limiters for the application.
|
||||
*/
|
||||
protected function configureRateLimiting(): void
|
||||
protected function configureRateLimiting()
|
||||
{
|
||||
// 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
|
||||
|
|
|
@ -57,7 +57,7 @@ class SettingsServiceProvider extends ServiceProvider
|
|||
/**
|
||||
* Boot the service provider.
|
||||
*/
|
||||
public function boot(ConfigRepository $config, Encrypter $encrypter, Log $log, SettingsRepositoryInterface $settings): void
|
||||
public function boot(ConfigRepository $config, Encrypter $encrypter, Log $log, SettingsRepositoryInterface $settings)
|
||||
{
|
||||
// Only set the email driver settings from the database if we
|
||||
// are configured using SMTP as the driver.
|
||||
|
|
|
@ -10,7 +10,7 @@ class ViewComposerServiceProvider extends ServiceProvider
|
|||
/**
|
||||
* Register bindings in the container.
|
||||
*/
|
||||
public function boot(): void
|
||||
public function boot()
|
||||
{
|
||||
$this->app->make('view')->composer('*', AssetComposer::class);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Repositories\SecurityKeys;
|
||||
|
||||
use Pterodactyl\Models\User;
|
||||
use Illuminate\Container\Container;
|
||||
use Pterodactyl\Models\SecurityKey;
|
||||
use Webauthn\PublicKeyCredentialSource;
|
||||
use Webauthn\PublicKeyCredentialUserEntity;
|
||||
use Webauthn\PublicKeyCredentialSourceRepository as PublicKeyRepositoryInterface;
|
||||
|
||||
class PublicKeyCredentialSourceRepository implements PublicKeyRepositoryInterface
|
||||
{
|
||||
protected User $user;
|
||||
|
||||
public function __construct(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a single hardware security token for a user by using the credential ID.
|
||||
*/
|
||||
public function findOneByCredentialId(string $id): ?PublicKeyCredentialSource
|
||||
{
|
||||
/** @var \Pterodactyl\Models\SecurityKey $key */
|
||||
$key = $this->user->securityKeys()
|
||||
->where('public_key_id', base64_encode($id))
|
||||
->first();
|
||||
|
||||
return optional($key)->getPublicKeyCredentialSource();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all the hardware tokens that exist for the user using the given
|
||||
* entity handle.
|
||||
*/
|
||||
public function findAllForUserEntity(PublicKeyCredentialUserEntity $entity): array
|
||||
{
|
||||
$results = $this->user->securityKeys()
|
||||
->where('user_handle', $entity->getId())
|
||||
->get();
|
||||
|
||||
return $results->map(function (SecurityKey $key) {
|
||||
return $key->getPublicKeyCredentialSource();
|
||||
})->values()->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a credential to the database and link it with the user.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function saveCredentialSource(PublicKeyCredentialSource $source): void
|
||||
{
|
||||
// no-op — we handle creation of the keys in StoreSecurityKeyService
|
||||
//
|
||||
// If you put logic in here it is triggered on each login.
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new instance of the repository with the provided user attached.
|
||||
*/
|
||||
public static function factory(User $user): self
|
||||
{
|
||||
return Container::getInstance()->make(static::class, ['user' => $user]);
|
||||
}
|
||||
}
|
161
app/Repositories/SecurityKeys/WebauthnServerRepository.php
Normal file
161
app/Repositories/SecurityKeys/WebauthnServerRepository.php
Normal file
|
@ -0,0 +1,161 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Repositories\SecurityKeys;
|
||||
|
||||
use Cose\Algorithms;
|
||||
use Illuminate\Support\Str;
|
||||
use Pterodactyl\Models\User;
|
||||
use Pterodactyl\Models\SecurityKey;
|
||||
use Webauthn\PublicKeyCredentialLoader;
|
||||
use Webauthn\PublicKeyCredentialSource;
|
||||
use Webauthn\PublicKeyCredentialRpEntity;
|
||||
use Webauthn\PublicKeyCredentialParameters;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Webauthn\AuthenticatorAssertionResponse;
|
||||
use Webauthn\AuthenticatorSelectionCriteria;
|
||||
use Webauthn\AuthenticatorAttestationResponse;
|
||||
use Cose\Algorithm\Manager as AlgorithmManager;
|
||||
use Webauthn\PublicKeyCredentialRequestOptions;
|
||||
use Webauthn\PublicKeyCredentialCreationOptions;
|
||||
use Webauthn\AuthenticatorAssertionResponseValidator;
|
||||
use Webauthn\AuthenticatorAttestationResponseValidator;
|
||||
use Webauthn\AttestationStatement\AttestationObjectLoader;
|
||||
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
|
||||
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
|
||||
|
||||
final class WebauthnServerRepository
|
||||
{
|
||||
private PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository;
|
||||
|
||||
private PublicKeyCredentialRpEntity $rpEntity;
|
||||
private PublicKeyCredentialLoader $credentialLoader;
|
||||
private AuthenticatorAssertionResponseValidator $assertionValidator;
|
||||
private AuthenticatorAttestationResponseValidator $attestationValidator;
|
||||
|
||||
public function __construct(PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository)
|
||||
{
|
||||
$url = str_replace(['http://', 'https://'], '', config('app.url'));
|
||||
|
||||
$this->publicKeyCredentialSourceRepository = $publicKeyCredentialSourceRepository;
|
||||
|
||||
$this->rpEntity = new PublicKeyCredentialRpEntity(config('app.name'), trim($url, '/'));
|
||||
$this->credentialLoader = new PublicKeyCredentialLoader(new AttestationObjectLoader(new AttestationStatementSupportManager()));
|
||||
$this->assertionValidator = new AuthenticatorAssertionResponseValidator(
|
||||
$this->publicKeyCredentialSourceRepository,
|
||||
null,
|
||||
ExtensionOutputCheckerHandler::create(),
|
||||
AlgorithmManager::create(),
|
||||
);
|
||||
$this->attestationValidator = new AuthenticatorAttestationResponseValidator(
|
||||
new AttestationStatementSupportManager(),
|
||||
$this->publicKeyCredentialSourceRepository,
|
||||
null,
|
||||
new ExtensionOutputCheckerHandler(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Webauthn\Exception\InvalidDataException
|
||||
*/
|
||||
public function getPublicKeyCredentialCreationOptions(User $user): PublicKeyCredentialCreationOptions
|
||||
{
|
||||
$excluded = $user->securityKeys->map(function (SecurityKey $key) {
|
||||
return $key->getPublicKeyCredentialDescriptor();
|
||||
})->values()->toArray();
|
||||
|
||||
$challenge = Str::random(16);
|
||||
|
||||
return (new PublicKeyCredentialCreationOptions(
|
||||
$this->rpEntity,
|
||||
$user->toPublicKeyCredentialEntity(),
|
||||
$challenge,
|
||||
[
|
||||
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_ES256),
|
||||
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_ES256K),
|
||||
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_ES384),
|
||||
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_ES512),
|
||||
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_RS256),
|
||||
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_RS384),
|
||||
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_RS512),
|
||||
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_PS256),
|
||||
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_PS384),
|
||||
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_PS512),
|
||||
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_ED256),
|
||||
PublicKeyCredentialParameters::create('public-key', Algorithms::COSE_ALGORITHM_ED512),
|
||||
],
|
||||
))
|
||||
->setTimeout(30_000)
|
||||
->excludeCredentials(...$excluded)
|
||||
->setAttestation(PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE)
|
||||
->setAuthenticatorSelection(AuthenticatorSelectionCriteria::create());
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Webauthn\Exception\InvalidDataException
|
||||
*/
|
||||
public function generatePublicKeyCredentialRequestOptions(User $user): PublicKeyCredentialRequestOptions
|
||||
{
|
||||
$allowedCredentials = $user->securityKeys->map(function (SecurityKey $key) {
|
||||
return $key->getPublicKeyCredentialDescriptor();
|
||||
})->values()->toArray();
|
||||
|
||||
return (new PublicKeyCredentialRequestOptions(Str::random(32)))
|
||||
->allowCredentials(...$allowedCredentials)
|
||||
->setUserVerification(PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Throwable
|
||||
* @throws \JsonException
|
||||
*/
|
||||
public function loadAndCheckAssertionResponse(
|
||||
User $user,
|
||||
array $data,
|
||||
PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions,
|
||||
ServerRequestInterface $request
|
||||
): PublicKeyCredentialSource {
|
||||
$credential = $this->credentialLoader->loadArray($data);
|
||||
|
||||
$authenticatorAssertionResponse = $credential->getResponse();
|
||||
if (!$authenticatorAssertionResponse instanceof AuthenticatorAssertionResponse) {
|
||||
// TODO
|
||||
throw new \Exception('');
|
||||
}
|
||||
|
||||
return $this->assertionValidator->check(
|
||||
$credential->getRawId(),
|
||||
$authenticatorAssertionResponse,
|
||||
$publicKeyCredentialRequestOptions,
|
||||
$request,
|
||||
null, // TODO: use handle?
|
||||
// $user->toPublicKeyCredentialEntity()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new security key for a user.
|
||||
*
|
||||
* @throws \Throwable
|
||||
* @throws \JsonException
|
||||
*/
|
||||
public function loadAndCheckAttestationResponse(
|
||||
User $user,
|
||||
array $data,
|
||||
PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions,
|
||||
ServerRequestInterface $request
|
||||
): PublicKeyCredentialSource {
|
||||
$credential = $this->credentialLoader->loadArray($data);
|
||||
|
||||
$authenticatorAttestationResponse = $credential->getResponse();
|
||||
if (!$authenticatorAttestationResponse instanceof AuthenticatorAttestationResponse) {
|
||||
// TODO
|
||||
throw new \Exception('');
|
||||
}
|
||||
|
||||
return $this->attestationValidator->check(
|
||||
$authenticatorAttestationResponse,
|
||||
$publicKeyCredentialCreationOptions,
|
||||
$request,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Services\Users\SecurityKeys;
|
||||
|
||||
use Pterodactyl\Models\User;
|
||||
use Webauthn\PublicKeyCredentialCreationOptions;
|
||||
use Pterodactyl\Repositories\SecurityKeys\WebauthnServerRepository;
|
||||
|
||||
class CreatePublicKeyCredentialService
|
||||
{
|
||||
protected WebauthnServerRepository $webauthnServerRepository;
|
||||
|
||||
public function __construct(WebauthnServerRepository $webauthnServerRepository)
|
||||
{
|
||||
$this->webauthnServerRepository = $webauthnServerRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Webauthn\Exception\InvalidDataException
|
||||
*/
|
||||
public function handle(User $user): PublicKeyCredentialCreationOptions
|
||||
{
|
||||
return $this->webauthnServerRepository->getPublicKeyCredentialCreationOptions($user);
|
||||
}
|
||||
}
|
79
app/Services/Users/SecurityKeys/StoreSecurityKeyService.php
Normal file
79
app/Services/Users/SecurityKeys/StoreSecurityKeyService.php
Normal file
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Services\Users\SecurityKeys;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Illuminate\Support\Str;
|
||||
use Pterodactyl\Models\User;
|
||||
use Webmozart\Assert\Assert;
|
||||
use Pterodactyl\Models\SecurityKey;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Webauthn\PublicKeyCredentialCreationOptions;
|
||||
use Pterodactyl\Repositories\SecurityKeys\WebauthnServerRepository;
|
||||
|
||||
class StoreSecurityKeyService
|
||||
{
|
||||
protected ?ServerRequestInterface $request = null;
|
||||
|
||||
protected ?string $keyName = null;
|
||||
|
||||
public function __construct(protected WebauthnServerRepository $webauthnServerRepository)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the server request interface on the service, this is needed by the attestation
|
||||
* checking service on the Webauthn server.
|
||||
*/
|
||||
public function setRequest(ServerRequestInterface $request): self
|
||||
{
|
||||
$this->request = $request;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the security key's name. If not provided a random string will be used.
|
||||
*/
|
||||
public function setKeyName(?string $name): self
|
||||
{
|
||||
$this->keyName = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and stores a new hardware security key on a user's account.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function handle(User $user, array $registration, PublicKeyCredentialCreationOptions $options): SecurityKey
|
||||
{
|
||||
Assert::notNull($this->request, 'A request interface must be set on the service before it can be called.');
|
||||
|
||||
$source = $this->webauthnServerRepository->loadAndCheckAttestationResponse($user, $registration, $options, $this->request);
|
||||
|
||||
// Unfortunately this repository interface doesn't define a response — it is explicitly
|
||||
// void — so we need to just query the database immediately after this to pull the information
|
||||
// we just stored to return to the caller.
|
||||
/** @var \Pterodactyl\Models\SecurityKey $key */
|
||||
$key = $user->securityKeys()->make()->forceFill([
|
||||
'uuid' => Uuid::uuid4(),
|
||||
'name' => $this->keyName ?? 'Security Key (' . Str::random() . ')',
|
||||
'public_key_id' => $source->getPublicKeyCredentialId(),
|
||||
'public_key' => $source->getCredentialPublicKey(),
|
||||
'aaguid' => $source->getAaguid(),
|
||||
'type' => $source->getType(),
|
||||
'transports' => $source->getTransports(),
|
||||
'attestation_type' => $source->getAttestationType(),
|
||||
'trust_path' => $source->getTrustPath(),
|
||||
'user_handle' => $user->uuid,
|
||||
'counter' => $source->getCounter(),
|
||||
'other_ui' => $source->getOtherUI(),
|
||||
]);
|
||||
|
||||
$key->saveOrFail();
|
||||
|
||||
return $key;
|
||||
}
|
||||
}
|
26
app/Transformers/Api/Client/SecurityKeyTransformer.php
Normal file
26
app/Transformers/Api/Client/SecurityKeyTransformer.php
Normal file
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Transformers\Api\Client;
|
||||
|
||||
use Pterodactyl\Models\SecurityKey;
|
||||
use Pterodactyl\Transformers\Api\Transformer;
|
||||
|
||||
class SecurityKeyTransformer extends Transformer
|
||||
{
|
||||
public function getResourceName(): string
|
||||
{
|
||||
return SecurityKey::RESOURCE_NAME;
|
||||
}
|
||||
|
||||
public function transform(SecurityKey $model): array
|
||||
{
|
||||
return [
|
||||
'uuid' => $model->uuid,
|
||||
'name' => $model->name,
|
||||
'type' => $model->type,
|
||||
'public_key_id' => base64_encode($model->public_key_id),
|
||||
'created_at' => self::formatTimestamp($model->created_at),
|
||||
'updated_at' => self::formatTimestamp($model->updated_at),
|
||||
];
|
||||
}
|
||||
}
|
|
@ -24,47 +24,50 @@
|
|||
"ext-pdo_mysql": "*",
|
||||
"ext-posix": "*",
|
||||
"ext-zip": "*",
|
||||
"aws/aws-sdk-php": "~3.260.1",
|
||||
"doctrine/dbal": "~3.6.0",
|
||||
"guzzlehttp/guzzle": "~7.5.0",
|
||||
"hashids/hashids": "~5.0.0",
|
||||
"laracasts/utilities": "~3.2.2",
|
||||
"laravel/framework": "~10.1.3",
|
||||
"laravel/helpers": "~1.6.0",
|
||||
"laravel/sanctum": "~3.2.1",
|
||||
"laravel/tinker": "~2.8.1",
|
||||
"laravel/ui": "~4.2.1",
|
||||
"lcobucci/jwt": "~4.3.0",
|
||||
"league/flysystem-aws-s3-v3": "~3.12.2",
|
||||
"league/flysystem-memory": "~3.10.3",
|
||||
"aws/aws-sdk-php": "~3.253",
|
||||
"doctrine/dbal": "~3.5",
|
||||
"guzzlehttp/guzzle": "~7.5",
|
||||
"hashids/hashids": "~4.1",
|
||||
"laracasts/utilities": "~3.2",
|
||||
"laravel/framework": "~9.43",
|
||||
"laravel/helpers": "~1.5",
|
||||
"laravel/sanctum": "~3.0",
|
||||
"laravel/tinker": "~2.7",
|
||||
"laravel/ui": "~4.1",
|
||||
"lcobucci/jwt": "~4.2",
|
||||
"league/flysystem-aws-s3-v3": "~3.10",
|
||||
"league/flysystem-memory": "~3.10",
|
||||
"matriphe/iso-639": "~1.2",
|
||||
"phpseclib/phpseclib": "~3.0.18",
|
||||
"pragmarx/google2fa": "~8.0.0",
|
||||
"predis/predis": "~2.1.1",
|
||||
"prologue/alerts": "~1.1.0",
|
||||
"psr/cache": "~3.0.0",
|
||||
"s1lentium/iptools": "~1.2.0",
|
||||
"spatie/laravel-fractal": "~6.0.3",
|
||||
"spatie/laravel-query-builder": "~5.1.2",
|
||||
"staudenmeir/belongs-to-through": "~2.13",
|
||||
"symfony/http-client": "~6.2.6",
|
||||
"symfony/mailgun-mailer": "~6.2.5",
|
||||
"symfony/postmark-mailer": "~6.2.5",
|
||||
"symfony/yaml": "~6.2.5",
|
||||
"webmozart/assert": "~1.11.0"
|
||||
"nyholm/psr7": "~1.5",
|
||||
"phpseclib/phpseclib": "~3.0",
|
||||
"pragmarx/google2fa": "~8.0",
|
||||
"predis/predis": "~2.0",
|
||||
"psr/cache": "~3.0",
|
||||
"s1lentium/iptools": "~1.1",
|
||||
"spatie/laravel-fractal": "~6.0",
|
||||
"spatie/laravel-query-builder": "~5.1",
|
||||
"staudenmeir/belongs-to-through": "~2.12",
|
||||
"symfony/http-client": "~6.0",
|
||||
"symfony/mailgun-mailer": "~6.0",
|
||||
"symfony/postmark-mailer": "~6.0",
|
||||
"symfony/psr-http-message-bridge": "~2.1",
|
||||
"symfony/yaml": "~6.0",
|
||||
"web-auth/webauthn-lib": "~4.3",
|
||||
"webmozart/assert": "~1.11"
|
||||
},
|
||||
"require-dev": {
|
||||
"barryvdh/laravel-ide-helper": "~2.13.0",
|
||||
"fakerphp/faker": "~1.21.0",
|
||||
"friendsofphp/php-cs-fixer": "~3.14.4",
|
||||
"itsgoingd/clockwork": "~5.1.12",
|
||||
"laravel/sail": "~1.21.0",
|
||||
"mockery/mockery": "~1.5.1",
|
||||
"nunomaduro/collision": "~7.0.5",
|
||||
"nunomaduro/larastan": "~2.4.1",
|
||||
"phpstan/phpstan": "~1.10.1",
|
||||
"phpunit/phpunit": "~10.0.11",
|
||||
"spatie/laravel-ignition": "~2.0.0"
|
||||
"barryvdh/laravel-ide-helper": "~2.12.3",
|
||||
"fakerphp/faker": "~1.20",
|
||||
"friendsofphp/php-cs-fixer": "~3.11",
|
||||
"itsgoingd/clockwork": "~5.1",
|
||||
"laravel/sail": "~1.16",
|
||||
"mockery/mockery": "~1.5",
|
||||
"nunomaduro/collision": "~6.3",
|
||||
"nunomaduro/larastan": "^2.0",
|
||||
"phpstan/phpstan": "~1.9",
|
||||
"php-mock/php-mock-phpunit": "~2.6",
|
||||
"phpunit/phpunit": "~9.5",
|
||||
"spatie/laravel-ignition": "~1.5"
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
|
@ -84,17 +87,18 @@
|
|||
"scripts": {
|
||||
"cs:fix": "php-cs-fixer fix",
|
||||
"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": [
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||
],
|
||||
"post-create-project-cmd": [
|
||||
"@php artisan key:generate --ansi"
|
||||
"@php artisan key:generate"
|
||||
],
|
||||
"post-autoload-dump": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
"@php artisan package:discover || true"
|
||||
]
|
||||
},
|
||||
"prefer-stable": true,
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
|
@ -102,7 +106,5 @@
|
|||
"platform": {
|
||||
"php": "8.1.0"
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
}
|
||||
|
|
2778
composer.lock
generated
2778
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -27,7 +27,7 @@ return [
|
|||
| 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.
|
||||
|
|
||||
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
|
||||
| Supported: "smtp", "sendmail", "mailgun", "ses",
|
||||
| "postmark", "log", "array", "failover"
|
||||
|
|
||||
*/
|
||||
|
|
|
@ -34,7 +34,7 @@ class EggVariableFactory extends Factory
|
|||
/**
|
||||
* Indicate that the egg variable is viewable.
|
||||
*/
|
||||
public function viewable(): static
|
||||
public function viewable(): Factory
|
||||
{
|
||||
return $this->state(function (array $attributes) {
|
||||
return [
|
||||
|
@ -46,7 +46,7 @@ class EggVariableFactory extends Factory
|
|||
/**
|
||||
* Indicate that the egg variable is editable.
|
||||
*/
|
||||
public function editable(): static
|
||||
public function editable(): Factory
|
||||
{
|
||||
return $this->state(function (array $attributes) {
|
||||
return [
|
||||
|
|
34
database/Factories/SecurityKeyFactory.php
Normal file
34
database/Factories/SecurityKeyFactory.php
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Pterodactyl\Models\SecurityKey;
|
||||
use Webauthn\TrustPath\EmptyTrustPath;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class SecurityKeyFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The name of the factory's corresponding model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $model = SecurityKey::class;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'uuid' => Uuid::uuid4()->toString(),
|
||||
'name' => $this->faker->word,
|
||||
'type' => 'public-key',
|
||||
'transports' => [],
|
||||
'attestation_type' => 'none',
|
||||
'trust_path' => new EmptyTrustPath(),
|
||||
'counter' => 0,
|
||||
];
|
||||
}
|
||||
}
|
|
@ -41,7 +41,7 @@ class UserFactory extends Factory
|
|||
/**
|
||||
* Indicate that the user is an admin.
|
||||
*/
|
||||
public function admin(): static
|
||||
public function admin(): Factory
|
||||
{
|
||||
return $this->state(['root_admin' => true]);
|
||||
}
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
{
|
||||
"_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO",
|
||||
"meta": {
|
||||
"version": "PTDL_v2",
|
||||
"version": "PTDL_v1",
|
||||
"update_url": null
|
||||
},
|
||||
"exported_at": "2023-03-25T13:37:00+00:00",
|
||||
"exported_at": "2022-01-18T11:44:55-05:00",
|
||||
"name": "Rust",
|
||||
"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.",
|
||||
"features": [
|
||||
"steam_disk_space"
|
||||
],
|
||||
"docker_images": {
|
||||
"quay.io\/pterodactyl\/core:rust": "quay.io\/pterodactyl\/core:rust"
|
||||
},
|
||||
"images": [
|
||||
"quay.io\/pterodactyl\/core:rust"
|
||||
],
|
||||
"file_denylist": [],
|
||||
"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}}",
|
||||
"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}}",
|
||||
"config": {
|
||||
"files": "{}",
|
||||
"startup": "{\r\n \"done\": \"Server startup complete\"\r\n}",
|
||||
|
@ -37,18 +37,16 @@
|
|||
"default_value": "A Rust Server",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|max:60",
|
||||
"field_type": "text"
|
||||
"rules": "required|string|max:60"
|
||||
},
|
||||
{
|
||||
"name": "Modding Framework",
|
||||
"description": "The modding framework to be used: carbon, oxide, vanilla.\r\nDefaults to \"vanilla\" for a non-modded server installation.",
|
||||
"env_variable": "FRAMEWORK",
|
||||
"default_value": "vanilla",
|
||||
"name": "OxideMod",
|
||||
"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.",
|
||||
"env_variable": "OXIDE",
|
||||
"default_value": "0",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|in:carbon,oxide,vanilla",
|
||||
"field_type": "text"
|
||||
"rules": "required|boolean"
|
||||
},
|
||||
{
|
||||
"name": "Level",
|
||||
|
@ -57,8 +55,7 @@
|
|||
"default_value": "Procedural Map",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string|max:20",
|
||||
"field_type": "text"
|
||||
"rules": "required|string|max:20"
|
||||
},
|
||||
{
|
||||
"name": "Description",
|
||||
|
@ -67,8 +64,7 @@
|
|||
"default_value": "Powered by Pterodactyl",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|string",
|
||||
"field_type": "text"
|
||||
"rules": "required|string"
|
||||
},
|
||||
{
|
||||
"name": "URL",
|
||||
|
@ -77,8 +73,7 @@
|
|||
"default_value": "http:\/\/pterodactyl.io",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|url",
|
||||
"field_type": "text"
|
||||
"rules": "nullable|url"
|
||||
},
|
||||
{
|
||||
"name": "World Size",
|
||||
|
@ -87,8 +82,7 @@
|
|||
"default_value": "3000",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|integer",
|
||||
"field_type": "text"
|
||||
"rules": "required|integer"
|
||||
},
|
||||
{
|
||||
"name": "World Seed",
|
||||
|
@ -97,8 +91,7 @@
|
|||
"default_value": "",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|string",
|
||||
"field_type": "text"
|
||||
"rules": "nullable|string"
|
||||
},
|
||||
{
|
||||
"name": "Max Players",
|
||||
|
@ -107,8 +100,7 @@
|
|||
"default_value": "40",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|integer",
|
||||
"field_type": "text"
|
||||
"rules": "required|integer"
|
||||
},
|
||||
{
|
||||
"name": "Server Image",
|
||||
|
@ -117,18 +109,7 @@
|
|||
"default_value": "",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"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"
|
||||
"rules": "nullable|url"
|
||||
},
|
||||
{
|
||||
"name": "RCON Port",
|
||||
|
@ -137,18 +118,16 @@
|
|||
"default_value": "28016",
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "required|integer",
|
||||
"field_type": "text"
|
||||
"rules": "required|integer"
|
||||
},
|
||||
{
|
||||
"name": "RCON Password",
|
||||
"description": "RCON access password.",
|
||||
"env_variable": "RCON_PASS",
|
||||
"default_value": "",
|
||||
"default_value": "CHANGEME",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|regex:\/^[\\w.-]*$\/|max:64",
|
||||
"field_type": "text"
|
||||
"rules": "required|regex:\/^[\\w.-]*$\/|max:64"
|
||||
},
|
||||
{
|
||||
"name": "Save Interval",
|
||||
|
@ -157,8 +136,7 @@
|
|||
"default_value": "60",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "required|integer",
|
||||
"field_type": "text"
|
||||
"rules": "required|integer"
|
||||
},
|
||||
{
|
||||
"name": "Additional Arguments",
|
||||
|
@ -167,8 +145,7 @@
|
|||
"default_value": "",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|string",
|
||||
"field_type": "text"
|
||||
"rules": "nullable|string"
|
||||
},
|
||||
{
|
||||
"name": "App Port",
|
||||
|
@ -177,8 +154,7 @@
|
|||
"default_value": "28082",
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "required|integer",
|
||||
"field_type": "text"
|
||||
"rules": "required|integer"
|
||||
},
|
||||
{
|
||||
"name": "Server Logo",
|
||||
|
@ -187,8 +163,7 @@
|
|||
"default_value": "",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|url",
|
||||
"field_type": "text"
|
||||
"rules": "nullable|url"
|
||||
},
|
||||
{
|
||||
"name": "Custom Map URL",
|
||||
|
@ -197,8 +172,7 @@
|
|||
"default_value": "",
|
||||
"user_viewable": true,
|
||||
"user_editable": true,
|
||||
"rules": "nullable|url",
|
||||
"field_type": "text"
|
||||
"rules": "nullable|url"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateSecurityKeysTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('security_keys', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->char('uuid', 36)->unique();
|
||||
$table->unsignedInteger('user_id');
|
||||
$table->string('name');
|
||||
$table->text('public_key_id');
|
||||
$table->text('public_key');
|
||||
$table->char('aaguid', 36)->nullable();
|
||||
$table->string('type');
|
||||
$table->json('transports');
|
||||
$table->string('attestation_type');
|
||||
$table->json('trust_path');
|
||||
$table->text('user_handle');
|
||||
$table->unsignedInteger('counter');
|
||||
$table->json('other_ui')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('security_keys');
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
<?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');
|
||||
});
|
||||
}
|
||||
};
|
|
@ -1,27 +0,0 @@
|
|||
<?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');
|
||||
});
|
||||
}
|
||||
};
|
346
flake.lock
346
flake.lock
|
@ -1,5 +1,61 @@
|
|||
{
|
||||
"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": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
|
@ -18,21 +74,28 @@
|
|||
},
|
||||
"dream2nix": {
|
||||
"inputs": {
|
||||
"alejandra": "alejandra",
|
||||
"all-cabal-json": "all-cabal-json",
|
||||
"crane": "crane",
|
||||
"devshell": "devshell",
|
||||
"flake-compat": "flake-compat",
|
||||
"flake-parts": "flake-parts",
|
||||
"nix-unit": "nix-unit",
|
||||
"flake-utils-pre-commit": "flake-utils-pre-commit",
|
||||
"ghc-utils": "ghc-utils",
|
||||
"gomod2nix": "gomod2nix",
|
||||
"mach-nix": "mach-nix",
|
||||
"nix-pypi-fetcher": "nix-pypi-fetcher",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"poetry2nix": "poetry2nix",
|
||||
"pre-commit-hooks": "pre-commit-hooks"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1695717405,
|
||||
"narHash": "sha256-MvHrU3h0Bw57s2p+wCUnSZliR4wvvPi3xkW+MRWB5HU=",
|
||||
"lastModified": 1669743839,
|
||||
"narHash": "sha256-zxnaRaWfCJxy0JlORD4Kmtzd0pfpcGLnyaCIJY8OlIo=",
|
||||
"owner": "nix-community",
|
||||
"repo": "dream2nix",
|
||||
"rev": "6dbd59e4a47bd916a655c4425a3e730c6aeae033",
|
||||
"rev": "b6af93946130748f72671dfd2ab84a5aeaf1f191",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -41,35 +104,39 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"fenix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"dream2nix",
|
||||
"alejandra",
|
||||
"nixpkgs"
|
||||
],
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1673956053,
|
||||
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
|
||||
"lastModified": 1657607339,
|
||||
"narHash": "sha256-HaqoAwlbVVZH2n4P3jN2FFPMpVuhxDy1poNOR7kzODc=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "b814c83d9e6aa5a28d0cf356ecfdafb2505ad37d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-parts": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": [
|
||||
"dream2nix",
|
||||
"nixpkgs"
|
||||
]
|
||||
"nixpkgs-lib": "nixpkgs-lib"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1675933616,
|
||||
"narHash": "sha256-/rczJkJHtx16IFxMmAWu5nNYcSXNg1YYXTHoGjLrLUA=",
|
||||
"lastModified": 1668450977,
|
||||
"narHash": "sha256-cfLhMhnvXn6x1vPm+Jow3RiFAUSCw/l1utktCw5rVA4=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "47478a4a003e745402acf63be7f9a092d51b83d7",
|
||||
"rev": "d591857e9d7dd9ddbfba0ea02b43b927c3c0f1fa",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -79,15 +146,12 @@
|
|||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1689068808,
|
||||
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
|
||||
"lastModified": 1667395993,
|
||||
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
|
||||
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -96,16 +160,13 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"flake-utils-pre-commit": {
|
||||
"locked": {
|
||||
"lastModified": 1694529238,
|
||||
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
|
||||
"lastModified": 1644229661,
|
||||
"narHash": "sha256-1YdnJAsNy69bpcjuoKdOYQX0YxZBiCYZo4Twxerqv7k=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
|
||||
"rev": "3cecb5b042f7f209c56ffd8371b2711a290ec797",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -114,6 +175,69 @@
|
|||
"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": {
|
||||
"inputs": {
|
||||
"flake-utils": [
|
||||
|
@ -139,62 +263,29 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix-github-actions": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"dream2nix",
|
||||
"nix-unit",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"nix-pypi-fetcher": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1688870561,
|
||||
"narHash": "sha256-4UYkifnPEw1nAzqqPOTL2MvWtm3sNGw1UTYTalkTcGY=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nix-github-actions",
|
||||
"rev": "165b1650b753316aa7f1787f3005a8d2da0f5301",
|
||||
"lastModified": 1669065297,
|
||||
"narHash": "sha256-UStjXjNIuIm7SzMOWvuYWIHBkPUKQ8Id63BMJjnIDoA=",
|
||||
"owner": "DavHau",
|
||||
"repo": "nix-pypi-fetcher",
|
||||
"rev": "a9885ac6a091576b5195d547ac743d45a2a615ac",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"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",
|
||||
"owner": "DavHau",
|
||||
"repo": "nix-pypi-fetcher",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1695644571,
|
||||
"narHash": "sha256-asS9dCCdlt1lPq0DLwkVBbVoEKuEuz+Zi3DG7pR/RxA=",
|
||||
"lastModified": 1669542132,
|
||||
"narHash": "sha256-DRlg++NJAwPh8io3ExBJdNW7Djs3plVI5jgYQ+iXAZQ=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "6500b4580c2a1f3d0f980d32d285739d8e156d92",
|
||||
"rev": "a115bb9bd56831941be3776c8a94005867f316a7",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -204,6 +295,24 @@
|
|||
"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": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
|
@ -238,9 +347,29 @@
|
|||
"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": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"flake-utils": [
|
||||
"dream2nix",
|
||||
"flake-utils-pre-commit"
|
||||
],
|
||||
"nixpkgs": [
|
||||
"dream2nix",
|
||||
"nixpkgs"
|
||||
|
@ -263,60 +392,25 @@
|
|||
"root": {
|
||||
"inputs": {
|
||||
"dream2nix": "dream2nix",
|
||||
"flake-utils": "flake-utils_2",
|
||||
"flake-utils": "flake-utils",
|
||||
"mk-node-package": "mk-node-package",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"lastModified": 1657557289,
|
||||
"narHash": "sha256-PRW+nUwuqNTRAEa83SfX+7g+g8nQ+2MMbasQ9nt6+UM=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "caf23f29144b371035b864a1017dbc32573ad56d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"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",
|
||||
"owner": "rust-lang",
|
||||
"ref": "nightly",
|
||||
"repo": "rust-analyzer",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -159,7 +159,6 @@
|
|||
}
|
||||
];
|
||||
systems = [system];
|
||||
autoProjects = true;
|
||||
})
|
||||
.packages
|
||||
."${system}"
|
||||
|
@ -172,7 +171,7 @@
|
|||
buildInputs = [];
|
||||
|
||||
buildPhase = ''
|
||||
pnpm run build
|
||||
yarn run build
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
|
@ -214,7 +213,6 @@
|
|||
copyToRoot = pkgs.buildEnv {
|
||||
name = "image-root";
|
||||
paths = [
|
||||
bash
|
||||
dockerTools.fakeNss
|
||||
caCertificates
|
||||
caddy
|
||||
|
@ -223,9 +221,11 @@
|
|||
coreutils
|
||||
mysql80
|
||||
nodejs-18_x
|
||||
nodePackages.npm
|
||||
nodePackages.pnpm
|
||||
nodePackages.yarn
|
||||
php81WithExtensions
|
||||
postgresql_15
|
||||
postgresql_14
|
||||
];
|
||||
pathsToLink = ["/bin" "/etc"];
|
||||
};
|
||||
|
|
87
package.json
87
package.json
|
@ -1,11 +1,9 @@
|
|||
{
|
||||
"name": "@pterodactyl/panel",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@8.7.6",
|
||||
"engines": {
|
||||
"node": ">=16.13"
|
||||
"node": ">=16.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
|
@ -41,15 +39,15 @@
|
|||
"@codemirror/view": "^6.0.0",
|
||||
"@floating-ui/react-dom-interactions": "0.13.3",
|
||||
"@flyyer/use-fit-text": "3.0.1",
|
||||
"@fortawesome/fontawesome-svg-core": "6.3.0",
|
||||
"@fortawesome/free-brands-svg-icons": "6.3.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.3.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.2.1",
|
||||
"@fortawesome/free-brands-svg-icons": "6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "6.2.1",
|
||||
"@fortawesome/react-fontawesome": "0.2.0",
|
||||
"@headlessui/react": "1.7.11",
|
||||
"@headlessui/react": "1.7.5",
|
||||
"@heroicons/react": "1.0.6",
|
||||
"@lezer/common": "1.0.2",
|
||||
"@lezer/highlight": "1.1.3",
|
||||
"@preact/signals-react": "1.2.2",
|
||||
"@preact/signals-react": "1.2.1",
|
||||
"axios": "0.27.2",
|
||||
"boring-avatars": "1.7.0",
|
||||
"chart.js": "3.9.1",
|
||||
|
@ -57,76 +55,75 @@
|
|||
"copy-to-clipboard": "3.3.3",
|
||||
"date-fns": "2.29.3",
|
||||
"debounce": "1.2.1",
|
||||
"deepmerge-ts": "4.3.0",
|
||||
"deepmerge-ts": "4.2.2",
|
||||
"easy-peasy": "5.2.0",
|
||||
"events": "3.3.0",
|
||||
"formik": "2.2.9",
|
||||
"framer-motion": "9.1.6",
|
||||
"i18next": "22.4.10",
|
||||
"i18next-http-backend": "2.1.1",
|
||||
"framer-motion": "7.7.2",
|
||||
"i18next": "22.4.3",
|
||||
"i18next-http-backend": "2.1.0",
|
||||
"i18next-multiload-backend-adapter": "2.2.0",
|
||||
"nanoid": "4.0.1",
|
||||
"nanoid": "4.0.0",
|
||||
"qrcode.react": "3.1.0",
|
||||
"react": "18.2.0",
|
||||
"react-chartjs-2": "4.3.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-fast-compare": "3.2.0",
|
||||
"react-i18next": "12.2.0",
|
||||
"react-router-dom": "6.8.1",
|
||||
"react-i18next": "12.1.1",
|
||||
"react-router-dom": "6.4.5",
|
||||
"react-select": "5.7.0",
|
||||
"reaptcha": "1.12.1",
|
||||
"sockette": "2.0.6",
|
||||
"styled-components": "5.3.6",
|
||||
"styled-components-breakpoint": "3.0.0-preview.20",
|
||||
"swr": "2.0.3",
|
||||
"xterm": "5.1.0",
|
||||
"xterm-addon-fit": "0.7.0",
|
||||
"xterm-addon-search": "0.11.0",
|
||||
"swr": "1.3.0",
|
||||
"xterm": "5.0.0",
|
||||
"xterm-addon-fit": "0.6.0",
|
||||
"xterm-addon-search": "0.10.0",
|
||||
"xterm-addon-search-bar": "0.2.0",
|
||||
"xterm-addon-web-links": "0.8.0",
|
||||
"yup": "1.0.0"
|
||||
"xterm-addon-web-links": "0.7.0",
|
||||
"yup": "0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "0.5.3",
|
||||
"@tailwindcss/line-clamp": "0.4.2",
|
||||
"@testing-library/dom": "9.0.0",
|
||||
"@testing-library/react": "14.0.0",
|
||||
"@testing-library/dom": "8.19.0",
|
||||
"@testing-library/react": "13.4.0",
|
||||
"@testing-library/user-event": "14.4.3",
|
||||
"@types/debounce": "1.2.1",
|
||||
"@types/events": "3.0.0",
|
||||
"@types/node": "18.14.1",
|
||||
"@types/react": "18.0.28",
|
||||
"@types/react-dom": "18.0.11",
|
||||
"@types/node": "18.11.13",
|
||||
"@types/react": "18.0.26",
|
||||
"@types/react-dom": "18.0.9",
|
||||
"@types/styled-components": "5.1.26",
|
||||
"@typescript-eslint/eslint-plugin": "5.53.0",
|
||||
"@typescript-eslint/parser": "5.53.0",
|
||||
"@vitejs/plugin-react": "3.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.46.1",
|
||||
"@typescript-eslint/parser": "5.46.1",
|
||||
"@vitejs/plugin-react": "3.0.0",
|
||||
"autoprefixer": "10.4.13",
|
||||
"babel-plugin-styled-components": "2.0.7",
|
||||
"babel-plugin-twin": "1.1.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "8.34.0",
|
||||
"eslint-config-prettier": "8.6.0",
|
||||
"eslint": "8.29.0",
|
||||
"eslint-config-prettier": "8.5.0",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"eslint-plugin-prettier": "4.2.1",
|
||||
"eslint-plugin-react": "7.32.2",
|
||||
"eslint-plugin-react": "7.31.11",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"happy-dom": "8.7.2",
|
||||
"laravel-vite-plugin": "0.7.4",
|
||||
"pathe": "1.1.0",
|
||||
"postcss": "8.4.21",
|
||||
"postcss-import": "15.1.0",
|
||||
"postcss-nesting": "11.2.1",
|
||||
"postcss-preset-env": "8.0.1",
|
||||
"prettier": "2.8.4",
|
||||
"prettier-plugin-tailwindcss": "0.2.3",
|
||||
"happy-dom": "8.1.1",
|
||||
"laravel-vite-plugin": "0.7.3",
|
||||
"pathe": "1.0.0",
|
||||
"postcss": "8.4.20",
|
||||
"postcss-nesting": "10.2.0",
|
||||
"postcss-preset-env": "7.8.3",
|
||||
"prettier": "2.8.1",
|
||||
"prettier-plugin-tailwindcss": "0.2.1",
|
||||
"rimraf": "3.0.2",
|
||||
"tailwindcss": "3.2.7",
|
||||
"tailwindcss": "3.2.4",
|
||||
"ts-essentials": "9.3.0",
|
||||
"twin.macro": "2.8.2",
|
||||
"typescript": "4.9.5",
|
||||
"vite": "4.1.4",
|
||||
"vitest": "0.28.5"
|
||||
"typescript": "4.9.4",
|
||||
"vite": "4.0.3",
|
||||
"vitest": "0.26.2"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 0.5%",
|
||||
|
|
|
@ -4,8 +4,9 @@
|
|||
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
|
||||
bootstrap="bootstrap/tests.php"
|
||||
colors="true"
|
||||
printerClass="NunoMaduro\Collision\Adapters\Phpunit\Printer"
|
||||
>
|
||||
<coverage>
|
||||
<coverage processUncoveredFiles="true">
|
||||
<include>
|
||||
<directory suffix=".php">./app</directory>
|
||||
</include>
|
||||
|
|
5803
pnpm-lock.yaml
5803
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
95
resources/scripts/api/account/security-keys.ts
Normal file
95
resources/scripts/api/account/security-keys.ts
Normal file
|
@ -0,0 +1,95 @@
|
|||
import type { AxiosError } from 'axios';
|
||||
import type { SWRConfiguration } from 'swr';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import type { SecurityKey } from '@definitions/user';
|
||||
import { Transformers } from '@definitions/user';
|
||||
import { LoginResponse } from '@/api/auth/login';
|
||||
import type { FractalResponseList } from '@/api/http';
|
||||
import http from '@/api/http';
|
||||
import { decodeBase64 } from '@/lib/base64';
|
||||
import { decodeBuffer, encodeBuffer } from '@/lib/buffer';
|
||||
import { useUserSWRKey } from '@/plugins/useSWRKey';
|
||||
|
||||
function decodeSecurityKeyCredentials(credentials: PublicKeyCredentialDescriptor[]) {
|
||||
return credentials.map(c => ({
|
||||
id: decodeBuffer(decodeBase64(c.id.toString())),
|
||||
type: c.type,
|
||||
transports: c.transports,
|
||||
}));
|
||||
}
|
||||
|
||||
function useSecurityKeys(config?: SWRConfiguration<SecurityKey[], AxiosError>) {
|
||||
const key = useUserSWRKey(['account', 'security-keys']);
|
||||
|
||||
return useSWR<SecurityKey[], AxiosError>(
|
||||
key,
|
||||
async (): Promise<SecurityKey[]> => {
|
||||
const { data } = await http.get('/api/client/account/security-keys');
|
||||
|
||||
return (data as FractalResponseList).data.map(datum => Transformers.toSecurityKey(datum.attributes));
|
||||
},
|
||||
{ revalidateOnMount: false, ...(config ?? {}) },
|
||||
);
|
||||
}
|
||||
|
||||
async function deleteSecurityKey(uuid: string): Promise<void> {
|
||||
await http.delete(`/api/client/account/security-keys/${uuid}`);
|
||||
}
|
||||
|
||||
async function registerCredentialForAccount(
|
||||
name: string,
|
||||
tokenId: string,
|
||||
credential: PublicKeyCredential,
|
||||
): Promise<SecurityKey> {
|
||||
const { data } = await http.post('/api/client/account/security-keys/register', {
|
||||
name,
|
||||
token_id: tokenId,
|
||||
registration: {
|
||||
id: credential.id,
|
||||
type: credential.type,
|
||||
rawId: encodeBuffer(credential.rawId),
|
||||
response: {
|
||||
attestationObject: encodeBuffer(
|
||||
(credential.response as AuthenticatorAttestationResponse).attestationObject,
|
||||
),
|
||||
clientDataJSON: encodeBuffer(credential.response.clientDataJSON),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return Transformers.toSecurityKey(data.attributes);
|
||||
}
|
||||
|
||||
async function registerSecurityKey(name: string): Promise<SecurityKey> {
|
||||
const { data } = await http.get('/api/client/account/security-keys/register');
|
||||
|
||||
const publicKey = data.data.credentials;
|
||||
publicKey.challenge = decodeBuffer(decodeBase64(publicKey.challenge));
|
||||
publicKey.user.id = decodeBuffer(publicKey.user.id);
|
||||
|
||||
if (publicKey.excludeCredentials) {
|
||||
publicKey.excludeCredentials = decodeSecurityKeyCredentials(publicKey.excludeCredentials);
|
||||
}
|
||||
|
||||
const credentials = await navigator.credentials.create({ publicKey });
|
||||
if (!credentials || credentials.type !== 'public-key') {
|
||||
throw new Error(
|
||||
`Unexpected type returned by navigator.credentials.create(): expected "public-key", got "${credentials?.type}"`,
|
||||
);
|
||||
}
|
||||
|
||||
return await registerCredentialForAccount(name, data.data.token_id, credentials as PublicKeyCredential);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
async function authenticateSecurityKey(data: { confirmation_token: string; data: string }): Promise<LoginResponse> {
|
||||
const response = await http.post('/auth/login/checkpoint/key', data);
|
||||
|
||||
return {
|
||||
complete: response.data.complete,
|
||||
intended: response.data.data?.intended || null,
|
||||
};
|
||||
}
|
||||
|
||||
export { useSecurityKeys, deleteSecurityKey, registerSecurityKey, authenticateSecurityKey };
|
|
@ -56,7 +56,7 @@ export interface EggVariable extends Model {
|
|||
* A standard API response with the minimum viable details for the frontend
|
||||
* to correctly render a egg.
|
||||
*/
|
||||
export type LoadedEgg = WithRelationships<Egg, 'nest' | 'variables'>;
|
||||
type LoadedEgg = WithRelationships<Egg, 'nest' | 'variables'>;
|
||||
|
||||
/**
|
||||
* Gets a single egg from the database and returns it.
|
||||
|
|
|
@ -14,13 +14,13 @@ export default (egg: Partial<Egg2>): Promise<Egg> => {
|
|||
config_files: egg.configFiles,
|
||||
config_startup: egg.configStartup,
|
||||
config_stop: egg.configStop,
|
||||
config_from: egg.configFrom,
|
||||
startup: egg.startup,
|
||||
script_container: egg.scriptContainer,
|
||||
copy_script_from: egg.copyScriptFrom,
|
||||
script_entry: egg.scriptEntry,
|
||||
script_is_privileged: egg.scriptIsPrivileged,
|
||||
script_install: egg.scriptInstall,
|
||||
// config_from: egg.configFrom,
|
||||
// copy_script_from: egg.copyScriptFrom,
|
||||
// script_is_privileged: egg.scriptIsPrivileged,
|
||||
})
|
||||
.then(({ data }) => resolve(rawDataToEgg(data)))
|
||||
.catch(reject);
|
||||
|
|
|
@ -43,15 +43,15 @@ export interface Egg {
|
|||
configFiles: Record<string, any> | null;
|
||||
configStartup: Record<string, any> | null;
|
||||
configStop: string | null;
|
||||
configFrom: number | null;
|
||||
startup: string;
|
||||
scriptContainer: string;
|
||||
copyScriptFrom: number | null;
|
||||
scriptEntry: string;
|
||||
scriptIsPrivileged: boolean;
|
||||
scriptInstall: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
// configFrom: number | null;
|
||||
// copyScriptFrom: number | null;
|
||||
// scriptIsPrivileged: boolean;
|
||||
|
||||
relations: {
|
||||
nest?: Nest;
|
||||
|
|
|
@ -17,7 +17,7 @@ export interface UpdateUserValues {
|
|||
}
|
||||
|
||||
const filters = ['id', 'uuid', 'external_id', 'username', 'email'] as const;
|
||||
type Filters = (typeof filters)[number];
|
||||
type Filters = typeof filters[number];
|
||||
|
||||
const useGetUsers = (
|
||||
params?: QueryBuilderParams<Filters>,
|
||||
|
|
|
@ -1,24 +1,6 @@
|
|||
import { Model, UUID } from '@/api/definitions';
|
||||
import { SubuserPermission } from '@/state/server/subusers';
|
||||
|
||||
interface User extends Model {
|
||||
uuid: string;
|
||||
username: string;
|
||||
email: string;
|
||||
image: string;
|
||||
twoFactorEnabled: boolean;
|
||||
createdAt: Date;
|
||||
permissions: SubuserPermission[];
|
||||
can(permission: SubuserPermission): boolean;
|
||||
}
|
||||
|
||||
interface SSHKey extends Model {
|
||||
name: string;
|
||||
publicKey: string;
|
||||
fingerprint: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
interface ActivityLog extends Model<'actor'> {
|
||||
id: string;
|
||||
batch: UUID | null;
|
||||
|
@ -33,3 +15,30 @@ interface ActivityLog extends Model<'actor'> {
|
|||
actor: User | null;
|
||||
};
|
||||
}
|
||||
|
||||
interface User extends Model {
|
||||
uuid: string;
|
||||
username: string;
|
||||
email: string;
|
||||
image: string;
|
||||
twoFactorEnabled: boolean;
|
||||
createdAt: Date;
|
||||
permissions: SubuserPermission[];
|
||||
can(permission: SubuserPermission): boolean;
|
||||
}
|
||||
|
||||
interface SecurityKey extends Model {
|
||||
uuid: UUID;
|
||||
name: string;
|
||||
type: 'public-key';
|
||||
publicKeyId: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
interface SSHKey extends Model {
|
||||
name: string;
|
||||
publicKey: string;
|
||||
fingerprint: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,36 @@ import { FractalResponseData } from '@/api/http';
|
|||
import { transform } from '@definitions/helpers';
|
||||
|
||||
export default class Transformers {
|
||||
static toActivityLog = ({ attributes }: FractalResponseData): Models.ActivityLog => {
|
||||
const { actor } = attributes.relationships || {};
|
||||
|
||||
return {
|
||||
id: attributes.id,
|
||||
batch: attributes.batch,
|
||||
event: attributes.event,
|
||||
ip: attributes.ip,
|
||||
isApi: attributes.is_api,
|
||||
description: attributes.description,
|
||||
properties: attributes.properties,
|
||||
hasAdditionalMetadata: attributes.has_additional_metadata ?? false,
|
||||
timestamp: new Date(attributes.timestamp),
|
||||
relationships: {
|
||||
actor: transform(actor as FractalResponseData, this.toUser, null),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
static toSecurityKey(data: Record<string, any>): Models.SecurityKey {
|
||||
return {
|
||||
uuid: data.uuid,
|
||||
name: data.name,
|
||||
type: data.type,
|
||||
publicKeyId: data.public_key_id,
|
||||
createdAt: new Date(data.created_at),
|
||||
updatedAt: new Date(data.updated_at),
|
||||
};
|
||||
}
|
||||
|
||||
static toSSHKey = (data: Record<any, any>): Models.SSHKey => {
|
||||
return {
|
||||
name: data.name,
|
||||
|
@ -26,25 +56,6 @@ export default class Transformers {
|
|||
},
|
||||
};
|
||||
};
|
||||
|
||||
static toActivityLog = ({ attributes }: FractalResponseData): Models.ActivityLog => {
|
||||
const { actor } = attributes.relationships || {};
|
||||
|
||||
return {
|
||||
id: attributes.id,
|
||||
batch: attributes.batch,
|
||||
event: attributes.event,
|
||||
ip: attributes.ip,
|
||||
isApi: attributes.is_api,
|
||||
description: attributes.description,
|
||||
properties: attributes.properties,
|
||||
hasAdditionalMetadata: attributes.has_additional_metadata ?? false,
|
||||
timestamp: new Date(attributes.timestamp),
|
||||
relationships: {
|
||||
actor: transform(actor as FractalResponseData, this.toUser, null),
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export class MetaTransformers {}
|
||||
|
|
|
@ -47,7 +47,7 @@ export const rawDataToFileObject = (data: FractalResponseData): FileObject => ({
|
|||
isEditable: function () {
|
||||
if (this.isArchiveType() || !this.isFile) return false;
|
||||
|
||||
const matches = ['application/jar', 'application/octet-stream', 'inode/directory', /^image\/(?!svg\+xml)/];
|
||||
const matches = ['application/jar', 'application/octet-stream', 'inode/directory', /^image\//];
|
||||
|
||||
return matches.every(m => !this.mimetype.match(m));
|
||||
},
|
||||
|
|
|
@ -11,16 +11,16 @@ import Select from '@/components/elements/Select';
|
|||
interface Props {
|
||||
nestId?: number;
|
||||
selectedEggId?: number;
|
||||
onEggSelect: (egg: WithRelationships<Egg, 'variables'> | undefined) => void;
|
||||
onEggSelect: (egg: Egg | null) => void;
|
||||
}
|
||||
|
||||
export default ({ nestId, selectedEggId, onEggSelect }: Props) => {
|
||||
const [, , { setValue, setTouched }] = useField<Record<string, string | undefined>>('environment');
|
||||
const [eggs, setEggs] = useState<WithRelationships<Egg, 'variables'>[] | undefined>(undefined);
|
||||
const [eggs, setEggs] = useState<WithRelationships<Egg, 'variables'>[] | null>(null);
|
||||
|
||||
const selectEgg = (egg: WithRelationships<Egg, 'variables'> | undefined) => {
|
||||
if (egg === undefined) {
|
||||
onEggSelect(undefined);
|
||||
const selectEgg = (egg: Egg | null) => {
|
||||
if (egg === null) {
|
||||
onEggSelect(null);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -40,29 +40,26 @@ export default ({ nestId, selectedEggId, onEggSelect }: Props) => {
|
|||
|
||||
useEffect(() => {
|
||||
if (!nestId) {
|
||||
setEggs(undefined);
|
||||
setEggs(null);
|
||||
return;
|
||||
}
|
||||
|
||||
searchEggs(nestId, {})
|
||||
.then(_eggs => {
|
||||
setEggs(_eggs);
|
||||
|
||||
// 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);
|
||||
.then(eggs => {
|
||||
setEggs(eggs);
|
||||
selectEgg(eggs[0] || null);
|
||||
})
|
||||
.catch(error => console.error(error));
|
||||
}, [nestId]);
|
||||
|
||||
const onSelectChange = (event: ChangeEvent<HTMLSelectElement>) => {
|
||||
selectEgg(eggs?.find(egg => egg.id.toString() === event.currentTarget.value) ?? undefined);
|
||||
const onSelectChange = (e: ChangeEvent<HTMLSelectElement>) => {
|
||||
selectEgg(eggs?.find(egg => egg.id.toString() === e.currentTarget.value) || null);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Label>Egg</Label>
|
||||
<Select id={'eggId'} name={'eggId'} value={selectedEggId} onChange={onSelectChange}>
|
||||
<Select id={'eggId'} name={'eggId'} defaultValue={selectedEggId} onChange={onSelectChange}>
|
||||
{!eggs ? (
|
||||
<option disabled>Loading...</option>
|
||||
) : (
|
||||
|
|
|
@ -30,7 +30,6 @@ import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
|||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
import AdminContentBlock from '@/components/admin/AdminContentBlock';
|
||||
import { WithRelationships } from '@/api/admin';
|
||||
|
||||
function InternalForm() {
|
||||
const {
|
||||
|
@ -40,12 +39,12 @@ function InternalForm() {
|
|||
values: { environment },
|
||||
} = useFormikContext<CreateServerRequest>();
|
||||
|
||||
const [egg, setEgg] = useState<WithRelationships<Egg, 'variables'> | undefined>(undefined);
|
||||
const [node, setNode] = useState<Node | undefined>(undefined);
|
||||
const [allocations, setAllocations] = useState<Allocation[] | undefined>(undefined);
|
||||
const [egg, setEgg] = useState<Egg | null>(null);
|
||||
const [node, setNode] = useState<Node | null>(null);
|
||||
const [allocations, setAllocations] = useState<Allocation[] | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (egg === undefined) {
|
||||
if (egg === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -55,7 +54,7 @@ function InternalForm() {
|
|||
}, [egg]);
|
||||
|
||||
useEffect(() => {
|
||||
if (node === undefined) {
|
||||
if (node === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -65,11 +64,11 @@ function InternalForm() {
|
|||
|
||||
return (
|
||||
<Form>
|
||||
<div className="grid grid-cols-2 gap-y-6 gap-x-8 mb-16">
|
||||
<div className="grid grid-cols-1 gap-y-6 col-span-2 md:col-span-1">
|
||||
<div css={tw`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`}>
|
||||
<BaseSettingsBox>
|
||||
<NodeSelect node={node} setNode={setNode} />
|
||||
<div className="xl:col-span-2 bg-neutral-800 border border-neutral-900 shadow-inner p-4 rounded">
|
||||
<div css={tw`xl:col-span-2 bg-neutral-800 border border-neutral-900 shadow-inner p-4 rounded`}>
|
||||
<FormikSwitch
|
||||
name={'startOnCompletion'}
|
||||
label={'Start after installation'}
|
||||
|
@ -78,20 +77,20 @@ function InternalForm() {
|
|||
</div>
|
||||
</BaseSettingsBox>
|
||||
<FeatureLimitsBox />
|
||||
<ServerServiceContainer selectedEggId={egg?.id} setEgg={setEgg} nestId={0} />
|
||||
<ServerServiceContainer egg={egg} setEgg={setEgg} nestId={0} />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-y-6 col-span-2 md:col-span-1">
|
||||
<AdminBox icon={faNetworkWired} title="Networking" isLoading={isSubmitting}>
|
||||
<div className="grid grid-cols-1 gap-4 lg:gap-6">
|
||||
<div css={tw`grid grid-cols-1 gap-y-6 col-span-2 md:col-span-1`}>
|
||||
<AdminBox icon={faNetworkWired} title={'Networking'} isLoading={isSubmitting}>
|
||||
<div css={tw`grid grid-cols-1 gap-4 lg:gap-6`}>
|
||||
<div>
|
||||
<Label htmlFor={'allocation.default'}>Primary Allocation</Label>
|
||||
<Select
|
||||
id={'allocation.default'}
|
||||
name={'allocation.default'}
|
||||
disabled={node === undefined}
|
||||
disabled={node === null}
|
||||
onChange={e => setFieldValue('allocation.default', Number(e.currentTarget.value))}
|
||||
>
|
||||
{node === undefined ? (
|
||||
{node === null ? (
|
||||
<option value="">Select a node...</option>
|
||||
) : (
|
||||
<option value="">Select an allocation...</option>
|
||||
|
@ -117,7 +116,7 @@ function InternalForm() {
|
|||
<ServerImageContainer />
|
||||
</div>
|
||||
|
||||
<AdminBox title={'Startup Command'} className="relative w-full col-span-2">
|
||||
<AdminBox title={'Startup Command'} css={tw`relative w-full col-span-2`}>
|
||||
<SpinnerOverlay visible={isSubmitting} />
|
||||
|
||||
<Field
|
||||
|
@ -132,7 +131,7 @@ function InternalForm() {
|
|||
/>
|
||||
</AdminBox>
|
||||
|
||||
<div className="col-span-2 grid grid-cols-1 md:grid-cols-2 gap-y-6 gap-x-8">
|
||||
<div css={tw`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. */}
|
||||
{egg?.relationships.variables
|
||||
?.filter(v => Object.keys(environment).find(e => e === v.environmentVariable) !== undefined)
|
||||
|
@ -141,9 +140,9 @@ function InternalForm() {
|
|||
))}
|
||||
</div>
|
||||
|
||||
<div className="bg-neutral-700 rounded shadow-md px-4 py-3 col-span-2">
|
||||
<div className="flex flex-row">
|
||||
<Button type="submit" size="small" className="ml-auto" disabled={isSubmitting || !isValid}>
|
||||
<div css={tw`bg-neutral-700 rounded shadow-md px-4 py-3 col-span-2`}>
|
||||
<div css={tw`flex flex-row`}>
|
||||
<Button type="submit" size="small" css={tw`ml-auto`} disabled={isSubmitting || !isValid}>
|
||||
Create Server
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
@ -3,9 +3,11 @@ import { useStoreActions } from 'easy-peasy';
|
|||
import type { FormikHelpers } from 'formik';
|
||||
import { Form, Formik, useField, useFormikContext } from 'formik';
|
||||
import { useEffect, useState } from 'react';
|
||||
import tw from 'twin.macro';
|
||||
import { object } from 'yup';
|
||||
|
||||
import type { Egg, EggVariable, LoadedEgg } from '@/api/admin/egg';
|
||||
import type { InferModel } from '@/api/admin';
|
||||
import type { Egg, EggVariable } from '@/api/admin/egg';
|
||||
import { getEgg } from '@/api/admin/egg';
|
||||
import type { Server } from '@/api/admin/server';
|
||||
import { useServerFromRoute } from '@/api/admin/server';
|
||||
|
@ -21,13 +23,12 @@ import Field from '@/components/elements/Field';
|
|||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||
import Label from '@/components/elements/Label';
|
||||
import type { ApplicationStore } from '@/state';
|
||||
import { WithRelationships } from '@/api/admin';
|
||||
|
||||
function ServerStartupLineContainer({ egg, server }: { egg?: Egg; server: Server }) {
|
||||
function ServerStartupLineContainer({ egg, server }: { egg: Egg | null; server: Server }) {
|
||||
const { isSubmitting, setFieldValue } = useFormikContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (egg === undefined) {
|
||||
if (egg === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -43,10 +44,10 @@ function ServerStartupLineContainer({ egg, server }: { egg?: Egg; server: Server
|
|||
}, [egg]);
|
||||
|
||||
return (
|
||||
<AdminBox title={'Startup Command'} className="relative w-full">
|
||||
<AdminBox title={'Startup Command'} css={tw`relative w-full`}>
|
||||
<SpinnerOverlay visible={isSubmitting} />
|
||||
|
||||
<div className="mb-6">
|
||||
<div css={tw`mb-6`}>
|
||||
<Field
|
||||
id={'startup'}
|
||||
name={'startup'}
|
||||
|
@ -68,12 +69,12 @@ function ServerStartupLineContainer({ egg, server }: { egg?: Egg; server: Server
|
|||
}
|
||||
|
||||
export function ServerServiceContainer({
|
||||
selectedEggId,
|
||||
egg,
|
||||
setEgg,
|
||||
nestId: _nestId,
|
||||
}: {
|
||||
selectedEggId?: number;
|
||||
setEgg: (value: WithRelationships<Egg, 'variables'> | undefined) => void;
|
||||
egg: Egg | null;
|
||||
setEgg: (value: Egg | null) => void;
|
||||
nestId: number;
|
||||
}) {
|
||||
const { isSubmitting } = useFormikContext();
|
||||
|
@ -81,14 +82,14 @@ export function ServerServiceContainer({
|
|||
const [nestId, setNestId] = useState<number>(_nestId);
|
||||
|
||||
return (
|
||||
<AdminBox title={'Service Configuration'} isLoading={isSubmitting} className="w-full">
|
||||
<div className="mb-6">
|
||||
<AdminBox title={'Service Configuration'} isLoading={isSubmitting} css={tw`w-full`}>
|
||||
<div css={tw`mb-6`}>
|
||||
<NestSelector selectedNestId={nestId} onNestSelect={setNestId} />
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<EggSelect nestId={nestId} selectedEggId={selectedEggId} onEggSelect={setEgg} />
|
||||
<div css={tw`mb-6`}>
|
||||
<EggSelect nestId={nestId} selectedEggId={egg?.id} onEggSelect={setEgg} />
|
||||
</div>
|
||||
<div className="bg-neutral-800 border border-neutral-900 shadow-inner p-4 rounded">
|
||||
<div css={tw`bg-neutral-800 border border-neutral-900 shadow-inner p-4 rounded`}>
|
||||
<FormikSwitch name={'skipScripts'} label={'Skip Egg Install Script'} description={'Soon™'} />
|
||||
</div>
|
||||
</AdminBox>
|
||||
|
@ -99,10 +100,10 @@ export function ServerImageContainer() {
|
|||
const { isSubmitting } = useFormikContext();
|
||||
|
||||
return (
|
||||
<AdminBox title={'Image Configuration'} className="relative w-full">
|
||||
<AdminBox title={'Image Configuration'} css={tw`relative w-full`}>
|
||||
<SpinnerOverlay visible={isSubmitting} />
|
||||
|
||||
<div className="md:w-full md:flex md:flex-col">
|
||||
<div css={tw`md:w-full md:flex md:flex-col`}>
|
||||
<div>
|
||||
{/* 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'} />
|
||||
|
@ -129,7 +130,7 @@ export function ServerVariableContainer({ variable, value }: { variable: EggVari
|
|||
}, [value]);
|
||||
|
||||
return (
|
||||
<AdminBox className="relative w-full" title={<p className="text-sm uppercase">{variable.name}</p>}>
|
||||
<AdminBox css={tw`relative w-full`} title={<p css={tw`text-sm uppercase`}>{variable.name}</p>}>
|
||||
<SpinnerOverlay visible={isSubmitting} />
|
||||
|
||||
<Field
|
||||
|
@ -144,14 +145,12 @@ export function ServerVariableContainer({ variable, value }: { variable: EggVari
|
|||
}
|
||||
|
||||
function ServerStartupForm({
|
||||
selectedEggId,
|
||||
egg,
|
||||
setEgg,
|
||||
server,
|
||||
}: {
|
||||
selectedEggId?: number;
|
||||
egg?: LoadedEgg;
|
||||
setEgg: (value: LoadedEgg | undefined) => void;
|
||||
egg: Egg | null;
|
||||
setEgg: (value: Egg | null) => void;
|
||||
server: Server;
|
||||
}) {
|
||||
const {
|
||||
|
@ -162,22 +161,22 @@ function ServerStartupForm({
|
|||
|
||||
return (
|
||||
<Form>
|
||||
<div className="flex flex-col mb-16">
|
||||
<div className="flex flex-row mb-6">
|
||||
<div css={tw`flex flex-col mb-16`}>
|
||||
<div css={tw`flex flex-row mb-6`}>
|
||||
<ServerStartupLineContainer egg={egg} server={server} />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6 mb-6">
|
||||
<div className="flex">
|
||||
<ServerServiceContainer selectedEggId={selectedEggId} setEgg={setEgg} nestId={server.nestId} />
|
||||
<div css={tw`grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6 mb-6`}>
|
||||
<div css={tw`flex`}>
|
||||
<ServerServiceContainer egg={egg} setEgg={setEgg} nestId={server.nestId} />
|
||||
</div>
|
||||
|
||||
<div className="flex">
|
||||
<div css={tw`flex`}>
|
||||
<ServerImageContainer />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-y-6 gap-x-8">
|
||||
<div css={tw`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. */}
|
||||
{egg?.relationships.variables
|
||||
?.filter(v => Object.keys(environment).find(e => e === v.environmentVariable) !== undefined)
|
||||
|
@ -194,9 +193,9 @@ function ServerStartupForm({
|
|||
))}
|
||||
</div>
|
||||
|
||||
<div className="bg-neutral-700 rounded shadow-md py-2 pr-6 mt-6">
|
||||
<div className="flex flex-row">
|
||||
<Button type="submit" size="small" className="ml-auto" disabled={isSubmitting || !isValid}>
|
||||
<div css={tw`bg-neutral-700 rounded shadow-md py-2 pr-6 mt-6`}>
|
||||
<div css={tw`flex flex-row`}>
|
||||
<Button type="submit" size="small" css={tw`ml-auto`} disabled={isSubmitting || !isValid}>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -211,12 +210,10 @@ export default () => {
|
|||
const { clearFlashes, clearAndAddHttpError } = useStoreActions(
|
||||
(actions: Actions<ApplicationStore>) => actions.flashes,
|
||||
);
|
||||
const [egg, setEgg] = useState<LoadedEgg | undefined>(undefined);
|
||||
const [egg, setEgg] = useState<InferModel<typeof getEgg> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!server) {
|
||||
return;
|
||||
}
|
||||
if (!server) return;
|
||||
|
||||
getEgg(server.eggId)
|
||||
.then(egg => setEgg(egg))
|
||||
|
@ -252,10 +249,10 @@ export default () => {
|
|||
validationSchema={object().shape({})}
|
||||
>
|
||||
<ServerStartupForm
|
||||
selectedEggId={egg?.id ?? server.eggId}
|
||||
egg={egg}
|
||||
// @ts-expect-error fix this
|
||||
setEgg={setEgg}
|
||||
server={server as Server}
|
||||
server={server}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
|
|
|
@ -4,7 +4,7 @@ import { useState } from 'react';
|
|||
import Checkbox from '@/components/elements/inputs/Checkbox';
|
||||
import { Dropdown } from '@/components/elements/dropdown';
|
||||
import { Dialog } from '@/components/elements/dialog';
|
||||
import type { User } from '@definitions/admin';
|
||||
import { User } from '@definitions/admin';
|
||||
|
||||
interface Props {
|
||||
user: User;
|
||||
|
@ -12,7 +12,7 @@ interface Props {
|
|||
onRowChange: (user: User, selected: boolean) => void;
|
||||
}
|
||||
|
||||
function UserTableRow({ user, selected, onRowChange }: Props) {
|
||||
const UserTableRow = ({ user, selected, onRowChange }: Props) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
return (
|
||||
|
@ -56,14 +56,12 @@ function UserTableRow({ user, selected, onRowChange }: Props) {
|
|||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-6 py-4">
|
||||
<td className={'whitespace-nowrap px-6 py-4'}>
|
||||
<Dropdown>
|
||||
<Dropdown.Button className="px-2">
|
||||
<Dropdown.Button className={'px-2'}>
|
||||
<DotsVerticalIcon />
|
||||
</Dropdown.Button>
|
||||
<Dropdown.Item to={`/admin/users/${user.id}`} icon={<PencilIcon />}>
|
||||
Edit
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item icon={<PencilIcon />}>Edit</Dropdown.Item>
|
||||
<Dropdown.Item icon={<SupportIcon />}>Reset Password</Dropdown.Item>
|
||||
<Dropdown.Item icon={<LockOpenIcon />} disabled={!user.isUsingTwoFactor}>
|
||||
Disable 2-FA
|
||||
|
@ -78,6 +76,6 @@ function UserTableRow({ user, selected, onRowChange }: Props) {
|
|||
</tr>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default UserTableRow;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { LockOpenIcon, PlusIcon, SupportIcon, TrashIcon } from '@heroicons/react/solid';
|
||||
import { Fragment, useEffect, useState } from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
import { useGetUsers } from '@/api/admin/users';
|
||||
import type { UUID } from '@/api/definitions';
|
||||
|
@ -17,7 +16,7 @@ import { Shape } from '@/components/elements/button/types';
|
|||
|
||||
const filters = ['id', 'uuid', 'external_id', 'username', 'email'] as const;
|
||||
|
||||
function UsersContainer() {
|
||||
const UsersContainer = () => {
|
||||
const [search, setSearch] = useDebouncedState('', 500);
|
||||
const [selected, setSelected] = useState<UUID[]>([]);
|
||||
const { data: users } = useGetUsers(
|
||||
|
@ -43,16 +42,13 @@ function UsersContainer() {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4 flex justify-end">
|
||||
<NavLink to="/admin/users/new">
|
||||
<Button className="shadow focus:ring-offset-2 focus:ring-offset-neutral-800">
|
||||
Add User <PlusIcon className="ml-2 h-5 w-5" />
|
||||
<div className={'mb-4 flex justify-end'}>
|
||||
<Button className={'shadow focus:ring-offset-2 focus:ring-offset-neutral-800'}>
|
||||
Add User <PlusIcon className={'ml-2 h-5 w-5'} />
|
||||
</Button>
|
||||
</NavLink>
|
||||
</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
|
||||
checked={selectAllChecked}
|
||||
disabled={!users?.items.length}
|
||||
|
@ -60,18 +56,22 @@ function UsersContainer() {
|
|||
onChange={onSelectAll}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className={'flex-1'}>
|
||||
<InputField
|
||||
type="text"
|
||||
name="filter"
|
||||
placeholder="Begin typing to filter..."
|
||||
className="w-56 focus:w-96"
|
||||
type={'text'}
|
||||
name={'filter'}
|
||||
placeholder={'Begin typing to filter...'}
|
||||
className={'w-56 focus:w-96'}
|
||||
onChange={e => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
</div>
|
||||
<Transition.Fade as={Fragment} show={selected.length > 0} duration="duration-75">
|
||||
<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">
|
||||
<div className="flex-1">
|
||||
<Transition.Fade as={Fragment} show={selected.length > 0} duration={'duration-75'}>
|
||||
<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'
|
||||
}
|
||||
>
|
||||
<div className={'flex-1'}>
|
||||
<Checkbox
|
||||
checked={selectAllChecked}
|
||||
indeterminate={selected.length !== users?.items.length}
|
||||
|
@ -79,26 +79,26 @@ function UsersContainer() {
|
|||
/>
|
||||
</div>
|
||||
<Button.Text shape={Shape.IconSquare}>
|
||||
<SupportIcon className="h-4 w-4" />
|
||||
<SupportIcon className={'h-4 w-4'} />
|
||||
</Button.Text>
|
||||
<Button.Text shape={Shape.IconSquare}>
|
||||
<LockOpenIcon className="h-4 w-4" />
|
||||
<LockOpenIcon className={'h-4 w-4'} />
|
||||
</Button.Text>
|
||||
<Button.Text shape={Shape.IconSquare}>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
<TrashIcon className={'h-4 w-4'} />
|
||||
</Button.Text>
|
||||
</div>
|
||||
</Transition.Fade>
|
||||
</div>
|
||||
<table className="min-w-full rounded bg-neutral-700">
|
||||
<thead className="bg-neutral-900">
|
||||
<table className={'min-w-full rounded bg-neutral-700'}>
|
||||
<thead className={'bg-neutral-900'}>
|
||||
<tr>
|
||||
<th scope="col" className="w-8" />
|
||||
<th scope="col" className="w-full px-6 py-2 text-left">
|
||||
<th scope={'col'} className={'w-8'} />
|
||||
<th scope={'col'} className={'w-full px-6 py-2 text-left'}>
|
||||
Email
|
||||
</th>
|
||||
<th scope="col" />
|
||||
<th scope="col" />
|
||||
<th scope={'col'} />
|
||||
<th scope={'col'} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -111,10 +111,10 @@ function UsersContainer() {
|
|||
/>
|
||||
))}
|
||||
</tbody>
|
||||
{users ? <TFootPaginated span={4} pagination={users.pagination} /> : null}
|
||||
{users && <TFootPaginated span={4} pagination={users.pagination} />}
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default UsersContainer;
|
||||
|
|
|
@ -40,7 +40,7 @@ const inputStyle = css<Props>`
|
|||
// Reset to normal styling.
|
||||
resize: none;
|
||||
${tw`appearance-none outline-none w-full min-w-0`};
|
||||
${tw`py-2.5 px-3 border-2 rounded text-sm transition-all duration-150`};
|
||||
${tw`p-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`};
|
||||
|
||||
& + .input-help {
|
||||
|
|
|
@ -1,29 +1,27 @@
|
|||
import { ElementType, forwardRef, useMemo } from 'react';
|
||||
import * as React from 'react';
|
||||
import { Menu, Transition } from '@headlessui/react';
|
||||
import classNames from 'classnames';
|
||||
import type { ElementType, ReactNode } from 'react';
|
||||
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';
|
||||
import classNames from 'classnames';
|
||||
import DropdownItem from '@/components/elements/dropdown/DropdownItem';
|
||||
import DropdownButton from '@/components/elements/dropdown/DropdownButton';
|
||||
|
||||
interface Props {
|
||||
as?: ElementType;
|
||||
children: ReactNode;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const DropdownGap = ({ invisible }: { invisible?: boolean }) => (
|
||||
<div className={classNames('m-2 border', { 'border-neutral-700': !invisible, 'border-transparent': invisible })} />
|
||||
);
|
||||
|
||||
type TypedChild = ReactNode & {
|
||||
type TypedChild = (React.ReactChild | React.ReactFragment | React.ReactPortal) & {
|
||||
type?: JSX.Element;
|
||||
};
|
||||
|
||||
const Dropdown = forwardRef<typeof Menu, Props>(({ as, children }, ref) => {
|
||||
const [Button, items] = useMemo(() => {
|
||||
const list = ReactChildren.toArray(children) as unknown as TypedChild[];
|
||||
const list = React.Children.toArray(children) as unknown as TypedChild[];
|
||||
|
||||
return [
|
||||
list.filter(child => child.type === DropdownButton),
|
||||
|
@ -36,18 +34,18 @@ const Dropdown = forwardRef<typeof Menu, Props>(({ as, children }, ref) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<Menu as={as ?? 'div'} className={styles.menu} ref={ref}>
|
||||
<Menu as={as || 'div'} className={styles.menu} ref={ref}>
|
||||
{Button}
|
||||
<Transition
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transition scale-95 opacity-0"
|
||||
enterTo="transform scale-100 opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform scale-100 opacity-100"
|
||||
leaveTo="transform scale-95 opacity-0"
|
||||
enter={'transition duration-100 ease-out'}
|
||||
enterFrom={'transition scale-95 opacity-0'}
|
||||
enterTo={'transform scale-100 opacity-100'}
|
||||
leave={'transition duration-75 ease-out'}
|
||||
leaveFrom={'transform scale-100 opacity-100'}
|
||||
leaveTo={'transform scale-95 opacity-0'}
|
||||
>
|
||||
<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>
|
||||
</Transition>
|
||||
</Menu>
|
||||
|
|
|
@ -1,29 +1,24 @@
|
|||
import { Menu } from '@headlessui/react';
|
||||
import { ChevronDownIcon } from '@heroicons/react/solid';
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import styles from './style.module.css';
|
||||
import styles from '@/components/elements/dropdown/style.module.css';
|
||||
import { ChevronDownIcon } from '@heroicons/react/solid';
|
||||
import { Menu } from '@headlessui/react';
|
||||
import * as React from 'react';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
animate?: boolean;
|
||||
children: ReactNode;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function DropdownButton({ className, animate = true, children }: Props) {
|
||||
return (
|
||||
<Menu.Button className={classNames(styles.button, className ?? 'px-4')}>
|
||||
export default ({ className, animate = true, children }: Props) => (
|
||||
<Menu.Button className={classNames(styles.button, className || 'px-4')}>
|
||||
{typeof children === 'string' ? (
|
||||
<>
|
||||
<span className="mr-2">{children}</span>
|
||||
<ChevronDownIcon aria-hidden="true" data-animated={animate.toString()} />
|
||||
<span className={'mr-2'}>{children}</span>
|
||||
<ChevronDownIcon aria-hidden={'true'} data-animated={animate.toString()} />
|
||||
</>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</Menu.Button>
|
||||
);
|
||||
}
|
||||
|
||||
export { DropdownButton };
|
||||
);
|
||||
|
|
|
@ -1,32 +1,26 @@
|
|||
import { Menu } from '@headlessui/react';
|
||||
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 * as React from 'react';
|
||||
import { Menu } from '@headlessui/react';
|
||||
import styles from './style.module.css';
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode | ((opts: { active: boolean; disabled: boolean }) => JSX.Element);
|
||||
children: React.ReactNode | ((opts: { active: boolean; disabled: boolean }) => JSX.Element);
|
||||
danger?: boolean;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
icon?: JSX.Element;
|
||||
onClick?: (e: MouseEvent) => void;
|
||||
onClick?: (e: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
const DropdownItem = forwardRef<HTMLAnchorElement | HTMLButtonElement, Props & Partial<Omit<NavLinkProps, 'children'>>>(
|
||||
({ disabled, danger, className, onClick, children, icon: IconComponent, ...props }, ref) => {
|
||||
const DropdownItem = forwardRef<HTMLAnchorElement, Props>(
|
||||
({ disabled, danger, className, onClick, children, icon: IconComponent }, ref) => {
|
||||
return (
|
||||
<Menu.Item disabled={disabled}>
|
||||
{({ disabled, active }) => (
|
||||
<>
|
||||
{'to' in props && props.to !== undefined ? (
|
||||
<NavLink
|
||||
{...props}
|
||||
to={props.to}
|
||||
ref={ref as unknown as Ref<HTMLAnchorElement>}
|
||||
<a
|
||||
ref={ref}
|
||||
href={'#'}
|
||||
className={classNames(
|
||||
styles.menu_item,
|
||||
{
|
||||
|
@ -39,30 +33,11 @@ const DropdownItem = forwardRef<HTMLAnchorElement | HTMLButtonElement, Props & P
|
|||
>
|
||||
{IconComponent}
|
||||
{typeof children === 'function' ? children({ disabled, active }) : children}
|
||||
</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>
|
||||
)}
|
||||
</>
|
||||
</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export { DropdownItem };
|
||||
export default DropdownItem;
|
||||
|
|
|
@ -119,14 +119,14 @@ export default ({ database, className }: Props) => {
|
|||
<Can action={'database.view_password'}>
|
||||
<div css={tw`mt-6`}>
|
||||
<Label>Password</Label>
|
||||
<CopyOnClick text={database.password} showInNotification={false}>
|
||||
<CopyOnClick text={database.password}>
|
||||
<Input type={'text'} readOnly value={database.password} />
|
||||
</CopyOnClick>
|
||||
</div>
|
||||
</Can>
|
||||
<div css={tw`mt-6`}>
|
||||
<Label>JDBC Connection String</Label>
|
||||
<CopyOnClick text={jdbcConnectionString} showInNotification={false}>
|
||||
<CopyOnClick text={jdbcConnectionString}>
|
||||
<Input type={'text'} readOnly value={jdbcConnectionString} />
|
||||
</CopyOnClick>
|
||||
</div>
|
||||
|
|
|
@ -18,12 +18,11 @@ import { ServerContext } from '@/state/server';
|
|||
import styles from './style.module.css';
|
||||
|
||||
function Clickable({ file, children }: { file: FileObject; children: ReactNode }) {
|
||||
const [canRead] = usePermissions(['file.read']);
|
||||
const [canReadContents] = usePermissions(['file.read-content']);
|
||||
const id = ServerContext.useStoreState(state => state.server.data!.id);
|
||||
const directory = ServerContext.useStoreState(state => state.files.directory);
|
||||
|
||||
return (file.isFile && (!file.isEditable() || !canReadContents)) || (!file.isFile && !canRead) ? (
|
||||
return !canReadContents || (file.isFile && !file.isEditable()) ? (
|
||||
<div className={styles.details}>{children}</div>
|
||||
) : (
|
||||
<NavLink
|
||||
|
|
|
@ -93,7 +93,7 @@ const MassActionsBar = () => {
|
|||
/>
|
||||
)}
|
||||
<Portal>
|
||||
<div className="pointer-events-none fixed bottom-0 z-50 mb-6 flex w-full justify-center">
|
||||
<div className="fixed bottom-0 z-50 mb-6 flex w-full justify-center">
|
||||
<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">
|
||||
<Button onClick={() => setShowMove(true)}>Move</Button>
|
||||
|
|
14
resources/scripts/lib/base64.spec.ts
Normal file
14
resources/scripts/lib/base64.spec.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { decodeBase64 } from '@/lib/base64';
|
||||
|
||||
describe('@/lib/base64.ts', function () {
|
||||
describe('decodeBase64()', function () {
|
||||
it.each([
|
||||
['', ''],
|
||||
['', ''],
|
||||
])('should decode "%s" to "%s"', function (input, output) {
|
||||
expect(decodeBase64(input)).toBe(output);
|
||||
});
|
||||
});
|
||||
});
|
16
resources/scripts/lib/base64.ts
Normal file
16
resources/scripts/lib/base64.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
function decodeBase64(input: string): string {
|
||||
input = input.replace(/-/g, '+').replace(/_/g, '/');
|
||||
|
||||
const pad = input.length % 4;
|
||||
if (pad) {
|
||||
if (pad === 1) {
|
||||
throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding');
|
||||
}
|
||||
|
||||
input += new Array(5 - pad).join('=');
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
export { decodeBase64 };
|
23
resources/scripts/lib/buffer.spec.ts
Normal file
23
resources/scripts/lib/buffer.spec.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { decodeBuffer, encodeBuffer } from '@/lib/buffer';
|
||||
|
||||
describe('@/lib/buffer.ts', function () {
|
||||
describe('decodeBuffer()', function () {
|
||||
it.each([
|
||||
['', ''],
|
||||
['', ''],
|
||||
])('should decode "%s" to "%s"', function (input, output) {
|
||||
expect(decodeBuffer(input)).toBe(output);
|
||||
});
|
||||
});
|
||||
|
||||
describe('encodeBuffer()', function () {
|
||||
it.each([
|
||||
[new Uint8Array(0), ''],
|
||||
[new Uint8Array(0), ''],
|
||||
])('should encode "%s" to "%s"', function (input, output) {
|
||||
expect(encodeBuffer(input)).toBe(output);
|
||||
});
|
||||
});
|
||||
});
|
9
resources/scripts/lib/buffer.ts
Normal file
9
resources/scripts/lib/buffer.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
function decodeBuffer(value: string): ArrayBuffer {
|
||||
return Uint8Array.from(window.atob(value), c => c.charCodeAt(0));
|
||||
}
|
||||
|
||||
function encodeBuffer(value: ArrayBuffer): string {
|
||||
return btoa(String.fromCharCode(...new Uint8Array(value)));
|
||||
}
|
||||
|
||||
export { decodeBuffer, encodeBuffer };
|
|
@ -1,6 +1,7 @@
|
|||
import type { ComponentType } from 'react';
|
||||
import { lazy } from 'react';
|
||||
|
||||
import ServerConsole from '@/components/server/console/ServerConsoleContainer';
|
||||
import DatabasesContainer from '@/components/server/databases/DatabasesContainer';
|
||||
import ScheduleContainer from '@/components/server/schedules/ScheduleContainer';
|
||||
import UsersContainer from '@/components/server/users/UsersContainer';
|
||||
|
@ -20,7 +21,6 @@ import ServerActivityLogContainer from '@/components/server/ServerActivityLogCon
|
|||
//
|
||||
// These specific lazy loaded routes are to avoid loading in heavy screens
|
||||
// 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 ScheduleEditContainer = lazy(() => import('@/components/server/schedules/ScheduleEditContainer'));
|
||||
|
||||
|
@ -86,7 +86,7 @@ export default {
|
|||
path: '',
|
||||
permission: null,
|
||||
name: 'Console',
|
||||
component: ServerConsoleContainer,
|
||||
component: ServerConsole,
|
||||
end: true,
|
||||
},
|
||||
{
|
||||
|
|
|
@ -36,6 +36,11 @@ Route::prefix('/account')->middleware(AccountSubject::class)->group(function ()
|
|||
Route::post('/api-keys', [Client\ApiKeyController::class, 'store']);
|
||||
Route::delete('/api-keys/{identifier}', [Client\ApiKeyController::class, 'delete']);
|
||||
|
||||
Route::get('/security-keys', [Client\SecurityKeyController::class, 'index'])->withoutMiddleware(RequireTwoFactorAuthentication::class);
|
||||
Route::get('/security-keys/register', [Client\SecurityKeyController::class, 'create'])->withoutMiddleware(RequireTwoFactorAuthentication::class);
|
||||
Route::post('/security-keys/register', [Client\SecurityKeyController::class, 'store'])->withoutMiddleware(RequireTwoFactorAuthentication::class);
|
||||
Route::delete('/security-keys/{securityKey}', [Client\SecurityKeyController::class, 'delete'])->withoutMiddleware(RequireTwoFactorAuthentication::class);
|
||||
|
||||
Route::prefix('/ssh-keys')->group(function () {
|
||||
Route::get('/', [Client\SSHKeyController::class, 'index']);
|
||||
Route::post('/', [Client\SSHKeyController::class, 'store']);
|
||||
|
|
|
@ -25,7 +25,8 @@ Route::get('/password/reset/{token}', [Auth\LoginController::class, 'index'])->n
|
|||
Route::middleware(['throttle:authentication'])->group(function () {
|
||||
// Login endpoints.
|
||||
Route::post('/login', [Auth\LoginController::class, 'login'])->middleware('recaptcha');
|
||||
Route::post('/login/checkpoint', Auth\LoginCheckpointController::class)->name('auth.login-checkpoint');
|
||||
Route::post('/login/checkpoint', [Auth\LoginCheckpointController::class, 'token'])->name('auth.checkpoint');
|
||||
Route::post('/login/checkpoint/key', [Auth\LoginCheckpointController::class, 'key'])->name('auth.checkpoint.key');
|
||||
|
||||
// Forgot password route. A post to this endpoint will trigger an
|
||||
// email to be sent containing a reset token.
|
||||
|
@ -46,5 +47,5 @@ Route::post('/logout', [Auth\LoginController::class, 'logout'])
|
|||
->middleware('auth')
|
||||
->name('auth.logout');
|
||||
|
||||
// Catch any other combinations of routes and pass them off to the React component.
|
||||
// Catch any other combinations of routes and pass them off to the React frontend.
|
||||
Route::fallback([Auth\LoginController::class, 'index']);
|
||||
|
|
|
@ -9,13 +9,7 @@ with pkgs;
|
|||
alejandra
|
||||
composer
|
||||
nodejs-18_x
|
||||
nodePackages.pnpm
|
||||
nodePackages.yarn
|
||||
php81WithExtensions
|
||||
|
||||
docker-compose
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
PATH="$PATH:${pkgs.docker-compose}/libexec/docker/cli-plugins"
|
||||
'';
|
||||
}
|
||||
|
|
|
@ -255,7 +255,7 @@ class UserControllerTest extends ApplicationApiIntegrationTestCase
|
|||
* Endpoints that should return a 403 error when the key does not have write
|
||||
* permissions for user management.
|
||||
*/
|
||||
public static function userWriteEndpointsDataProvider(): array
|
||||
public function userWriteEndpointsDataProvider(): array
|
||||
{
|
||||
return [
|
||||
['postJson', '/api/application/users'],
|
||||
|
|
|
@ -241,7 +241,7 @@ class ApiKeyControllerTest extends ClientApiIntegrationTestCase
|
|||
* Provides some different IP address combinations that can be used when
|
||||
* testing that we accept the expected IP values.
|
||||
*/
|
||||
public static function validIPAddressDataProvider(): array
|
||||
public function validIPAddressDataProvider(): array
|
||||
{
|
||||
return [
|
||||
[[]],
|
||||
|
|
|
@ -331,7 +331,7 @@ class ClientControllerTest extends ClientApiIntegrationTestCase
|
|||
$response->assertJsonPath('data.0.attributes.relationships.allocations.data.0.attributes.notes', null);
|
||||
}
|
||||
|
||||
public static function filterTypeDataProvider(): array
|
||||
public function filterTypeDataProvider(): array
|
||||
{
|
||||
return [['admin'], ['admin-all']];
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue