mirror of
https://github.com/pelican-dev/panel.git
synced 2026-07-16 04:03:50 +03:00
Compare commits
67 Commits
v1.0.0-bet
...
lance/impr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d7675caca | ||
|
|
f0dfadda98 | ||
|
|
d1b7ac026b | ||
|
|
476ce81ebc | ||
|
|
07f1dfe8b8 | ||
|
|
2fd3833e95 | ||
|
|
234d4f0c0d | ||
|
|
2b9c08dc5d | ||
|
|
21ed58839c | ||
|
|
69e88a2ea7 | ||
|
|
cc2aa38ad4 | ||
|
|
1bd4492cdc | ||
|
|
90fbc87a8a | ||
|
|
ac3db5af71 | ||
|
|
a46443e5e4 | ||
|
|
f791a3a686 | ||
|
|
b3fe96f262 | ||
|
|
4d114571e0 | ||
|
|
136224224c | ||
|
|
28452e2820 | ||
|
|
be6abcefe7 | ||
|
|
2a328a24ac | ||
|
|
02e4527b84 | ||
|
|
f676b6c794 | ||
|
|
cb21f13059 | ||
|
|
13bb47e6eb | ||
|
|
447a8c1ff7 | ||
|
|
a99f18b189 | ||
|
|
8f01095f9d | ||
|
|
110fdd2bdb | ||
|
|
81bb09f4cf | ||
|
|
dcba4a91d0 | ||
|
|
8f7dbe5e16 | ||
|
|
92bfccb2a4 | ||
|
|
11a1581a1d | ||
|
|
38620a95f4 | ||
|
|
d520224bc4 | ||
|
|
4464ccb469 | ||
|
|
4ad305c709 | ||
|
|
4b22010576 | ||
|
|
eb4e947ddb | ||
|
|
2bbfb0eef9 | ||
|
|
2a64ea8536 | ||
|
|
4fdbbff74b | ||
|
|
2ed891633c | ||
|
|
58dcbeac0b | ||
|
|
91c5ddb2bd | ||
|
|
562be98b20 | ||
|
|
fd3b8a7ab3 | ||
|
|
1817383bf5 | ||
|
|
d39a0c4464 | ||
|
|
2e48095379 | ||
|
|
06c662988a | ||
|
|
98d7158dfc | ||
|
|
e01d9f2cf3 | ||
|
|
b693d0e728 | ||
|
|
612041e1f8 | ||
|
|
64bcdb514b | ||
|
|
c1105db702 | ||
|
|
761492d09f | ||
|
|
e35ce1e79d | ||
|
|
42c127c004 | ||
|
|
593f209142 | ||
|
|
9bf5b2cf0a | ||
|
|
f76e864a30 | ||
|
|
01cfa31ee1 | ||
|
|
dead664e4d |
8
.github/workflows/build.yaml
vendored
8
.github/workflows/build.yaml
vendored
@@ -13,15 +13,15 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
node-version: [20, 22]
|
||||
node-version: [22, 24]
|
||||
steps:
|
||||
- name: Code Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
php-version: "8.5"
|
||||
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
run: composer install --no-interaction --no-suggest --no-progress --no-autoloader --no-scripts --no-dev
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: "yarn"
|
||||
|
||||
60
.github/workflows/ci.yaml
vendored
60
.github/workflows/ci.yaml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
DB_DATABASE: testing.sqlite
|
||||
steps:
|
||||
- name: Code Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Get cache directory
|
||||
id: composer-cache
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
|
||||
@@ -65,13 +65,13 @@ jobs:
|
||||
run: php artisan migrate --force --seed
|
||||
|
||||
- name: Unit tests
|
||||
run: vendor/bin/pest tests/Unit --parallel
|
||||
run: vendor/bin/pest tests/Unit --parallel --do-not-fail-on-phpunit-warning
|
||||
env:
|
||||
DB_HOST: UNIT_NO_DB
|
||||
SKIP_MIGRATIONS: true
|
||||
|
||||
- name: Integration tests
|
||||
run: vendor/bin/pest tests/Integration --parallel
|
||||
run: vendor/bin/pest tests/Integration --parallel --do-not-fail-on-phpunit-warning
|
||||
|
||||
mysql:
|
||||
name: MySQL
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
MYSQL_DATABASE: testing
|
||||
ports:
|
||||
- 3306
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
|
||||
options: --tmpfs /var/lib/mysql:rw --health-cmd="mysqladmin ping" --health-interval=2s --health-timeout=5s --health-retries=30
|
||||
env:
|
||||
DB_CONNECTION: mysql
|
||||
DB_HOST: 127.0.0.1
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
DB_USERNAME: root
|
||||
steps:
|
||||
- name: Code Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Get cache directory
|
||||
id: composer-cache
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
|
||||
@@ -129,14 +129,8 @@ jobs:
|
||||
DB_PORT: ${{ job.services.database.ports[3306] }}
|
||||
DB_USERNAME: root
|
||||
|
||||
- name: Unit tests
|
||||
run: vendor/bin/pest tests/Unit --parallel
|
||||
env:
|
||||
DB_HOST: UNIT_NO_DB
|
||||
SKIP_MIGRATIONS: true
|
||||
|
||||
- name: Integration tests
|
||||
run: vendor/bin/pest tests/Integration --parallel
|
||||
run: vendor/bin/pest tests/Integration --parallel --do-not-fail-on-phpunit-warning
|
||||
env:
|
||||
DB_PORT: ${{ job.services.database.ports[3306] }}
|
||||
DB_USERNAME: root
|
||||
@@ -157,7 +151,7 @@ jobs:
|
||||
MYSQL_DATABASE: testing
|
||||
ports:
|
||||
- 3306
|
||||
options: --health-cmd="mariadb-admin ping || mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
|
||||
options: --tmpfs /var/lib/mysql:rw --health-cmd="mariadb-admin ping || mysqladmin ping" --health-interval=2s --health-timeout=5s --health-retries=30
|
||||
env:
|
||||
DB_CONNECTION: mariadb
|
||||
DB_HOST: 127.0.0.1
|
||||
@@ -165,7 +159,7 @@ jobs:
|
||||
DB_USERNAME: root
|
||||
steps:
|
||||
- name: Code Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Get cache directory
|
||||
id: composer-cache
|
||||
@@ -173,7 +167,7 @@ jobs:
|
||||
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
|
||||
@@ -197,14 +191,8 @@ jobs:
|
||||
DB_PORT: ${{ job.services.database.ports[3306] }}
|
||||
DB_USERNAME: root
|
||||
|
||||
- name: Unit tests
|
||||
run: vendor/bin/pest tests/Unit --parallel
|
||||
env:
|
||||
DB_HOST: UNIT_NO_DB
|
||||
SKIP_MIGRATIONS: true
|
||||
|
||||
- name: Integration tests
|
||||
run: vendor/bin/pest tests/Integration --parallel
|
||||
run: vendor/bin/pest tests/Integration --parallel --do-not-fail-on-phpunit-warning
|
||||
env:
|
||||
DB_PORT: ${{ job.services.database.ports[3306] }}
|
||||
DB_USERNAME: root
|
||||
@@ -217,6 +205,13 @@ jobs:
|
||||
matrix:
|
||||
php: [8.5]
|
||||
database: ["postgres:17", "postgres:18"]
|
||||
include:
|
||||
# postgres:17 stores data in /var/lib/postgresql/data, 18+ requires a
|
||||
# single mount at /var/lib/postgresql and errors on a data submount
|
||||
- database: "postgres:17"
|
||||
tmpfs: /var/lib/postgresql/data
|
||||
- database: "postgres:18"
|
||||
tmpfs: /var/lib/postgresql
|
||||
services:
|
||||
database:
|
||||
image: ${{ matrix.database }}
|
||||
@@ -228,10 +223,11 @@ jobs:
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--tmpfs ${{ matrix.tmpfs }}:rw
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-interval 2s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
--health-retries 30
|
||||
env:
|
||||
DB_CONNECTION: pgsql
|
||||
DB_HOST: 127.0.0.1
|
||||
@@ -240,7 +236,7 @@ jobs:
|
||||
DB_PASSWORD: postgres
|
||||
steps:
|
||||
- name: Code Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Get cache directory
|
||||
id: composer-cache
|
||||
@@ -248,7 +244,7 @@ jobs:
|
||||
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
|
||||
@@ -269,11 +265,5 @@ jobs:
|
||||
- name: Run Migrations
|
||||
run: php artisan migrate --force --seed
|
||||
|
||||
- name: Unit tests
|
||||
run: vendor/bin/pest tests/Unit --parallel
|
||||
env:
|
||||
DB_HOST: UNIT_NO_DB
|
||||
SKIP_MIGRATIONS: true
|
||||
|
||||
- name: Integration tests
|
||||
run: vendor/bin/pest tests/Integration --parallel
|
||||
run: vendor/bin/pest tests/Integration --parallel --do-not-fail-on-phpunit-warning
|
||||
|
||||
28
.github/workflows/docker-publish.yml
vendored
28
.github/workflows/docker-publish.yml
vendored
@@ -31,13 +31,13 @@ jobs:
|
||||
platform: linux/arm64
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Docker buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Build the base PHP image
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.base
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
run: docker save -o base-php-${{ matrix.arch }}.tar base-php:${{ matrix.arch }}
|
||||
|
||||
- name: Push the docker build to the artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: base-php-${{ matrix.arch }}.tar
|
||||
path: base-php-${{ matrix.arch }}.tar
|
||||
@@ -76,11 +76,11 @@ jobs:
|
||||
if: "!contains(github.ref, 'main') || (!contains(github.event.head_commit.message, 'skip docker') && !contains(github.event.head_commit.message, 'docker skip'))"
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Docker metadata
|
||||
id: docker_meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
flavor: |
|
||||
@@ -91,16 +91,16 @@ jobs:
|
||||
type=ref,event=branch
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
# We Need to start it in host mode else it can't acces the local registry on port 5000
|
||||
- name: Setup Docker buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
with:
|
||||
driver-opts: network=host
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -113,12 +113,12 @@ jobs:
|
||||
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
# Download the base PHP image AMD64
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: base-php-amd64.tar
|
||||
|
||||
# Download the base PHP image ARM64
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: base-php-arm64.tar
|
||||
|
||||
@@ -135,10 +135,10 @@ jobs:
|
||||
- name: Update version in config/app.php (tag)
|
||||
if: "github.event_name == 'release' && github.event.action == 'published'"
|
||||
run: |
|
||||
sed -i "s/'version' => 'canary',/'version' => '${{ steps.build_info.outputs.version_tag }}',/" config/app.php
|
||||
sed -i "s|'version' => 'canary',|'version' => '${{ steps.build_info.outputs.version_tag }}',|" config/app.php
|
||||
|
||||
- name: Build and Push (tag)
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
if: "github.event_name == 'release' && github.event.action == 'published'"
|
||||
with:
|
||||
context: .
|
||||
@@ -153,7 +153,7 @@ jobs:
|
||||
cache-to: type=gha,scope=tagged${{ matrix.os }},mode=max
|
||||
|
||||
- name: Build and Push (main)
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
if: "github.event_name == 'push' && contains(github.ref, 'main')"
|
||||
with:
|
||||
context: .
|
||||
|
||||
36
.github/workflows/download-translations.yml
vendored
Normal file
36
.github/workflows/download-translations.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: Download Crowdin Translations
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0' # Every Sunday at midnight
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
download-translations:
|
||||
name: Download Translations from Crowdin
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download Translations from Crowdin
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
upload_sources: false
|
||||
upload_translations: false
|
||||
download_translations: true
|
||||
export_only_approved: true
|
||||
localization_branch_name: i18n_crowdin_translations
|
||||
create_pull_request: true
|
||||
pull_request_title: 'New Crowdin Translations'
|
||||
pull_request_body: 'New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)'
|
||||
pull_request_base_branch_name: 'main'
|
||||
crowdin_branch_name: main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
10
.github/workflows/lint.yaml
vendored
10
.github/workflows/lint.yaml
vendored
@@ -11,12 +11,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Code Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: "8.4"
|
||||
php-version: "8.5"
|
||||
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
@@ -35,10 +35,10 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
php: [8.2, 8.3, 8.4, 8.5]
|
||||
php: [8.3, 8.4, 8.5]
|
||||
steps:
|
||||
- name: Code Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Get cache directory
|
||||
id: composer-cache
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
|
||||
|
||||
8
.github/workflows/release.yaml
vendored
8
.github/workflows/release.yaml
vendored
@@ -14,12 +14,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Code checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
php-version: "8.5"
|
||||
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
|
||||
tools: composer:v2
|
||||
coverage: none
|
||||
@@ -28,9 +28,9 @@ jobs:
|
||||
run: composer install --no-interaction --no-suggest --no-progress --no-autoloader --no-scripts --no-dev
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install JS dependencies
|
||||
|
||||
32
.github/workflows/upload-sources.yml
vendored
Normal file
32
.github/workflows/upload-sources.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: Upload Crowdin Sources
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- 'lang/en/**'
|
||||
- 'crowdin.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
upload-sources:
|
||||
name: Upload Sources to Crowdin
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Upload Sources to Crowdin
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
download_translations: false
|
||||
crowdin_branch_name: main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
10
Dockerfile
10
Dockerfile
@@ -22,7 +22,7 @@ RUN composer install --no-dev --no-interaction --no-autoloader --no-scripts
|
||||
# ================================
|
||||
# Stage 1-2: Yarn Install
|
||||
# ================================
|
||||
FROM --platform=$TARGETOS/$TARGETARCH node:20-alpine AS yarn
|
||||
FROM --platform=$TARGETOS/$TARGETARCH node:22-alpine AS yarn
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
@@ -74,16 +74,14 @@ COPY --chown=root:www-data --chmod=770 --from=composerbuild /build .
|
||||
COPY --chown=root:www-data --chmod=770 --from=yarnbuild /build/public ./public
|
||||
|
||||
# Create and remove directories
|
||||
RUN mkdir -p /pelican-data/storage /pelican-data/plugins /var/run/supervisord \
|
||||
&& rm -rf /var/www/html/plugins \
|
||||
# Symlinks for env, database, storage, and plugins
|
||||
RUN mkdir -p /pelican-data/storage /var/run/supervisord \
|
||||
# Symlinks for env, database, storage
|
||||
&& ln -s /pelican-data/.env /var/www/html/.env \
|
||||
&& ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \
|
||||
&& ln -s /pelican-data/storage /var/www/html/public/storage \
|
||||
&& ln -s /pelican-data/storage /var/www/html/storage/app/public \
|
||||
&& ln -s /pelican-data/plugins /var/www/html \
|
||||
# Allow www-data write permissions where necessary
|
||||
&& chown -R www-data: /pelican-data .env ./storage ./plugins ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
|
||||
&& chown -R www-data: /pelican-data .env ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
|
||||
&& chmod -R 770 /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \
|
||||
&& chown -R www-data: /usr/local/etc/php/ /usr/local/etc/php-fpm.d/ /var/www/html/composer.json /var/www/html/composer.lock
|
||||
# Configure Supervisor
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# ================================
|
||||
# Stage 0: Build PHP Base Image
|
||||
# ================================
|
||||
FROM --platform=$TARGETOS/$TARGETARCH php:8.4-fpm-alpine
|
||||
FROM --platform=$TARGETOS/$TARGETARCH php:8.5-fpm-alpine
|
||||
|
||||
ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# syntax=docker.io/docker/dockerfile:1.13-labs
|
||||
# Pelican Development Dockerfile
|
||||
|
||||
FROM --platform=$TARGETOS/$TARGETARCH php:8.4-fpm-alpine AS base
|
||||
FROM --platform=$TARGETOS/$TARGETARCH php:8.5-fpm-alpine AS base
|
||||
|
||||
ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
|
||||
|
||||
@@ -26,7 +26,7 @@ RUN composer install --no-dev --no-interaction --no-autoloader --no-scripts
|
||||
# ================================
|
||||
# Stage 1-2: Yarn Install
|
||||
# ================================
|
||||
FROM --platform=$TARGETOS/$TARGETARCH node:20-alpine AS yarn
|
||||
FROM --platform=$TARGETOS/$TARGETARCH node:22-alpine AS yarn
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
@@ -79,16 +79,14 @@ COPY --chown=root:www-data --chmod=770 --from=composerbuild /build .
|
||||
COPY --chown=root:www-data --chmod=770 --from=yarnbuild /build/public ./public
|
||||
|
||||
# Create and remove directories
|
||||
RUN mkdir -p /pelican-data/storage /pelican-data/plugins /var/run/supervisord \
|
||||
&& rm -rf /var/www/html/plugins \
|
||||
# Symlinks for env, database, storage, and plugins
|
||||
RUN mkdir -p /pelican-data/storage /var/run/supervisord \
|
||||
# Symlinks for env, database, storage
|
||||
&& ln -s /pelican-data/.env /var/www/html/.env \
|
||||
&& ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \
|
||||
&& ln -s /pelican-data/storage /var/www/html/public/storage \
|
||||
&& ln -s /pelican-data/storage /var/www/html/storage/app/public \
|
||||
&& ln -s /pelican-data/plugins /var/www/html \
|
||||
# Allow www-data write permissions where necessary
|
||||
&& chown -R www-data: /pelican-data .env ./storage ./plugins ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
|
||||
&& chown -R www-data: /pelican-data .env ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
|
||||
&& chmod -R 770 /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \
|
||||
&& chown -R www-data: /usr/local/etc/php/ /usr/local/etc/php-fpm.d/ /var/www/html/composer.json /var/www/html/composer.lock
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ use App\Services\Eggs\Sharing\EggExporterService;
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use JsonException;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class CheckEggUpdatesCommand extends Command
|
||||
@@ -22,14 +21,12 @@ class CheckEggUpdatesCommand extends Command
|
||||
try {
|
||||
$this->check($egg, $exporterService);
|
||||
} catch (Exception $exception) {
|
||||
$this->error("{$egg->name}: Error ({$exception->getMessage()})");
|
||||
$this->error("$egg->name: Error ({$exception->getMessage()})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws JsonException
|
||||
*/
|
||||
/** @throws Exception */
|
||||
private function check(Egg $egg, EggExporterService $exporterService): void
|
||||
{
|
||||
if (is_null($egg->update_url)) {
|
||||
@@ -38,14 +35,25 @@ class CheckEggUpdatesCommand extends Command
|
||||
return;
|
||||
}
|
||||
|
||||
$ext = strtolower(pathinfo(parse_url($egg->update_url, PHP_URL_PATH), PATHINFO_EXTENSION));
|
||||
$isYaml = in_array($ext, ['yaml', 'yml']);
|
||||
$extension = strtolower(pathinfo(parse_url($egg->update_url, PHP_URL_PATH), PATHINFO_EXTENSION));
|
||||
|
||||
if (empty($extension)) {
|
||||
throw new Exception('Unsupported file format.');
|
||||
}
|
||||
|
||||
$isYaml = in_array($extension, ['yaml', 'yml']);
|
||||
|
||||
$local = $isYaml
|
||||
? Yaml::parse($exporterService->handle($egg->id, EggFormat::YAML))
|
||||
: json_decode($exporterService->handle($egg->id, EggFormat::JSON), true);
|
||||
|
||||
$remote = Http::timeout(5)->connectTimeout(1)->get($egg->update_url)->throw()->body();
|
||||
$remote = Http::timeout(5)->connectTimeout(1)->get($egg->update_url);
|
||||
|
||||
if ($remote->failed()) {
|
||||
throw new Exception("HTTP request returned status code {$remote->status()}");
|
||||
}
|
||||
|
||||
$remote = $remote->body();
|
||||
$remote = $isYaml ? Yaml::parse($remote) : json_decode($remote, true);
|
||||
|
||||
unset($local['exported_at'], $remote['exported_at']);
|
||||
|
||||
@@ -13,7 +13,7 @@ class UpdateEggIndexCommand extends Command
|
||||
public function handle(): int
|
||||
{
|
||||
try {
|
||||
$data = Http::timeout(5)->connectTimeout(1)->get('https://raw.githubusercontent.com/pelican-eggs/pelican-eggs.github.io/refs/heads/main/content/pelican.json')->throw()->json();
|
||||
$data = Http::timeout(5)->connectTimeout(1)->get(config('panel.cdn.egg_index_url'))->throw()->json();
|
||||
} catch (Exception $exception) {
|
||||
$this->error($exception->getMessage());
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ class DatabaseSettingsCommand extends Command
|
||||
'sqlite' => 'SQLite (recommended)',
|
||||
'mariadb' => 'MariaDB',
|
||||
'mysql' => 'MySQL',
|
||||
'pgsql' => 'PostgreSQL',
|
||||
];
|
||||
|
||||
protected $description = 'Configure database settings for the Panel.';
|
||||
@@ -23,10 +24,10 @@ class DatabaseSettingsCommand extends Command
|
||||
protected $signature = 'p:environment:database
|
||||
{--driver= : The database driver backend to use.}
|
||||
{--database= : The database to use.}
|
||||
{--host= : The connection address for the MySQL/ MariaDB server.}
|
||||
{--port= : The connection port for the MySQL/ MariaDB server.}
|
||||
{--username= : Username to use when connecting to the MySQL/ MariaDB server.}
|
||||
{--password= : Password to use for the MySQL/ MariaDB database.}';
|
||||
{--host= : The connection address for the MySQL/ MariaDB/ PostgreSQL server.}
|
||||
{--port= : The connection port for the MySQL/ MariaDB/ PostgreSQL server.}
|
||||
{--username= : Username to use when connecting to the MySQL/ MariaDB/ PostgreSQL server.}
|
||||
{--password= : Password to use for the MySQL/ MariaDB/ PostgreSQL database.}';
|
||||
|
||||
/** @var array<array-key, mixed> */
|
||||
protected array $variables = [];
|
||||
@@ -176,6 +177,68 @@ class DatabaseSettingsCommand extends Command
|
||||
return $this->handle();
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
} elseif ($this->variables['DB_CONNECTION'] === 'pgsql') {
|
||||
$this->output->note(trans('commands.database_settings.DB_HOST_note'));
|
||||
$this->variables['DB_HOST'] = $this->option('host') ?? $this->ask(
|
||||
'Database Host',
|
||||
config('database.connections.pgsql.host', '127.0.0.1')
|
||||
);
|
||||
|
||||
$this->variables['DB_PORT'] = $this->option('port') ?? $this->ask(
|
||||
'Database Port',
|
||||
config('database.connections.pgsql.port', 5432)
|
||||
);
|
||||
|
||||
$this->variables['DB_DATABASE'] = $this->option('database') ?? $this->ask(
|
||||
'Database Name',
|
||||
config('database.connections.pgsql.database', 'panel')
|
||||
);
|
||||
|
||||
$this->output->note(trans('commands.database_settings.DB_USERNAME_note'));
|
||||
$this->variables['DB_USERNAME'] = $this->option('username') ?? $this->ask(
|
||||
'Database Username',
|
||||
config('database.connections.pgsql.username', 'pelican')
|
||||
);
|
||||
|
||||
$askForPgSQLPassword = true;
|
||||
if (!empty(config('database.connections.pgsql.password')) && $this->input->isInteractive()) {
|
||||
$this->variables['DB_PASSWORD'] = config('database.connections.pgsql.password');
|
||||
$askForPgSQLPassword = $this->confirm(trans('commands.database_settings.DB_PASSWORD_note'));
|
||||
}
|
||||
|
||||
if ($askForPgSQLPassword) {
|
||||
$this->variables['DB_PASSWORD'] = $this->option('password') ?? $this->secret('Database Password');
|
||||
}
|
||||
|
||||
try {
|
||||
// Test connection
|
||||
config()->set('database.connections._panel_command_test', [
|
||||
'driver' => 'pgsql',
|
||||
'host' => $this->variables['DB_HOST'],
|
||||
'port' => $this->variables['DB_PORT'],
|
||||
'database' => $this->variables['DB_DATABASE'],
|
||||
'username' => $this->variables['DB_USERNAME'],
|
||||
'password' => $this->variables['DB_PASSWORD'],
|
||||
'charset' => 'utf8',
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'search_path' => 'public',
|
||||
'sslmode' => 'prefer',
|
||||
]);
|
||||
|
||||
$this->database->connection('_panel_command_test')->getPdo();
|
||||
} catch (PDOException $exception) {
|
||||
$this->output->error(sprintf('Unable to connect to the PostgreSQL server using the provided credentials. The error returned was "%s".', $exception->getMessage()));
|
||||
$this->output->error(trans('commands.database_settings.DB_error_2'));
|
||||
|
||||
if ($this->confirm(trans('commands.database_settings.go_back'))) {
|
||||
$this->database->disconnect('_panel_command_test');
|
||||
|
||||
return $this->handle();
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
} elseif ($this->variables['DB_CONNECTION'] === 'sqlite') {
|
||||
|
||||
@@ -16,7 +16,7 @@ class DisablePluginCommand extends Command
|
||||
{
|
||||
$id = $this->argument('id') ?? $this->choice('Plugin', Plugin::pluck('name', 'id')->toArray());
|
||||
|
||||
$plugin = Plugin::find($id);
|
||||
$plugin = Plugin::find(str($id)->lower()->toString());
|
||||
|
||||
if (!$plugin) {
|
||||
$this->error('Plugin does not exist!');
|
||||
|
||||
@@ -18,7 +18,7 @@ class InstallPluginCommand extends Command
|
||||
{
|
||||
$id = $this->argument('id') ?? $this->choice('Plugin', Plugin::pluck('name', 'id')->toArray());
|
||||
|
||||
$plugin = Plugin::find($id);
|
||||
$plugin = Plugin::find(str($id)->lower()->toString());
|
||||
|
||||
if (!$plugin) {
|
||||
$this->error('Plugin does not exist!');
|
||||
|
||||
@@ -18,7 +18,7 @@ class UninstallPluginCommand extends Command
|
||||
{
|
||||
$id = $this->argument('id') ?? $this->choice('Plugin', Plugin::pluck('name', 'id')->toArray());
|
||||
|
||||
$plugin = Plugin::find($id);
|
||||
$plugin = Plugin::find(str($id)->lower()->toString());
|
||||
|
||||
if (!$plugin) {
|
||||
$this->error('Plugin does not exist!');
|
||||
|
||||
@@ -17,7 +17,7 @@ class UpdatePluginCommand extends Command
|
||||
{
|
||||
$id = $this->argument('id') ?? $this->choice('Plugin', Plugin::pluck('name', 'id')->toArray());
|
||||
|
||||
$plugin = Plugin::find($id);
|
||||
$plugin = Plugin::find(str($id)->lower()->toString());
|
||||
|
||||
if (!$plugin) {
|
||||
$this->error('Plugin does not exist!');
|
||||
|
||||
@@ -41,17 +41,8 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
|
||||
};
|
||||
}
|
||||
|
||||
public function getColor(bool $hex = false): string
|
||||
public function getColor(): string
|
||||
{
|
||||
if ($hex) {
|
||||
return match ($this) {
|
||||
self::Created, self::Restarting => '#2563EB',
|
||||
self::Starting, self::Paused, self::Removing, self::Stopping => '#D97706',
|
||||
self::Running => '#22C55E',
|
||||
self::Exited, self::Missing, self::Dead, self::Offline => '#EF4444',
|
||||
};
|
||||
}
|
||||
|
||||
return match ($this) {
|
||||
self::Created => 'primary',
|
||||
self::Starting => 'warning',
|
||||
|
||||
@@ -43,12 +43,12 @@ enum ResourceLimit: string
|
||||
public function limit(): Limit
|
||||
{
|
||||
return match ($this) {
|
||||
self::Websocket => Limit::perMinute(5),
|
||||
self::BackupRestore => Limit::perMinutes(15, 3),
|
||||
self::DatabaseCreate => Limit::perMinute(2),
|
||||
self::SubuserCreate => Limit::perMinutes(15, 10),
|
||||
self::FilePull => Limit::perMinutes(10, 5),
|
||||
default => Limit::perMinute(2),
|
||||
self::Websocket => Limit::perMinutes(config('http.rate_limit.websocket_period'), config('http.rate_limit.websocket')),
|
||||
self::BackupRestore => Limit::perMinutes(config('http.rate_limit.backup_restore_period'), config('http.rate_limit.backup_restore')),
|
||||
self::DatabaseCreate => Limit::perMinutes(config('http.rate_limit.database_create_period'), config('http.rate_limit.database_create')),
|
||||
self::SubuserCreate => Limit::perMinutes(config('http.rate_limit.subuser_create_period'), config('http.rate_limit.subuser_create')),
|
||||
self::FilePull => Limit::perMinutes(config('http.rate_limit.file_pull_period'), config('http.rate_limit.file_pull')),
|
||||
default => Limit::perMinutes(config('http.rate_limit.default_period'), config('http.rate_limit.default')),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -25,16 +25,8 @@ enum ServerState: string implements HasColor, HasIcon, HasLabel
|
||||
};
|
||||
}
|
||||
|
||||
public function getColor(bool $hex = false): string
|
||||
public function getColor(): string
|
||||
{
|
||||
if ($hex) {
|
||||
return match ($this) {
|
||||
self::Installing, self::RestoringBackup => '#2563EB',
|
||||
self::Suspended => '#D97706',
|
||||
self::InstallFailed, self::ReinstallFailed => '#EF4444',
|
||||
};
|
||||
}
|
||||
|
||||
return match ($this) {
|
||||
self::Installing => 'primary',
|
||||
self::InstallFailed => 'danger',
|
||||
|
||||
@@ -50,6 +50,9 @@ enum SubuserPermission: string
|
||||
|
||||
case ActivityRead = 'activity.read';
|
||||
|
||||
case MountRead = 'mount.read';
|
||||
case MountUpdate = 'mount.update';
|
||||
|
||||
case StartupRead = 'startup.read';
|
||||
case StartupUpdate = 'startup.update';
|
||||
case StartupDockerImage = 'startup.docker-image';
|
||||
@@ -57,6 +60,7 @@ enum SubuserPermission: string
|
||||
case SettingsRename = 'settings.rename';
|
||||
case SettingsDescription = 'settings.description';
|
||||
case SettingsReinstall = 'settings.reinstall';
|
||||
case SettingsChangeIcon = 'settings.change-icon';
|
||||
|
||||
/** @return string[] */
|
||||
public function split(): array
|
||||
@@ -84,6 +88,7 @@ enum SubuserPermission: string
|
||||
'schedule' => TablerIcon::Clock,
|
||||
'settings' => TablerIcon::Settings,
|
||||
'activity' => TablerIcon::Stack,
|
||||
'mount' => TablerIcon::LayersLinked,
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
17
app/Events/Server/BackupCompleted.php
Normal file
17
app/Events/Server/BackupCompleted.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\Server;
|
||||
|
||||
use App\Events\Event;
|
||||
use App\Models\Backup;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class BackupCompleted extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public Backup $backup) {}
|
||||
}
|
||||
17
app/Events/User/Deleting.php
Normal file
17
app/Events/User/Deleting.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\User;
|
||||
|
||||
use App\Events\Event;
|
||||
use App\Models\User;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Deleting extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct(public User $user) {}
|
||||
}
|
||||
13
app/Events/User/PasswordChanged.php
Normal file
13
app/Events/User/PasswordChanged.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events\User;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
|
||||
final class PasswordChanged
|
||||
{
|
||||
use Dispatchable;
|
||||
|
||||
public function __construct(public readonly User $user) {}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ class Dashboard extends BaseDashboard
|
||||
|
||||
public function getHeading(): string
|
||||
{
|
||||
return trans('admin/dashboard.heading');
|
||||
return trans('admin/dashboard.heading', ['app' => config('app.name')]);
|
||||
}
|
||||
|
||||
public function getSubheading(): string
|
||||
|
||||
@@ -14,13 +14,14 @@ use Boquizo\FilamentLogViewer\Utils\Level;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Contracts\Support\Htmlable;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class ListLogs extends BaseListLogs
|
||||
{
|
||||
protected string $view = 'filament.components.list-logs';
|
||||
|
||||
public function getHeading(): string|null|\Illuminate\Contracts\Support\Htmlable
|
||||
public function getHeading(): string|null|Htmlable
|
||||
{
|
||||
return trans('admin/log.navigation.panel_logs');
|
||||
}
|
||||
|
||||
@@ -31,11 +31,13 @@ use Filament\Pages\Concerns\InteractsWithHeaderActions;
|
||||
use Filament\Pages\Page;
|
||||
use Filament\Schemas\Components\Actions;
|
||||
use Filament\Schemas\Components\Component;
|
||||
use Filament\Schemas\Components\Fieldset;
|
||||
use Filament\Schemas\Components\Group;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\StateCasts\BooleanStateCast;
|
||||
use Filament\Schemas\Components\Tabs;
|
||||
use Filament\Schemas\Components\Tabs\Tab;
|
||||
use Filament\Schemas\Components\Text;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
use Filament\Schemas\Components\Utilities\Set;
|
||||
use Filament\Schemas\Contracts\HasSchemas;
|
||||
@@ -683,6 +685,36 @@ class Settings extends Page implements HasSchemas
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
Toggle::make('PANEL_SEND_ACCOUNT_CREATED_NOTIFICATION')
|
||||
->label(trans('admin/setting.misc.mail_notifications.account_created'))
|
||||
->onIcon(TablerIcon::Check)
|
||||
->offIcon(TablerIcon::X)
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->live()
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_ACCOUNT_CREATED_NOTIFICATION', (bool) $state))
|
||||
->default(env('PANEL_SEND_ACCOUNT_CREATED_NOTIFICATION', config('panel.email.send_account_created_notification'))),
|
||||
Toggle::make('PANEL_SEND_ADDED_TO_SERVER_NOTIFICATION')
|
||||
->label(trans('admin/setting.misc.mail_notifications.added_to_server'))
|
||||
->onIcon(TablerIcon::Check)
|
||||
->offIcon(TablerIcon::X)
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->live()
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_ADDED_TO_SERVER_NOTIFICATION', (bool) $state))
|
||||
->default(env('PANEL_SEND_ADDED_TO_SERVER_NOTIFICATION', config('panel.email.send_added_to_server_notification'))),
|
||||
Toggle::make('PANEL_SEND_REMOVED_FROM_SERVER_NOTIFICATION')
|
||||
->label(trans('admin/setting.misc.mail_notifications.removed_from_server'))
|
||||
->onIcon(TablerIcon::Check)
|
||||
->offIcon(TablerIcon::X)
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->live()
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_REMOVED_FROM_SERVER_NOTIFICATION', (bool) $state))
|
||||
->default(env('PANEL_SEND_REMOVED_FROM_SERVER_NOTIFICATION', config('panel.email.send_removed_from_server_notification'))),
|
||||
Toggle::make('PANEL_SEND_INSTALL_NOTIFICATION')
|
||||
->label(trans('admin/setting.misc.mail_notifications.server_installed'))
|
||||
->onIcon(TablerIcon::Check)
|
||||
@@ -703,6 +735,16 @@ class Settings extends Page implements HasSchemas
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_REINSTALL_NOTIFICATION', (bool) $state))
|
||||
->default(env('PANEL_SEND_REINSTALL_NOTIFICATION', config('panel.email.send_reinstall_notification'))),
|
||||
Toggle::make('PANEL_SEND_BACKUP_COMPLETED_NOTIFICATION')
|
||||
->label(trans('admin/setting.misc.mail_notifications.backup_completed'))
|
||||
->onIcon(TablerIcon::Check)
|
||||
->offIcon(TablerIcon::X)
|
||||
->onColor('success')
|
||||
->offColor('danger')
|
||||
->live()
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_SEND_BACKUP_COMPLETED_NOTIFICATION', (bool) $state))
|
||||
->default(env('PANEL_SEND_BACKUP_COMPLETED_NOTIFICATION', config('panel.email.send_backup_completed_notification'))),
|
||||
]),
|
||||
Section::make(trans('admin/setting.misc.connections.title'))
|
||||
->description(trans('admin/setting.misc.connections.helper'))
|
||||
@@ -754,24 +796,172 @@ class Settings extends Page implements HasSchemas
|
||||
]),
|
||||
Section::make(trans('admin/setting.misc.api.title'))
|
||||
->description(trans('admin/setting.misc.api.helper'))
|
||||
->columns()
|
||||
->columns(1)
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->schema([
|
||||
TextInput::make('APP_API_CLIENT_RATELIMIT')
|
||||
->label(trans('admin/setting.misc.api.client_rate'))
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->suffix(trans('admin/setting.misc.api.rpm'))
|
||||
->default(env('APP_API_CLIENT_RATELIMIT', config('http.rate_limit.client'))),
|
||||
TextInput::make('APP_API_APPLICATION_RATELIMIT')
|
||||
->label(trans('admin/setting.misc.api.app_rate'))
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->suffix(trans('admin/setting.misc.api.rpm'))
|
||||
->default(env('APP_API_APPLICATION_RATELIMIT', config('http.rate_limit.application'))),
|
||||
Fieldset::make()
|
||||
->label(trans('admin/setting.misc.api.client_title'))
|
||||
->schema([
|
||||
TextInput::make('APP_API_CLIENT_RATELIMIT')
|
||||
->label(trans('admin/setting.misc.api.requests_per_period'))
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->suffix(trans('admin/setting.misc.api.requests'))->default(env('APP_API_CLIENT_RATELIMIT', config('http.rate_limit.client'))),
|
||||
|
||||
TextInput::make('APP_API_CLIENT_RATELIMIT_PERIOD')
|
||||
->label(trans('admin/setting.misc.api.period_length'))
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->suffix(trans('admin/setting.misc.api.minutes'))->default(env('APP_API_CLIENT_RATELIMIT_PERIOD', config('http.rate_limit.client_period'))),
|
||||
Text::make(trans('admin/setting.misc.api.client_helper')),
|
||||
]),
|
||||
Fieldset::make()
|
||||
->label(trans('admin/setting.misc.api.application_title'))
|
||||
->schema([
|
||||
TextInput::make('APP_API_APPLICATION_RATELIMIT')
|
||||
->label(trans('admin/setting.misc.api.requests_per_period'))
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->suffix(trans('admin/setting.misc.api.requests'))->default(env('APP_API_APPLICATION_RATELIMIT', config('http.rate_limit.application'))),
|
||||
|
||||
TextInput::make('APP_API_APPLICATION_RATELIMIT_PERIOD')
|
||||
->label(trans('admin/setting.misc.api.period_length'))
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->suffix(trans('admin/setting.misc.api.minutes'))->default(env('APP_API_APPLICATION_RATELIMIT_PERIOD', config('http.rate_limit.application_period'))),
|
||||
Text::make(trans('admin/setting.misc.api.application_helper')),
|
||||
]),
|
||||
Fieldset::make()
|
||||
->label(trans('admin/setting.misc.api.password_reset_title'))
|
||||
->schema([
|
||||
TextInput::make('APP_API_PASSWORD_RESET_RATELIMIT')
|
||||
->label(trans('admin/setting.misc.api.requests_per_period'))
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->suffix(trans('admin/setting.misc.api.requests'))->default(env('APP_API_PASSWORD_RESET_RATELIMIT', config('http.rate_limit.password_reset'))),
|
||||
|
||||
TextInput::make('APP_API_PASSWORD_RESET_RATELIMIT_PERIOD')
|
||||
->label(trans('admin/setting.misc.api.period_length'))
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->suffix(trans('admin/setting.misc.api.minutes'))->default(env('APP_API_PASSWORD_RESET_RATELIMIT_PERIOD', config('http.rate_limit.password_reset_period'))),
|
||||
Text::make(trans('admin/setting.misc.api.password_reset_helper')),
|
||||
]),
|
||||
Fieldset::make()
|
||||
->label(trans('admin/setting.misc.api.websocket_title'))
|
||||
->schema([
|
||||
TextInput::make('APP_API_WEBSOCKET_RATELIMIT')
|
||||
->label(trans('admin/setting.misc.api.requests_per_period'))
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->suffix(trans('admin/setting.misc.api.requests'))->default(env('APP_API_WEBSOCKET_RATELIMIT', config('http.rate_limit.websocket'))),
|
||||
|
||||
TextInput::make('APP_API_WEBSOCKET_RATELIMIT_PERIOD')
|
||||
->label(trans('admin/setting.misc.api.period_length'))
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->suffix(trans('admin/setting.misc.api.minutes'))->default(env('APP_API_WEBSOCKET_RATELIMIT_PERIOD', config('http.rate_limit.websocket_period'))),
|
||||
Text::make(trans('admin/setting.misc.api.websocket_helper')),
|
||||
]),
|
||||
Fieldset::make()
|
||||
->label(trans('admin/setting.misc.api.backup_restore_title'))
|
||||
->schema([
|
||||
TextInput::make('APP_API_BACKUP_RESTORE_RATELIMIT')
|
||||
->label(trans('admin/setting.misc.api.requests_per_period'))
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->suffix(trans('admin/setting.misc.api.requests'))->default(env('APP_API_BACKUP_RESTORE_RATELIMIT', config('http.rate_limit.backup_restore'))),
|
||||
|
||||
TextInput::make('APP_API_BACKUP_RESTORE_RATELIMIT_PERIOD')
|
||||
->label(trans('admin/setting.misc.api.period_length'))
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->suffix(trans('admin/setting.misc.api.minutes'))->default(env('APP_API_BACKUP_RESTORE_RATELIMIT_PERIOD', config('http.rate_limit.backup_restore_period'))),
|
||||
Text::make(trans('admin/setting.misc.api.backup_restore_helper')),
|
||||
]),
|
||||
Fieldset::make()
|
||||
->label(trans('admin/setting.misc.api.database_create_title'))
|
||||
->schema([
|
||||
TextInput::make('APP_API_DATABASE_CREATE_RATELIMIT')
|
||||
->label(trans('admin/setting.misc.api.requests_per_period'))
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->suffix(trans('admin/setting.misc.api.requests'))->default(env('APP_API_DATABASE_CREATE_RATELIMIT', config('http.rate_limit.database_create'))),
|
||||
|
||||
TextInput::make('APP_API_DATABASE_CREATE_RATELIMIT_PERIOD')
|
||||
->label(trans('admin/setting.misc.api.period_length'))
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->suffix(trans('admin/setting.misc.api.minutes'))->default(env('APP_API_DATABASE_CREATE_RATELIMIT_PERIOD', config('http.rate_limit.database_create_period'))),
|
||||
Text::make(trans('admin/setting.misc.api.database_create_helper')),
|
||||
]),
|
||||
Fieldset::make()
|
||||
->label(trans('admin/setting.misc.api.subuser_create_title'))
|
||||
->schema([
|
||||
TextInput::make('APP_API_SUBUSER_CREATE_RATELIMIT')
|
||||
->label(trans('admin/setting.misc.api.requests_per_period'))
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->suffix(trans('admin/setting.misc.api.requests'))->default(env('APP_API_SUBUSER_CREATE_RATELIMIT', config('http.rate_limit.subuser_create'))),
|
||||
|
||||
TextInput::make('APP_API_SUBUSER_CREATE_RATELIMIT_PERIOD')
|
||||
->label(trans('admin/setting.misc.api.period_length'))
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->suffix(trans('admin/setting.misc.api.minutes'))->default(env('APP_API_SUBUSER_CREATE_RATELIMIT_PERIOD', config('http.rate_limit.subuser_create_period'))),
|
||||
Text::make(trans('admin/setting.misc.api.subuser_create_helper')),
|
||||
]),
|
||||
Fieldset::make()
|
||||
->label(trans('admin/setting.misc.api.file_pull_title'))
|
||||
->schema([
|
||||
TextInput::make('APP_API_FILE_PULL_RATELIMIT')
|
||||
->label(trans('admin/setting.misc.api.requests_per_period'))
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->suffix(trans('admin/setting.misc.api.requests'))->default(env('APP_API_FILE_PULL_RATELIMIT', config('http.rate_limit.file_pull'))),
|
||||
|
||||
TextInput::make('APP_API_FILE_PULL_RATELIMIT_PERIOD')
|
||||
->label(trans('admin/setting.misc.api.period_length'))
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->suffix(trans('admin/setting.misc.api.minutes'))->default(env('APP_API_FILE_PULL_RATELIMIT_PERIOD', config('http.rate_limit.file_pull_period'))),
|
||||
Text::make(trans('admin/setting.misc.api.file_pull_helper')),
|
||||
]),
|
||||
Fieldset::make()
|
||||
->label(trans('admin/setting.misc.api.default_title'))
|
||||
->schema([
|
||||
TextInput::make('APP_API_DEFAULT_RATELIMIT')
|
||||
->label(trans('admin/setting.misc.api.requests_per_period'))
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->suffix(trans('admin/setting.misc.api.requests'))->default(env('APP_API_DEFAULT_RATELIMIT', config('http.rate_limit.default'))),
|
||||
|
||||
TextInput::make('APP_API_DEFAULT_RATELIMIT_PERIOD')
|
||||
->label(trans('admin/setting.misc.api.period_length'))
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->suffix(trans('admin/setting.misc.api.minutes'))->default(env('APP_API_DEFAULT_RATELIMIT_PERIOD', config('http.rate_limit.default_period'))),
|
||||
Text::make(trans('admin/setting.misc.api.default_helper')),
|
||||
]),
|
||||
]),
|
||||
Section::make(trans('admin/setting.misc.server.title'))
|
||||
->description(trans('admin/setting.misc.server.helper'))
|
||||
@@ -790,13 +980,14 @@ class Settings extends Page implements HasSchemas
|
||||
->formatStateUsing(fn ($state): bool => (bool) $state)
|
||||
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_EDITABLE_SERVER_DESCRIPTIONS', (bool) $state))
|
||||
->default(env('PANEL_EDITABLE_SERVER_DESCRIPTIONS', config('panel.editable_server_descriptions'))),
|
||||
FileUpload::make('ConsoleFonts')
|
||||
FileUpload::make('console_font')
|
||||
->hint(trans('admin/setting.misc.server.console_font_hint'))
|
||||
->label(trans('admin/setting.misc.server.console_font_upload'))
|
||||
->directory('fonts')
|
||||
->disk('public')
|
||||
->columnSpan(1)
|
||||
->maxFiles(1)
|
||||
->acceptedFileTypes(['font/*'])
|
||||
->preserveFilenames(),
|
||||
]),
|
||||
Section::make(trans('admin/setting.misc.webhook.title'))
|
||||
@@ -824,9 +1015,11 @@ class Settings extends Page implements HasSchemas
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
abort_unless(user()?->can('update settings'), 403);
|
||||
|
||||
try {
|
||||
$data = $this->form->getState();
|
||||
unset($data['ConsoleFonts']);
|
||||
unset($data['console_font']);
|
||||
|
||||
$data = array_map(function ($value) {
|
||||
// Convert bools to a string, so they are correctly written to the .env file
|
||||
@@ -873,6 +1066,5 @@ class Settings extends Page implements HasSchemas
|
||||
->authorize(fn () => user()?->can('update settings'))
|
||||
->keyBindings(['mod+s']),
|
||||
];
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,7 @@ class CreateEgg extends CreateRecord
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
->required()
|
||||
->reorderable()
|
||||
->addActionLabel(trans('admin/egg.add_startup'))
|
||||
->keyLabel(trans('admin/egg.startup_name'))
|
||||
->keyPlaceholder('Default')
|
||||
@@ -135,6 +136,7 @@ class CreateEgg extends CreateRecord
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
->required()
|
||||
->reorderable()
|
||||
->addActionLabel(trans('admin/egg.add_image'))
|
||||
->keyLabel(trans('admin/egg.docker_name'))
|
||||
->keyPlaceholder('Java 21')
|
||||
@@ -176,7 +178,6 @@ class CreateEgg extends CreateRecord
|
||||
->grid()
|
||||
->relationship('variables')
|
||||
->orderColumn()
|
||||
->reorderAction(fn (Action $action) => $action->hiddenLabel()->tooltip(fn () => $action->getLabel()))
|
||||
->collapsible()->collapsed()
|
||||
->columnSpan(2)
|
||||
->defaultItems(0)
|
||||
|
||||
@@ -5,8 +5,10 @@ namespace App\Filament\Admin\Resources\Eggs\Pages;
|
||||
use App\Enums\EditorLanguages;
|
||||
use App\Enums\TablerIcon;
|
||||
use App\Filament\Admin\Resources\Eggs\EggResource;
|
||||
use App\Filament\Components\Actions\DeleteIcon;
|
||||
use App\Filament\Components\Actions\ExportEggAction;
|
||||
use App\Filament\Components\Actions\ImportEggAction;
|
||||
use App\Filament\Components\Actions\UploadIcon;
|
||||
use App\Filament\Components\Forms\Fields\CopyFrom;
|
||||
use App\Filament\Components\Forms\Fields\MonacoEditor;
|
||||
use App\Models\Egg;
|
||||
@@ -14,12 +16,10 @@ use App\Models\EggVariable;
|
||||
use App\Traits\Filament\CanCustomizeHeaderActions;
|
||||
use App\Traits\Filament\CanCustomizeHeaderWidgets;
|
||||
use App\Traits\Filament\CanCustomizeTabs;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\ActionGroup;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\KeyValue;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
@@ -28,11 +28,8 @@ use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Filament\Schemas\Components\Fieldset;
|
||||
use Filament\Schemas\Components\Flex;
|
||||
use Filament\Schemas\Components\Grid;
|
||||
use Filament\Schemas\Components\Image;
|
||||
use Filament\Schemas\Components\Tabs;
|
||||
@@ -40,10 +37,7 @@ use Filament\Schemas\Components\Tabs\Tab;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
use Filament\Schemas\Components\Utilities\Set;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Enums\IconSize;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Validation\Rules\Unique;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
|
||||
class EditEgg extends EditRecord
|
||||
{
|
||||
@@ -74,163 +68,17 @@ class EditEgg extends EditRecord
|
||||
->icon(TablerIcon::Egg)
|
||||
->schema([
|
||||
Grid::make(2)
|
||||
->columnSpan(1)
|
||||
->columnStart(1)
|
||||
->schema([
|
||||
Image::make('', '')
|
||||
->hidden(fn ($record) => !$record->image)
|
||||
->url(fn ($record) => $record->image)
|
||||
->alt('')
|
||||
->alignJustify()
|
||||
Image::make('', 'icon')
|
||||
->hidden(fn ($record) => !$record->icon)
|
||||
->url(fn ($record) => $record->icon)
|
||||
->imageSize(150)
|
||||
->columnSpanFull(),
|
||||
Flex::make([
|
||||
Action::make('uploadImage')
|
||||
->hiddenLabel()
|
||||
->tooltip(trans('admin/egg.import.import_image'))
|
||||
->iconSize(IconSize::Large)
|
||||
->icon(TablerIcon::PhotoUp)
|
||||
->modal()
|
||||
->modalHeading('')
|
||||
->modalSubmitActionLabel(trans('admin/egg.import.import_image'))
|
||||
->schema([
|
||||
Tabs::make()
|
||||
->contained(false)
|
||||
->tabs([
|
||||
Tab::make(trans('admin/egg.import.url'))
|
||||
->schema([
|
||||
Hidden::make('imageUrl'),
|
||||
Hidden::make('imageExtension'),
|
||||
TextInput::make('image_url')
|
||||
->label(trans('admin/egg.import.image_url'))
|
||||
->reactive()
|
||||
->autocomplete(false)
|
||||
->debounce(500)
|
||||
->afterStateUpdated(function ($state, Set $set) {
|
||||
if (!$state) {
|
||||
$set('image_url_error', null);
|
||||
$set('imageUrl', null);
|
||||
$set('imageExtension', null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!filter_var($state, FILTER_VALIDATE_URL)) {
|
||||
throw new Exception(trans('admin/egg.import.invalid_url'));
|
||||
}
|
||||
|
||||
$extension = strtolower(pathinfo(parse_url($state, PHP_URL_PATH), PATHINFO_EXTENSION));
|
||||
|
||||
if (!array_key_exists($extension, Egg::IMAGE_FORMATS)) {
|
||||
throw new Exception(trans('admin/egg.import.unsupported_format', ['format' => implode(', ', array_keys(Egg::IMAGE_FORMATS))]));
|
||||
}
|
||||
|
||||
$host = parse_url($state, PHP_URL_HOST);
|
||||
$ip = gethostbyname($host);
|
||||
|
||||
if (
|
||||
filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false
|
||||
) {
|
||||
throw new Exception(trans('admin/egg.import.no_local_ip'));
|
||||
}
|
||||
|
||||
$set('imageUrl', $state);
|
||||
$set('imageExtension', $extension);
|
||||
$set('image_url_error', null);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$set('image_url_error', $e->getMessage());
|
||||
$set('imageUrl', null);
|
||||
$set('imageExtension', null);
|
||||
}
|
||||
}),
|
||||
TextEntry::make('image_url_error')
|
||||
->hiddenLabel()
|
||||
->visible(fn ($get) => $get('image_url_error') !== null)
|
||||
->afterStateHydrated(fn ($set, $get) => $get('image_url_error')),
|
||||
Image::make(fn (Get $get) => $get('image_url'), '')
|
||||
->imageSize(150)
|
||||
->visible(fn ($get) => $get('image_url') && !$get('image_url_error'))
|
||||
->alignCenter(),
|
||||
]),
|
||||
Tab::make(trans('admin/egg.import.file'))
|
||||
->schema([
|
||||
FileUpload::make('image')
|
||||
->hiddenLabel()
|
||||
->previewable()
|
||||
->openable(false)
|
||||
->downloadable(false)
|
||||
->maxSize(256)
|
||||
->maxFiles(1)
|
||||
->columnSpanFull()
|
||||
->alignCenter()
|
||||
->imageEditor()
|
||||
->image()
|
||||
->disk('public')
|
||||
->directory(Egg::ICON_STORAGE_PATH)
|
||||
->acceptedFileTypes([
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/webp',
|
||||
'image/svg+xml',
|
||||
])
|
||||
->getUploadedFileNameForStorageUsing(function (TemporaryUploadedFile $file, $record) {
|
||||
return $record->uuid . '.' . $file->getClientOriginalExtension();
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
->action(function (array $data, $record): void {
|
||||
if (!empty($data['imageUrl']) && !empty($data['imageExtension'])) {
|
||||
$this->saveImageFromUrl($data['imageUrl'], $data['imageExtension'], $record);
|
||||
|
||||
Notification::make()
|
||||
->title(trans('admin/egg.import.image_updated'))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!empty($data['image'])) {
|
||||
Notification::make()
|
||||
->title(trans('admin/egg.import.image_updated'))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (empty($data['imageUrl']) && empty($data['image'])) {
|
||||
Notification::make()
|
||||
->title(trans('admin/egg.import.no_image'))
|
||||
->warning()
|
||||
->send();
|
||||
}
|
||||
}),
|
||||
Action::make('delete_image')
|
||||
->visible(fn ($record) => $record->image)
|
||||
->hiddenLabel()
|
||||
->tooltip(trans('admin/egg.import.delete_image'))
|
||||
->icon(TablerIcon::Trash)
|
||||
->iconSize(IconSize::Large)
|
||||
->color('danger')
|
||||
->action(function ($record) {
|
||||
foreach (array_keys(Egg::IMAGE_FORMATS) as $ext) {
|
||||
$path = Egg::ICON_STORAGE_PATH . "/$record->uuid.$ext";
|
||||
if (Storage::disk('public')->exists($path)) {
|
||||
Storage::disk('public')->delete($path);
|
||||
}
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title(trans('admin/egg.import.image_deleted'))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
$record->refresh();
|
||||
}),
|
||||
]),
|
||||
->columnSpanFull()
|
||||
->alignJustify(),
|
||||
UploadIcon::make(),
|
||||
DeleteIcon::make()
|
||||
->iconStoragePath(Egg::getIconStoragePath()),
|
||||
]),
|
||||
TextInput::make('name')
|
||||
->label(trans('admin/egg.name'))
|
||||
@@ -270,6 +118,7 @@ class EditEgg extends EditRecord
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
->required()
|
||||
->reorderable()
|
||||
->addActionLabel(trans('admin/egg.add_startup'))
|
||||
->keyLabel(trans('admin/egg.startup_name'))
|
||||
->valueLabel(trans('admin/egg.startup_command'))
|
||||
@@ -297,6 +146,7 @@ class EditEgg extends EditRecord
|
||||
->live()
|
||||
->columnSpanFull()
|
||||
->required()
|
||||
->reorderable()
|
||||
->addActionLabel(trans('admin/egg.add_image'))
|
||||
->keyLabel(trans('admin/egg.docker_name'))
|
||||
->valueLabel(trans('admin/egg.docker_uri'))
|
||||
@@ -334,7 +184,6 @@ class EditEgg extends EditRecord
|
||||
->grid()
|
||||
->relationship('variables')
|
||||
->orderColumn()
|
||||
->reorderAction(fn (Action $action) => $action->hiddenLabel()->tooltip(fn () => $action->getLabel()))
|
||||
->collapsible()->collapsed()
|
||||
->addActionLabel(trans('admin/egg.add_new_variable'))
|
||||
->itemLabel(fn (array $state) => $state['name'])
|
||||
@@ -469,39 +318,6 @@ class EditEgg extends EditRecord
|
||||
$this->fillForm();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an image from URL download to a file.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
private function saveImageFromUrl(string $imageUrl, string $extension, Egg $egg): void
|
||||
{
|
||||
$context = stream_context_create([
|
||||
'http' => ['timeout' => 3],
|
||||
'https' => [
|
||||
'timeout' => 3,
|
||||
'verify_peer' => true,
|
||||
'verify_peer_name' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$normalizedExtension = match ($extension) {
|
||||
'svg+xml', 'svg' => 'svg',
|
||||
'jpeg', 'jpg' => 'jpg',
|
||||
'png' => 'png',
|
||||
'webp' => 'webp',
|
||||
default => throw new Exception(trans('admin/egg.import.unknown_extension')),
|
||||
};
|
||||
|
||||
$data = @file_get_contents($imageUrl, false, $context, 0, 1048576); // 1024KB
|
||||
|
||||
if (empty($data)) {
|
||||
throw new Exception(trans('admin/egg.import.invalid_url'));
|
||||
}
|
||||
|
||||
Storage::disk('public')->put(Egg::ICON_STORAGE_PATH . "/$egg->uuid.$normalizedExtension", $data);
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return [];
|
||||
|
||||
@@ -38,6 +38,10 @@ class ListEggs extends ListRecords
|
||||
*/
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
$defaultEggIcon = config('app.logo');
|
||||
$defaultEggIcon = empty($defaultEggIcon) || !is_file(public_path($defaultEggIcon)) ? 'pelican.svg' : $defaultEggIcon;
|
||||
$defaultEggIcon = 'data:image/svg+xml;base64,' . base64_encode(file_get_contents(public_path($defaultEggIcon)));
|
||||
|
||||
return $table
|
||||
->searchable(true)
|
||||
->defaultPaginationPageOption(25)
|
||||
@@ -45,13 +49,11 @@ class ListEggs extends ListRecords
|
||||
TextColumn::make('id')
|
||||
->label('Id')
|
||||
->hidden(),
|
||||
ImageColumn::make('image')
|
||||
ImageColumn::make('icon')
|
||||
->label('')
|
||||
->alignCenter()
|
||||
->circular()
|
||||
->getStateUsing(fn ($record) => $record->image
|
||||
? $record->image
|
||||
: 'data:image/svg+xml;base64,' . base64_encode(file_get_contents(public_path('pelican.svg')))),
|
||||
->getStateUsing(fn (Egg $record) => $record->icon ?: $defaultEggIcon),
|
||||
TextColumn::make('name')
|
||||
->label(trans('admin/egg.name'))
|
||||
->description(fn ($record): ?string => (strlen($record->description) > 120) ? substr($record->description, 0, 120).'...' : $record->description)
|
||||
|
||||
@@ -95,6 +95,12 @@ class MountResource extends Resource
|
||||
->icon(fn ($state) => $state ? TablerIcon::WritingOff : TablerIcon::Writing)
|
||||
->color(fn ($state) => $state ? 'success' : 'warning')
|
||||
->formatStateUsing(fn ($state) => $state ? trans('admin/mount.toggles.read_only') : trans('admin/mount.toggles.writable')),
|
||||
TextColumn::make('user_mountable')
|
||||
->label(trans('admin/mount.table.user_mountable'))
|
||||
->badge()
|
||||
->icon(fn ($state) => $state ? TablerIcon::User : TablerIcon::UserOff)
|
||||
->color(fn ($state) => $state ? 'success' : 'warning')
|
||||
->formatStateUsing(fn ($state) => $state ? trans('admin/mount.toggles.user_mountable') : trans('admin/mount.toggles.not_user_mountable')),
|
||||
])
|
||||
->recordActions([
|
||||
ViewAction::make()
|
||||
@@ -124,7 +130,8 @@ class MountResource extends Resource
|
||||
->label(trans('admin/mount.name'))
|
||||
->required()
|
||||
->helperText(trans('admin/mount.name_help'))
|
||||
->maxLength(64),
|
||||
->maxLength(64)
|
||||
->columnSpanFull(),
|
||||
ToggleButtons::make('read_only')
|
||||
->label(trans('admin/mount.read_only'))
|
||||
->helperText(trans('admin/mount.read_only_help'))
|
||||
@@ -143,6 +150,24 @@ class MountResource extends Resource
|
||||
])
|
||||
->inline()
|
||||
->default(false),
|
||||
ToggleButtons::make('user_mountable')
|
||||
->label(trans('admin/mount.user_mountable'))
|
||||
->helperText(trans('admin/mount.user_mountable_help'))
|
||||
->stateCast(new BooleanStateCast(false, true))
|
||||
->options([
|
||||
false => trans('admin/mount.toggles.not_user_mountable'),
|
||||
true => trans('admin/mount.toggles.user_mountable'),
|
||||
])
|
||||
->icons([
|
||||
false => TablerIcon::UserOff,
|
||||
true => TablerIcon::User,
|
||||
])
|
||||
->colors([
|
||||
false => 'warning',
|
||||
true => 'success',
|
||||
])
|
||||
->inline()
|
||||
->default(true),
|
||||
TextInput::make('source')
|
||||
->label(trans('admin/mount.source'))
|
||||
->required()
|
||||
|
||||
@@ -42,7 +42,6 @@ class CreateMount extends CreateRecord
|
||||
protected function handleRecordCreation(array $data): Model
|
||||
{
|
||||
$data['uuid'] ??= Str::uuid()->toString();
|
||||
$data['user_mountable'] = 1;
|
||||
|
||||
return parent::handleRecordCreation($data);
|
||||
}
|
||||
|
||||
@@ -64,9 +64,8 @@ class CreateNode extends CreateRecord
|
||||
->icon(TablerIcon::Server)
|
||||
->columnSpanFull()
|
||||
->columns([
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 3,
|
||||
'default' => 1,
|
||||
'md' => 2,
|
||||
'lg' => 4,
|
||||
])
|
||||
->schema([
|
||||
@@ -76,81 +75,83 @@ class CreateNode extends CreateRecord
|
||||
->autofocus()
|
||||
->live(debounce: 1500)
|
||||
->rules(Node::getRulesForField('fqdn'))
|
||||
->prohibited(fn ($state) => is_ip($state) && request()->isSecure())
|
||||
->label(fn ($state) => is_ip($state) ? trans('admin/node.ip_address') : trans('admin/node.domain'))
|
||||
->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com')
|
||||
->helperText(function ($state) {
|
||||
->helperText(fn () => request()->isSecure() ? trans('admin/node.fqdn_ssl') : null)
|
||||
->validationMessages([
|
||||
'prohibited' => trans('admin/node.dns_error'),
|
||||
])
|
||||
->prohibited(function ($state, Get $get) {
|
||||
if (!$state) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_ip($state)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$ip = $get('ip');
|
||||
|
||||
return !is_ip($ip);
|
||||
})
|
||||
->hintColor(function ($state, Get $get) {
|
||||
if (!$state) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_ip($state)) {
|
||||
if (request()->isSecure()) {
|
||||
return trans('admin/node.fqdn_help');
|
||||
return 'warning';
|
||||
}
|
||||
} else {
|
||||
$ip = $get('ip');
|
||||
|
||||
return '';
|
||||
return is_ip($ip) ? 'success' : 'danger';
|
||||
}
|
||||
|
||||
return trans('admin/node.error');
|
||||
return null;
|
||||
})
|
||||
->hintColor('danger')
|
||||
->hint(function ($state) {
|
||||
if (is_ip($state) && request()->isSecure()) {
|
||||
return trans('admin/node.ssl_ip');
|
||||
->hint(function ($state, Get $get) {
|
||||
if (!$state) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return '';
|
||||
if (is_ip($state)) {
|
||||
if (request()->isSecure()) {
|
||||
return trans('admin/node.ssl_ip');
|
||||
}
|
||||
} else {
|
||||
$ip = $get('ip');
|
||||
|
||||
return is_ip($ip) ? trans('admin/node.valid') . ': ' . $ip : trans('admin/node.invalid');
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
->afterStateUpdated(function (Set $set, ?string $state) {
|
||||
$set('dns', null);
|
||||
$set('ip', null);
|
||||
|
||||
if (!$state) {
|
||||
return;
|
||||
}
|
||||
|
||||
[$subdomain] = str($state)->explode('.', 2);
|
||||
if (!is_numeric($subdomain)) {
|
||||
$set('name', $subdomain);
|
||||
}
|
||||
|
||||
if (!$state || is_ip($state)) {
|
||||
$set('dns', null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$ip = get_ip_from_hostname($state);
|
||||
if ($ip) {
|
||||
$set('dns', true);
|
||||
|
||||
$set('ip', $ip);
|
||||
} else {
|
||||
$set('dns', false);
|
||||
if (!is_ip($state)) {
|
||||
$ip = get_ip_from_hostname($state);
|
||||
if (is_ip($ip)) {
|
||||
$set('ip', $ip);
|
||||
} else {
|
||||
$set('ip', null);
|
||||
}
|
||||
}
|
||||
})
|
||||
->maxLength(255),
|
||||
|
||||
TextInput::make('ip')
|
||||
->disabled()
|
||||
->hidden(),
|
||||
|
||||
ToggleButtons::make('dns')
|
||||
->label(trans('admin/node.dns'))
|
||||
->helperText(trans('admin/node.dns_help'))
|
||||
->disabled()
|
||||
->inline()
|
||||
->default(null)
|
||||
->hint(fn (Get $get) => $get('ip'))
|
||||
->hintColor('success')
|
||||
->options([
|
||||
true => trans('admin/node.valid'),
|
||||
false => trans('admin/node.invalid'),
|
||||
])
|
||||
->colors([
|
||||
true => 'success',
|
||||
false => 'danger',
|
||||
])
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
]),
|
||||
|
||||
Hidden::make('ip')
|
||||
->saved(false),
|
||||
TextInput::make('daemon_connect')
|
||||
->columnSpan(1)
|
||||
->label(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port') : trans('admin/node.port'))
|
||||
@@ -160,7 +161,16 @@ class CreateNode extends CreateRecord
|
||||
->default(8080)
|
||||
->required()
|
||||
->integer(),
|
||||
|
||||
TextInput::make('daemon_listen')
|
||||
->columnSpan(1)
|
||||
->label(trans('admin/node.listen_port'))
|
||||
->helperText(trans('admin/node.listen_port_help'))
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(8080)
|
||||
->required()
|
||||
->integer()
|
||||
->visible(fn (Get $get) => $get('connection') === 'https_proxy'),
|
||||
TextInput::make('name')
|
||||
->label(trans('admin/node.display_name'))
|
||||
->columnSpan([
|
||||
@@ -171,27 +181,16 @@ class CreateNode extends CreateRecord
|
||||
])
|
||||
->required()
|
||||
->maxLength(100),
|
||||
|
||||
Hidden::make('scheme')
|
||||
->default(fn () => request()->isSecure() ? 'https' : 'http'),
|
||||
|
||||
Hidden::make('behind_proxy')
|
||||
->default(false),
|
||||
|
||||
ToggleButtons::make('connection')
|
||||
->label(trans('admin/node.ssl'))
|
||||
->columnSpan(1)
|
||||
->columnSpan(2)
|
||||
->inline()
|
||||
->helperText(function (Get $get) {
|
||||
->helperText(function () {
|
||||
if (request()->isSecure()) {
|
||||
return new HtmlString(trans('admin/node.panel_on_ssl'));
|
||||
return trans('admin/node.panel_on_ssl');
|
||||
}
|
||||
|
||||
if (is_ip($get('fqdn'))) {
|
||||
return trans('admin/node.ssl_help');
|
||||
}
|
||||
|
||||
return '';
|
||||
return null;
|
||||
})
|
||||
->disableOptionWhen(fn (string $value) => $value === 'http' && request()->isSecure())
|
||||
->options([
|
||||
@@ -219,17 +218,10 @@ class CreateNode extends CreateRecord
|
||||
$set('daemon_connect', $state === 'https_proxy' ? 443 : 8080);
|
||||
$set('daemon_listen', 8080);
|
||||
}),
|
||||
|
||||
TextInput::make('daemon_listen')
|
||||
->columnSpan(1)
|
||||
->label(trans('admin/node.listen_port'))
|
||||
->helperText(trans('admin/node.listen_port_help'))
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(8080)
|
||||
->required()
|
||||
->integer()
|
||||
->visible(fn (Get $get) => $get('connection') === 'https_proxy'),
|
||||
Hidden::make('scheme')
|
||||
->default(fn () => request()->isSecure() ? 'https' : 'http'),
|
||||
Hidden::make('behind_proxy')
|
||||
->default(false),
|
||||
]),
|
||||
Step::make('advanced')
|
||||
->label(trans('admin/node.tabs.advanced_settings'))
|
||||
|
||||
@@ -137,74 +137,83 @@ class EditNode extends EditRecord
|
||||
->autofocus()
|
||||
->live(debounce: 1500)
|
||||
->rules(Node::getRulesForField('fqdn'))
|
||||
->prohibited(fn ($state) => is_ip($state) && request()->isSecure())
|
||||
->label(fn ($state) => is_ip($state) ? trans('admin/node.ip_address') : trans('admin/node.domain'))
|
||||
->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com')
|
||||
->helperText(function ($state) {
|
||||
->helperText(fn () => request()->isSecure() ? trans('admin/node.fqdn_ssl') : null)
|
||||
->validationMessages([
|
||||
'prohibited' => trans('admin/node.dns_error'),
|
||||
])
|
||||
->prohibited(function ($state, Get $get) {
|
||||
if (!$state) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_ip($state)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$ip = $get('ip');
|
||||
|
||||
return !is_ip($ip);
|
||||
})
|
||||
->hintColor(function ($state, Get $get) {
|
||||
if (!$state) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_ip($state)) {
|
||||
if (request()->isSecure()) {
|
||||
return trans('admin/node.fqdn_help');
|
||||
return 'warning';
|
||||
}
|
||||
} else {
|
||||
$ip = $get('ip');
|
||||
|
||||
return '';
|
||||
return is_ip($ip) ? 'success' : 'danger';
|
||||
}
|
||||
|
||||
return trans('admin/node.error');
|
||||
return null;
|
||||
})
|
||||
->hintColor('danger')
|
||||
->hint(function ($state) {
|
||||
if (is_ip($state) && request()->isSecure()) {
|
||||
return trans('admin/node.ssl_ip');
|
||||
->hint(function ($state, Get $get) {
|
||||
if (!$state) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return '';
|
||||
if (is_ip($state)) {
|
||||
if (request()->isSecure()) {
|
||||
return trans('admin/node.ssl_ip');
|
||||
}
|
||||
} else {
|
||||
$ip = $get('ip');
|
||||
|
||||
return is_ip($ip) ? trans('admin/node.valid') . ': ' . $ip : trans('admin/node.invalid');
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
->afterStateUpdated(function (Set $set, ?string $state) {
|
||||
$set('dns', null);
|
||||
$set('ip', null);
|
||||
|
||||
if (!$state) {
|
||||
return;
|
||||
}
|
||||
|
||||
[$subdomain] = str($state)->explode('.', 2);
|
||||
if (!is_numeric($subdomain)) {
|
||||
$set('name', $subdomain);
|
||||
}
|
||||
|
||||
if (!$state || is_ip($state)) {
|
||||
$set('dns', null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$ip = get_ip_from_hostname($state);
|
||||
if ($ip) {
|
||||
$set('dns', true);
|
||||
|
||||
$set('ip', $ip);
|
||||
} else {
|
||||
$set('dns', false);
|
||||
if (!is_ip($state)) {
|
||||
$ip = get_ip_from_hostname($state);
|
||||
if (is_ip($ip)) {
|
||||
$set('ip', $ip);
|
||||
} else {
|
||||
$set('ip', null);
|
||||
}
|
||||
}
|
||||
})
|
||||
->maxLength(255),
|
||||
TextInput::make('ip')
|
||||
->disabled()
|
||||
->hidden(),
|
||||
ToggleButtons::make('dns')
|
||||
->label(trans('admin/node.dns'))
|
||||
->helperText(trans('admin/node.dns_help'))
|
||||
->disabled()
|
||||
->inline()
|
||||
->default(null)
|
||||
->hint(fn (Get $get) => $get('ip'))
|
||||
->hintColor('success')
|
||||
->stateCast(new BooleanStateCast(false, true))
|
||||
->options([
|
||||
1 => trans('admin/node.valid'),
|
||||
0 => trans('admin/node.invalid'),
|
||||
])
|
||||
->colors([
|
||||
1 => 'success',
|
||||
0 => 'danger',
|
||||
])
|
||||
->columnSpan(1),
|
||||
Hidden::make('ip')
|
||||
->saved(false),
|
||||
TextInput::make('daemon_connect')
|
||||
->columnSpan(1)
|
||||
->label(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port') : trans('admin/node.port'))
|
||||
@@ -214,6 +223,16 @@ class EditNode extends EditRecord
|
||||
->default(8080)
|
||||
->required()
|
||||
->integer(),
|
||||
TextInput::make('daemon_listen')
|
||||
->columnSpan(1)
|
||||
->label(trans('admin/node.listen_port'))
|
||||
->helperText(trans('admin/node.listen_port_help'))
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(8080)
|
||||
->required()
|
||||
->integer()
|
||||
->visible(fn (Get $get) => $get('connection') === 'https_proxy'),
|
||||
TextInput::make('name')
|
||||
->label(trans('admin/node.display_name'))
|
||||
->columnSpan([
|
||||
@@ -224,22 +243,16 @@ class EditNode extends EditRecord
|
||||
])
|
||||
->required()
|
||||
->maxLength(100),
|
||||
Hidden::make('scheme'),
|
||||
Hidden::make('behind_proxy'),
|
||||
ToggleButtons::make('connection')
|
||||
->label(trans('admin/node.ssl'))
|
||||
->columnSpan(1)
|
||||
->columnSpan(2)
|
||||
->inline()
|
||||
->helperText(function (Get $get) {
|
||||
->helperText(function () {
|
||||
if (request()->isSecure()) {
|
||||
return new HtmlString(trans('admin/node.panel_on_ssl'));
|
||||
return trans('admin/node.panel_on_ssl');
|
||||
}
|
||||
|
||||
if (is_ip($get('fqdn'))) {
|
||||
return trans('admin/node.ssl_help');
|
||||
}
|
||||
|
||||
return '';
|
||||
return null;
|
||||
})
|
||||
->disableOptionWhen(fn (string $value) => $value === 'http' && request()->isSecure())
|
||||
->options([
|
||||
@@ -267,16 +280,10 @@ class EditNode extends EditRecord
|
||||
$set('daemon_connect', $state === 'https_proxy' ? 443 : 8080);
|
||||
$set('daemon_listen', 8080);
|
||||
}),
|
||||
TextInput::make('daemon_listen')
|
||||
->columnSpan(1)
|
||||
->label(trans('admin/node.listen_port'))
|
||||
->helperText(trans('admin/node.listen_port_help'))
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->default(8080)
|
||||
->required()
|
||||
->integer()
|
||||
->visible(fn (Get $get) => $get('connection') === 'https_proxy'),
|
||||
Hidden::make('scheme')
|
||||
->default(fn () => request()->isSecure() ? 'https' : 'http'),
|
||||
Hidden::make('behind_proxy')
|
||||
->default(false),
|
||||
]),
|
||||
Tab::make('advanced_settings')
|
||||
->label(trans('admin/node.tabs.advanced_settings'))
|
||||
@@ -735,7 +742,7 @@ class EditNode extends EditRecord
|
||||
$set('pulled', false);
|
||||
$set('uploaded', true);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
} catch (Exception $e) {
|
||||
Notification::make()
|
||||
->title(trans('admin/node.diagnostics.upload_failed'))
|
||||
->body($e->getMessage())
|
||||
|
||||
@@ -5,6 +5,9 @@ namespace App\Filament\Admin\Resources\Plugins;
|
||||
use App\Enums\PluginStatus;
|
||||
use App\Enums\TablerIcon;
|
||||
use App\Filament\Admin\Resources\Plugins\Pages\ListPlugins;
|
||||
use App\Jobs\Plugin\InstallPlugin;
|
||||
use App\Jobs\Plugin\UninstallPlugin;
|
||||
use App\Jobs\Plugin\UpdatePlugin;
|
||||
use App\Models\Plugin;
|
||||
use App\Services\Helpers\PluginService;
|
||||
use BackedEnum;
|
||||
@@ -119,15 +122,14 @@ class PluginResource extends Resource
|
||||
->icon(TablerIcon::Terminal)
|
||||
->color('success')
|
||||
->hidden(fn (Plugin $plugin) => $plugin->status !== PluginStatus::NotInstalled)
|
||||
->action(function (Plugin $plugin, $livewire, PluginService $pluginService) {
|
||||
->action(function (Plugin $plugin) {
|
||||
try {
|
||||
$pluginService->installPlugin($plugin, !$plugin->isTheme() || !$pluginService->hasThemePluginEnabled());
|
||||
|
||||
redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab]));
|
||||
InstallPlugin::dispatch(user(), $plugin);
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title(trans('admin/plugin.notifications.installed'))
|
||||
->title(trans('admin/plugin.notifications.install_started'))
|
||||
->body(trans('admin/plugin.notifications.background_info'))
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
@@ -143,15 +145,14 @@ class PluginResource extends Resource
|
||||
->icon(TablerIcon::Download)
|
||||
->color('success')
|
||||
->visible(fn (Plugin $plugin) => $plugin->status !== PluginStatus::NotInstalled && $plugin->isUpdateAvailable())
|
||||
->action(function (Plugin $plugin, $livewire, PluginService $pluginService) {
|
||||
->action(function (Plugin $plugin) {
|
||||
try {
|
||||
$pluginService->updatePlugin($plugin);
|
||||
|
||||
redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab]));
|
||||
UpdatePlugin::dispatch(user(), $plugin);
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title(trans('admin/plugin.notifications.updated'))
|
||||
->title(trans('admin/plugin.notifications.update_started'))
|
||||
->body(trans('admin/plugin.notifications.background_info'))
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
@@ -220,15 +221,14 @@ class PluginResource extends Resource
|
||||
->color('danger')
|
||||
->requiresConfirmation()
|
||||
->hidden(fn (Plugin $plugin) => $plugin->status === PluginStatus::NotInstalled || $plugin->status === PluginStatus::Errored)
|
||||
->action(function (Plugin $plugin, $livewire, PluginService $pluginService) {
|
||||
->action(function (Plugin $plugin) {
|
||||
try {
|
||||
$pluginService->uninstallPlugin($plugin);
|
||||
|
||||
redirect(ListPlugins::getUrl(['tab' => $livewire->activeTab]));
|
||||
UninstallPlugin::dispatch(user(), $plugin);
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title(trans('admin/plugin.notifications.uninstalled'))
|
||||
->title(trans('admin/plugin.notifications.uninstall_started'))
|
||||
->body(trans('admin/plugin.notifications.background_info'))
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
Notification::make()
|
||||
@@ -260,7 +260,7 @@ class PluginResource extends Resource
|
||||
/** @var UploadedFile $file */
|
||||
$file = $data['file'];
|
||||
|
||||
$pluginName = str($file->getClientOriginalName())->before('.zip')->toString();
|
||||
$pluginName = str($file->getClientOriginalName())->basename()->before('.zip')->toString();
|
||||
|
||||
if (Plugin::where('id', $pluginName)->exists()) {
|
||||
throw new Exception(trans('admin/plugin.notifications.import_exists'));
|
||||
|
||||
@@ -5,8 +5,9 @@ namespace App\Filament\Admin\Resources\Servers\Pages;
|
||||
use App\Enums\SuspendAction;
|
||||
use App\Enums\TablerIcon;
|
||||
use App\Filament\Admin\Resources\Servers\ServerResource;
|
||||
use App\Filament\Components\Actions\DeleteServerIcon;
|
||||
use App\Filament\Components\Actions\DeleteIcon;
|
||||
use App\Filament\Components\Actions\PreviewStartupAction;
|
||||
use App\Filament\Components\Actions\UploadIcon;
|
||||
use App\Filament\Components\Forms\Fields\MonacoEditor;
|
||||
use App\Filament\Components\Forms\Fields\StartupVariable;
|
||||
use App\Filament\Components\StateCasts\ServerConditionStateCast;
|
||||
@@ -31,7 +32,6 @@ use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\ActionGroup;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\KeyValue;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
@@ -41,7 +41,6 @@ use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Filament\Schemas\Components\Actions;
|
||||
@@ -60,9 +59,7 @@ use Filament\Support\Enums\Alignment;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
use LogicException;
|
||||
use Random\RandomException;
|
||||
|
||||
@@ -111,141 +108,18 @@ class EditServer extends EditRecord
|
||||
->icon(TablerIcon::InfoCircle)
|
||||
->schema([
|
||||
Grid::make()
|
||||
->columns(2)
|
||||
->columnStart(1)
|
||||
->schema([
|
||||
Image::make('', 'icon')
|
||||
->hidden(fn ($record) => !$record->icon && !$record->egg->image)
|
||||
->url(fn ($record) => $record->icon ?: $record->egg->image)
|
||||
->hidden(fn ($record) => !$record->icon && !$record->egg->icon)
|
||||
->url(fn ($record) => $record->icon ?: $record->egg->icon)
|
||||
->tooltip(fn ($record) => $record->icon ? '' : trans('server/setting.server_info.icon.tooltip'))
|
||||
->columnSpan(2)
|
||||
->imageSize(150)
|
||||
->columnSpanFull()
|
||||
->alignJustify(),
|
||||
Action::make('uploadIcon')
|
||||
->hiddenLabel()
|
||||
->icon(TablerIcon::PhotoUp)
|
||||
->tooltip(trans('admin/server.import_image'))
|
||||
->modal()
|
||||
->modalSubmitActionLabel(trans('server/setting.server_info.icon.upload'))
|
||||
->schema([
|
||||
Tabs::make()
|
||||
->contained(false)
|
||||
->tabs([
|
||||
Tab::make(trans('admin/egg.import.url'))
|
||||
->schema([
|
||||
Hidden::make('imageUrl'),
|
||||
Hidden::make('imageExtension'),
|
||||
TextInput::make('image_url')
|
||||
->label(trans('admin/egg.import.image_url'))
|
||||
->reactive()
|
||||
->autocomplete(false)
|
||||
->debounce(500)
|
||||
->afterStateUpdated(function ($state, Set $set) {
|
||||
if (!$state) {
|
||||
$set('image_url_error', null);
|
||||
$set('imageUrl', null);
|
||||
$set('imageExtension', null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!in_array(parse_url($state, PHP_URL_SCHEME), ['http', 'https'], true)) {
|
||||
throw new \Exception(trans('admin/egg.import.invalid_url'));
|
||||
}
|
||||
|
||||
if (!filter_var($state, FILTER_VALIDATE_URL)) {
|
||||
throw new \Exception(trans('admin/egg.import.invalid_url'));
|
||||
}
|
||||
|
||||
$extension = strtolower(pathinfo(parse_url($state, PHP_URL_PATH), PATHINFO_EXTENSION));
|
||||
|
||||
if (!array_key_exists($extension, Server::IMAGE_FORMATS)) {
|
||||
throw new \Exception(trans('admin/egg.import.unsupported_format', ['format' => implode(', ', array_keys(Server::IMAGE_FORMATS))]));
|
||||
}
|
||||
|
||||
$host = parse_url($state, PHP_URL_HOST);
|
||||
$ip = gethostbyname($host);
|
||||
|
||||
if (
|
||||
filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false
|
||||
) {
|
||||
throw new \Exception(trans('admin/egg.import.no_local_ip'));
|
||||
}
|
||||
|
||||
$set('imageUrl', $state);
|
||||
$set('imageExtension', $extension);
|
||||
$set('image_url_error', null);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$set('image_url_error', $e->getMessage());
|
||||
$set('imageUrl', null);
|
||||
$set('imageExtension', null);
|
||||
}
|
||||
}),
|
||||
TextEntry::make('image_url_error')
|
||||
->hiddenLabel()
|
||||
->visible(fn (Get $get) => $get('image_url_error') !== null)
|
||||
->afterStateHydrated(fn (Get $get) => $get('image_url_error')),
|
||||
Image::make(fn (Get $get) => $get('image_url'), '')
|
||||
->imageSize(150)
|
||||
->visible(fn (Get $get) => $get('image_url') && !$get('image_url_error'))
|
||||
->alignCenter(),
|
||||
]),
|
||||
Tab::make(trans('admin/egg.import.file'))
|
||||
->schema([
|
||||
FileUpload::make('image')
|
||||
->hiddenLabel()
|
||||
->previewable()
|
||||
->openable(false)
|
||||
->downloadable(false)
|
||||
->maxSize(256)
|
||||
->maxFiles(1)
|
||||
->columnSpanFull()
|
||||
->alignCenter()
|
||||
->imageEditor()
|
||||
->image()
|
||||
->disk('public')
|
||||
->directory(Server::ICON_STORAGE_PATH)
|
||||
->acceptedFileTypes([
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/webp',
|
||||
'image/svg+xml',
|
||||
])
|
||||
->getUploadedFileNameForStorageUsing(function (TemporaryUploadedFile $file, $record) {
|
||||
return $record->uuid . '.' . $file->getClientOriginalExtension();
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
->action(function (array $data, $record): void {
|
||||
if (!empty($data['imageUrl']) && !empty($data['imageExtension'])) {
|
||||
$this->saveIconFromUrl($data['imageUrl'], $data['imageExtension'], $record);
|
||||
Notification::make()
|
||||
->title(trans('server/setting.server_info.icon.updated'))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!empty($data['image'])) {
|
||||
Notification::make()
|
||||
->title(trans('server/setting.server_info.icon.updated'))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (empty($data['imageUrl']) && empty($data['image'])) {
|
||||
Notification::make()
|
||||
->title(trans('admin/egg.import.no_image'))
|
||||
->warning()
|
||||
->send();
|
||||
}
|
||||
}),
|
||||
DeleteServerIcon::make(),
|
||||
UploadIcon::make(),
|
||||
DeleteIcon::make()
|
||||
->iconStoragePath(Server::getIconStoragePath()),
|
||||
]),
|
||||
Grid::make()
|
||||
->columns(3)
|
||||
@@ -321,7 +195,7 @@ class EditServer extends EditRecord
|
||||
try {
|
||||
$logs = $serverRepository->setServer($server)->getInstallLogs();
|
||||
|
||||
return mb_convert_encoding($logs, 'UTF-8', ['UTF-8', 'UTF-16', 'ISO-8859-1', 'ASCII']);
|
||||
return convert_to_utf8($logs);
|
||||
} catch (ConnectionException) {
|
||||
Notification::make()
|
||||
->title(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
|
||||
@@ -1127,7 +1001,7 @@ class EditServer extends EditRecord
|
||||
->hidden(fn () => $canForceDelete)
|
||||
->authorize(fn (Server $server) => user()?->can('delete server', $server))
|
||||
->icon(TablerIcon::Trash),
|
||||
Action::make('ForceDelete')
|
||||
Action::make('exclude_force_delete')
|
||||
->color('danger')
|
||||
->label(trans('filament-actions::force-delete.single.label'))
|
||||
->modalHeading(trans('filament-actions::force-delete.single.modal.heading', ['label' => $this->getRecordTitle()]))
|
||||
@@ -1202,37 +1076,4 @@ class EditServer extends EditRecord
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an icon from URL download to a file.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
private function saveIconFromUrl(string $imageUrl, string $extension, Server $server): void
|
||||
{
|
||||
$context = stream_context_create([
|
||||
'http' => ['timeout' => 3],
|
||||
'https' => [
|
||||
'timeout' => 3,
|
||||
'verify_peer' => true,
|
||||
'verify_peer_name' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$normalizedExtension = match ($extension) {
|
||||
'svg+xml', 'svg' => 'svg',
|
||||
'jpeg', 'jpg' => 'jpg',
|
||||
'png' => 'png',
|
||||
'webp' => 'webp',
|
||||
default => throw new Exception(trans('admin/egg.import.unknown_extension')),
|
||||
};
|
||||
|
||||
$data = @file_get_contents($imageUrl, false, $context, 0, 262144); //256KB
|
||||
|
||||
if (empty($data)) {
|
||||
throw new Exception(trans('admin/egg.import.invalid_url'));
|
||||
}
|
||||
|
||||
Storage::disk('public')->put(Server::ICON_STORAGE_PATH . "/$server->uuid.$normalizedExtension", $data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ class ListServers extends ListRecords
|
||||
ImageColumn::make('icon')
|
||||
->label('')
|
||||
->imageSize(46)
|
||||
->state(fn (Server $server) => $server->icon ?: $server->egg->image),
|
||||
->state(fn (Server $server) => $server->icon ?: $server->egg->icon),
|
||||
TextColumn::make('condition')
|
||||
->label(trans('server/dashboard.status'))
|
||||
->badge()
|
||||
@@ -81,7 +81,8 @@ class ListServers extends ListRecords
|
||||
->label(trans('server/dashboard.title'))
|
||||
->description(fn (Server $server) => $server->description)
|
||||
->grow()
|
||||
->searchable(),
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('allocation.address')
|
||||
->label('')
|
||||
->badge()
|
||||
|
||||
79
app/Filament/Components/Actions/DeleteIcon.php
Normal file
79
app/Filament/Components/Actions/DeleteIcon.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Components\Actions;
|
||||
|
||||
use App\Enums\TablerIcon;
|
||||
use App\Models\Traits\HasIcon;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class DeleteIcon extends Action
|
||||
{
|
||||
/** @var string[] */
|
||||
protected ?array $iconFormats = null;
|
||||
|
||||
protected ?string $iconStoragePath = null;
|
||||
|
||||
public static function getDefaultName(): ?string
|
||||
{
|
||||
return 'delete_icon';
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->visible(fn ($record) => $record->icon);
|
||||
|
||||
$this->hiddenLabel();
|
||||
|
||||
$this->tooltip(trans('admin/egg.import.delete_icon'));
|
||||
|
||||
$this->icon(TablerIcon::Trash);
|
||||
|
||||
$this->color('danger');
|
||||
|
||||
$this->action(function ($record) {
|
||||
foreach ($this->getIconFormats() as $ext) {
|
||||
$path = $this->getIconStoragePath() . "/$record->uuid.$ext";
|
||||
if (Storage::disk('public')->exists($path)) {
|
||||
Storage::disk('public')->delete($path);
|
||||
}
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title(trans('admin/egg.import.icon_deleted'))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
$record->refresh();
|
||||
});
|
||||
}
|
||||
|
||||
/** @param string[] $iconFormats */
|
||||
public function iconFormats(?array $iconFormats): static
|
||||
{
|
||||
$this->iconFormats = $iconFormats;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function iconStoragePath(?string $iconStoragePath): static
|
||||
{
|
||||
$this->iconStoragePath = $iconStoragePath;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
public function getIconFormats(): array
|
||||
{
|
||||
return $this->iconFormats ?? array_keys(HasIcon::$iconFormats);
|
||||
}
|
||||
|
||||
public function getIconStoragePath(): ?string
|
||||
{
|
||||
return $this->iconStoragePath;
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Components\Actions;
|
||||
|
||||
use App\Enums\TablerIcon;
|
||||
use App\Models\Server;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class DeleteServerIcon extends Action
|
||||
{
|
||||
public static function getDefaultName(): ?string
|
||||
{
|
||||
return 'delete_icon';
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->visible(fn ($record) => $record->icon);
|
||||
|
||||
$this->hiddenLabel();
|
||||
|
||||
$this->tooltip(trans('admin/server.import_image'));
|
||||
|
||||
$this->icon(TablerIcon::Trash);
|
||||
|
||||
$this->color('danger');
|
||||
|
||||
$this->action(function ($record) {
|
||||
foreach (array_keys(Server::IMAGE_FORMATS) as $ext) {
|
||||
$path = Server::ICON_STORAGE_PATH . "/$record->uuid.$ext";
|
||||
if (Storage::disk('public')->exists($path)) {
|
||||
Storage::disk('public')->delete($path);
|
||||
}
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title(trans('server/setting.server_info.icon.deleted'))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
$record->refresh();
|
||||
});
|
||||
}
|
||||
}
|
||||
168
app/Filament/Components/Actions/UploadIcon.php
Normal file
168
app/Filament/Components/Actions/UploadIcon.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Components\Actions;
|
||||
|
||||
use App\Enums\TablerIcon;
|
||||
use App\Models\Traits\HasIcon;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Schemas\Components\Image;
|
||||
use Filament\Schemas\Components\Tabs;
|
||||
use Filament\Schemas\Components\Tabs\Tab;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
use Filament\Schemas\Components\Utilities\Set;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
|
||||
class UploadIcon extends Action
|
||||
{
|
||||
/** @var string[] */
|
||||
protected ?array $iconFormats = null;
|
||||
|
||||
public static function getDefaultName(): ?string
|
||||
{
|
||||
return 'upload_icon';
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->hiddenLabel();
|
||||
|
||||
$this->tooltip(trans('admin/egg.import.import_icon'));
|
||||
|
||||
$this->icon(TablerIcon::PhotoUp);
|
||||
|
||||
$this->modal();
|
||||
|
||||
$this->modalHeading('');
|
||||
|
||||
$this->modalSubmitActionLabel(trans('admin/egg.import.import_icon'));
|
||||
|
||||
$this->schema([
|
||||
Tabs::make()
|
||||
->contained(false)
|
||||
->tabs([
|
||||
Tab::make(trans('admin/egg.import.url'))
|
||||
->schema([
|
||||
TextInput::make('icon_url')
|
||||
->label(trans('admin/egg.import.icon_url'))
|
||||
->reactive()
|
||||
->autocomplete(false)
|
||||
->debounce(500)
|
||||
->afterStateUpdated(function ($state, Set $set) {
|
||||
if (!$state) {
|
||||
$set('icon_url_error', null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->validateIconUrl($state);
|
||||
|
||||
$set('icon_url_error', null);
|
||||
} catch (Exception $exception) {
|
||||
$set('icon_url_error', $exception->getMessage());
|
||||
}
|
||||
}),
|
||||
TextEntry::make('icon_url_error')
|
||||
->hiddenLabel()
|
||||
->visible(fn (Get $get) => $get('icon_url_error') !== null)
|
||||
->afterStateHydrated(fn (Get $get) => $get('icon_url_error')),
|
||||
Image::make(fn (Get $get) => $get('icon_url'), '')
|
||||
->imageSize(150)
|
||||
->visible(fn (Get $get) => $get('icon_url') && !$get('icon_url_error'))
|
||||
->alignCenter(),
|
||||
]),
|
||||
Tab::make(trans('admin/egg.import.file'))
|
||||
->schema([
|
||||
FileUpload::make('icon')
|
||||
->hiddenLabel()
|
||||
->previewable()
|
||||
->openable(false)
|
||||
->downloadable(false)
|
||||
->maxSize(256)
|
||||
->maxFiles(1)
|
||||
->columnSpanFull()
|
||||
->alignCenter()
|
||||
->imageEditor()
|
||||
->image()
|
||||
->acceptedFileTypes(fn () => $this->getIconFormats())
|
||||
->saveUploadedFileUsing(fn (TemporaryUploadedFile $file, $record) => $record->writeIcon($file->getClientOriginalExtension(), $file->getContent())),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
|
||||
$this->action(function (array $data, $record) {
|
||||
if (!empty($data['icon_url'])) {
|
||||
$this->validateIconUrl($data['icon_url']);
|
||||
|
||||
$content = Http::timeout(5)->connectTimeout(1)->withoutRedirecting()->get($data['icon_url'])->body();
|
||||
|
||||
if (empty($content)) {
|
||||
throw new Exception(trans('admin/egg.import.invalid_url'));
|
||||
}
|
||||
|
||||
$extension = strtolower(pathinfo(parse_url($data['icon_url'], PHP_URL_PATH), PATHINFO_EXTENSION));
|
||||
|
||||
if (empty($extension)) {
|
||||
throw new Exception(trans('admin/egg.import.invalid_url'));
|
||||
}
|
||||
|
||||
$record->writeIcon($extension, $content);
|
||||
|
||||
Notification::make()
|
||||
->title(trans('admin/egg.import.icon_updated'))
|
||||
->success()
|
||||
->send();
|
||||
} elseif (!empty($data['icon'])) {
|
||||
Notification::make()
|
||||
->title(trans('admin/egg.import.icon_updated'))
|
||||
->success()
|
||||
->send();
|
||||
} else {
|
||||
Notification::make()
|
||||
->title(trans('admin/egg.import.no_icon'))
|
||||
->warning()
|
||||
->send();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected function validateIconUrl(string $url): void
|
||||
{
|
||||
if (!in_array(parse_url($url, PHP_URL_SCHEME), ['http', 'https'], true)) {
|
||||
throw new Exception(trans('admin/egg.import.invalid_url'));
|
||||
}
|
||||
|
||||
if (!filter_var($url, FILTER_VALIDATE_URL)) {
|
||||
throw new Exception(trans('admin/egg.import.invalid_url'));
|
||||
}
|
||||
|
||||
$host = parse_url($url, PHP_URL_HOST);
|
||||
$ip = gethostbyname($host);
|
||||
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
|
||||
throw new Exception(trans('admin/egg.import.no_local_ip'));
|
||||
}
|
||||
}
|
||||
|
||||
/** @param string[] $iconFormats */
|
||||
public function iconFormats(?array $iconFormats): static
|
||||
{
|
||||
$this->iconFormats = $iconFormats;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
public function getIconFormats(): array
|
||||
{
|
||||
return $this->iconFormats ?? array_values(HasIcon::$iconFormats);
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ class StartupVariable extends Field
|
||||
|
||||
$this->hintIcon(TablerIcon::Code, fn (StartupVariable $component) => implode('|', $component->getVariableRules()));
|
||||
|
||||
$this->helperText(fn (StartupVariable $component) => !$component->getVariableDesc() ? '—' : $component->getVariableDesc());
|
||||
$this->helperText(fn (StartupVariable $component) => $component->getVariableDesc());
|
||||
|
||||
$this->rules(fn (StartupVariable $component) => $component->getVariableRules());
|
||||
|
||||
@@ -70,7 +70,7 @@ class StartupVariable extends Field
|
||||
],
|
||||
StartupVariableType::Toggle => [
|
||||
...parent::getDefaultStateCasts(),
|
||||
new BooleanStateCast(false),
|
||||
new BooleanStateCast(false, true),
|
||||
],
|
||||
default => parent::getDefaultStateCasts()
|
||||
};
|
||||
|
||||
@@ -21,9 +21,9 @@ class TagsFilter extends BaseFilter
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->query(fn (Builder $query, array $data) => $query->when($data['tag'], fn (Builder $query, $tag) => $query->whereJsonContains('tags', $tag)));
|
||||
$this->query(fn (Builder $query, array $data) => $query->when($data['tag'] ?? null, fn (Builder $query, $tag) => $query->whereJsonContains('tags', $tag)));
|
||||
|
||||
$this->indicateUsing(fn (array $data) => $data['tag'] ? 'Tag: ' . $data['tag'] : null);
|
||||
$this->indicateUsing(fn (array $data) => ($data['tag'] ?? null) ? 'Tag: ' . $data['tag'] : null);
|
||||
|
||||
$this->resetState(['tag' => null]);
|
||||
|
||||
|
||||
@@ -426,13 +426,13 @@ class EditProfile extends BaseEditProfile
|
||||
->label(trans('profile.tabs.activity'))
|
||||
->icon(TablerIcon::History)
|
||||
->schema([
|
||||
Repeater::make('activity')
|
||||
->hiddenLabel()
|
||||
Repeater::make('activity') // TODO: move to a table
|
||||
->label(trans('profile.activity_info'))
|
||||
->inlineLabel(false)
|
||||
->deletable(false)
|
||||
->addable(false)
|
||||
->relationship(null, function (Builder $query) {
|
||||
$query->orderBy('timestamp', 'desc');
|
||||
$query->orderBy('timestamp', 'desc')->limit(50);
|
||||
})
|
||||
->schema([
|
||||
TextEntry::make('log')
|
||||
|
||||
@@ -13,6 +13,7 @@ use Filament\Schemas\Components\Actions;
|
||||
use Filament\Schemas\Components\Component;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Colors\Color;
|
||||
use Filament\Support\Enums\Alignment;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class Login extends BaseLogin
|
||||
@@ -32,6 +33,12 @@ class Login extends BaseLogin
|
||||
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
if (config('auth.disable_password_login', false)) {
|
||||
return $schema->components([
|
||||
$this->getOAuthFormComponent(),
|
||||
]);
|
||||
}
|
||||
|
||||
$components = [
|
||||
$this->getLoginFormComponent(),
|
||||
$this->getPasswordFormComponent(),
|
||||
@@ -108,11 +115,22 @@ class Login extends BaseLogin
|
||||
->url(route('auth.oauth.redirect', ['driver' => $id], false));
|
||||
}
|
||||
|
||||
return Actions::make($actions);
|
||||
return Actions::make($actions)->alignment(fn () => config('auth.disable_password_login', false) ? Alignment::Center : null);
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return config('auth.disable_password_login', false) ? [] : parent::getFormActions();
|
||||
}
|
||||
|
||||
protected function getCredentialsFromFormData(array $data): array
|
||||
{
|
||||
if (config('auth.disable_password_login', false)) {
|
||||
throw ValidationException::withMessages([
|
||||
'data.login' => trans('auth.password_login_disabled'),
|
||||
]);
|
||||
}
|
||||
|
||||
$loginType = filter_var($data['login'], FILTER_VALIDATE_EMAIL) ? 'email' : 'username';
|
||||
|
||||
return [
|
||||
|
||||
114
app/Filament/Server/Pages/Mounts.php
Normal file
114
app/Filament/Server/Pages/Mounts.php
Normal file
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Server\Pages;
|
||||
|
||||
use App\Enums\SubuserPermission;
|
||||
use App\Enums\TablerIcon;
|
||||
use App\Facades\Activity;
|
||||
use App\Models\Mount;
|
||||
use App\Models\Server;
|
||||
use BackedEnum;
|
||||
use Exception;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Schema;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
class Mounts extends ServerFormPage
|
||||
{
|
||||
protected static string|BackedEnum|null $navigationIcon = TablerIcon::LayersLinked;
|
||||
|
||||
protected static ?int $navigationSort = 9;
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
return parent::canAccess() && user()?->can(SubuserPermission::MountRead, Filament::getTenant());
|
||||
}
|
||||
|
||||
protected function authorizeAccess(): void
|
||||
{
|
||||
abort_unless(user()?->can(SubuserPermission::MountRead, Filament::getTenant()), 403);
|
||||
}
|
||||
|
||||
protected function fillForm(): void
|
||||
{
|
||||
$this->form->fill([
|
||||
'mounts' => $this->getRecord()->mounts->pluck('id')->toArray(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
$server = $this->getRecord();
|
||||
|
||||
$allowedMounts = Mount::query()
|
||||
->where('user_mountable', true)
|
||||
->where(function ($query) use ($server) {
|
||||
$query->whereDoesntHave('nodes')
|
||||
->orWhereHas('nodes', fn ($q) => $q->where('nodes.id', $server->node_id));
|
||||
})
|
||||
->where(function ($query) use ($server) {
|
||||
$query->whereDoesntHave('eggs')
|
||||
->orWhereHas('eggs', fn ($q) => $q->where('eggs.id', $server->egg_id));
|
||||
})
|
||||
->get();
|
||||
|
||||
return parent::form($schema)
|
||||
->components([
|
||||
Section::make([
|
||||
CheckboxList::make('mounts')
|
||||
->label(trans('server/mount.description'))
|
||||
->relationship('mounts')
|
||||
->options(fn () => $allowedMounts->mapWithKeys(fn (Mount $mount) => [$mount->id => $mount->name]))
|
||||
->descriptions(fn () => $allowedMounts->mapWithKeys(fn (Mount $mount) => [$mount->id => new HtmlString(str("$mount->source -> $mount->target")->stripTags() . ($mount->description ? '<br>' . str($mount->description)->stripTags() : ''))]))
|
||||
->helperText(fn () => $allowedMounts->isEmpty() ? trans('server/mount.no_mounts') : null)
|
||||
->disabled(fn (Server $server) => !user()?->can(SubuserPermission::MountUpdate, $server))
|
||||
->bulkToggleable()
|
||||
->live()
|
||||
->afterStateUpdated(function ($state) {
|
||||
$this->save();
|
||||
})
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
abort_unless(user()?->can(SubuserPermission::MountUpdate, $this->getRecord()), 403);
|
||||
|
||||
try {
|
||||
$this->form->getState();
|
||||
$this->form->saveRelationships();
|
||||
|
||||
Activity::event('server:mount.update')
|
||||
->log();
|
||||
|
||||
Notification::make()
|
||||
->title(trans('server/mount.notification_updated'))
|
||||
->body(trans('server/mount.notification_updated_body'))
|
||||
->success()
|
||||
->send();
|
||||
} catch (Exception $exception) {
|
||||
report($exception);
|
||||
|
||||
Notification::make()
|
||||
->title(trans('server/mount.notification_failed'))
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return trans('server/mount.title');
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return trans('server/mount.title');
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,13 @@ namespace App\Filament\Server\Pages;
|
||||
use App\Enums\SubuserPermission;
|
||||
use App\Enums\TablerIcon;
|
||||
use App\Facades\Activity;
|
||||
use App\Filament\Components\Actions\DeleteServerIcon;
|
||||
use App\Filament\Components\Actions\DeleteIcon;
|
||||
use App\Filament\Components\Actions\UploadIcon;
|
||||
use App\Models\Server;
|
||||
use App\Services\Servers\ReinstallServerService;
|
||||
use BackedEnum;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
@@ -21,20 +20,14 @@ use Filament\Schemas\Components\Fieldset;
|
||||
use Filament\Schemas\Components\Grid;
|
||||
use Filament\Schemas\Components\Image;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Tabs;
|
||||
use Filament\Schemas\Components\Tabs\Tab;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
use Filament\Schemas\Components\Utilities\Set;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Enums\Alignment;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
|
||||
class Settings extends ServerFormPage
|
||||
{
|
||||
protected static string|BackedEnum|null $navigationIcon = TablerIcon::Settings;
|
||||
|
||||
protected static ?int $navigationSort = 10;
|
||||
protected static ?int $navigationSort = 11;
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
@@ -79,140 +72,20 @@ class Settings extends ServerFormPage
|
||||
->afterStateUpdated(fn ($state, Server $server) => $this->updateDescription($state ?? '', $server)),
|
||||
]),
|
||||
Grid::make()
|
||||
->columns(2)
|
||||
->columnStart(6)
|
||||
->schema([
|
||||
Image::make('', 'icon')
|
||||
->hidden(fn ($record) => !$record->icon && !$record->egg->image)
|
||||
->url(fn ($record) => $record->icon ?: $record->egg->image)
|
||||
->hidden(fn ($record) => !$record->icon && !$record->egg->icon)
|
||||
->url(fn ($record) => $record->icon ?: $record->egg->icon)
|
||||
->tooltip(fn ($record) => $record->icon ? '' : trans('server/setting.server_info.icon.tooltip'))
|
||||
->columnSpan(2)
|
||||
->imageSize(150)
|
||||
->columnSpanFull()
|
||||
->alignJustify(),
|
||||
Action::make('uploadIcon')
|
||||
->hiddenLabel()
|
||||
->tooltip(trans('admin/server.import_image'))
|
||||
->icon(TablerIcon::PhotoUp)
|
||||
->modal()
|
||||
->modalSubmitActionLabel(trans('server/setting.server_info.icon.upload'))
|
||||
->schema([
|
||||
Tabs::make()
|
||||
->contained(false)
|
||||
->tabs([
|
||||
Tab::make(trans('admin/egg.import.url'))
|
||||
->schema([
|
||||
Hidden::make('imageUrl'),
|
||||
Hidden::make('imageExtension'),
|
||||
TextInput::make('image_url')
|
||||
->label(trans('admin/egg.import.image_url'))
|
||||
->reactive()
|
||||
->autocomplete(false)
|
||||
->debounce(500)
|
||||
->afterStateUpdated(function ($state, Set $set) {
|
||||
if (!$state) {
|
||||
$set('image_url_error', null);
|
||||
$set('imageUrl', null);
|
||||
$set('imageExtension', null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!in_array(parse_url($state, PHP_URL_SCHEME), ['http', 'https'], true)) {
|
||||
throw new \Exception(trans('admin/egg.import.invalid_url'));
|
||||
}
|
||||
|
||||
if (!filter_var($state, FILTER_VALIDATE_URL)) {
|
||||
throw new \Exception(trans('admin/egg.import.invalid_url'));
|
||||
}
|
||||
|
||||
$extension = strtolower(pathinfo(parse_url($state, PHP_URL_PATH), PATHINFO_EXTENSION));
|
||||
|
||||
if (!array_key_exists($extension, Server::IMAGE_FORMATS)) {
|
||||
throw new \Exception(trans('admin/egg.import.unsupported_format', ['format' => implode(', ', array_keys(Server::IMAGE_FORMATS))]));
|
||||
}
|
||||
|
||||
$host = parse_url($state, PHP_URL_HOST);
|
||||
$ip = gethostbyname($host);
|
||||
|
||||
if (
|
||||
filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false
|
||||
) {
|
||||
throw new \Exception(trans('admin/egg.import.no_local_ip'));
|
||||
}
|
||||
|
||||
$set('imageUrl', $state);
|
||||
$set('imageExtension', $extension);
|
||||
$set('image_url_error', null);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$set('image_url_error', $e->getMessage());
|
||||
$set('imageUrl', null);
|
||||
$set('imageExtension', null);
|
||||
}
|
||||
}),
|
||||
TextEntry::make('image_url_error')
|
||||
->hiddenLabel()
|
||||
->visible(fn (Get $get) => $get('image_url_error') !== null)
|
||||
->afterStateHydrated(fn (Get $get) => $get('image_url_error')),
|
||||
Image::make(fn (Get $get) => $get('image_url'), '')
|
||||
->imageSize(150)
|
||||
->visible(fn (Get $get) => $get('image_url') && !$get('image_url_error'))
|
||||
->alignCenter(),
|
||||
]),
|
||||
Tab::make(trans('admin/egg.import.file'))
|
||||
->schema([
|
||||
FileUpload::make('image')
|
||||
->hiddenLabel()
|
||||
->previewable()
|
||||
->openable(false)
|
||||
->downloadable(false)
|
||||
->maxSize(256)
|
||||
->maxFiles(1)
|
||||
->columnSpanFull()
|
||||
->alignCenter()
|
||||
->imageEditor()
|
||||
->image()
|
||||
->disk('public')
|
||||
->directory(Server::ICON_STORAGE_PATH)
|
||||
->acceptedFileTypes([
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/webp',
|
||||
'image/svg+xml',
|
||||
])
|
||||
->getUploadedFileNameForStorageUsing(function (TemporaryUploadedFile $file, $record) {
|
||||
return $record->uuid . '.' . $file->getClientOriginalExtension();
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
->action(function (array $data, $record): void {
|
||||
|
||||
if (!empty($data['imageUrl']) && !empty($data['imageExtension'])) {
|
||||
$this->saveIconFromUrl($data['imageUrl'], $data['imageExtension'], $record);
|
||||
Notification::make()
|
||||
->title(trans('server/setting.server_info.icon.updated'))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!empty($data['image'])) {
|
||||
Notification::make()
|
||||
->title(trans('server/setting.server_info.icon.updated'))
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
|
||||
if (empty($data['imageUrl']) && empty($data['image'])) {
|
||||
Notification::make()
|
||||
->title(trans('admin/egg.import.no_image'))
|
||||
->warning()
|
||||
->send();
|
||||
}
|
||||
}),
|
||||
DeleteServerIcon::make(),
|
||||
UploadIcon::make()
|
||||
->authorize(fn (Server $server) => user()?->can(SubuserPermission::SettingsChangeIcon, $server)),
|
||||
DeleteIcon::make()
|
||||
->iconStoragePath(Server::getIconStoragePath())
|
||||
->authorize(fn (Server $server) => user()?->can(SubuserPermission::SettingsChangeIcon, $server)),
|
||||
]),
|
||||
TextInput::make('uuid')
|
||||
->label(trans('server/setting.server_info.uuid'))
|
||||
@@ -446,39 +319,6 @@ class Settings extends ServerFormPage
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an icon from URL download to a file.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
private function saveIconFromUrl(string $imageUrl, string $extension, Server $server): void
|
||||
{
|
||||
$context = stream_context_create([
|
||||
'http' => ['timeout' => 3],
|
||||
'https' => [
|
||||
'timeout' => 3,
|
||||
'verify_peer' => true,
|
||||
'verify_peer_name' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$normalizedExtension = match ($extension) {
|
||||
'svg+xml', 'svg' => 'svg',
|
||||
'jpeg', 'jpg' => 'jpg',
|
||||
'png' => 'png',
|
||||
'webp' => 'webp',
|
||||
default => throw new Exception(trans('admin/egg.import.unknown_extension')),
|
||||
};
|
||||
|
||||
$data = @file_get_contents($imageUrl, false, $context, 0, 262144); //256KB
|
||||
|
||||
if (empty($data)) {
|
||||
throw new Exception(trans('admin/egg.import.invalid_url'));
|
||||
}
|
||||
|
||||
Storage::disk('public')->put(Server::ICON_STORAGE_PATH . "/$server->uuid.$normalizedExtension", $data);
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return trans('server/setting.title');
|
||||
|
||||
@@ -28,7 +28,7 @@ class Startup extends ServerFormPage
|
||||
{
|
||||
protected static string|BackedEnum|null $navigationIcon = TablerIcon::PlayerPlay;
|
||||
|
||||
protected static ?int $navigationSort = 9;
|
||||
protected static ?int $navigationSort = 10;
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
@@ -149,12 +149,16 @@ class Startup extends ServerFormPage
|
||||
return parent::canAccess() && user()?->can(SubuserPermission::StartupRead, Filament::getTenant());
|
||||
}
|
||||
|
||||
public function update(?string $state, ServerVariable $serverVariable): void
|
||||
public function update(null|string|bool $state, ServerVariable $serverVariable): void
|
||||
{
|
||||
if (!$serverVariable->variable->user_editable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_bool($state)) {
|
||||
$state = $state ? '1' : '0';
|
||||
}
|
||||
|
||||
$original = $serverVariable->variable_value;
|
||||
|
||||
try {
|
||||
|
||||
@@ -139,6 +139,7 @@ class ActivityResource extends Resource
|
||||
]);
|
||||
}
|
||||
|
||||
/** @return Builder<ActivityLog> */
|
||||
public static function getEloquentQuery(): Builder
|
||||
{
|
||||
/** @var Server $server */
|
||||
|
||||
@@ -149,7 +149,7 @@ class EditFiles extends Page
|
||||
try {
|
||||
$contents = $this->getDaemonFileRepository()->getContent($this->path, config('panel.files.max_edit_size'));
|
||||
|
||||
return mb_convert_encoding($contents, 'UTF-8', ['UTF-8', 'UTF-16', 'ISO-8859-1', 'ASCII']);
|
||||
return convert_to_utf8($contents);
|
||||
} catch (FileSizeTooLargeException) {
|
||||
AlertBanner::make('file_too_large')
|
||||
->title(trans('server/file.alerts.file_too_large.title', ['name' => basename($this->path)]))
|
||||
@@ -259,9 +259,9 @@ class EditFiles extends Page
|
||||
return $this->fileRepository;
|
||||
}
|
||||
|
||||
public static function getUrl(array $parameters = [], bool $isAbsolute = true, ?string $panel = null, ?Model $tenant = null, bool $shouldGuessMissingParameters = false): string
|
||||
public static function getUrl(array $parameters = [], bool $isAbsolute = true, ?string $panel = null, ?Model $tenant = null, bool $shouldGuessMissingParameters = false, ?string $configuration = null): string
|
||||
{
|
||||
return parent::getUrl($parameters, $isAbsolute, $panel, $tenant) . '/';
|
||||
return parent::getUrl($parameters, $isAbsolute, $panel, $tenant, $shouldGuessMissingParameters, $configuration) . '/';
|
||||
}
|
||||
|
||||
public static function route(string $path): PageRegistration
|
||||
|
||||
@@ -16,10 +16,8 @@ use App\Livewire\AlertBanner;
|
||||
use App\Models\File;
|
||||
use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonFileRepository;
|
||||
use App\Services\Nodes\NodeJWTService;
|
||||
use App\Traits\Filament\CanCustomizeHeaderActions;
|
||||
use App\Traits\Filament\CanCustomizeHeaderWidgets;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\ActionGroup;
|
||||
@@ -616,28 +614,6 @@ class ListFiles extends ListRecords
|
||||
};
|
||||
}
|
||||
|
||||
public function getUploadUrl(NodeJWTService $jwtService): string
|
||||
{
|
||||
/** @var Server $server */
|
||||
$server = Filament::getTenant();
|
||||
|
||||
if (!user()?->can(SubuserPermission::FileCreate, $server)) {
|
||||
abort(403, 'You do not have permission to upload files.');
|
||||
}
|
||||
|
||||
$token = $jwtService
|
||||
->setExpiresAt(CarbonImmutable::now()->addMinutes(15))
|
||||
->setUser(user())
|
||||
->setClaims(['server_uuid' => $server->uuid])
|
||||
->handle($server->node, user()->id . $server->uuid);
|
||||
|
||||
return sprintf(
|
||||
'%s/upload/file?token=%s',
|
||||
$server->node->getConnectionAddress(),
|
||||
$token->toString()
|
||||
);
|
||||
}
|
||||
|
||||
public function getUploadSizeLimit(): int
|
||||
{
|
||||
/** @var Server $server */
|
||||
|
||||
@@ -79,15 +79,15 @@ class SubuserResource extends Resource
|
||||
|
||||
foreach ($data['permissions'] as $permission) {
|
||||
$options[$permission] = str($permission)->headline();
|
||||
$descriptions[$permission] = trans('server/user.permissions.' . $data['name'] . '_' . str($permission)->replace('-', '_'));
|
||||
$descriptions[$permission] = trans($data['translation_prefix']. '.' . $data['name'] . '_' . str($permission)->replace('-', '_'));
|
||||
$permissionsArray[$data['name']][] = $permission;
|
||||
}
|
||||
|
||||
$tabs[] = Tab::make($data['name'])
|
||||
->label(str($data['name'])->headline())
|
||||
->label(trans($data['translation_prefix']. '.' . $data['name'] . '_title'))
|
||||
->schema([
|
||||
Section::make()
|
||||
->description(trans('server/user.permissions.' . $data['name'] . '_desc'))
|
||||
->description(trans($data['translation_prefix']. '.' . $data['name'] . '_desc'))
|
||||
->icon($data['icon'])
|
||||
->contained(false)
|
||||
->schema([
|
||||
|
||||
@@ -13,10 +13,17 @@ use Illuminate\Auth\SessionGuard;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
|
||||
use Throwable;
|
||||
|
||||
class AccountController extends ClientApiController
|
||||
{
|
||||
/**
|
||||
* The number of seconds that must elapse before the email change throttle resets.
|
||||
*/
|
||||
private const EMAIL_UPDATE_THROTTLE = 60 * 60 * 24;
|
||||
|
||||
/**
|
||||
* AccountController constructor.
|
||||
*/
|
||||
@@ -63,10 +70,22 @@ class AccountController extends ClientApiController
|
||||
*/
|
||||
public function updateEmail(UpdateEmailRequest $request): JsonResponse
|
||||
{
|
||||
$original = $request->user()->email;
|
||||
$this->updateService->handle($request->user(), $request->validated());
|
||||
$user = $request->user();
|
||||
|
||||
// Only allow a user to change their email three times in the span
|
||||
// of 24 hours. This prevents malicious users from trying to find
|
||||
// existing accounts in the system by constantly changing their email.
|
||||
if (RateLimiter::tooManyAttempts($key = "user:update-email:{$user->uuid}", 3)) {
|
||||
throw new TooManyRequestsHttpException(message: 'Your email address has been changed too many times today. Please try again later.');
|
||||
}
|
||||
|
||||
$original = $user->email;
|
||||
|
||||
if (mb_strtolower($original) !== mb_strtolower($request->validated('email'))) {
|
||||
RateLimiter::hit($key, self::EMAIL_UPDATE_THROTTLE);
|
||||
|
||||
$this->updateService->handle($user, $request->validated());
|
||||
|
||||
if ($original !== $request->input('email')) {
|
||||
Activity::event('user:account.email-changed')
|
||||
->property(['old' => $original, 'new' => $request->input('email')])
|
||||
->log();
|
||||
@@ -85,7 +104,9 @@ class AccountController extends ClientApiController
|
||||
*/
|
||||
public function updatePassword(UpdatePasswordRequest $request): JsonResponse
|
||||
{
|
||||
$user = $this->updateService->handle($request->user(), $request->validated());
|
||||
$user = Activity::event('user:account.password-changed')->transaction(function () use ($request) {
|
||||
return $this->updateService->handle($request->user(), $request->validated());
|
||||
});
|
||||
|
||||
$guard = $this->manager->guard();
|
||||
// If you do not update the user in the session you'll end up working with a
|
||||
@@ -98,8 +119,6 @@ class AccountController extends ClientApiController
|
||||
$guard->logoutOtherDevices($request->input('password'));
|
||||
}
|
||||
|
||||
Activity::event('user:account.password-changed')->log();
|
||||
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ class BackupController extends ClientApiController
|
||||
}
|
||||
|
||||
$backup = Activity::event('server:backup.start')->transaction(function ($log) use ($action, $server, $request) {
|
||||
$server->backups()->lockForUpdate();
|
||||
$server->backups()->lockForUpdate()->count();
|
||||
|
||||
$backup = $action->handle($server, $request->input('name'));
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ class DatabaseController extends ClientApiController
|
||||
public function store(StoreDatabaseRequest $request, Server $server): array
|
||||
{
|
||||
$database = Activity::event('server:database.create')->transaction(function ($log) use ($request, $server) {
|
||||
$server->databases()->lockForUpdate();
|
||||
$server->databases()->lockForUpdate()->count();
|
||||
|
||||
$database = $this->deployDatabaseService->handle($server, $request->validated());
|
||||
|
||||
@@ -87,15 +87,12 @@ class DatabaseController extends ClientApiController
|
||||
*/
|
||||
public function rotatePassword(RotatePasswordRequest $request, Server $server, Database $database): array
|
||||
{
|
||||
$this->managementService->rotatePassword($database);
|
||||
$database->refresh();
|
||||
|
||||
Activity::event('server:database.rotate-password')
|
||||
->subject($database)
|
||||
->property('name', $database->database)
|
||||
->log();
|
||||
->transaction(fn () => $this->managementService->rotatePassword($database));
|
||||
|
||||
return $this->fractal->item($database)
|
||||
return $this->fractal->item($database->refresh())
|
||||
->parseIncludes(['password'])
|
||||
->transformWith($this->getTransformer(DatabaseTransformer::class))
|
||||
->toArray();
|
||||
|
||||
@@ -77,7 +77,7 @@ class FileController extends ClientApiController
|
||||
->property('file', $request->get('file'))
|
||||
->log();
|
||||
|
||||
return new Response($response, Response::HTTP_OK, ['Content-Type' => 'text/plain']);
|
||||
return new Response(convert_to_utf8($response), Response::HTTP_OK, ['Content-Type' => 'text/plain; charset=utf-8']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -87,13 +87,13 @@ class StartupController extends ClientApiController
|
||||
|
||||
$startup = $this->startupCommandService->handle($server);
|
||||
|
||||
if ($variable->env_variable !== $request->input('value')) {
|
||||
if ($original !== $request->input('value')) {
|
||||
Activity::event('server:startup.edit')
|
||||
->subject($variable)
|
||||
->property([
|
||||
'variable' => $variable->env_variable,
|
||||
'old' => $original,
|
||||
'new' => $request->input('value'),
|
||||
'new' => $request->input('value') ?? '',
|
||||
])
|
||||
->log();
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ class BackupRemoteUploadController extends Controller
|
||||
/** @var Server $server */
|
||||
$server = $model->server;
|
||||
if ($server->node_id !== $node->id) {
|
||||
throw new HttpForbiddenException('You do not have permission to access that backup.');
|
||||
throw new HttpForbiddenException('Requesting node does not have permission to access this server.');
|
||||
}
|
||||
|
||||
// Prevent backups that have already been completed from trying to
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers\Api\Remote\Backups;
|
||||
|
||||
use App\Events\Server\BackupCompleted;
|
||||
use App\Exceptions\DisplayException;
|
||||
use App\Exceptions\Http\HttpForbiddenException;
|
||||
use App\Extensions\Backups\BackupManager;
|
||||
@@ -47,7 +48,7 @@ class BackupStatusController extends Controller
|
||||
/** @var Server $server */
|
||||
$server = $model->server;
|
||||
if ($server->node_id !== $node->id) {
|
||||
throw new HttpForbiddenException('You do not have permission to access that backup.');
|
||||
throw new HttpForbiddenException('Requesting node does not have permission to access this server.');
|
||||
}
|
||||
|
||||
if ($model->is_successful) {
|
||||
@@ -79,6 +80,8 @@ class BackupStatusController extends Controller
|
||||
}
|
||||
});
|
||||
|
||||
event(new BackupCompleted($model));
|
||||
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
@@ -97,6 +100,11 @@ class BackupStatusController extends Controller
|
||||
/** @var Backup $model */
|
||||
$model = Backup::query()->where('uuid', $backup)->firstOrFail();
|
||||
|
||||
$node = $request->attributes->get('node');
|
||||
if (!$model->server->node->is($node)) {
|
||||
throw new HttpForbiddenException('Requesting node does not have permission to access this server.');
|
||||
}
|
||||
|
||||
$model->server->update(['status' => null]);
|
||||
|
||||
Activity::event($request->boolean('successful') ? 'server:backup.restore-complete' : 'server.backup.restore-failed')
|
||||
|
||||
@@ -3,18 +3,23 @@
|
||||
namespace App\Http\Controllers\Api\Remote\Servers;
|
||||
|
||||
use App\Enums\ContainerStatus;
|
||||
use App\Exceptions\Http\HttpForbiddenException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Api\Remote\ServerRequest;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ServerContainersController extends Controller
|
||||
{
|
||||
/**
|
||||
* Updates the server container's status on the Panel
|
||||
*/
|
||||
public function status(ServerRequest $request, Server $server): JsonResponse
|
||||
public function status(Request $request, Server $server): JsonResponse
|
||||
{
|
||||
if (!$server->node->is($request->attributes->get('node'))) {
|
||||
throw new HttpForbiddenException('Requesting node does not have permission to access this server.');
|
||||
}
|
||||
|
||||
$status = ContainerStatus::tryFrom($request->json('data.new_state')) ?? ContainerStatus::Missing;
|
||||
|
||||
cache()->put("servers.$server->uuid.status", $status, now()->addHour());
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
namespace App\Http\Controllers\Api\Remote\Servers;
|
||||
|
||||
use App\Enums\ServerState;
|
||||
use App\Exceptions\Http\HttpForbiddenException;
|
||||
use App\Facades\Activity;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Api\Remote\ServerRequest;
|
||||
use App\Http\Resources\Daemon\ServerConfigurationCollection;
|
||||
use App\Models\ActivityLog;
|
||||
use App\Models\Backup;
|
||||
@@ -17,6 +17,7 @@ use Illuminate\Database\ConnectionInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Throwable;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
class ServerDetailsController extends Controller
|
||||
{
|
||||
@@ -33,8 +34,21 @@ class ServerDetailsController extends Controller
|
||||
* Returns details about the server that allows daemon to self-recover and ensure
|
||||
* that the state of the server matches the Panel at all times.
|
||||
*/
|
||||
public function __invoke(ServerRequest $request, Server $server): JsonResponse
|
||||
public function __invoke(Request $request, Server $server): JsonResponse
|
||||
{
|
||||
Assert::isInstanceOf($node = $request->attributes->get('node'), Node::class);
|
||||
|
||||
$transfer = $server->transfer;
|
||||
|
||||
// If the server is being transferred allow either node to request information about
|
||||
// the server. If the server is not being transferred only the target node is allowed
|
||||
// to fetch these details.
|
||||
$valid = $transfer ? $node->id === $transfer->old_node || $node->id === $transfer->new_node : $node->id === $server->node_id;
|
||||
|
||||
if (!$valid) {
|
||||
throw new HttpForbiddenException('Requesting node does not have permission to access this server.');
|
||||
}
|
||||
|
||||
return new JsonResponse([
|
||||
'settings' => $this->configurationStructureService->handle($server),
|
||||
'process_configuration' => $this->eggConfigurationService->handle($server),
|
||||
|
||||
@@ -4,12 +4,13 @@ namespace App\Http\Controllers\Api\Remote\Servers;
|
||||
|
||||
use App\Enums\ServerState;
|
||||
use App\Events\Server\Installed as ServerInstalled;
|
||||
use App\Exceptions\Http\HttpForbiddenException;
|
||||
use App\Exceptions\Model\DataValidationException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Api\Remote\InstallationDataRequest;
|
||||
use App\Http\Requests\Api\Remote\ServerRequest;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class ServerInstallController extends Controller
|
||||
@@ -17,12 +18,18 @@ class ServerInstallController extends Controller
|
||||
/**
|
||||
* Returns installation information for a server.
|
||||
*/
|
||||
public function index(ServerRequest $request, Server $server): JsonResponse
|
||||
public function index(Request $request, Server $server): JsonResponse
|
||||
{
|
||||
if (!$server->node->is($request->attributes->get('node'))) {
|
||||
throw new HttpForbiddenException('Requesting node does not have permission to access this server.');
|
||||
}
|
||||
|
||||
$egg = $server->egg;
|
||||
|
||||
return new JsonResponse([
|
||||
'container_image' => $server->egg->copy_script_container,
|
||||
'entrypoint' => $server->egg->copy_script_entry,
|
||||
'script' => $server->egg->copy_script_install,
|
||||
'container_image' => $egg->copy_script_container,
|
||||
'entrypoint' => $egg->copy_script_entry,
|
||||
'script' => $egg->copy_script_install,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -35,6 +42,10 @@ class ServerInstallController extends Controller
|
||||
{
|
||||
$status = null;
|
||||
|
||||
if (!$server->node->is($request->attributes->get('node'))) {
|
||||
throw new HttpForbiddenException('Requesting node does not have permission to access this server.');
|
||||
}
|
||||
|
||||
$successful = $request->boolean('successful');
|
||||
|
||||
// Make sure the type of failure is accurate
|
||||
|
||||
@@ -2,17 +2,20 @@
|
||||
|
||||
namespace App\Http\Controllers\Api\Remote\Servers;
|
||||
|
||||
use App\Exceptions\Http\HttpForbiddenException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Api\Remote\ServerRequest;
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Node;
|
||||
use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonServerRepository;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
|
||||
use Throwable;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
class ServerTransferController extends Controller
|
||||
{
|
||||
@@ -29,13 +32,22 @@ class ServerTransferController extends Controller
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function failure(ServerRequest $request, Server $server): JsonResponse
|
||||
public function failure(Request $request, Server $server): JsonResponse
|
||||
{
|
||||
$transfer = $server->transfer;
|
||||
if (is_null($transfer)) {
|
||||
throw new ConflictHttpException('Server is not being transferred.');
|
||||
}
|
||||
|
||||
/* @var Node $node */
|
||||
Assert::isInstanceOf($node = $request->attributes->get('node'), Node::class);
|
||||
|
||||
// Either node can tell the panel that the transfer has failed. Only the new node
|
||||
// can tell the panel that it was successful.
|
||||
if (!$node->is($transfer->newNode) && !$node->is($transfer->oldNode)) {
|
||||
throw new HttpForbiddenException('Requesting node does not have permission to access this server.');
|
||||
}
|
||||
|
||||
$this->connection->transaction(function () use ($transfer) {
|
||||
$transfer->forceFill(['successful' => false])->saveOrFail();
|
||||
|
||||
@@ -53,13 +65,22 @@ class ServerTransferController extends Controller
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function success(ServerRequest $request, Server $server): JsonResponse
|
||||
public function success(Request $request, Server $server): JsonResponse
|
||||
{
|
||||
$transfer = $server->transfer;
|
||||
if (is_null($transfer)) {
|
||||
throw new ConflictHttpException('Server is not being transferred.');
|
||||
}
|
||||
|
||||
/* @var Node $node */
|
||||
Assert::isInstanceOf($node = $request->attributes->get('node'), Node::class);
|
||||
|
||||
// Only the new node communicates a successful state to the panel, so we should
|
||||
// not allow the old node to hit this endpoint.
|
||||
if (!$node->is($transfer->newNode)) {
|
||||
throw new HttpForbiddenException('Requesting node does not have permission to access this server.');
|
||||
}
|
||||
|
||||
/** @var Server $server */
|
||||
$server = $this->connection->transaction(function () use ($server, $transfer) {
|
||||
$data = [];
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseVerifier;
|
||||
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery as BaseMiddleware;
|
||||
|
||||
class VerifyCsrfToken extends BaseVerifier
|
||||
class PreventRequestForgery extends BaseMiddleware
|
||||
{
|
||||
/**
|
||||
* The URIs that should be excluded from CSRF verification. These are
|
||||
@@ -23,7 +23,7 @@ class RequireTwoFactorAuthentication
|
||||
* order to perform actions. If so, we check the level at which it is required (all users
|
||||
* or just admins) and then check if the user has enabled it for their account.
|
||||
*
|
||||
* @throws \App\Exceptions\Http\TwoFactorAuthRequiredException
|
||||
* @throws TwoFactorAuthRequiredException
|
||||
*/
|
||||
public function handle(Request $request, \Closure $next): mixed
|
||||
{
|
||||
|
||||
46
app/Http/Middleware/SetSecurityHeaders.php
Normal file
46
app/Http/Middleware/SetSecurityHeaders.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class SetSecurityHeaders
|
||||
{
|
||||
/**
|
||||
* Ideally we move away from X-Frame-Options/X-XSS-Protection and implement a
|
||||
* proper standard CSP, but I can guarantee that will break for a lot of folks
|
||||
* using custom plugins and who knows what image embeds.
|
||||
*
|
||||
* We'll circle back to that at a later date when it can be more fully controlled
|
||||
* by the admin to support those cases without too much trouble.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected static array $headers = [
|
||||
'X-Frame-Options' => 'DENY',
|
||||
'X-Content-Type-Options' => 'nosniff',
|
||||
'X-XSS-Protection' => '1; mode=block',
|
||||
'Referrer-Policy' => 'no-referrer-when-downgrade',
|
||||
];
|
||||
|
||||
/**
|
||||
* Enforces some basic security headers on all responses returned by the software.
|
||||
* If a header has already been set in another location within the code it will be
|
||||
* skipped over here.
|
||||
*
|
||||
* @param (\Closure(mixed): Response) $next
|
||||
*/
|
||||
public function handle(Request $request, \Closure $next): mixed
|
||||
{
|
||||
$response = $next($request);
|
||||
|
||||
foreach (static::$headers as $key => $value) {
|
||||
if (!$response->headers->has($key)) {
|
||||
$response->headers->set($key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@ abstract class ApplicationApiRequest extends FormRequest
|
||||
$value = $this->route()->parameter($key);
|
||||
|
||||
Assert::isInstanceOf($value, $expect);
|
||||
Assert::isInstanceOf($value, Model::class);
|
||||
Assert::isInstanceOf($value, Model::class); // @phpstan-ignore staticMethod.alreadyNarrowedType
|
||||
Assert::true($value->exists);
|
||||
|
||||
/* @var T $value */
|
||||
|
||||
@@ -2,8 +2,15 @@
|
||||
|
||||
namespace App\Http\Requests\Api\Remote;
|
||||
|
||||
class InstallationDataRequest extends ServerRequest
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class InstallationDataRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string|string[]>
|
||||
*/
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\Remote;
|
||||
|
||||
use App\Models\Node;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ServerRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
/** @var Node $node */
|
||||
$node = $this->attributes->get('node');
|
||||
|
||||
/** @var ?Server $server */
|
||||
$server = $this->route()->parameter('server');
|
||||
|
||||
if ($server) {
|
||||
if ($server->transfer) {
|
||||
return $server->transfer->old_node === $node->id || $server->transfer->new_node === $node->id;
|
||||
}
|
||||
|
||||
return $server->node_id === $node->id;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
|
||||
abstract class Job
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queueable Jobs
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This job base class provides a central location to place any logic that
|
||||
| is shared across all of your jobs. The trait included with the class
|
||||
| provides access to the "onQueue" and "delay" queue helper methods.
|
||||
|
|
||||
*/
|
||||
|
||||
use Queueable;
|
||||
}
|
||||
55
app/Jobs/Plugin/InstallPlugin.php
Normal file
55
app/Jobs/Plugin/InstallPlugin.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\Plugin;
|
||||
|
||||
use App\Filament\Admin\Resources\Plugins\Pages\ListPlugins;
|
||||
use App\Models\Plugin;
|
||||
use App\Models\User;
|
||||
use App\Services\Helpers\PluginService;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class InstallPlugin implements ShouldBeUnique, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public function __construct(public User $user, public Plugin $plugin) {}
|
||||
|
||||
public function handle(PluginService $pluginService): void
|
||||
{
|
||||
try {
|
||||
$pluginService->installPlugin($this->plugin, !$this->plugin->isTheme() || !$pluginService->hasThemePluginEnabled());
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title(trans('admin/plugin.notifications.installed'))
|
||||
->body($this->plugin->name)
|
||||
->actions([
|
||||
Action::make('goto_plugins')
|
||||
->label(trans('admin/plugin.notifications.goto_plugins'))
|
||||
->url(ListPlugins::getUrl(panel: 'admin')),
|
||||
])
|
||||
->sendToDatabase($this->user);
|
||||
} catch (Exception $exception) {
|
||||
report($exception);
|
||||
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title(trans('admin/plugin.notifications.install_error'))
|
||||
->body($exception->getMessage())
|
||||
->sendToDatabase($this->user);
|
||||
}
|
||||
}
|
||||
|
||||
public function uniqueId(): string
|
||||
{
|
||||
return 'plugin:install:' . $this->plugin->id;
|
||||
}
|
||||
}
|
||||
55
app/Jobs/Plugin/UninstallPlugin.php
Normal file
55
app/Jobs/Plugin/UninstallPlugin.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\Plugin;
|
||||
|
||||
use App\Filament\Admin\Resources\Plugins\Pages\ListPlugins;
|
||||
use App\Models\Plugin;
|
||||
use App\Models\User;
|
||||
use App\Services\Helpers\PluginService;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class UninstallPlugin implements ShouldBeUnique, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public function __construct(public User $user, public Plugin $plugin) {}
|
||||
|
||||
public function handle(PluginService $pluginService): void
|
||||
{
|
||||
try {
|
||||
$pluginService->uninstallPlugin($this->plugin);
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title(trans('admin/plugin.notifications.uninstalled'))
|
||||
->body($this->plugin->name)
|
||||
->actions([
|
||||
Action::make('goto_plugins')
|
||||
->label(trans('admin/plugin.notifications.goto_plugins'))
|
||||
->url(ListPlugins::getUrl(panel: 'admin')),
|
||||
])
|
||||
->sendToDatabase($this->user);
|
||||
} catch (Exception $exception) {
|
||||
report($exception);
|
||||
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title(trans('admin/plugin.notifications.uninstall_error'))
|
||||
->body($exception->getMessage())
|
||||
->sendToDatabase($this->user);
|
||||
}
|
||||
}
|
||||
|
||||
public function uniqueId(): string
|
||||
{
|
||||
return 'plugin:uninstall:' . $this->plugin->id;
|
||||
}
|
||||
}
|
||||
55
app/Jobs/Plugin/UpdatePlugin.php
Normal file
55
app/Jobs/Plugin/UpdatePlugin.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\Plugin;
|
||||
|
||||
use App\Filament\Admin\Resources\Plugins\Pages\ListPlugins;
|
||||
use App\Models\Plugin;
|
||||
use App\Models\User;
|
||||
use App\Services\Helpers\PluginService;
|
||||
use Exception;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class UpdatePlugin implements ShouldBeUnique, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public function __construct(public User $user, public Plugin $plugin) {}
|
||||
|
||||
public function handle(PluginService $pluginService): void
|
||||
{
|
||||
try {
|
||||
$pluginService->updatePlugin($this->plugin);
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
->title(trans('admin/plugin.notifications.updated'))
|
||||
->body($this->plugin->name)
|
||||
->actions([
|
||||
Action::make('goto_plugins')
|
||||
->label(trans('admin/plugin.notifications.goto_plugins'))
|
||||
->url(ListPlugins::getUrl(panel: 'admin')),
|
||||
])
|
||||
->sendToDatabase($this->user);
|
||||
} catch (Exception $exception) {
|
||||
report($exception);
|
||||
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title(trans('admin/plugin.notifications.update_error'))
|
||||
->body($exception->getMessage())
|
||||
->sendToDatabase($this->user);
|
||||
}
|
||||
}
|
||||
|
||||
public function uniqueId(): string
|
||||
{
|
||||
return 'plugin:update:' . $this->plugin->id;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use App\Models\WebhookConfiguration;
|
||||
use Exception;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
@@ -34,7 +35,9 @@ class ProcessWebhook implements ShouldQueue
|
||||
$data = reset($data);
|
||||
}
|
||||
|
||||
if (is_object($data)) {
|
||||
if ($data instanceof Arrayable) {
|
||||
$data = $data->toArray();
|
||||
} elseif (is_object($data)) {
|
||||
$data = get_object_vars($data);
|
||||
}
|
||||
|
||||
@@ -64,7 +67,7 @@ class ProcessWebhook implements ShouldQueue
|
||||
$headers = [];
|
||||
|
||||
if ($this->webhookConfiguration->type === WebhookType::Regular) {
|
||||
foreach ($this->webhookConfiguration->headers as $key => $value) {
|
||||
foreach ($this->webhookConfiguration->headers ?? [] as $key => $value) {
|
||||
$headers[$key] = $this->webhookConfiguration->replaceVars($data, $value);
|
||||
}
|
||||
}
|
||||
|
||||
55
app/Jobs/RevokeSftpAccessJob.php
Normal file
55
app/Jobs/RevokeSftpAccessJob.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Node;
|
||||
use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonServerRepository;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Queue\Attributes\DeleteWhenMissingModels;
|
||||
use Illuminate\Queue\Attributes\WithoutRelations;
|
||||
|
||||
/**
|
||||
* Revokes all SFTP access for a user on a given node or for a specific server.
|
||||
*/
|
||||
#[DeleteWhenMissingModels]
|
||||
class RevokeSftpAccessJob implements ShouldBeUnique, ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public int $tries = 3;
|
||||
|
||||
public int $maxExceptions = 1;
|
||||
|
||||
public function __construct(
|
||||
public readonly string $user,
|
||||
#[WithoutRelations]
|
||||
public readonly Server|Node $target,
|
||||
) {}
|
||||
|
||||
public function uniqueId(): string
|
||||
{
|
||||
$target = $this->target instanceof Node ? "node:{$this->target->uuid}" : "server:{$this->target->uuid}";
|
||||
|
||||
return "revoke-sftp:{$this->user}:{$target}";
|
||||
}
|
||||
|
||||
public function handle(DaemonServerRepository $repository): void
|
||||
{
|
||||
try {
|
||||
if ($this->target instanceof Server) {
|
||||
$repository->setServer($this->target)->deauthorize($this->user);
|
||||
} else {
|
||||
$repository->setNode($this->target)->deauthorize($this->user);
|
||||
}
|
||||
} catch (ConnectionException) {
|
||||
// Keep retrying this job with a longer and longer backoff until we hit three
|
||||
// attempts at which point we stop and will assume the node is fully offline
|
||||
// and we are just wasting time.
|
||||
$this->release($this->attempts() * 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use App\Jobs\Job;
|
||||
use App\Models\Task;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Exception;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
@@ -14,9 +15,10 @@ use Illuminate\Queue\SerializesModels;
|
||||
use InvalidArgumentException;
|
||||
use Throwable;
|
||||
|
||||
class RunTaskJob extends Job implements ShouldQueue
|
||||
class RunTaskJob implements ShouldQueue
|
||||
{
|
||||
use InteractsWithQueue;
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
|
||||
25
app/Listeners/RevocationListener.php
Normal file
25
app/Listeners/RevocationListener.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Events\User\Deleting;
|
||||
use App\Events\User\PasswordChanged;
|
||||
use App\Jobs\RevokeSftpAccessJob;
|
||||
use App\Models\Node;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
class RevocationListener
|
||||
{
|
||||
public function handle(Deleting|PasswordChanged $event): void
|
||||
{
|
||||
$user = $event->user;
|
||||
|
||||
// Look at all of the nodes that a user is associated with and trigger a job
|
||||
// that disconnects them from websockets and SFTP.
|
||||
Node::query()
|
||||
->whereIn('nodes.id', $user->directAccessibleServers()->select('servers.node_id')->distinct())
|
||||
->chunk(50, function (Collection $nodes) use ($user) {
|
||||
$nodes->each(fn (Node $node) => RevokeSftpAccessJob::dispatch($user->uuid, $node));
|
||||
});
|
||||
}
|
||||
}
|
||||
37
app/Listeners/Server/BackupCompletedListener.php
Normal file
37
app/Listeners/Server/BackupCompletedListener.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listeners\Server;
|
||||
|
||||
use App\Events\Server\BackupCompleted;
|
||||
use App\Filament\Server\Resources\Backups\Pages\ListBackups;
|
||||
use App\Notifications\BackupCompleted as BackupCompletedNotification;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Notifications\Notification;
|
||||
|
||||
class BackupCompletedListener
|
||||
{
|
||||
public function handle(BackupCompleted $event): void
|
||||
{
|
||||
$event->backup->loadMissing('server');
|
||||
$event->backup->server->loadMissing('user');
|
||||
|
||||
$locale = $event->backup->server->user->language ?? 'en';
|
||||
|
||||
Notification::make()
|
||||
->status($event->backup->is_successful ? 'success' : 'danger')
|
||||
->title(trans('notifications.backup_' . ($event->backup->is_successful ? 'completed' : 'failed'), locale: $locale))
|
||||
->body(trans('notifications.backup_body', ['name' => $event->backup->name, 'server' => $event->backup->server->name], $locale))
|
||||
->actions([
|
||||
Action::make('exclude_view')
|
||||
->button()
|
||||
->label(trans('notifications.view_backups', locale: $locale))
|
||||
->markAsRead()
|
||||
->url(fn () => ListBackups::getUrl(panel: 'server', tenant: $event->backup->server)),
|
||||
])
|
||||
->sendToDatabase($event->backup->server->user);
|
||||
|
||||
if (config()->get('panel.email.send_backup_completed_notification', true)) {
|
||||
$event->backup->server->user->notify(new BackupCompletedNotification($event->backup));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,8 @@ class SubUserAddedListener
|
||||
])
|
||||
->sendToDatabase($event->subuser->user);
|
||||
|
||||
$event->subuser->user->notify(new AddedToServer($event->subuser->server));
|
||||
if (config('panel.email.send_added_to_server_notification', true)) {
|
||||
$event->subuser->user->notify(new AddedToServer($event->subuser->server));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ class SubUserRemovedListener
|
||||
->body(trans('notifications.user_removed.body', ['server' => $event->server->name], $locale))
|
||||
->sendToDatabase($event->user);
|
||||
|
||||
$event->user->notify(new RemovedFromServer($event->server));
|
||||
if (config('panel.email.send_removed_from_server_notification', true)) {
|
||||
$event->user->notify(new RemovedFromServer($event->server));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ class AlertBannerContainer extends Component
|
||||
}
|
||||
}
|
||||
|
||||
#[On('removeAlertBanner')]
|
||||
public function remove(string $id): void
|
||||
{
|
||||
if ($this->alertBanners->has($id)) {
|
||||
|
||||
@@ -11,7 +11,7 @@ use Filament\Support\Exceptions\Halt;
|
||||
|
||||
class RequirementsStep
|
||||
{
|
||||
public const MIN_PHP_VERSION = '8.2';
|
||||
public const MIN_PHP_VERSION = '8.3';
|
||||
|
||||
public static function make(): Step
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Services\Nodes\NodeJWTService;
|
||||
use App\Services\Servers\GetUserPermissionsService;
|
||||
use Filament\Support\Enums\IconSize;
|
||||
use Filament\Tables\View\Components\Columns\IconColumnComponent\IconComponent;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\View\ComponentAttributeBag;
|
||||
use Livewire\Attributes\Locked;
|
||||
@@ -30,7 +31,7 @@ class NodeClientConnectivity extends Component
|
||||
$this->nodeJWTService = $nodeJWTService;
|
||||
}
|
||||
|
||||
public function render(): \Illuminate\Contracts\View\View
|
||||
public function render(): View
|
||||
{
|
||||
$httpUrl = $this->node->getConnectionAddress();
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ use App\Enums\TablerIcon;
|
||||
use App\Events\ActivityLogged;
|
||||
use App\Traits\HasValidation;
|
||||
use BackedEnum;
|
||||
use Carbon\Carbon;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Support\Contracts\HasIcon;
|
||||
use Filament\Support\Contracts\HasLabel;
|
||||
@@ -17,6 +16,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Str;
|
||||
@@ -31,28 +31,28 @@ use LogicException;
|
||||
* @property string|null $description
|
||||
* @property string|null $actor_type
|
||||
* @property int|null $actor_id
|
||||
* @property Collection<array-key, mixed> $properties
|
||||
* @property Carbon $timestamp
|
||||
* @property int|null $api_key_id
|
||||
* @property Collection|null $properties
|
||||
* @property \Carbon\Carbon $timestamp
|
||||
* @property Model|\Eloquent $actor
|
||||
* @property \Illuminate\Database\Eloquent\Collection|ActivityLogSubject[] $subjects
|
||||
* @property int|null $subjects_count
|
||||
* @property ApiKey|null $apiKey
|
||||
* @property-read Model|\Eloquent|null $actor
|
||||
* @property-read ApiKey|null $apiKey
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, ActivityLogSubject> $subjects
|
||||
* @property-read int|null $subjects_count
|
||||
*
|
||||
* @method static Builder|ActivityLog forActor(Model $actor)
|
||||
* @method static Builder|ActivityLog forEvent(string $action)
|
||||
* @method static Builder|ActivityLog newModelQuery()
|
||||
* @method static Builder|ActivityLog newQuery()
|
||||
* @method static Builder|ActivityLog query()
|
||||
* @method static Builder|ActivityLog whereActorId($value)
|
||||
* @method static Builder|ActivityLog whereActorType($value)
|
||||
* @method static Builder|ActivityLog whereApiKeyId($value)
|
||||
* @method static Builder|ActivityLog whereDescription($value)
|
||||
* @method static Builder|ActivityLog whereEvent($value)
|
||||
* @method static Builder|ActivityLog whereId($value)
|
||||
* @method static Builder|ActivityLog whereIp($value)
|
||||
* @method static Builder|ActivityLog whereProperties($value)
|
||||
* @method static Builder|ActivityLog whereTimestamp($value)
|
||||
* @method static Builder<static>|ActivityLog forActor(\Illuminate\Database\Eloquent\Model $actor)
|
||||
* @method static Builder<static>|ActivityLog forEvent(string $action)
|
||||
* @method static Builder<static>|ActivityLog newModelQuery()
|
||||
* @method static Builder<static>|ActivityLog newQuery()
|
||||
* @method static Builder<static>|ActivityLog query()
|
||||
* @method static Builder<static>|ActivityLog whereActorId($value)
|
||||
* @method static Builder<static>|ActivityLog whereActorType($value)
|
||||
* @method static Builder<static>|ActivityLog whereApiKeyId($value)
|
||||
* @method static Builder<static>|ActivityLog whereDescription($value)
|
||||
* @method static Builder<static>|ActivityLog whereEvent($value)
|
||||
* @method static Builder<static>|ActivityLog whereId($value)
|
||||
* @method static Builder<static>|ActivityLog whereIp($value)
|
||||
* @method static Builder<static>|ActivityLog whereProperties($value)
|
||||
* @method static Builder<static>|ActivityLog whereTimestamp($value)
|
||||
*/
|
||||
class ActivityLog extends Model implements HasIcon, HasLabel
|
||||
{
|
||||
|
||||
@@ -14,14 +14,18 @@ use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $activity_log_id
|
||||
* @property int $subject_id
|
||||
* @property string $subject_type
|
||||
* @property ActivityLog|null $activityLog
|
||||
* @property Model|\Eloquent $subject
|
||||
* @property int $subject_id
|
||||
* @property-read ActivityLog $activityLog
|
||||
* @property-read Model|\Eloquent $subject
|
||||
*
|
||||
* @method static Builder|ActivityLogSubject newModelQuery()
|
||||
* @method static Builder|ActivityLogSubject newQuery()
|
||||
* @method static Builder|ActivityLogSubject query()
|
||||
* @method static Builder<static>|ActivityLogSubject newModelQuery()
|
||||
* @method static Builder<static>|ActivityLogSubject newQuery()
|
||||
* @method static Builder<static>|ActivityLogSubject query()
|
||||
* @method static Builder<static>|ActivityLogSubject whereActivityLogId($value)
|
||||
* @method static Builder<static>|ActivityLogSubject whereId($value)
|
||||
* @method static Builder<static>|ActivityLogSubject whereSubjectId($value)
|
||||
* @method static Builder<static>|ActivityLogSubject whereSubjectType($value)
|
||||
*/
|
||||
class ActivityLogSubject extends Pivot
|
||||
{
|
||||
|
||||
@@ -4,13 +4,12 @@ namespace App\Models;
|
||||
|
||||
use App\Exceptions\Service\Allocation\ServerUsingAllocationException;
|
||||
use App\Traits\HasValidation;
|
||||
use Carbon\Carbon;
|
||||
use Database\Factories\AllocationFactory;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* App\Models\Allocation.
|
||||
@@ -18,32 +17,33 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
* @property int $id
|
||||
* @property int $node_id
|
||||
* @property string $ip
|
||||
* @property string|null $ip_alias
|
||||
* @property int $port
|
||||
* @property int|null $server_id
|
||||
* @property string|null $notes
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property string $alias
|
||||
* @property bool $has_alias
|
||||
* @property string $address
|
||||
* @property Server|null $server
|
||||
* @property Node $node
|
||||
* @property string|null $ip_alias
|
||||
* @property string|null $notes
|
||||
* @property bool $is_locked
|
||||
* @property-read string $address
|
||||
* @property-read string $alias
|
||||
* @property-read bool $has_alias
|
||||
* @property-read Node $node
|
||||
* @property-read Server|null $server
|
||||
*
|
||||
* @method static AllocationFactory factory(...$parameters)
|
||||
* @method static Builder|Allocation newModelQuery()
|
||||
* @method static Builder|Allocation newQuery()
|
||||
* @method static Builder|Allocation query()
|
||||
* @method static Builder|Allocation whereCreatedAt($value)
|
||||
* @method static Builder|Allocation whereId($value)
|
||||
* @method static Builder|Allocation whereIp($value)
|
||||
* @method static Builder|Allocation whereIpAlias($value)
|
||||
* @method static Builder|Allocation whereNodeId($value)
|
||||
* @method static Builder|Allocation whereNotes($value)
|
||||
* @method static Builder|Allocation wherePort($value)
|
||||
* @method static Builder|Allocation whereServerId($value)
|
||||
* @method static Builder|Allocation whereUpdatedAt($value)
|
||||
* @method static \Database\Factories\AllocationFactory factory($count = null, $state = [])
|
||||
* @method static Builder<static>|Allocation newModelQuery()
|
||||
* @method static Builder<static>|Allocation newQuery()
|
||||
* @method static Builder<static>|Allocation query()
|
||||
* @method static Builder<static>|Allocation whereCreatedAt($value)
|
||||
* @method static Builder<static>|Allocation whereId($value)
|
||||
* @method static Builder<static>|Allocation whereIp($value)
|
||||
* @method static Builder<static>|Allocation whereIpAlias($value)
|
||||
* @method static Builder<static>|Allocation whereIsLocked($value)
|
||||
* @method static Builder<static>|Allocation whereNodeId($value)
|
||||
* @method static Builder<static>|Allocation whereNotes($value)
|
||||
* @method static Builder<static>|Allocation wherePort($value)
|
||||
* @method static Builder<static>|Allocation whereServerId($value)
|
||||
* @method static Builder<static>|Allocation whereUpdatedAt($value)
|
||||
*/
|
||||
class Allocation extends Model
|
||||
{
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace App\Models;
|
||||
|
||||
use App\Services\Acl\Api\AdminAcl;
|
||||
use App\Traits\HasValidation;
|
||||
use Database\Factories\ApiKeyFactory;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
@@ -17,41 +16,35 @@ use Webmozart\Assert\Assert;
|
||||
* App\Models\ApiKey.
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $user_id
|
||||
* @property int $key_type
|
||||
* @property string $identifier
|
||||
* @property string $token
|
||||
* @property string[]|null $permissions
|
||||
* @property string[]|null $allowed_ips
|
||||
* @property string|null $memo
|
||||
* @property Carbon|null $last_used_at
|
||||
* @property Carbon|null $expires_at
|
||||
* @property string[] $allowed_ips
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property User $tokenable
|
||||
* @property User $user
|
||||
* @property int|null $user_id
|
||||
* @property string|null $memo
|
||||
* @property string|null $identifier
|
||||
* @property int $key_type
|
||||
* @property Carbon|null $last_used_at
|
||||
* @property Carbon|null $expires_at
|
||||
* @property array<string, int> $permissions
|
||||
* @property-read User|null $user
|
||||
*
|
||||
* @method static ApiKeyFactory factory(...$parameters)
|
||||
* @method static Builder|ApiKey newModelQuery()
|
||||
* @method static Builder|ApiKey newQuery()
|
||||
* @method static Builder|ApiKey query()
|
||||
* @method static Builder|ApiKey whereAllowedIps($value)
|
||||
* @method static Builder|ApiKey whereCreatedAt($value)
|
||||
* @method static Builder|ApiKey whereId($value)
|
||||
* @method static Builder|ApiKey whereIdentifier($value)
|
||||
* @method static Builder|ApiKey whereKeyType($value)
|
||||
* @method static Builder|ApiKey whereLastUsedAt($value)
|
||||
* @method static Builder|ApiKey whereMemo($value)
|
||||
* @method static Builder|ApiKey whereRAllocations($value)
|
||||
* @method static Builder|ApiKey whereRDatabaseHosts($value)
|
||||
* @method static Builder|ApiKey whereREggs($value)
|
||||
* @method static Builder|ApiKey whereRNodes($value)
|
||||
* @method static Builder|ApiKey whereRServerDatabases($value)
|
||||
* @method static Builder|ApiKey whereRServers($value)
|
||||
* @method static Builder|ApiKey whereRUsers($value)
|
||||
* @method static Builder|ApiKey whereToken($value)
|
||||
* @method static Builder|ApiKey whereUpdatedAt($value)
|
||||
* @method static Builder|ApiKey whereUserId($value)
|
||||
* @method static \Database\Factories\ApiKeyFactory factory($count = null, $state = [])
|
||||
* @method static Builder<static>|ApiKey newModelQuery()
|
||||
* @method static Builder<static>|ApiKey newQuery()
|
||||
* @method static Builder<static>|ApiKey query()
|
||||
* @method static Builder<static>|ApiKey whereAllowedIps($value)
|
||||
* @method static Builder<static>|ApiKey whereCreatedAt($value)
|
||||
* @method static Builder<static>|ApiKey whereExpiresAt($value)
|
||||
* @method static Builder<static>|ApiKey whereId($value)
|
||||
* @method static Builder<static>|ApiKey whereIdentifier($value)
|
||||
* @method static Builder<static>|ApiKey whereKeyType($value)
|
||||
* @method static Builder<static>|ApiKey whereLastUsedAt($value)
|
||||
* @method static Builder<static>|ApiKey whereMemo($value)
|
||||
* @method static Builder<static>|ApiKey wherePermissions($value)
|
||||
* @method static Builder<static>|ApiKey whereToken($value)
|
||||
* @method static Builder<static>|ApiKey whereUpdatedAt($value)
|
||||
* @method static Builder<static>|ApiKey whereUserId($value)
|
||||
*/
|
||||
class ApiKey extends PersonalAccessToken
|
||||
{
|
||||
|
||||
@@ -18,20 +18,44 @@ use Illuminate\Database\Query\Builder;
|
||||
* @property int $id
|
||||
* @property int $server_id
|
||||
* @property string $uuid
|
||||
* @property bool $is_successful
|
||||
* @property bool $is_locked
|
||||
* @property string $name
|
||||
* @property string[] $ignored_files
|
||||
* @property string $disk
|
||||
* @property string|null $checksum
|
||||
* @property int $bytes
|
||||
* @property string|null $upload_id
|
||||
* @property CarbonImmutable|null $completed_at
|
||||
* @property BackupStatus $status
|
||||
* @property CarbonImmutable $created_at
|
||||
* @property CarbonImmutable $updated_at
|
||||
* @property CarbonImmutable|null $created_at
|
||||
* @property CarbonImmutable|null $updated_at
|
||||
* @property CarbonImmutable|null $deleted_at
|
||||
* @property Server $server
|
||||
* @property bool $is_successful
|
||||
* @property string|null $upload_id
|
||||
* @property bool $is_locked
|
||||
* @property-read Server $server
|
||||
* @property-read BackupStatus $status
|
||||
*
|
||||
* @method static \Database\Factories\BackupFactory factory($count = null, $state = [])
|
||||
* @method static BackupQueryBuilder<static>|Backup newModelQuery()
|
||||
* @method static BackupQueryBuilder<static>|Backup newQuery()
|
||||
* @method static BackupQueryBuilder<static>|Backup nonFailed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Backup onlyTrashed()
|
||||
* @method static BackupQueryBuilder<static>|Backup query()
|
||||
* @method static BackupQueryBuilder<static>|Backup whereBytes($value)
|
||||
* @method static BackupQueryBuilder<static>|Backup whereChecksum($value)
|
||||
* @method static BackupQueryBuilder<static>|Backup whereCompletedAt($value)
|
||||
* @method static BackupQueryBuilder<static>|Backup whereCreatedAt($value)
|
||||
* @method static BackupQueryBuilder<static>|Backup whereDeletedAt($value)
|
||||
* @method static BackupQueryBuilder<static>|Backup whereDisk($value)
|
||||
* @method static BackupQueryBuilder<static>|Backup whereId($value)
|
||||
* @method static BackupQueryBuilder<static>|Backup whereIgnoredFiles($value)
|
||||
* @method static BackupQueryBuilder<static>|Backup whereIsLocked($value)
|
||||
* @method static BackupQueryBuilder<static>|Backup whereIsSuccessful($value)
|
||||
* @method static BackupQueryBuilder<static>|Backup whereName($value)
|
||||
* @method static BackupQueryBuilder<static>|Backup whereServerId($value)
|
||||
* @method static BackupQueryBuilder<static>|Backup whereUpdatedAt($value)
|
||||
* @method static BackupQueryBuilder<static>|Backup whereUploadId($value)
|
||||
* @method static BackupQueryBuilder<static>|Backup whereUuid($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Backup withTrashed(bool $withTrashed = true)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Backup withoutTrashed()
|
||||
*/
|
||||
class Backup extends Model implements Validatable
|
||||
{
|
||||
|
||||
@@ -4,11 +4,11 @@ namespace App\Models;
|
||||
|
||||
use App\Contracts\Validatable;
|
||||
use App\Traits\HasValidation;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Carbon;
|
||||
use PDOException;
|
||||
|
||||
/**
|
||||
@@ -19,12 +19,27 @@ use PDOException;
|
||||
* @property string $username
|
||||
* @property string $remote
|
||||
* @property string $password
|
||||
* @property ?int $max_connections
|
||||
* @property string $jdbc
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
* @property Server $server
|
||||
* @property DatabaseHost $host
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property int|null $max_connections
|
||||
* @property-read DatabaseHost $host
|
||||
* @property-read string $jdbc
|
||||
* @property-read Server $server
|
||||
*
|
||||
* @method static \Database\Factories\DatabaseFactory factory($count = null, $state = [])
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Database newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Database newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Database query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Database whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Database whereDatabase($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Database whereDatabaseHostId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Database whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Database whereMaxConnections($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Database wherePassword($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Database whereRemote($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Database whereServerId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Database whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Database whereUsername($value)
|
||||
*/
|
||||
class Database extends Model implements Validatable
|
||||
{
|
||||
|
||||
@@ -21,13 +21,26 @@ use Illuminate\Support\Facades\DB;
|
||||
* @property string $username
|
||||
* @property string $password
|
||||
* @property int|null $max_databases
|
||||
* @property int|null $node_id
|
||||
* @property CarbonImmutable $created_at
|
||||
* @property CarbonImmutable $updated_at
|
||||
* @property Collection|Node[] $nodes
|
||||
* @property int|null $nodes_count
|
||||
* @property Collection|Database[] $databases
|
||||
* @property int|null $databases_count
|
||||
* @property CarbonImmutable|null $created_at
|
||||
* @property CarbonImmutable|null $updated_at
|
||||
* @property-read Collection<int, Database> $databases
|
||||
* @property-read int|null $databases_count
|
||||
* @property-read Collection<int, Node> $nodes
|
||||
* @property-read int|null $nodes_count
|
||||
*
|
||||
* @method static \Database\Factories\DatabaseHostFactory factory($count = null, $state = [])
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|DatabaseHost newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|DatabaseHost newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|DatabaseHost query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|DatabaseHost whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|DatabaseHost whereHost($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|DatabaseHost whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|DatabaseHost whereMaxDatabases($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|DatabaseHost whereName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|DatabaseHost wherePassword($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|DatabaseHost wherePort($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|DatabaseHost whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|DatabaseHost whereUsername($value)
|
||||
*/
|
||||
class DatabaseHost extends Model implements Validatable
|
||||
{
|
||||
|
||||
@@ -5,62 +5,96 @@ namespace App\Models;
|
||||
use App\Contracts\Validatable;
|
||||
use App\Exceptions\Service\Egg\HasChildrenException;
|
||||
use App\Exceptions\Service\HasActiveServersException;
|
||||
use App\Models\Traits\HasIcon;
|
||||
use App\Traits\HasValidation;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $uuid
|
||||
* @property string $author
|
||||
* @property string $name
|
||||
* @property string|null $description
|
||||
* @property string|null $image
|
||||
* @property string[]|null $features
|
||||
* @property array<string, string> $docker_images
|
||||
* @property string|null $update_url
|
||||
* @property bool $force_outgoing_ip
|
||||
* @property string[]|null $file_denylist
|
||||
* @property string|null $config_files
|
||||
* @property string|null $config_startup
|
||||
* @property string|null $config_logs
|
||||
* @property string|null $config_stop
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property int|null $config_from
|
||||
* @property array<string, string> $startup_commands
|
||||
* @property bool $script_is_privileged
|
||||
* @property string|null $config_stop
|
||||
* @property string|null $config_logs
|
||||
* @property string|null $config_startup
|
||||
* @property string|null $config_files
|
||||
* @property string|null $script_install
|
||||
* @property bool $script_is_privileged
|
||||
* @property string $script_entry
|
||||
* @property string $script_container
|
||||
* @property int|null $copy_script_from
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
* @property string|null $copy_script_install
|
||||
* @property string $copy_script_entry
|
||||
* @property string $copy_script_container
|
||||
* @property string|null $inherit_config_files
|
||||
* @property string|null $inherit_config_startup
|
||||
* @property string|null $inherit_config_logs
|
||||
* @property string|null $inherit_config_stop
|
||||
* @property string $inherit_file_denylist
|
||||
* @property string[]|null $inherit_features
|
||||
* @property string|null $uuid
|
||||
* @property string $author
|
||||
* @property string[]|null $features
|
||||
* @property array<string, string> $docker_images
|
||||
* @property string|null $update_url
|
||||
* @property string[]|null $file_denylist
|
||||
* @property bool $force_outgoing_ip
|
||||
* @property string[] $tags
|
||||
* @property Collection|Server[] $servers
|
||||
* @property int|null $servers_count
|
||||
* @property Collection|EggVariable[] $variables
|
||||
* @property int|null $variables_count
|
||||
* @property \App\Models\Egg|null $scriptFrom
|
||||
* @property \App\Models\Egg|null $configFrom
|
||||
* @property array<string, string> $startup_commands
|
||||
* @property-read Collection<int, Egg> $children
|
||||
* @property-read int|null $children_count
|
||||
* @property-read Egg|null $configFrom
|
||||
* @property-read string $copy_script_container
|
||||
* @property-read string $copy_script_entry
|
||||
* @property-read string|null $copy_script_install
|
||||
* @property-read string|null $icon
|
||||
* @property-read string|null $inherit_config_files
|
||||
* @property-read string|null $inherit_config_logs
|
||||
* @property-read string|null $inherit_config_startup
|
||||
* @property-read string|null $inherit_config_stop
|
||||
* @property-read string[]|null $inherit_features
|
||||
* @property-read string[]|null $inherit_file_denylist
|
||||
* @property-read Collection<int, Mount> $mounts
|
||||
* @property-read int|null $mounts_count
|
||||
* @property-read Egg|null $scriptFrom
|
||||
* @property-read Collection<int, Server> $servers
|
||||
* @property-read int|null $servers_count
|
||||
* @property-read Collection<int, EggVariable> $variables
|
||||
* @property-read int|null $variables_count
|
||||
*
|
||||
* @method static \Database\Factories\EggFactory factory($count = null, $state = [])
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereAuthor($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereConfigFiles($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereConfigFrom($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereConfigLogs($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereConfigStartup($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereConfigStop($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereCopyScriptFrom($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereDescription($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereDockerImages($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereFeatures($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereFileDenylist($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereForceOutgoingIp($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereScriptContainer($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereScriptEntry($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereScriptInstall($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereScriptIsPrivileged($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereStartupCommands($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereTags($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereUpdateUrl($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Egg whereUuid($value)
|
||||
*/
|
||||
class Egg extends Model implements Validatable
|
||||
{
|
||||
use HasFactory;
|
||||
use HasIcon;
|
||||
use HasValidation;
|
||||
|
||||
/**
|
||||
@@ -74,22 +108,6 @@ class Egg extends Model implements Validatable
|
||||
*/
|
||||
public const EXPORT_VERSION = 'PLCN_v3';
|
||||
|
||||
/**
|
||||
* Path to store egg icons relative to storage path.
|
||||
*/
|
||||
public const ICON_STORAGE_PATH = 'icons/egg';
|
||||
|
||||
/**
|
||||
* Supported image formats: file extension => MIME type
|
||||
*/
|
||||
public const IMAGE_FORMATS = [
|
||||
'png' => 'image/png',
|
||||
'jpg' => 'image/jpeg',
|
||||
'jpeg' => 'image/jpeg',
|
||||
'webp' => 'image/webp',
|
||||
'svg' => 'image/svg+xml',
|
||||
];
|
||||
|
||||
/**
|
||||
* Fields that are not mass assignable.
|
||||
*/
|
||||
@@ -344,16 +362,4 @@ class Egg extends Model implements Validatable
|
||||
{
|
||||
return str($this->name)->kebab()->lower()->trim()->split('/[^\w\-]/')->join('');
|
||||
}
|
||||
|
||||
public function getImageAttribute(): ?string
|
||||
{
|
||||
foreach (array_keys(static::IMAGE_FORMATS) as $ext) {
|
||||
$path = static::ICON_STORAGE_PATH . "/$this->uuid.$ext";
|
||||
if (Storage::disk('public')->exists($path)) {
|
||||
return Storage::disk('public')->url($path);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Models;
|
||||
use App\Contracts\Validatable;
|
||||
use App\Traits\HasValidation;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
@@ -13,7 +14,6 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $egg_id
|
||||
* @property null $sort
|
||||
* @property string $name
|
||||
* @property string $description
|
||||
* @property string $env_variable
|
||||
@@ -21,15 +21,32 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
* @property bool $user_viewable
|
||||
* @property bool $user_editable
|
||||
* @property string[] $rules
|
||||
* @property CarbonImmutable $created_at
|
||||
* @property CarbonImmutable $updated_at
|
||||
* @property bool $required
|
||||
* @property Egg $egg
|
||||
* @property ServerVariable $serverVariable
|
||||
* @property CarbonImmutable|null $created_at
|
||||
* @property CarbonImmutable|null $updated_at
|
||||
* @property int|null $sort
|
||||
* @property-read Egg|null $egg
|
||||
* @property-read bool $required
|
||||
* @property-read Collection<int, ServerVariable> $serverVariable
|
||||
* @property-read int|null $server_variable_count
|
||||
*
|
||||
* The "server_value" variable is only present on the object if you've loaded this model
|
||||
* using the server relationship.
|
||||
* @property string|null $server_value
|
||||
* @method static \Database\Factories\EggVariableFactory factory($count = null, $state = [])
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|EggVariable newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|EggVariable newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|EggVariable query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|EggVariable whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|EggVariable whereDefaultValue($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|EggVariable whereDescription($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|EggVariable whereEggId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|EggVariable whereEnvVariable($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|EggVariable whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|EggVariable whereName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|EggVariable whereRules($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|EggVariable whereSort($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|EggVariable whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|EggVariable whereUserEditable($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|EggVariable whereUserViewable($value)
|
||||
*
|
||||
* @property string|null $server_value This variable is only present on the object if you've loaded this model using the server relationship.
|
||||
*/
|
||||
class EggVariable extends Model implements Validatable
|
||||
{
|
||||
|
||||
@@ -6,12 +6,12 @@ use App\Enums\TablerIcon;
|
||||
use App\Livewire\AlertBanner;
|
||||
use App\Repositories\Daemon\DaemonFileRepository;
|
||||
use BackedEnum;
|
||||
use Carbon\Carbon;
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Sushi\Sushi;
|
||||
|
||||
/**
|
||||
@@ -28,6 +28,21 @@ use Sushi\Sushi;
|
||||
* @property bool $is_file
|
||||
* @property bool $is_symlink
|
||||
* @property string $mime_type
|
||||
*
|
||||
* @method static Builder<static>|File newModelQuery()
|
||||
* @method static Builder<static>|File newQuery()
|
||||
* @method static Builder<static>|File query()
|
||||
* @method static Builder<static>|File whereCreatedAt($value)
|
||||
* @method static Builder<static>|File whereId($value)
|
||||
* @method static Builder<static>|File whereIsDirectory($value)
|
||||
* @method static Builder<static>|File whereIsFile($value)
|
||||
* @method static Builder<static>|File whereIsSymlink($value)
|
||||
* @method static Builder<static>|File whereMimeType($value)
|
||||
* @method static Builder<static>|File whereMode($value)
|
||||
* @method static Builder<static>|File whereModeBits($value)
|
||||
* @method static Builder<static>|File whereModifiedAt($value)
|
||||
* @method static Builder<static>|File whereName($value)
|
||||
* @method static Builder<static>|File whereSize($value)
|
||||
*/
|
||||
class File extends Model
|
||||
{
|
||||
@@ -138,6 +153,9 @@ class File extends Model
|
||||
return [
|
||||
'created_at' => 'datetime',
|
||||
'modified_at' => 'datetime',
|
||||
'is_directory' => 'boolean',
|
||||
'is_file' => 'boolean',
|
||||
'is_symlink' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -12,14 +12,29 @@ use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
* @property int $id
|
||||
* @property string $uuid
|
||||
* @property string $name
|
||||
* @property string $description
|
||||
* @property string|null $description
|
||||
* @property string $source
|
||||
* @property string $target
|
||||
* @property bool $read_only
|
||||
* @property bool $user_mountable
|
||||
* @property Egg[]|Collection $eggs
|
||||
* @property Node[]|Collection $nodes
|
||||
* @property Server[]|Collection $servers
|
||||
* @property-read Collection<int, Egg> $eggs
|
||||
* @property-read int|null $eggs_count
|
||||
* @property-read Collection<int, Node> $nodes
|
||||
* @property-read int|null $nodes_count
|
||||
* @property-read Collection<int, Server> $servers
|
||||
* @property-read int|null $servers_count
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Mount newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Mount newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Mount query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Mount whereDescription($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Mount whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Mount whereName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Mount whereReadOnly($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Mount whereSource($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Mount whereTarget($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Mount whereUserMountable($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Mount whereUuid($value)
|
||||
*/
|
||||
class Mount extends Model implements Validatable
|
||||
{
|
||||
|
||||
@@ -6,7 +6,6 @@ use App\Contracts\Validatable;
|
||||
use App\Exceptions\Service\HasActiveServersException;
|
||||
use App\Repositories\Daemon\DaemonSystemRepository;
|
||||
use App\Traits\HasValidation;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -15,46 +14,84 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
use Illuminate\Notifications\DatabaseNotification;
|
||||
use Illuminate\Notifications\DatabaseNotificationCollection;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $uuid
|
||||
* @property bool $public
|
||||
* @property string $name
|
||||
* @property string|null $description
|
||||
* @property string $fqdn
|
||||
* @property string $scheme
|
||||
* @property bool $behind_proxy
|
||||
* @property bool $maintenance_mode
|
||||
* @property int $memory
|
||||
* @property int $memory_overallocate
|
||||
* @property int $disk
|
||||
* @property int $disk_overallocate
|
||||
* @property int $cpu
|
||||
* @property int $cpu_overallocate
|
||||
* @property int $upload_size
|
||||
* @property string $daemon_token_id
|
||||
* @property string $daemon_token
|
||||
* @property int $daemon_listen
|
||||
* @property int $daemon_connect
|
||||
* @property int $daemon_sftp
|
||||
* @property string|null $daemon_sftp_alias
|
||||
* @property string $daemon_base
|
||||
* @property string[] $tags
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
* @property Mount[]|Collection $mounts
|
||||
* @property int|null $mounts_count
|
||||
* @property Server[]|Collection $servers
|
||||
* @property int|null $servers_count
|
||||
* @property Allocation[]|Collection $allocations
|
||||
* @property int|null $allocations_count
|
||||
* @property Role[]|Collection $roles
|
||||
* @property int|null $roles_count
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property int $upload_size
|
||||
* @property bool $behind_proxy
|
||||
* @property string|null $description
|
||||
* @property bool $maintenance_mode
|
||||
* @property string|null $uuid
|
||||
* @property string|null $daemon_token_id
|
||||
* @property array<array-key, mixed>|null $tags
|
||||
* @property int $cpu
|
||||
* @property int $cpu_overallocate
|
||||
* @property string|null $daemon_sftp_alias
|
||||
* @property int $daemon_connect
|
||||
* @property-read Collection<int, Allocation> $allocations
|
||||
* @property-read int|null $allocations_count
|
||||
* @property-read Collection<int, DatabaseHost> $databaseHosts
|
||||
* @property-read int|null $database_hosts_count
|
||||
* @property-read Collection<int, Mount> $mounts
|
||||
* @property-read int|null $mounts_count
|
||||
* @property-read DatabaseNotificationCollection<int, DatabaseNotification> $notifications
|
||||
* @property-read int|null $notifications_count
|
||||
* @property-read Collection<int, Role> $roles
|
||||
* @property-read int|null $roles_count
|
||||
* @property-read Collection<int, Server> $servers
|
||||
* @property-read int|null $servers_count
|
||||
*
|
||||
* @method static \Database\Factories\NodeFactory factory($count = null, $state = [])
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereBehindProxy($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereCpu($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereCpuOverallocate($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereDaemonBase($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereDaemonConnect($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereDaemonListen($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereDaemonSftp($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereDaemonSftpAlias($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereDaemonToken($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereDaemonTokenId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereDescription($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereDisk($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereDiskOverallocate($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereFqdn($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereMaintenanceMode($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereMemory($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereMemoryOverallocate($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node wherePublic($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereScheme($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereTags($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereUploadSize($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Node whereUuid($value)
|
||||
*/
|
||||
class Node extends Model implements Validatable
|
||||
{
|
||||
|
||||
@@ -4,6 +4,16 @@ namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||
|
||||
/**
|
||||
* @property int $node_id
|
||||
* @property int $role_id
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|NodeRole newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|NodeRole newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|NodeRole query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|NodeRole whereNodeId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|NodeRole whereRoleId($value)
|
||||
*/
|
||||
class NodeRole extends Pivot
|
||||
{
|
||||
protected $table = 'node_role';
|
||||
|
||||
@@ -34,6 +34,26 @@ use Sushi\Sushi;
|
||||
* @property PluginStatus $status
|
||||
* @property string|null $status_message
|
||||
* @property int $load_order
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin whereAuthor($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin whereCategory($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin whereClass($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin whereComposerPackages($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin whereDescription($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin whereLoadOrder($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin whereName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin whereNamespace($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin wherePanelVersion($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin wherePanels($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin whereStatus($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin whereStatusMessage($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin whereUpdateUrl($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin whereUrl($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Plugin whereVersion($value)
|
||||
*/
|
||||
class Plugin extends Model implements HasPluginSettings
|
||||
{
|
||||
@@ -109,13 +129,11 @@ class Plugin extends Model implements HasPluginSettings
|
||||
continue;
|
||||
}
|
||||
|
||||
$plugin = Str::lower($plugin);
|
||||
|
||||
try {
|
||||
$data = File::json($path, JSON_THROW_ON_ERROR);
|
||||
$data['id'] = Str::lower($data['id']);
|
||||
|
||||
if ($data['id'] !== $plugin) {
|
||||
if ($data['id'] !== Str::lower($plugin)) {
|
||||
throw new PluginIdMismatchException("Plugin id mismatch for folder name ($plugin) and id in plugin.json ({$data['id']})!");
|
||||
}
|
||||
|
||||
@@ -161,7 +179,7 @@ class Plugin extends Model implements HasPluginSettings
|
||||
|
||||
if (!$exception instanceof JsonException) {
|
||||
$plugins[] = [
|
||||
'id' => $data['id'] ?? Str::uuid(),
|
||||
'id' => $exception instanceof PluginIdMismatchException ? $plugin : ($data['id'] ?? Str::uuid()),
|
||||
'name' => $data['name'] ?? Str::headline($plugin),
|
||||
'author' => $data['author'] ?? 'Unknown',
|
||||
'version' => $data['version'] ?? '0.0.0',
|
||||
@@ -261,7 +279,7 @@ class Plugin extends Model implements HasPluginSettings
|
||||
report($exception);
|
||||
}
|
||||
|
||||
return null;
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ use BackedEnum;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role as BaseRole;
|
||||
|
||||
@@ -16,12 +17,27 @@ use Spatie\Permission\Models\Role as BaseRole;
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
* @property string $guard_name
|
||||
* @property Collection|Permission[] $permissions
|
||||
* @property int|null $permissions_count
|
||||
* @property Collection|User[] $users
|
||||
* @property int|null $users_count
|
||||
* @property Collection|Node[] $nodes
|
||||
* @property int|null $nodes_count
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property-read NodeRole|null $pivot
|
||||
* @property-read Collection<int, Node> $nodes
|
||||
* @property-read int|null $nodes_count
|
||||
* @property-read Collection<int, Permission> $permissions
|
||||
* @property-read int|null $permissions_count
|
||||
* @property-read Collection<int, User> $users
|
||||
* @property-read int|null $users_count
|
||||
*
|
||||
* @method static \Database\Factories\RoleFactory factory($count = null, $state = [])
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Role newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Role newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Role permission($permissions, $without = false)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Role query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Role whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Role whereGuardName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Role whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Role whereName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Role whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Role withoutPermission($permissions)
|
||||
*/
|
||||
class Role extends BaseRole
|
||||
{
|
||||
|
||||
@@ -6,34 +6,55 @@ use App\Contracts\Validatable;
|
||||
use App\Enums\ScheduleStatus;
|
||||
use App\Helpers\Utilities;
|
||||
use App\Traits\HasValidation;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $server_id
|
||||
* @property string $name
|
||||
* @property string $cron_day_of_week
|
||||
* @property string $cron_month
|
||||
* @property string $cron_day_of_month
|
||||
* @property string $cron_hour
|
||||
* @property string $cron_minute
|
||||
* @property bool $is_active
|
||||
* @property bool $is_processing
|
||||
* @property bool $only_when_online
|
||||
* @property Carbon|null $last_run_at
|
||||
* @property Carbon|null $next_run_at
|
||||
* @property ScheduleStatus $status
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
* @property Server $server
|
||||
* @property Task[]|Collection $tasks
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property string $cron_month
|
||||
* @property bool $only_when_online
|
||||
* @property-read Server $server
|
||||
* @property-read ScheduleStatus $status
|
||||
* @property-read Collection<int, Task> $tasks
|
||||
* @property-read int|null $tasks_count
|
||||
*
|
||||
* @method static \Database\Factories\ScheduleFactory factory($count = null, $state = [])
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule whereCronDayOfMonth($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule whereCronDayOfWeek($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule whereCronHour($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule whereCronMinute($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule whereCronMonth($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule whereIsActive($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule whereIsProcessing($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule whereLastRunAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule whereName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule whereNextRunAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule whereOnlyWhenOnline($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule whereServerId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Schedule whereUpdatedAt($value)
|
||||
*/
|
||||
class Schedule extends Model implements Validatable
|
||||
{
|
||||
|
||||
@@ -7,11 +7,12 @@ use App\Enums\ContainerStatus;
|
||||
use App\Enums\ServerResourceType;
|
||||
use App\Enums\ServerState;
|
||||
use App\Exceptions\Http\Server\ServerStateConflictException;
|
||||
use App\Models\Traits\HasIcon;
|
||||
use App\Repositories\Daemon\DaemonServerRepository;
|
||||
use App\Services\Subusers\SubuserDeletionService;
|
||||
use App\Traits\HasValidation;
|
||||
use Carbon\CarbonInterface;
|
||||
use Database\Factories\ServerFactory;
|
||||
use Exception;
|
||||
use Filament\Models\Contracts\HasAvatar;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
@@ -30,111 +31,106 @@ use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
/**
|
||||
* \App\Models\Server.
|
||||
*
|
||||
* @property int $id
|
||||
* @property string|null $external_id
|
||||
* @property string $uuid
|
||||
* @property string $uuid_short
|
||||
* @property int $node_id
|
||||
* @property string $name
|
||||
* @property string $description
|
||||
* @property ServerState|null $status
|
||||
* @property bool $skip_scripts
|
||||
* @property int $owner_id
|
||||
* @property int $memory
|
||||
* @property int $swap
|
||||
* @property int $disk
|
||||
* @property int $io
|
||||
* @property int $cpu
|
||||
* @property string|null $threads
|
||||
* @property bool $oom_killer
|
||||
* @property int|null $allocation_id
|
||||
* @property int $egg_id
|
||||
* @property string $startup
|
||||
* @property string $image
|
||||
* @property string|null $icon
|
||||
* @property int|null $allocation_limit
|
||||
* @property int|null $database_limit
|
||||
* @property int|null $backup_limit
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property int|null $allocation_id
|
||||
* @property string $image
|
||||
* @property string|null $description
|
||||
* @property bool $skip_scripts
|
||||
* @property string|null $external_id
|
||||
* @property int|null $database_limit
|
||||
* @property int|null $allocation_limit
|
||||
* @property string|null $threads
|
||||
* @property int $backup_limit
|
||||
* @property ServerState|null $status
|
||||
* @property Carbon|null $installed_at
|
||||
* @property Collection|ActivityLog[] $activity
|
||||
* @property int|null $activity_count
|
||||
* @property Allocation|null $allocation
|
||||
* @property Collection|Allocation[] $allocations
|
||||
* @property int|null $allocations_count
|
||||
* @property Collection|Backup[] $backups
|
||||
* @property int|null $backups_count
|
||||
* @property Collection|Database[] $databases
|
||||
* @property int|null $databases_count
|
||||
* @property Egg $egg
|
||||
* @property Collection|Mount[] $mounts
|
||||
* @property int|null $mounts_count
|
||||
* @property Node $node
|
||||
* @property DatabaseNotificationCollection|DatabaseNotification[] $notifications
|
||||
* @property int|null $notifications_count
|
||||
* @property Collection|Schedule[] $schedules
|
||||
* @property int|null $schedules_count
|
||||
* @property Collection|Subuser[] $subusers
|
||||
* @property int|null $subusers_count
|
||||
* @property ServerTransfer|null $transfer
|
||||
* @property User $user
|
||||
* @property Collection|EggVariable[] $variables
|
||||
* @property int|null $variables_count
|
||||
*
|
||||
* @method static ServerFactory factory(...$parameters)
|
||||
* @method static Builder|Server newModelQuery()
|
||||
* @method static Builder|Server newQuery()
|
||||
* @method static Builder|Server query()
|
||||
* @method static Builder|Server whereAllocationId($value)
|
||||
* @method static Builder|Server whereAllocationLimit($value)
|
||||
* @method static Builder|Server whereBackupLimit($value)
|
||||
* @method static Builder|Server whereCpu($value)
|
||||
* @method static Builder|Server whereCreatedAt($value)
|
||||
* @method static Builder|Server whereDatabaseLimit($value)
|
||||
* @method static Builder|Server whereDescription($value)
|
||||
* @method static Builder|Server whereDisk($value)
|
||||
* @method static Builder|Server whereEggId($value)
|
||||
* @method static Builder|Server whereExternalId($value)
|
||||
* @method static Builder|Server whereId($value)
|
||||
* @method static Builder|Server whereImage($value)
|
||||
* @method static Builder|Server whereIo($value)
|
||||
* @method static Builder|Server whereMemory($value)
|
||||
* @method static Builder|Server whereName($value)
|
||||
* @method static Builder|Server whereNodeId($value)
|
||||
* @method static Builder|Server whereOomKiller($value)
|
||||
* @method static Builder|Server whereOwnerId($value)
|
||||
* @method static Builder|Server whereSkipScripts($value)
|
||||
* @method static Builder|Server whereStartup($value)
|
||||
* @method static Builder|Server whereStatus($value)
|
||||
* @method static Builder|Server whereSwap($value)
|
||||
* @method static Builder|Server whereThreads($value)
|
||||
* @method static Builder|Server whereUpdatedAt($value)
|
||||
* @method static Builder|Server whereUuid($value)
|
||||
* @method static Builder|Server whereuuid_short($value)
|
||||
*
|
||||
* @property string[]|null $docker_labels
|
||||
* @property string|null $ports
|
||||
* @property-read ContainerStatus|ServerState $condition
|
||||
* @property bool $oom_killer
|
||||
* @property array<array-key, mixed>|null $docker_labels
|
||||
* @property-read Collection<int, ActivityLog> $activity
|
||||
* @property-read int|null $activity_count
|
||||
* @property-read Allocation|null $allocation
|
||||
* @property-read Collection<int, Allocation> $allocations
|
||||
* @property-read int|null $allocations_count
|
||||
* @property-read Collection<int, Backup> $backups
|
||||
* @property-read int|null $backups_count
|
||||
* @property-read ServerState|ContainerStatus $condition
|
||||
* @property-read Collection<int, Database> $databases
|
||||
* @property-read int|null $databases_count
|
||||
* @property-read Egg $egg
|
||||
* @property-read Collection<int, EggVariable> $eggVariables
|
||||
* @property-read int|null $egg_variables_count
|
||||
* @property-read string|null $icon
|
||||
* @property-read Collection<int, Mount> $mounts
|
||||
* @property-read int|null $mounts_count
|
||||
* @property-read Node $node
|
||||
* @property-read DatabaseNotificationCollection<int, DatabaseNotification> $notifications
|
||||
* @property-read int|null $notifications_count
|
||||
* @property-read Collection<int, Schedule> $schedules
|
||||
* @property-read int|null $schedules_count
|
||||
* @property-read Collection<int, ServerVariable> $serverVariables
|
||||
* @property-read int|null $server_variables_count
|
||||
* @property-read Collection<int, Subuser> $subusers
|
||||
* @property-read int|null $subusers_count
|
||||
* @property-read ServerTransfer|null $transfer
|
||||
* @property-read User $user
|
||||
* @property-read Collection<int, EggVariable> $variables
|
||||
* @property-read int|null $variables_count
|
||||
*
|
||||
* @method static Builder|Server whereDockerLabels($value)
|
||||
* @method static Builder|Server whereInstalledAt($value)
|
||||
* @method static Builder|Server wherePorts($value)
|
||||
* @method static Builder|Server whereUuidShort($value)
|
||||
* @method static \Database\Factories\ServerFactory factory($count = null, $state = [])
|
||||
* @method static Builder<static>|Server newModelQuery()
|
||||
* @method static Builder<static>|Server newQuery()
|
||||
* @method static Builder<static>|Server query()
|
||||
* @method static Builder<static>|Server whereAllocationId($value)
|
||||
* @method static Builder<static>|Server whereAllocationLimit($value)
|
||||
* @method static Builder<static>|Server whereBackupLimit($value)
|
||||
* @method static Builder<static>|Server whereCpu($value)
|
||||
* @method static Builder<static>|Server whereCreatedAt($value)
|
||||
* @method static Builder<static>|Server whereDatabaseLimit($value)
|
||||
* @method static Builder<static>|Server whereDescription($value)
|
||||
* @method static Builder<static>|Server whereDisk($value)
|
||||
* @method static Builder<static>|Server whereDockerLabels($value)
|
||||
* @method static Builder<static>|Server whereEggId($value)
|
||||
* @method static Builder<static>|Server whereExternalId($value)
|
||||
* @method static Builder<static>|Server whereId($value)
|
||||
* @method static Builder<static>|Server whereImage($value)
|
||||
* @method static Builder<static>|Server whereInstalledAt($value)
|
||||
* @method static Builder<static>|Server whereIo($value)
|
||||
* @method static Builder<static>|Server whereMemory($value)
|
||||
* @method static Builder<static>|Server whereName($value)
|
||||
* @method static Builder<static>|Server whereNodeId($value)
|
||||
* @method static Builder<static>|Server whereOomKiller($value)
|
||||
* @method static Builder<static>|Server whereOwnerId($value)
|
||||
* @method static Builder<static>|Server whereSkipScripts($value)
|
||||
* @method static Builder<static>|Server whereStartup($value)
|
||||
* @method static Builder<static>|Server whereStatus($value)
|
||||
* @method static Builder<static>|Server whereSwap($value)
|
||||
* @method static Builder<static>|Server whereThreads($value)
|
||||
* @method static Builder<static>|Server whereUpdatedAt($value)
|
||||
* @method static Builder<static>|Server whereUuid($value)
|
||||
* @method static Builder<static>|Server whereUuidShort($value)
|
||||
*/
|
||||
class Server extends Model implements HasAvatar, Validatable
|
||||
{
|
||||
use HasFactory;
|
||||
use HasIcon;
|
||||
use HasValidation;
|
||||
use Notifiable;
|
||||
|
||||
@@ -144,22 +140,6 @@ class Server extends Model implements HasAvatar, Validatable
|
||||
*/
|
||||
public const RESOURCE_NAME = 'server';
|
||||
|
||||
/**
|
||||
* Path to store server icons relative to storage path.
|
||||
*/
|
||||
public const ICON_STORAGE_PATH = 'icons/server';
|
||||
|
||||
/**
|
||||
* Supported image formats: file extension => MIME type
|
||||
*/
|
||||
public const IMAGE_FORMATS = [
|
||||
'png' => 'image/png',
|
||||
'jpg' => 'image/jpeg',
|
||||
'jpeg' => 'image/jpeg',
|
||||
'webp' => 'image/webp',
|
||||
'svg' => 'image/svg+xml',
|
||||
];
|
||||
|
||||
/**
|
||||
* Default values when creating the model. We want to switch to disabling OOM killer
|
||||
* on server instances unless the user specifies otherwise in the request.
|
||||
@@ -477,6 +457,10 @@ class Server extends Model implements HasAvatar, Validatable
|
||||
|
||||
public function retrieveStatus(): ContainerStatus
|
||||
{
|
||||
if ($this->node->isUnderMaintenance()) {
|
||||
return ContainerStatus::Missing;
|
||||
}
|
||||
|
||||
return cache()->remember("servers.$this->uuid.status", now()->addSeconds(15), function () {
|
||||
// @phpstan-ignore myCustomRules.forbiddenGlobalFunctions
|
||||
$details = app(DaemonServerRepository::class)->setServer($this)->getDetails();
|
||||
@@ -490,6 +474,10 @@ class Server extends Model implements HasAvatar, Validatable
|
||||
*/
|
||||
public function retrieveResources(): array
|
||||
{
|
||||
if ($this->node->isUnderMaintenance()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return cache()->remember("servers.$this->uuid.resources", now()->addSeconds(15), function () {
|
||||
// @phpstan-ignore myCustomRules.forbiddenGlobalFunctions
|
||||
$details = app(DaemonServerRepository::class)->setServer($this)->getDetails();
|
||||
@@ -533,20 +521,8 @@ class Server extends Model implements HasAvatar, Validatable
|
||||
);
|
||||
}
|
||||
|
||||
public function getIconAttribute(): ?string
|
||||
{
|
||||
foreach (array_keys(static::IMAGE_FORMATS) as $ext) {
|
||||
$path = static::ICON_STORAGE_PATH . "/$this->uuid.$ext";
|
||||
if (Storage::disk('public')->exists($path)) {
|
||||
return Storage::disk('public')->url($path);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getFilamentAvatarUrl(): ?string
|
||||
{
|
||||
return $this->icon ?? $this->egg->image;
|
||||
return $this->icon ?? $this->egg->icon;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,30 +4,48 @@ namespace App\Models;
|
||||
|
||||
use App\Contracts\Validatable;
|
||||
use App\Traits\HasValidation;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $server_id
|
||||
* @property bool|null $successful
|
||||
* @property int $old_node
|
||||
* @property int $new_node
|
||||
* @property int|null $old_allocation
|
||||
* @property int|null $new_allocation
|
||||
* @property array<int>|null $old_additional_allocations array of allocation.id's
|
||||
* @property array<int>|null $new_additional_allocations array of allocation.id's
|
||||
* @property bool|null $successful
|
||||
* @property int[]|null $old_additional_allocations
|
||||
* @property int[]|null $new_additional_allocations
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property bool $archived
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
* @property Server $server
|
||||
* @property Node $oldNode
|
||||
* @property Node $newNode
|
||||
* @property-read Node|null $newNode
|
||||
* @property-read Node|null $oldNode
|
||||
* @property-read Server $server
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerTransfer newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerTransfer newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerTransfer query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerTransfer whereArchived($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerTransfer whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerTransfer whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerTransfer whereNewAdditionalAllocations($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerTransfer whereNewAllocation($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerTransfer whereNewNode($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerTransfer whereOldAdditionalAllocations($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerTransfer whereOldAllocation($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerTransfer whereOldNode($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerTransfer whereServerId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerTransfer whereSuccessful($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ServerTransfer whereUpdatedAt($value)
|
||||
*/
|
||||
class ServerTransfer extends Model implements Validatable
|
||||
{
|
||||
use HasFactory;
|
||||
use HasValidation;
|
||||
|
||||
/**
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user