Compare commits

..

1 Commits

Author SHA1 Message Date
github-actions[bot]
b979f7c165 ci(release): bump version 2025-08-11 21:19:27 +00:00
1650 changed files with 24871 additions and 54961 deletions

View File

@@ -64,9 +64,10 @@ body:
label: Error Logs label: Error Logs
description: | description: |
Run the following command to collect logs on your system. Run the following command to collect logs on your system.
Wings: `sudo wings diagnostics --hastebin-url=https://logs.pelican.dev`
Panel: `tail -n 300 /var/www/pelican/storage/logs/laravel-$(date +%F).log | curl --data-binary @- https://logs.pelican.dev` Wings: `sudo wings diagnostics`
placeholder: "https://logs.pelican.dev/c17f750e" Panel: `tail -n 150 /var/www/pelican/storage/logs/laravel-$(date +%F).log | curl -X POST -F 'c=@-' paste.pelistuff.com`
placeholder: "https://pelipaste.com/a1h6z"
render: bash render: bash
validations: validations:
required: false required: false

View File

@@ -11,9 +11,9 @@ jobs:
name: UI name: UI
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: true fail-fast: false
matrix: matrix:
node-version: [20, 22] node-version: [18, 20]
steps: steps:
- name: Code Checkout - name: Code Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4

View File

@@ -6,30 +6,174 @@ on:
- main - main
pull_request: pull_request:
env:
APP_ENV: testing
APP_DEBUG: "false"
APP_KEY: ThisIsARandomStringForTests12345
APP_TIMEZONE: UTC
APP_URL: http://localhost/
CACHE_DRIVER: array
MAIL_MAILER: array
SESSION_DRIVER: array
QUEUE_CONNECTION: sync
GUZZLE_TIMEOUT: 60
GUZZLE_CONNECT_TIMEOUT: 60
jobs: jobs:
mysql:
name: MySQL
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: [8.2, 8.3, 8.4]
database: ["mysql:8"]
services:
database:
image: ${{ matrix.database }}
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
MYSQL_DATABASE: testing
ports:
- 3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
env:
APP_ENV: testing
APP_DEBUG: "false"
APP_KEY: ThisIsARandomStringForTests12345
APP_TIMEZONE: UTC
APP_URL: http://localhost/
CACHE_DRIVER: array
MAIL_MAILER: array
SESSION_DRIVER: array
QUEUE_CONNECTION: sync
DB_CONNECTION: mysql
DB_HOST: 127.0.0.1
DB_DATABASE: testing
DB_USERNAME: root
GUZZLE_TIMEOUT: 60
GUZZLE_CONNECT_TIMEOUT: 60
steps:
- name: Code Checkout
uses: actions/checkout@v4
- name: Get cache directory
id: composer-cache
run: |
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-${{ matrix.php }}-
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2
coverage: none
- name: Install dependencies
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
- name: Unit tests
run: vendor/bin/pest tests/Unit
env:
DB_HOST: UNIT_NO_DB
SKIP_MIGRATIONS: true
- name: Integration tests
run: vendor/bin/pest tests/Integration
env:
DB_PORT: ${{ job.services.database.ports[3306] }}
DB_USERNAME: root
mariadb:
name: MariaDB
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: [8.2, 8.3, 8.4]
database: ["mariadb:10.6", "mariadb:10.11", "mariadb:11.4"]
services:
database:
image: ${{ matrix.database }}
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
MYSQL_DATABASE: testing
ports:
- 3306
options: --health-cmd="mariadb-admin ping || mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
env:
APP_ENV: testing
APP_DEBUG: "false"
APP_KEY: ThisIsARandomStringForTests12345
APP_TIMEZONE: UTC
APP_URL: http://localhost/
CACHE_DRIVER: array
MAIL_MAILER: array
SESSION_DRIVER: array
QUEUE_CONNECTION: sync
DB_CONNECTION: mariadb
DB_HOST: 127.0.0.1
DB_DATABASE: testing
DB_USERNAME: root
GUZZLE_TIMEOUT: 60
GUZZLE_CONNECT_TIMEOUT: 60
steps:
- name: Code Checkout
uses: actions/checkout@v4
- name: Get cache directory
id: composer-cache
run: |
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-${{ matrix.php }}-
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2
coverage: none
- name: Install dependencies
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
- name: Unit tests
run: vendor/bin/pest tests/Unit
env:
DB_HOST: UNIT_NO_DB
SKIP_MIGRATIONS: true
- name: Integration tests
run: vendor/bin/pest tests/Integration
env:
DB_PORT: ${{ job.services.database.ports[3306] }}
DB_USERNAME: root
sqlite: sqlite:
name: SQLite name: SQLite
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: true fail-fast: false
matrix: matrix:
php: [8.3, 8.4, 8.5] php: [8.2, 8.3, 8.4]
env: env:
APP_ENV: testing
APP_DEBUG: "false"
APP_KEY: ThisIsARandomStringForTests12345
APP_TIMEZONE: UTC
APP_URL: http://localhost/
CACHE_DRIVER: array
MAIL_MAILER: array
SESSION_DRIVER: array
QUEUE_CONNECTION: sync
DB_CONNECTION: sqlite DB_CONNECTION: sqlite
DB_DATABASE: testing.sqlite DB_DATABASE: testing.sqlite
GUZZLE_TIMEOUT: 60
GUZZLE_CONNECT_TIMEOUT: 60
steps: steps:
- name: Code Checkout - name: Code Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -61,162 +205,23 @@ jobs:
- name: Create SQLite file - name: Create SQLite file
run: touch database/testing.sqlite run: touch database/testing.sqlite
- name: Run Migrations
run: php artisan migrate --force --seed
- name: Unit tests - name: Unit tests
run: vendor/bin/pest tests/Unit --parallel run: vendor/bin/pest tests/Unit
env: env:
DB_HOST: UNIT_NO_DB DB_HOST: UNIT_NO_DB
SKIP_MIGRATIONS: true SKIP_MIGRATIONS: true
- name: Integration tests - name: Integration tests
run: vendor/bin/pest tests/Integration --parallel run: vendor/bin/pest tests/Integration
mysql:
name: MySQL
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
php: [8.5]
database: ["mysql:8.4", "mysql:9.6"]
services:
database:
image: ${{ matrix.database }}
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
MYSQL_DATABASE: testing
ports:
- 3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
env:
DB_CONNECTION: mysql
DB_HOST: 127.0.0.1
DB_DATABASE: testing
DB_USERNAME: root
steps:
- name: Code Checkout
uses: actions/checkout@v4
- name: Get cache directory
id: composer-cache
run: |
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-${{ matrix.php }}-
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2
coverage: none
- name: Install dependencies
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
- name: Run Migrations
run: php artisan migrate --force --seed
env:
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
env:
DB_PORT: ${{ job.services.database.ports[3306] }}
DB_USERNAME: root
mariadb:
name: MariaDB
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
php: [8.5]
database: ["mariadb:10.11", "mariadb:11.4"]
services:
database:
image: ${{ matrix.database }}
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
MYSQL_DATABASE: testing
ports:
- 3306
options: --health-cmd="mariadb-admin ping || mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
env:
DB_CONNECTION: mariadb
DB_HOST: 127.0.0.1
DB_DATABASE: testing
DB_USERNAME: root
steps:
- name: Code Checkout
uses: actions/checkout@v4
- name: Get cache directory
id: composer-cache
run: |
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-${{ matrix.php }}-
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2
coverage: none
- name: Install dependencies
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
- name: Run Migrations
run: php artisan migrate --force --seed
env:
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
env:
DB_PORT: ${{ job.services.database.ports[3306] }}
DB_USERNAME: root
postgresql: postgresql:
name: PostgreSQL name: PostgreSQL
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: true fail-fast: false
matrix: matrix:
php: [8.5] php: [8.2, 8.3, 8.4]
database: ["postgres:17", "postgres:18"] database: ["postgres:14"]
services: services:
database: database:
image: ${{ matrix.database }} image: ${{ matrix.database }}
@@ -233,11 +238,22 @@ jobs:
--health-timeout 5s --health-timeout 5s
--health-retries 5 --health-retries 5
env: env:
APP_ENV: testing
APP_DEBUG: "false"
APP_KEY: ThisIsARandomStringForTests12345
APP_TIMEZONE: UTC
APP_URL: http://localhost/
CACHE_DRIVER: array
MAIL_MAILER: array
SESSION_DRIVER: array
QUEUE_CONNECTION: sync
DB_CONNECTION: pgsql DB_CONNECTION: pgsql
DB_HOST: 127.0.0.1 DB_HOST: 127.0.0.1
DB_DATABASE: testing DB_DATABASE: testing
DB_USERNAME: postgres DB_USERNAME: postgres
DB_PASSWORD: postgres DB_PASSWORD: postgres
GUZZLE_TIMEOUT: 60
GUZZLE_CONNECT_TIMEOUT: 60
steps: steps:
- name: Code Checkout - name: Code Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -253,7 +269,6 @@ jobs:
path: ${{ steps.composer-cache.outputs.dir }} path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }} key: ${{ runner.os }}-composer-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
restore-keys: | restore-keys: |
${{ runner.os }}-composer-${{ matrix.php }}-
- name: Setup PHP - name: Setup PHP
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2
@@ -266,14 +281,11 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: composer install --no-interaction --no-suggest --no-progress --no-scripts run: composer install --no-interaction --no-suggest --no-progress --no-scripts
- name: Run Migrations
run: php artisan migrate --force --seed
- name: Unit tests - name: Unit tests
run: vendor/bin/pest tests/Unit --parallel run: vendor/bin/pest tests/Unit
env: env:
DB_HOST: UNIT_NO_DB DB_HOST: UNIT_NO_DB
SKIP_MIGRATIONS: true SKIP_MIGRATIONS: true
- name: Integration tests - name: Integration tests
run: vendor/bin/pest tests/Integration --parallel run: vendor/bin/pest tests/Integration

View File

@@ -66,6 +66,8 @@ jobs:
permissions: permissions:
contents: read contents: read
packages: write packages: write
strategy:
fail-fast: false
# Start a temp local registry because workflow can not pull from localy loaded images # Start a temp local registry because workflow can not pull from localy loaded images
services: services:
registry: registry:
@@ -132,11 +134,6 @@ jobs:
docker push localhost:5000/base-php:arm64 docker push localhost:5000/base-php:arm64
rm base-php-arm64.tar base-php-amd64.tar rm base-php-arm64.tar base-php-amd64.tar
- 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
- name: Build and Push (tag) - name: Build and Push (tag)
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
if: "github.event_name == 'release' && github.event.action == 'published'" if: "github.event_name == 'release' && github.event.action == 'published'"

View File

@@ -3,7 +3,7 @@ name: Lint
on: on:
pull_request: pull_request:
branches: branches:
- "**" - '**'
jobs: jobs:
pint: pint:
@@ -16,7 +16,7 @@ jobs:
- name: Setup PHP - name: Setup PHP
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2
with: with:
php-version: "8.4" php-version: "8.3"
extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip extensions: bcmath, curl, gd, mbstring, mysql, openssl, pdo, tokenizer, xml, zip
tools: composer:v2 tools: composer:v2
coverage: none coverage: none
@@ -33,9 +33,9 @@ jobs:
name: PHPStan name: PHPStan
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: true fail-fast: false
matrix: matrix:
php: [8.2, 8.3, 8.4, 8.5] php: [ 8.2, 8.3, 8.4 ]
steps: steps:
- name: Code Checkout - name: Code Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4

2
.gitignore vendored
View File

@@ -21,9 +21,9 @@ yarn-error.log
/.idea /.idea
/.nova /.nova
/.vscode /.vscode
/.ddev
public/assets/manifest.json public/assets/manifest.json
/database/*.sqlite* /database/*.sqlite*
filament-monaco-editor/
_ide_helper* _ide_helper*
/.phpstorm.meta.php /.phpstorm.meta.php

View File

@@ -2,7 +2,7 @@
# Pelican Production Dockerfile # Pelican Production Dockerfile
## ##
# If you want to build this locally you want to run `docker build -f Dockerfile.dev .` # If you want to build this locally you want to run `docker build -f Dockerfile.dev`
## ##
# ================================ # ================================
@@ -38,7 +38,7 @@ RUN yarn config set network-timeout 300000 \
FROM --platform=$TARGETOS/$TARGETARCH composer AS composerbuild FROM --platform=$TARGETOS/$TARGETARCH composer AS composerbuild
# Copy full code to optimize autoload # Copy full code to optimize autoload
COPY --exclude=docker/ . ./ COPY --exclude=Caddyfile --exclude=docker/ . ./
RUN composer dump-autoload --optimize RUN composer dump-autoload --optimize
@@ -50,7 +50,7 @@ FROM --platform=$TARGETOS/$TARGETARCH yarn AS yarnbuild
WORKDIR /build WORKDIR /build
# Copy full code # Copy full code
COPY --exclude=docker/ . ./ COPY --exclude=Caddyfile --exclude=docker/ . ./
COPY --from=composer /build . COPY --from=composer /build .
RUN yarn run build RUN yarn run build
@@ -62,35 +62,36 @@ FROM --platform=$TARGETOS/$TARGETARCH localhost:5000/base-php:$TARGETARCH AS fin
WORKDIR /var/www/html WORKDIR /var/www/html
# Install additional required libraries
RUN apk add --no-cache \ RUN apk add --no-cache \
# packages for running the panel caddy ca-certificates supervisor supercronic
caddy ca-certificates supervisor supercronic fcgi \
# required for installing plugins. Pulled from https://github.com/pelican-dev/panel/pull/2034
zip unzip 7zip bzip2-dev yarn git
# Copy composer binary for runtime plugin dependency management COPY --chown=root:www-data --chmod=640 --from=composerbuild /build .
COPY --from=composer /usr/local/bin/composer /usr/local/bin/composer COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public
COPY --chown=root:www-data --chmod=770 --from=composerbuild /build .
COPY --chown=root:www-data --chmod=770 --from=yarnbuild /build/public ./public # Set permissions
# First ensure all files are owned by root and restrict www-data to read access
RUN chown root:www-data ./ \
&& chmod 750 ./ \
# Files should not have execute set, but directories need it
&& find ./ -type d -exec chmod 750 {} \; \
# Create necessary directories
&& mkdir -p /pelican-data/storage /var/www/html/storage/app/public /var/run/supervisord /etc/supercronic \
# Symlinks for env, database, and avatars
&& ln -s /pelican-data/.env ./.env \
&& ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \
&& ln -sf /var/www/html/storage/app/public /var/www/html/public/storage \
&& ln -s /pelican-data/storage/avatars /var/www/html/storage/app/public/avatars \
&& ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \
# Allow www-data write permissions where necessary
&& chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord
# 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
&& 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 \
&& 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 # Configure Supervisor
COPY docker/supervisord.conf /etc/supervisord.conf COPY docker/supervisord.conf /etc/supervisord.conf
COPY docker/Caddyfile /etc/caddy/Caddyfile COPY docker/Caddyfile /etc/caddy/Caddyfile
# Add Laravel scheduler to crontab # Add Laravel scheduler to crontab
COPY docker/crontab /etc/crontabs/crontab COPY docker/crontab /etc/supercronic/crontab
COPY docker/entrypoint.sh /entrypoint.sh COPY docker/entrypoint.sh /entrypoint.sh
COPY docker/healthcheck.sh /healthcheck.sh COPY docker/healthcheck.sh /healthcheck.sh

View File

@@ -5,6 +5,6 @@ FROM --platform=$TARGETOS/$TARGETARCH php:8.4-fpm-alpine
ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/ ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
RUN install-php-extensions bcmath gd intl zip pcntl pdo_mysql pdo_pgsql bz2 RUN install-php-extensions bcmath gd intl zip opcache pcntl posix pdo_mysql pdo_pgsql
RUN rm /usr/local/bin/install-php-extensions RUN rm /usr/local/bin/install-php-extensions

View File

@@ -5,7 +5,7 @@ FROM --platform=$TARGETOS/$TARGETARCH php:8.4-fpm-alpine AS base
ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/ ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
RUN install-php-extensions bcmath gd intl zip pcntl pdo_mysql pdo_pgsql bz2 RUN install-php-extensions bcmath gd intl zip opcache pcntl posix pdo_mysql pdo_pgsql
RUN rm /usr/local/bin/install-php-extensions RUN rm /usr/local/bin/install-php-extensions
@@ -42,7 +42,7 @@ RUN yarn config set network-timeout 300000 \
FROM --platform=$TARGETOS/$TARGETARCH composer AS composerbuild FROM --platform=$TARGETOS/$TARGETARCH composer AS composerbuild
# Copy full code to optimize autoload # Copy full code to optimize autoload
COPY --exclude=docker/ . ./ COPY --exclude=Caddyfile --exclude=docker/ . ./
RUN composer dump-autoload --optimize RUN composer dump-autoload --optimize
@@ -54,7 +54,7 @@ FROM --platform=$TARGETOS/$TARGETARCH yarn AS yarnbuild
WORKDIR /build WORKDIR /build
# Copy full code # Copy full code
COPY --exclude=docker/ . ./ COPY --exclude=Caddyfile --exclude=docker/ . ./
COPY --from=composer /build . COPY --from=composer /build .
RUN yarn run build RUN yarn run build
@@ -68,35 +68,34 @@ WORKDIR /var/www/html
# Install additional required libraries # Install additional required libraries
RUN apk add --no-cache \ RUN apk add --no-cache \
# packages for running the panel caddy ca-certificates supervisor supercronic coreutils
caddy ca-certificates supervisor supercronic fcgi coreutils \
# required for installing plugins. Pulled from https://github.com/pelican-dev/panel/pull/2034
zip unzip 7zip bzip2-dev yarn git
# Copy composer binary for runtime plugin dependency management COPY --chown=root:www-data --chmod=640 --from=composerbuild /build .
COPY --from=composer /usr/local/bin/composer /usr/local/bin/composer COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public
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 # Set permissions
RUN mkdir -p /pelican-data/storage /pelican-data/plugins /var/run/supervisord \ # First ensure all files are owned by root and restrict www-data to read access
&& rm -rf /var/www/html/plugins \ RUN chown root:www-data ./ \
# Symlinks for env, database, storage, and plugins && chmod 750 ./ \
&& ln -s /pelican-data/.env /var/www/html/.env \ # Files should not have execute set, but directories need it
&& ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \ && find ./ -type d -exec chmod 750 {} \; \
&& ln -s /pelican-data/storage /var/www/html/public/storage \ # Create necessary directories
&& ln -s /pelican-data/storage /var/www/html/storage/app/public \ && mkdir -p /pelican-data/storage /var/www/html/storage/app/public /var/run/supervisord /etc/supercronic \
&& ln -s /pelican-data/plugins /var/www/html \ # Symlinks for env, database, and avatars
# Allow www-data write permissions where necessary && ln -s /pelican-data/.env ./.env \
&& chown -R www-data: /pelican-data .env ./storage ./plugins ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \ && ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \
&& chmod -R 770 /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \ && ln -sf /var/www/html/storage/app/public /var/www/html/public/storage \
&& chown -R www-data: /usr/local/etc/php/ /usr/local/etc/php-fpm.d/ /var/www/html/composer.json /var/www/html/composer.lock && ln -s /pelican-data/storage/avatars /var/www/html/storage/app/public/avatars \
&& ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \
# Allow www-data write permissions where necessary
&& chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord
# Configure Supervisor # Configure Supervisor
COPY docker/supervisord.conf /etc/supervisord.conf COPY docker/supervisord.conf /etc/supervisord.conf
COPY docker/Caddyfile /etc/caddy/Caddyfile COPY docker/Caddyfile /etc/caddy/Caddyfile
# Add Laravel scheduler to crontab # Add Laravel scheduler to crontab
COPY docker/crontab /etc/crontabs/crontab COPY docker/crontab /etc/supercronic/crontab
COPY docker/entrypoint.sh /entrypoint.sh COPY docker/entrypoint.sh /entrypoint.sh
COPY docker/healthcheck.sh /healthcheck.sh COPY docker/healthcheck.sh /healthcheck.sh

View File

@@ -1,48 +0,0 @@
<?php
namespace App\Console\Commands\Dev;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
class GenerateTablerIconsEnum extends Command
{
protected $signature = 'dev:generate-tabler-icons-enum';
protected $description = 'Generate an enum for tabler icons based on the secondnetwork/blade-tabler-icons svgs';
public function handle(): void
{
$files = File::files(base_path('vendor/secondnetwork/blade-tabler-icons/resources/svg'));
$files = array_filter($files, fn ($file) => $file->getExtension() === 'svg');
$enumContent = "<?php\n\n";
$enumContent .= "namespace App\\Enums;\n\n";
$enumContent .= "enum TablerIcon: string\n{\n";
foreach ($files as $file) {
$filename = pathinfo($file->getFilename(), PATHINFO_FILENAME);
// Letter V is duplicate, as "letter-v" and "letter-letter-v"
if (str($filename)->contains('letter-letter')) {
continue;
}
// Filled icons exist with "-f" and "-filled", we only want the later
if (str($filename)->endsWith('-f') && file_exists(base_path("vendor/secondnetwork/blade-tabler-icons/resources/svg/{$filename}illed.svg"))) {
continue;
}
$caseName = str($filename)->title()->replace('-', '');
$value = str($filename)->slug()->prepend('tabler-');
$enumContent .= " case $caseName = '$value';\n";
}
$enumContent .= "}\n";
File::put(base_path('app/Enums/TablerIcon.php'), $enumContent);
$this->info('Enum generated');
}
}

View File

@@ -7,7 +7,7 @@ use App\Models\Egg;
use App\Services\Eggs\Sharing\EggExporterService; use App\Services\Eggs\Sharing\EggExporterService;
use Exception; use Exception;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http; use JsonException;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
class CheckEggUpdatesCommand extends Command class CheckEggUpdatesCommand extends Command
@@ -21,12 +21,14 @@ class CheckEggUpdatesCommand extends Command
try { try {
$this->check($egg, $exporterService); $this->check($egg, $exporterService);
} catch (Exception $exception) { } catch (Exception $exception) {
$this->error("$egg->name: Error ({$exception->getMessage()})"); $this->error("{$egg->name}: Error ({$exception->getMessage()})");
} }
} }
} }
/** @throws Exception */ /**
* @throws JsonException
*/
private function check(Egg $egg, EggExporterService $exporterService): void private function check(Egg $egg, EggExporterService $exporterService): void
{ {
if (is_null($egg->update_url)) { if (is_null($egg->update_url)) {
@@ -42,13 +44,9 @@ class CheckEggUpdatesCommand extends Command
? Yaml::parse($exporterService->handle($egg->id, EggFormat::YAML)) ? Yaml::parse($exporterService->handle($egg->id, EggFormat::YAML))
: json_decode($exporterService->handle($egg->id, EggFormat::JSON), true); : json_decode($exporterService->handle($egg->id, EggFormat::JSON), true);
$remote = Http::timeout(5)->connectTimeout(1)->get($egg->update_url); $remote = file_get_contents($egg->update_url);
assert($remote !== false);
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); $remote = $isYaml ? Yaml::parse($remote) : json_decode($remote, true);
unset($local['exported_at'], $remote['exported_at']); unset($local['exported_at'], $remote['exported_at']);

View File

@@ -4,7 +4,6 @@ namespace App\Console\Commands\Egg;
use Exception; use Exception;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
class UpdateEggIndexCommand extends Command class UpdateEggIndexCommand extends Command
{ {
@@ -13,7 +12,8 @@ class UpdateEggIndexCommand extends Command
public function handle(): int public function handle(): int
{ {
try { try {
$data = Http::timeout(5)->connectTimeout(1)->get(config('panel.cdn.egg_index_url'))->throw()->json(); $data = file_get_contents('https://raw.githubusercontent.com/pelican-eggs/pelican-eggs.github.io/refs/heads/main/content/pelican.json');
$data = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
} catch (Exception $exception) { } catch (Exception $exception) {
$this->error($exception->getMessage()); $this->error($exception->getMessage());

View File

@@ -6,7 +6,6 @@ use App\Traits\EnvironmentWriterTrait;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Kernel; use Illuminate\Contracts\Console\Kernel;
use Illuminate\Database\DatabaseManager; use Illuminate\Database\DatabaseManager;
use PDOException;
class DatabaseSettingsCommand extends Command class DatabaseSettingsCommand extends Command
{ {
@@ -106,7 +105,7 @@ class DatabaseSettingsCommand extends Command
]); ]);
$this->database->connection('_panel_command_test')->getPdo(); $this->database->connection('_panel_command_test')->getPdo();
} catch (PDOException $exception) { } catch (\PDOException $exception) {
$this->output->error(sprintf('Unable to connect to the MySQL server using the provided credentials. The error returned was "%s".', $exception->getMessage())); $this->output->error(sprintf('Unable to connect to the MySQL server using the provided credentials. The error returned was "%s".', $exception->getMessage()));
$this->output->error(trans('commands.database_settings.DB_error_2')); $this->output->error(trans('commands.database_settings.DB_error_2'));
@@ -166,7 +165,7 @@ class DatabaseSettingsCommand extends Command
]); ]);
$this->database->connection('_panel_command_test')->getPdo(); $this->database->connection('_panel_command_test')->getPdo();
} catch (PDOException $exception) { } catch (\PDOException $exception) {
$this->output->error(sprintf('Unable to connect to the MariaDB server using the provided credentials. The error returned was "%s".', $exception->getMessage())); $this->output->error(sprintf('Unable to connect to the MariaDB server using the provided credentials. The error returned was "%s".', $exception->getMessage()));
$this->output->error(trans('commands.database_settings.DB_error_2')); $this->output->error(trans('commands.database_settings.DB_error_2'));

View File

@@ -2,7 +2,6 @@
namespace App\Console\Commands\Environment; namespace App\Console\Commands\Environment;
use App\Exceptions\PanelException;
use App\Traits\EnvironmentWriterTrait; use App\Traits\EnvironmentWriterTrait;
use Illuminate\Console\Command; use Illuminate\Console\Command;
@@ -29,7 +28,7 @@ class EmailSettingsCommand extends Command
/** /**
* Handle command execution. * Handle command execution.
* *
* @throws PanelException * @throws \App\Exceptions\PanelException
*/ */
public function handle(): void public function handle(): void
{ {

View File

@@ -4,8 +4,8 @@ namespace App\Console\Commands\Maintenance;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory;
use Illuminate\Contracts\Filesystem\Filesystem; use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory;
use SplFileInfo; use SplFileInfo;
class CleanServiceBackupFilesCommand extends Command class CleanServiceBackupFilesCommand extends Command

View File

@@ -5,7 +5,6 @@ namespace App\Console\Commands\Maintenance;
use App\Models\Backup; use App\Models\Backup;
use Carbon\CarbonImmutable; use Carbon\CarbonImmutable;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use InvalidArgumentException;
class PruneOrphanedBackupsCommand extends Command class PruneOrphanedBackupsCommand extends Command
{ {
@@ -17,7 +16,7 @@ class PruneOrphanedBackupsCommand extends Command
{ {
$since = $this->option('prune-age') ?? config('backups.prune_age', 360); $since = $this->option('prune-age') ?? config('backups.prune_age', 360);
if (!$since || !is_digit($since)) { if (!$since || !is_digit($since)) {
throw new InvalidArgumentException('The "--prune-age" argument must be a value greater than 0.'); throw new \InvalidArgumentException('The "--prune-age" argument must be a value greater than 0.');
} }
$query = Backup::query() $query = Backup::query()

View File

@@ -2,7 +2,6 @@
namespace App\Console\Commands\Node; namespace App\Console\Commands\Node;
use App\Exceptions\Model\DataValidationException;
use App\Models\Node; use App\Models\Node;
use Illuminate\Console\Command; use Illuminate\Console\Command;
@@ -35,7 +34,7 @@ class MakeNodeCommand extends Command
/** /**
* Handle the command execution process. * Handle the command execution process.
* *
* @throws DataValidationException * @throws \App\Exceptions\Model\DataValidationException
*/ */
public function handle(): void public function handle(): void
{ {

View File

@@ -17,7 +17,7 @@ class NodeConfigurationCommand extends Command
{ {
$column = ctype_digit((string) $this->argument('node')) ? 'id' : 'uuid'; $column = ctype_digit((string) $this->argument('node')) ? 'id' : 'uuid';
/** @var Node $node */ /** @var \App\Models\Node $node */
$node = Node::query()->where($column, $this->argument('node'))->firstOr(function () { $node = Node::query()->where($column, $this->argument('node'))->firstOr(function () {
$this->error(trans('commands.node_config.error_not_exist')); $this->error(trans('commands.node_config.error_not_exist'));

View File

@@ -1,19 +0,0 @@
<?php
namespace App\Console\Commands\Overrides;
use Illuminate\Foundation\Console\ConfigCacheCommand as BaseConfigCacheCommand;
class ConfigCacheCommand extends BaseConfigCacheCommand
{
/**
* Prevent config from being cached
*/
public function handle()
{
$this->components->warn('Configuration caching has been disabled.');
$this->line(' Reason: This application uses dynamic plugins. Caching config');
$this->line(' prevents /plugins/config/*.php files from being loaded correctly.');
}
}

View File

@@ -1,18 +0,0 @@
<?php
namespace App\Console\Commands\Overrides;
use Illuminate\Foundation\Console\OptimizeCommand as BaseOptimizeCommand;
class OptimizeCommand extends BaseOptimizeCommand
{
/**
* Prevent config from being cached
*
* @return array<string, string>
*/
protected function getOptimizeTasks()
{
return array_except(parent::getOptimizeTasks(), 'config');
}
}

View File

@@ -1,25 +0,0 @@
<?php
namespace App\Console\Commands\Plugin;
use App\Services\Helpers\PluginService;
use Exception;
use Illuminate\Console\Command;
class ComposerPluginsCommand extends Command
{
protected $signature = 'p:plugin:composer';
protected $description = 'Makes sure the needed composer packages for all installed plugins are available.';
public function handle(PluginService $pluginService): void
{
try {
$pluginService->manageComposerPackages();
} catch (Exception $exception) {
report($exception);
$this->error($exception->getMessage());
}
}
}

View File

@@ -1,37 +0,0 @@
<?php
namespace App\Console\Commands\Plugin;
use App\Models\Plugin;
use App\Services\Helpers\PluginService;
use Illuminate\Console\Command;
class DisablePluginCommand extends Command
{
protected $signature = 'p:plugin:disable {id?}';
protected $description = 'Disables a plugin';
public function handle(PluginService $pluginService): void
{
$id = $this->argument('id') ?? $this->choice('Plugin', Plugin::pluck('name', 'id')->toArray());
$plugin = Plugin::find(str($id)->lower()->toString());
if (!$plugin) {
$this->error('Plugin does not exist!');
return;
}
if (!$plugin->canDisable()) {
$this->error("Plugin can't be disabled!");
return;
}
$pluginService->disablePlugin($plugin);
$this->info('Plugin disabled.');
}
}

View File

@@ -1,43 +0,0 @@
<?php
namespace App\Console\Commands\Plugin;
use App\Enums\PluginStatus;
use App\Models\Plugin;
use App\Services\Helpers\PluginService;
use Exception;
use Illuminate\Console\Command;
class InstallPluginCommand extends Command
{
protected $signature = 'p:plugin:install {id?}';
protected $description = 'Installs a plugin';
public function handle(PluginService $pluginService): void
{
$id = $this->argument('id') ?? $this->choice('Plugin', Plugin::pluck('name', 'id')->toArray());
$plugin = Plugin::find(str($id)->lower()->toString());
if (!$plugin) {
$this->error('Plugin does not exist!');
return;
}
if ($plugin->status !== PluginStatus::NotInstalled) {
$this->error('Plugin is already installed!');
return;
}
try {
$pluginService->installPlugin($plugin);
$this->info('Plugin installed and enabled.');
} catch (Exception $exception) {
$this->error('Could not install plugin: ' . $exception->getMessage());
}
}
}

View File

@@ -1,28 +0,0 @@
<?php
namespace App\Console\Commands\Plugin;
use App\Models\Plugin;
use Illuminate\Console\Command;
class ListPluginsCommand extends Command
{
protected $signature = 'p:plugin:list';
protected $description = 'List all installed plugins';
public function handle(): void
{
$plugins = Plugin::query()->get(['name', 'author', 'status', 'version', 'panels', 'category']);
if (count($plugins) < 1) {
$this->warn('No plugins installed');
return;
}
$this->table(['Name', 'Author', 'Status', 'Version', 'Panels', 'Category'], $plugins->toArray());
$this->output->newLine();
}
}

View File

@@ -1,135 +0,0 @@
<?php
namespace App\Console\Commands\Plugin;
use App\Enums\PluginCategory;
use App\Enums\PluginStatus;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Str;
class MakePluginCommand extends Command
{
protected $signature = 'p:plugin:make
{--name=}
{--author=}
{--description=}
{--category=}
{--url=}
{--updateUrl=}
{--panels=}
{--panelVersion=}';
protected $description = 'Create a new plugin';
public function __construct(private Filesystem $filesystem)
{
parent::__construct();
}
public function handle(): void
{
$name = $this->option('name') ?? $this->ask('Name');
$name = preg_replace('/[^A-Za-z0-9 ]/', '', Str::ascii($name));
$id = Str::slug($name);
if ($this->filesystem->exists(plugin_path($id))) {
$this->error('Plugin with that name already exists!');
return;
}
$author = $this->option('author') ?? $this->ask('Author', cache('plugin.author'));
$author = preg_replace('/[^A-Za-z0-9 ]/', '', Str::ascii($author));
cache()->forever('plugin.author', $author);
$namespace = Str::studly($author) . '\\' . Str::studly($name);
$class = Str::studly($name . 'Plugin');
if (class_exists('\\' . $namespace . '\\' . $class)) {
$this->error('Plugin class with that name already exists!');
return;
}
$this->info('Creating Plugin "' . $name . '" (' . $id . ') by ' . $author);
$description = $this->option('description') ?? $this->ask('Description (can be empty)');
$category = $this->option('category') ?? $this->choice('Category', collect(PluginCategory::cases())->mapWithKeys(fn (PluginCategory $category) => [$category->value => $category->getLabel()])->toArray(), PluginCategory::Plugin->value);
if (!PluginCategory::tryFrom($category)) {
$this->error('Unknown plugin category!');
return;
}
$url = $this->option('url') ?? $this->ask('URL (can be empty)');
$updateUrl = $this->option('updateUrl') ?? $this->ask('Update URL (can be empty)');
$panels = $this->option('panels');
if (!$panels) {
if ($this->confirm('Should the plugin be available on all panels?', true)) {
$panels = null;
} else {
$panels = $this->choice('Panels (comma separated list)', [
'admin' => 'Admin Area',
'server' => 'Client Area',
'app' => 'Server List',
], multiple: true);
}
}
$panels = is_string($panels) ? explode(',', $panels) : $panels;
$panelVersion = $this->option('panelVersion');
if (!$panelVersion) {
$panelVersion = $this->ask('Required panel version (leave empty for no constraint)', config('app.version') === 'canary' ? null : config('app.version'));
if ($panelVersion && $this->confirm("Should the version constraint be minimal instead of strict? ($panelVersion or higher instead of only $panelVersion)")) {
$panelVersion = "^$panelVersion";
}
}
$composerPackages = null;
// TODO: ask for composer packages?
// Create base directory
$this->filesystem->makeDirectory(plugin_path($id));
// Write plugin.json
$this->filesystem->put(plugin_path($id, 'plugin.json'), json_encode([
'id' => $id,
'name' => $name,
'author' => $author,
'version' => '1.0.0',
'description' => $description,
'category' => $category,
'url' => $url,
'update_url' => $updateUrl,
'namespace' => $namespace,
'class' => $class,
'panels' => $panels,
'panel_version' => $panelVersion,
'composer_packages' => $composerPackages,
'meta' => [
'status' => PluginStatus::Enabled,
'status_message' => null,
],
], JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
// Create src directory and create main class
$this->filesystem->makeDirectory(plugin_path($id, 'src'));
$this->filesystem->put(plugin_path($id, 'src', $class . '.php'), Str::replace(['$namespace$', '$class$', '$id$'], [$namespace, $class, $id], file_get_contents(__DIR__ . '/Plugin.stub')));
// Create Providers directory and create service provider
$this->filesystem->makeDirectory(plugin_path($id, 'src', 'Providers'));
$this->filesystem->put(plugin_path($id, 'src', 'Providers', $class . 'Provider.php'), Str::replace(['$namespace$', '$class$'], [$namespace, $class], file_get_contents(__DIR__ . '/PluginProvider.stub')));
// Create config directory and create config file
$this->filesystem->makeDirectory(plugin_path($id, 'config'));
$this->filesystem->put(plugin_path($id, 'config', $id . '.php'), Str::replace(['$name$'], [$name], file_get_contents(__DIR__ . '/PluginConfig.stub')));
$this->info('Plugin created.');
}
}

View File

@@ -1,25 +0,0 @@
<?php
namespace $namespace$;
use Filament\Contracts\Plugin;
use Filament\Panel;
class $class$ implements Plugin
{
public function getId(): string
{
return '$id$';
}
public function register(Panel $panel): void
{
// Allows you to use any configuration option that is available to the panel.
// This includes registering resources, custom pages, themes, render hooks and more.
}
public function boot(Panel $panel): void
{
// Is run only when the panel that the plugin is being registered to is actually in-use. It is executed by a middleware class.
}
}

View File

@@ -1,5 +0,0 @@
<?php
return [
// Config values for $name$
];

View File

@@ -1,18 +0,0 @@
<?php
namespace $namespace$\Providers;
use Illuminate\Support\ServiceProvider;
class $class$Provider extends ServiceProvider
{
public function register(): void
{
//
}
public function boot(): void
{
//
}
}

View File

@@ -1,48 +0,0 @@
<?php
namespace App\Console\Commands\Plugin;
use App\Enums\PluginStatus;
use App\Models\Plugin;
use App\Services\Helpers\PluginService;
use Exception;
use Illuminate\Console\Command;
class UninstallPluginCommand extends Command
{
protected $signature = 'p:plugin:uninstall {id?} {--delete : Delete the plugin files}';
protected $description = 'Uninstalls a plugin';
public function handle(PluginService $pluginService): void
{
$id = $this->argument('id') ?? $this->choice('Plugin', Plugin::pluck('name', 'id')->toArray());
$plugin = Plugin::find(str($id)->lower()->toString());
if (!$plugin) {
$this->error('Plugin does not exist!');
return;
}
if ($plugin->status === PluginStatus::NotInstalled) {
$this->error('Plugin is not installed!');
return;
}
$deleteFiles = $this->option('delete');
if ($this->input->isInteractive() && !$deleteFiles) {
$deleteFiles = $this->confirm('Do you also want to delete the plugin files?');
}
try {
$pluginService->uninstallPlugin($plugin, $deleteFiles);
$this->info('Plugin uninstalled' . ($deleteFiles ? ' and files deleted' : '') . '.');
} catch (Exception $exception) {
$this->error('Could not uninstall plugin: ' . $exception->getMessage());
}
}
}

View File

@@ -1,42 +0,0 @@
<?php
namespace App\Console\Commands\Plugin;
use App\Models\Plugin;
use App\Services\Helpers\PluginService;
use Exception;
use Illuminate\Console\Command;
class UpdatePluginCommand extends Command
{
protected $signature = 'p:plugin:update {id?}';
protected $description = 'Updates a plugin';
public function handle(PluginService $pluginService): void
{
$id = $this->argument('id') ?? $this->choice('Plugin', Plugin::pluck('name', 'id')->toArray());
$plugin = Plugin::find(str($id)->lower()->toString());
if (!$plugin) {
$this->error('Plugin does not exist!');
return;
}
if (!$plugin->isUpdateAvailable()) {
$this->error("Plugin doesn't need updating!");
return;
}
try {
$pluginService->updatePlugin($plugin);
$this->info('Plugin updated.');
} catch (Exception $exception) {
$this->error('Could not update plugin: ' . $exception->getMessage());
}
}
}

View File

@@ -2,10 +2,10 @@
namespace App\Console\Commands\Schedule; namespace App\Console\Commands\Schedule;
use App\Models\Schedule;
use App\Services\Schedules\ProcessScheduleService;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use App\Models\Schedule;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use App\Services\Schedules\ProcessScheduleService;
use Throwable; use Throwable;
class ProcessRunnableCommand extends Command class ProcessRunnableCommand extends Command
@@ -64,7 +64,7 @@ class ProcessRunnableCommand extends Command
} catch (Throwable $exception) { } catch (Throwable $exception) {
logger()->error($exception, ['schedule_id' => $schedule->id]); logger()->error($exception, ['schedule_id' => $schedule->id]);
$this->error(trans('commands.schedule.process.error_message', ['schedules' => " #$schedule->id: " . $exception->getMessage()])); $this->error(trans('commands.schedule.process.no_tasks') . " #$schedule->id: " . $exception->getMessage());
} }
} }
} }

View File

@@ -3,12 +3,12 @@
namespace App\Console\Commands\Server; namespace App\Console\Commands\Server;
use App\Models\Server; use App\Models\Server;
use App\Repositories\Daemon\DaemonServerRepository;
use Exception;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Validation\Factory as ValidatorFactory;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
use Illuminate\Validation\Factory as ValidatorFactory;
use App\Repositories\Daemon\DaemonPowerRepository;
use Exception;
class BulkPowerActionCommand extends Command class BulkPowerActionCommand extends Command
{ {
@@ -19,7 +19,7 @@ class BulkPowerActionCommand extends Command
protected $description = 'Perform bulk power management on large groupings of servers or nodes at once.'; protected $description = 'Perform bulk power management on large groupings of servers or nodes at once.';
public function handle(DaemonServerRepository $serverRepository, ValidatorFactory $validator): void public function handle(DaemonPowerRepository $powerRepository, ValidatorFactory $validator): void
{ {
$action = $this->argument('action'); $action = $this->argument('action');
$nodes = empty($this->option('nodes')) ? [] : explode(',', $this->option('nodes')); $nodes = empty($this->option('nodes')) ? [] : explode(',', $this->option('nodes'));
@@ -52,7 +52,7 @@ class BulkPowerActionCommand extends Command
$bar = $this->output->createProgressBar($count); $bar = $this->output->createProgressBar($count);
$this->getQueryBuilder($servers, $nodes)->get()->each(function ($server, int $index) use ($action, $serverRepository, &$bar): mixed { $this->getQueryBuilder($servers, $nodes)->get()->each(function ($server, int $index) use ($action, $powerRepository, &$bar): mixed {
$bar->clear(); $bar->clear();
if (!$server instanceof Server) { if (!$server instanceof Server) {
@@ -60,7 +60,7 @@ class BulkPowerActionCommand extends Command
} }
try { try {
$serverRepository->setServer($server)->power($action); $powerRepository->setServer($server)->send($action);
} catch (Exception $exception) { } catch (Exception $exception) {
$this->output->error(trans('command/messages.server.power.action_failed', [ $this->output->error(trans('command/messages.server.power.action_failed', [
'name' => $server->name, 'name' => $server->name,

View File

@@ -0,0 +1,193 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Console\Kernel;
use Symfony\Component\Process\Process;
use Symfony\Component\Console\Helper\ProgressBar;
class UpgradeCommand extends Command
{
protected const DEFAULT_URL = 'https://github.com/pelican-dev/panel/releases/%s/panel.tar.gz';
protected $signature = 'p:upgrade
{--user= : The user that PHP runs under. All files will be owned by this user.}
{--group= : The group that PHP runs under. All files will be owned by this group.}
{--url= : The specific archive to download.}
{--release= : A specific version to download from GitHub. Leave blank to use latest.}
{--skip-download : If set no archive will be downloaded.}';
protected $description = 'Downloads a new archive from GitHub and then executes the normal upgrade commands.';
/**
* Executes an upgrade command which will run through all of our standard
* Panel commands and enable users to basically just download
* the archive and execute this and be done.
*
* This places the application in maintenance mode as well while the commands
* are being executed.
*
* @throws \Exception
*/
public function handle(): void
{
$skipDownload = $this->option('skip-download');
if (!$skipDownload) {
$this->output->warning(trans('commands.upgrade.integrity'));
$this->output->comment(trans('commands.upgrade.source_url'));
$this->line($this->getUrl());
}
$user = 'www-data';
$group = 'www-data';
if ($this->input->isInteractive()) {
if (!$skipDownload) {
$skipDownload = !$this->confirm(trans('commands.upgrade.skipDownload'), true);
}
if (is_null($this->option('user'))) {
$userDetails = function_exists('posix_getpwuid') ? posix_getpwuid(fileowner('public')) : [];
$user = $userDetails['name'] ?? 'www-data';
$message = trans('commands.upgrade.webserver_user', ['user' => $user]);
if (!$this->confirm($message, true)) {
$user = $this->anticipate(
trans('commands.upgrade.name_webserver'),
[
'www-data',
'nginx',
'apache',
]
);
}
}
if (is_null($this->option('group'))) {
$groupDetails = function_exists('posix_getgrgid') ? posix_getgrgid(filegroup('public')) : [];
$group = $groupDetails['name'] ?? 'www-data';
$message = trans('commands.upgrade.group_webserver', ['group' => $user]);
if (!$this->confirm($message, true)) {
$group = $this->anticipate(
trans('commands.upgrade.group_webserver_question'),
[
'www-data',
'nginx',
'apache',
]
);
}
}
if (!$this->confirm(trans('commands.upgrade.are_your_sure'))) {
$this->warn(trans('commands.upgrade.terminated'));
return;
}
}
ini_set('output_buffering', '0');
$bar = $this->output->createProgressBar($skipDownload ? 9 : 10);
$bar->start();
if (!$skipDownload) {
$this->withProgress($bar, function () {
$this->line("\$upgrader> curl -L \"{$this->getUrl()}\" | tar -xzv");
$process = Process::fromShellCommandline("curl -L \"{$this->getUrl()}\" | tar -xzv");
$process->run(function ($type, $buffer) {
$this->{$type === Process::ERR ? 'error' : 'line'}($buffer);
});
});
}
$this->withProgress($bar, function () {
$this->line('$upgrader> php artisan down');
$this->call('down');
});
$this->withProgress($bar, function () {
$this->line('$upgrader> chmod -R 755 storage bootstrap/cache');
$process = new Process(['chmod', '-R', '755', 'storage', 'bootstrap/cache']);
$process->run(function ($type, $buffer) {
$this->{$type === Process::ERR ? 'error' : 'line'}($buffer);
});
});
$this->withProgress($bar, function () {
$command = ['composer', 'install', '--no-ansi'];
if (config('app.env') === 'production' && !config('app.debug')) {
$command[] = '--optimize-autoloader';
$command[] = '--no-dev';
}
$this->line('$upgrader> ' . implode(' ', $command));
$process = new Process($command);
$process->setTimeout(10 * 60);
$process->run(function ($type, $buffer) {
$this->line($buffer);
});
});
/** @var \Illuminate\Foundation\Application $app */
$app = require __DIR__ . '/../../../bootstrap/app.php';
/** @var \App\Console\Kernel $kernel */
$kernel = $app->make(Kernel::class);
$kernel->bootstrap();
$this->setLaravel($app);
$this->withProgress($bar, function () {
$this->line('$upgrader> php artisan view:clear');
$this->call('view:clear');
});
$this->withProgress($bar, function () {
$this->line('$upgrader> php artisan config:clear');
$this->call('config:clear');
});
$this->withProgress($bar, function () {
$this->line('$upgrader> php artisan migrate --force --seed');
$this->call('migrate', ['--force' => true, '--seed' => true]);
});
$this->withProgress($bar, function () use ($user, $group) {
$this->line("\$upgrader> chown -R {$user}:{$group} *");
$process = Process::fromShellCommandline("chown -R {$user}:{$group} *", $this->getLaravel()->basePath());
$process->setTimeout(10 * 60);
$process->run(function ($type, $buffer) {
$this->{$type === Process::ERR ? 'error' : 'line'}($buffer);
});
});
$this->withProgress($bar, function () {
$this->line('$upgrader> php artisan queue:restart');
$this->call('queue:restart');
});
$this->withProgress($bar, function () {
$this->line('$upgrader> php artisan up');
$this->call('up');
});
$this->newLine(2);
$this->info(trans('commands.upgrade.success'));
}
protected function withProgress(ProgressBar $bar, \Closure $callback): void
{
$bar->clear();
$callback();
$bar->advance();
$bar->display();
}
protected function getUrl(): string
{
if ($this->option('url')) {
return $this->option('url');
}
return sprintf(self::DEFAULT_URL, $this->option('release') ? 'download/v' . $this->option('release') : 'latest/download');
}
}

View File

@@ -3,8 +3,8 @@
namespace App\Console\Commands\User; namespace App\Console\Commands\User;
use App\Models\User; use App\Models\User;
use Illuminate\Console\Command;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert;
use Illuminate\Console\Command;
class DeleteUserCommand extends Command class DeleteUserCommand extends Command
{ {
@@ -35,7 +35,7 @@ class DeleteUserCommand extends Command
if ($this->input->isInteractive()) { if ($this->input->isInteractive()) {
$tableValues = []; $tableValues = [];
foreach ($results as $user) { foreach ($results as $user) {
$tableValues[] = [$user->id, $user->email, $user->username]; $tableValues[] = [$user->id, $user->email, $user->name];
} }
$this->table(['User ID', 'Email', 'Name'], $tableValues); $this->table(['User ID', 'Email', 'Name'], $tableValues);

View File

@@ -2,7 +2,6 @@
namespace App\Console\Commands\User; namespace App\Console\Commands\User;
use App\Exceptions\Model\DataValidationException;
use App\Models\User; use App\Models\User;
use Illuminate\Console\Command; use Illuminate\Console\Command;
@@ -15,22 +14,20 @@ class DisableTwoFactorCommand extends Command
/** /**
* Handle command execution process. * Handle command execution process.
* *
* @throws DataValidationException * @throws \App\Exceptions\Model\DataValidationException
*/ */
public function handle(): void public function handle(): void
{ {
if ($this->input->isInteractive()) { if ($this->input->isInteractive()) {
$this->output->warning(trans('command/messages.user.2fa_help_text')); $this->output->warning(trans('command/messages.user.2fa_help_text.0') . trans('command/messages.user.2fa_help_text.1'));
} }
$email = $this->option('email') ?? $this->ask(trans('command/messages.user.ask_email')); $email = $this->option('email') ?? $this->ask(trans('command/messages.user.ask_email'));
$user = User::where('email', $email)->firstOrFail(); $user = User::query()->where('email', $email)->firstOrFail();
$user->update([ $user->use_totp = false;
'mfa_app_secret' => null, $user->totp_secret = null;
'mfa_app_recovery_codes' => null, $user->save();
'mfa_email_enabled' => false,
]);
$this->info(trans('command/messages.user.2fa_disabled', ['email' => $user->email])); $this->info(trans('command/messages.user.2fa_disabled', ['email' => $user->email]));
} }

View File

@@ -2,10 +2,9 @@
namespace App\Console\Commands\User; namespace App\Console\Commands\User;
use App\Exceptions\Model\DataValidationException;
use App\Services\Users\UserCreationService;
use Exception; use Exception;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use App\Services\Users\UserCreationService;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
class MakeUserCommand extends Command class MakeUserCommand extends Command
@@ -26,7 +25,7 @@ class MakeUserCommand extends Command
* Handle command request to create a new user. * Handle command request to create a new user.
* *
* @throws Exception * @throws Exception
* @throws DataValidationException * @throws \App\Exceptions\Model\DataValidationException
*/ */
public function handle(): int public function handle(): int
{ {

View File

@@ -2,13 +2,12 @@
namespace App\Contracts\Http; namespace App\Contracts\Http;
use App\Enums\SubuserPermission;
interface ClientPermissionsRequest interface ClientPermissionsRequest
{ {
/** /**
* Returns the permission used to validate that the authenticated user may perform * Returns the permissions string indicating which permission should be used to
* this action against the given resource (server). * validate that the authenticated user has permission to perform this action against
* the given resource (server).
*/ */
public function permission(): SubuserPermission|string; public function permission(): string;
} }

View File

@@ -1,18 +0,0 @@
<?php
namespace App\Contracts\Plugins;
use Filament\Schemas\Components\Component;
interface HasPluginSettings
{
/**
* @return Component[]
*/
public function getSettingsForm(): array;
/**
* @param array<mixed, mixed> $data
*/
public function saveSettings(array $data): void;
}

View File

@@ -2,7 +2,6 @@
namespace App\Enums; namespace App\Enums;
use BackedEnum;
use Filament\Support\Contracts\HasColor; use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasIcon; use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel; use Filament\Support\Contracts\HasLabel;
@@ -13,12 +12,12 @@ enum BackupStatus: string implements HasColor, HasIcon, HasLabel
case Successful = 'successful'; case Successful = 'successful';
case Failed = 'failed'; case Failed = 'failed';
public function getIcon(): BackedEnum public function getIcon(): string
{ {
return match ($this) { return match ($this) {
self::InProgress => TablerIcon::CircleDashed, self::InProgress => 'tabler-circle-dashed',
self::Successful => TablerIcon::CircleCheck, self::Successful => 'tabler-circle-check',
self::Failed => TablerIcon::CircleX, self::Failed => 'tabler-circle-x',
}; };
} }
@@ -33,6 +32,6 @@ enum BackupStatus: string implements HasColor, HasIcon, HasLabel
public function getLabel(): string public function getLabel(): string
{ {
return trans('server/backup.backup_status.' . $this->value); return trans('server/backup.backup_status.' . strtolower($this->value));
} }
} }

View File

@@ -2,7 +2,6 @@
namespace App\Enums; namespace App\Enums;
use BackedEnum;
use Filament\Support\Contracts\HasColor; use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasIcon; use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel; use Filament\Support\Contracts\HasLabel;
@@ -24,20 +23,20 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
// HTTP Based // HTTP Based
case Missing = 'missing'; case Missing = 'missing';
public function getIcon(): BackedEnum public function getIcon(): string
{ {
return match ($this) { return match ($this) {
self::Created => TablerIcon::HeartPlus, self::Created => 'tabler-heart-plus',
self::Starting => TablerIcon::HeartUp, self::Starting => 'tabler-heart-up',
self::Running => TablerIcon::Heartbeat, self::Running => 'tabler-heartbeat',
self::Restarting => TablerIcon::HeartBolt, self::Restarting => 'tabler-heart-bolt',
self::Exited => TablerIcon::HeartExclamation, self::Exited => 'tabler-heart-exclamation',
self::Paused => TablerIcon::HeartPause, self::Paused => 'tabler-heart-pause',
self::Dead, self::Offline => TablerIcon::HeartX, self::Dead, self::Offline => 'tabler-heart-x',
self::Removing => TablerIcon::HeartDown, self::Removing => 'tabler-heart-down',
self::Missing => TablerIcon::HeartSearch, self::Missing => 'tabler-heart-search',
self::Stopping => TablerIcon::HeartMinus, self::Stopping => 'tabler-heart-minus',
}; };
} }
@@ -69,7 +68,7 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
public function getLabel(): string public function getLabel(): string
{ {
return trans('server/console.status.' . $this->value); return trans('server/console.status.' . strtolower($this->value));
} }
public function isOffline(): bool public function isOffline(): bool

View File

@@ -1,9 +0,0 @@
<?php
namespace App\Enums;
enum CustomRenderHooks: string
{
case FooterStart = 'pelican::footer.start';
case FooterEnd = 'pelican::footer.end';
}

View File

@@ -1,42 +0,0 @@
<?php
namespace App\Enums;
enum CustomizationKey: string
{
case ConsoleRows = 'console_rows';
case ConsoleFont = 'console_font';
case ConsoleFontSize = 'console_font_size';
case ConsoleGraphPeriod = 'console_graph_period';
case TopNavigation = 'top_navigation';
case DashboardLayout = 'dashboard_layout';
case ButtonStyle = 'button_style';
case RedirectToAdmin = 'redirect_to_admin';
public function getDefaultValue(): string|int|bool
{
return match ($this) {
self::ConsoleRows => 30,
self::ConsoleFont => 'monospace',
self::ConsoleFontSize => 14,
self::ConsoleGraphPeriod => 30,
self::TopNavigation => config('panel.filament.default-navigation', 'sidebar'),
self::DashboardLayout => 'grid',
self::ButtonStyle => true,
self::RedirectToAdmin => false,
};
}
/** @return array<string, string|int|bool> */
public static function getDefaultCustomization(): array
{
$default = [];
foreach (self::cases() as $key) {
$default[$key->value] = $key->getDefaultValue();
}
return $default;
}
}

View File

@@ -13,7 +13,7 @@ enum EditorLanguages: string implements HasLabel
case bat = 'bat'; case bat = 'bat';
case bicep = 'bicep'; case bicep = 'bicep';
case cameligo = 'cameligo'; case cameligo = 'cameligo';
case clojure = 'clojure'; case coljure = 'coljure';
case coffeescript = 'coffeescript'; case coffeescript = 'coffeescript';
case c = 'c'; case c = 'c';
case cpp = 'cpp'; case cpp = 'cpp';

View File

@@ -1,28 +0,0 @@
<?php
namespace App\Enums;
use BackedEnum;
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
enum PluginCategory: string implements HasIcon, HasLabel
{
case Plugin = 'plugin';
case Theme = 'theme';
case Language = 'language';
public function getIcon(): BackedEnum
{
return match ($this) {
self::Plugin => TablerIcon::Package,
self::Theme => TablerIcon::Palette,
self::Language => TablerIcon::Language,
};
}
public function getLabel(): string
{
return trans('admin/plugin.category_enum.' . $this->value);
}
}

View File

@@ -1,44 +0,0 @@
<?php
namespace App\Enums;
use BackedEnum;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
enum PluginStatus: string implements HasColor, HasIcon, HasLabel
{
case NotInstalled = 'not_installed';
case Disabled = 'disabled';
case Enabled = 'enabled';
case Errored = 'errored';
case Incompatible = 'incompatible';
public function getIcon(): BackedEnum
{
return match ($this) {
self::NotInstalled => TablerIcon::HeartOff,
self::Disabled => TablerIcon::HeartX,
self::Enabled => TablerIcon::HeartCheck,
self::Errored => TablerIcon::HeartBroken,
self::Incompatible => TablerIcon::HeartCancel,
};
}
public function getColor(): string
{
return match ($this) {
self::NotInstalled => 'gray',
self::Disabled => 'warning',
self::Enabled => 'success',
self::Errored => 'danger',
self::Incompatible => 'danger',
};
}
public function getLabel(): string
{
return trans('admin/plugin.status_enum.' . $this->value);
}
}

View File

@@ -1,65 +0,0 @@
<?php
namespace App\Enums;
use App\Models\Server;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Support\Facades\RateLimiter;
use Webmozart\Assert\Assert;
/**
* A basic resource throttler for individual servers. This is applied in addition
* to existing rate limits and allows the code to slow down speedy users that might
* be creating resources a little too quickly for comfort. This throttle generally
* only applies to creation flows, and not general view/edit/delete flows.
*/
enum ResourceLimit: string
{
case Websocket = 'websocket';
case AllocationCreate = 'allocation-create';
case BackupRestore = 'backup-restore';
case DatabaseCreate = 'database-create';
case ScheduleCreate = 'schedule-create';
case SubuserCreate = 'subuser-create';
case FilePull = 'file-pull';
public function throttleKey(): string
{
return "api.client:server-resource:{$this->name}";
}
/**
* Returns a middleware that will throttle the specific resource by server. This
* throttle applies to any user making changes to that resource on the specific
* server, it is NOT per-user.
*/
public function middleware(): string
{
return ThrottleRequests::using($this->throttleKey());
}
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),
};
}
public static function boot(): void
{
foreach (self::cases() as $case) {
RateLimiter::for($case->throttleKey(), function (Request $request) use ($case) {
Assert::isInstanceOf($server = $request->route()->parameter('server'), Server::class);
return $case->limit()->by($server->uuid);
});
}
}
}

View File

@@ -5,7 +5,6 @@ namespace App\Enums;
enum RolePermissionModels: string enum RolePermissionModels: string
{ {
case ApiKey = 'apiKey'; case ApiKey = 'apiKey';
case Allocation = 'allocation';
case DatabaseHost = 'databaseHost'; case DatabaseHost = 'databaseHost';
case Database = 'database'; case Database = 'database';
case Egg = 'egg'; case Egg = 'egg';
@@ -35,9 +34,4 @@ enum RolePermissionModels: string
{ {
return RolePermissionPrefixes::Update->value . ' ' . $this->value; return RolePermissionPrefixes::Update->value . ' ' . $this->value;
} }
public function delete(): string
{
return RolePermissionPrefixes::Delete->value . ' ' . $this->value;
}
} }

View File

@@ -1,27 +0,0 @@
<?php
namespace App\Enums;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasLabel;
enum ScheduleStatus: string implements HasColor, HasLabel
{
case Inactive = 'inactive';
case Processing = 'processing';
case Active = 'active';
public function getColor(): string
{
return match ($this) {
self::Inactive => 'danger',
self::Processing => 'warning',
self::Active => 'success',
};
}
public function getLabel(): string
{
return trans('server/schedule.schedule_status.' . $this->value);
}
}

View File

@@ -2,7 +2,6 @@
namespace App\Enums; namespace App\Enums;
use BackedEnum;
use Filament\Support\Contracts\HasColor; use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasIcon; use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel; use Filament\Support\Contracts\HasLabel;
@@ -15,13 +14,14 @@ enum ServerState: string implements HasColor, HasIcon, HasLabel
case Suspended = 'suspended'; case Suspended = 'suspended';
case RestoringBackup = 'restoring_backup'; case RestoringBackup = 'restoring_backup';
public function getIcon(): BackedEnum public function getIcon(): string
{ {
return match ($this) { return match ($this) {
self::Installing => TablerIcon::HeartBolt, self::Installing => 'tabler-heart-bolt',
self::InstallFailed, self::ReinstallFailed => TablerIcon::HeartX, self::InstallFailed => 'tabler-heart-x',
self::Suspended => TablerIcon::HeartCancel, self::ReinstallFailed => 'tabler-heart-x',
self::RestoringBackup => TablerIcon::HeartUp, self::Suspended => 'tabler-heart-cancel',
self::RestoringBackup => 'tabler-heart-up',
}; };
} }

View File

@@ -1,11 +0,0 @@
<?php
namespace App\Enums;
enum StartupVariableType: string
{
case Text = 'text';
case Number = 'number';
case Select = 'select';
case Toggle = 'toggle';
}

View File

@@ -1,9 +0,0 @@
<?php
namespace App\Enums;
enum StepPosition: string
{
case Before = 'before';
case After = 'after';
}

View File

@@ -1,95 +0,0 @@
<?php
namespace App\Enums;
use BackedEnum;
enum SubuserPermission: string
{
case WebsocketConnect = 'websocket.connect';
case ControlConsole = 'control.console';
case ControlStart = 'control.start';
case ControlStop = 'control.stop';
case ControlRestart = 'control.restart';
case FileRead = 'file.read';
case FileReadContent = 'file.read-content';
case FileCreate = 'file.create';
case FileUpdate = 'file.update';
case FileDelete = 'file.delete';
case FileArchive = 'file.archive';
case FileSftp = 'file.sftp';
case BackupRead = 'backup.read';
case BackupCreate = 'backup.create';
case BackupDelete = 'backup.delete';
case BackupDownload = 'backup.download';
case BackupRestore = 'backup.restore';
case ScheduleRead = 'schedule.read';
case ScheduleCreate = 'schedule.create';
case ScheduleUpdate = 'schedule.update';
case ScheduleDelete = 'schedule.delete';
case UserRead = 'user.read';
case UserCreate = 'user.create';
case UserUpdate = 'user.update';
case UserDelete = 'user.delete';
case DatabaseRead = 'database.read';
case DatabaseCreate = 'database.create';
case DatabaseUpdate = 'database.update';
case DatabaseDelete = 'database.delete';
case DatabaseViewPassword = 'database.view-password';
case AllocationRead = 'allocation.read';
case AllocationCreate = 'allocation.create';
case AllocationUpdate = 'allocation.update';
case AllocationDelete = 'allocation.delete';
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';
case SettingsRename = 'settings.rename';
case SettingsDescription = 'settings.description';
case SettingsReinstall = 'settings.reinstall';
case SettingsChangeIcon = 'settings.change-icon';
/** @return string[] */
public function split(): array
{
return explode('.', $this->value, 2);
}
public function isHidden(): bool
{
return $this === self::WebsocketConnect;
}
public function getIcon(): ?BackedEnum
{
[$group, $permission] = $this->split();
return match ($group) {
'control' => TablerIcon::Terminal2,
'user' => TablerIcon::Users,
'file' => TablerIcon::Files,
'backup' => TablerIcon::FileZip,
'allocation' => TablerIcon::Network,
'startup' => TablerIcon::PlayerPlay,
'database' => TablerIcon::Database,
'schedule' => TablerIcon::Clock,
'settings' => TablerIcon::Settings,
'activity' => TablerIcon::Stack,
'mount' => TablerIcon::LayersLinked,
default => null,
};
}
}

View File

@@ -1,9 +0,0 @@
<?php
namespace App\Enums;
enum TabPosition: string
{
case Before = 'before';
case After = 'after';
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,10 +2,9 @@
namespace App\Enums; namespace App\Enums;
use BackedEnum; use Filament\Support\Contracts\HasLabel;
use Filament\Support\Contracts\HasColor; use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasIcon; use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
enum WebhookType: string implements HasColor, HasIcon, HasLabel enum WebhookType: string implements HasColor, HasIcon, HasLabel
{ {
@@ -25,11 +24,11 @@ enum WebhookType: string implements HasColor, HasIcon, HasLabel
}; };
} }
public function getIcon(): BackedEnum public function getIcon(): string
{ {
return match ($this) { return match ($this) {
self::Regular => TablerIcon::WorldWww, self::Regular => 'tabler-world-www',
self::Discord => TablerIcon::BrandDiscord, self::Discord => 'tabler-brand-discord',
}; };
} }
} }

View File

@@ -2,9 +2,9 @@
namespace App\Events; namespace App\Events;
use Illuminate\Support\Str;
use App\Models\ActivityLog; use App\Models\ActivityLog;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
class ActivityLogged extends Event class ActivityLogged extends Event
{ {

View File

@@ -2,8 +2,8 @@
namespace App\Events\Auth; namespace App\Events\Auth;
use App\Events\Event;
use App\Models\User; use App\Models\User;
use App\Events\Event;
class ProvidedAuthenticationToken extends Event class ProvidedAuthenticationToken extends Event
{ {

View File

@@ -1,17 +0,0 @@
<?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) {}
}

View File

@@ -1,13 +0,0 @@
<?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) {}
}

View File

@@ -4,14 +4,13 @@ namespace App\Exceptions;
use Exception; use Exception;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Illuminate\Container\Container;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Illuminate\Http\Response;
use Illuminate\Container\Container;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Throwable;
/** /**
* @deprecated * @deprecated
@@ -29,7 +28,7 @@ class DisplayException extends PanelException implements HttpExceptionInterface
/** /**
* DisplayException constructor. * DisplayException constructor.
*/ */
public function __construct(string $message, ?Throwable $previous = null, protected string $level = self::LEVEL_ERROR, int $code = 0) public function __construct(string $message, ?\Throwable $previous = null, protected string $level = self::LEVEL_ERROR, int $code = 0)
{ {
parent::__construct($message, $code, $previous); parent::__construct($message, $code, $previous);
} }
@@ -80,11 +79,11 @@ class DisplayException extends PanelException implements HttpExceptionInterface
* Log the exception to the logs using the defined error level only if the previous * Log the exception to the logs using the defined error level only if the previous
* exception is set. * exception is set.
* *
* @throws Throwable * @throws \Throwable
*/ */
public function report(): void public function report(): void
{ {
if (!$this->getPrevious() instanceof Exception || !Handler::isReportable($this->getPrevious())) { if (!$this->getPrevious() instanceof \Exception || !Handler::isReportable($this->getPrevious())) {
return; return;
} }

View File

@@ -2,27 +2,24 @@
namespace App\Exceptions; namespace App\Exceptions;
use Exception; use Illuminate\Support\Arr;
use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Support\Str;
use Illuminate\Auth\AuthenticationException; use Illuminate\Http\JsonResponse;
use Illuminate\Support\Collection;
use Illuminate\Container\Container; use Illuminate\Container\Container;
use Illuminate\Database\Connection; use Illuminate\Database\Connection;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Foundation\Application;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Session\TokenMismatchException; use Illuminate\Session\TokenMismatchException;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
use PDOException;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\Mailer\Exception\TransportException; use Symfony\Component\Mailer\Exception\TransportException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Throwable; use Throwable;
class Handler extends ExceptionHandler class Handler extends ExceptionHandler
@@ -82,7 +79,7 @@ class Handler extends ExceptionHandler
$this->dontReport = []; $this->dontReport = [];
} }
$this->reportable(function (PDOException $ex) { $this->reportable(function (\PDOException $ex) {
$ex = $this->generateCleanedExceptionStack($ex); $ex = $this->generateCleanedExceptionStack($ex);
}); });
@@ -91,7 +88,7 @@ class Handler extends ExceptionHandler
}); });
} }
private function generateCleanedExceptionStack(Throwable $exception): string private function generateCleanedExceptionStack(\Throwable $exception): string
{ {
$cleanedStack = ''; $cleanedStack = '';
foreach ($exception->getTrace() as $index => $item) { foreach ($exception->getTrace() as $index => $item) {
@@ -120,11 +117,11 @@ class Handler extends ExceptionHandler
/** /**
* Render an exception into an HTTP response. * Render an exception into an HTTP response.
* *
* @param Request $request * @param \Illuminate\Http\Request $request
* *
* @throws Throwable * @throws \Throwable
*/ */
public function render($request, Throwable $e): Response public function render($request, \Throwable $e): Response
{ {
$connections = $this->container->make(Connection::class); $connections = $this->container->make(Connection::class);
@@ -146,7 +143,7 @@ class Handler extends ExceptionHandler
* Transform a validation exception into a consistent format to be returned for * Transform a validation exception into a consistent format to be returned for
* calls to the API. * calls to the API.
* *
* @param Request $request * @param \Illuminate\Http\Request $request
*/ */
public function invalidJson($request, ValidationException $exception): JsonResponse public function invalidJson($request, ValidationException $exception): JsonResponse
{ {
@@ -252,7 +249,7 @@ class Handler extends ExceptionHandler
/** /**
* Return an array of exceptions that should not be reported. * Return an array of exceptions that should not be reported.
*/ */
public static function isReportable(Exception $exception): bool public static function isReportable(\Exception $exception): bool
{ {
return (new self(Container::getInstance()))->shouldReport($exception); return (new self(Container::getInstance()))->shouldReport($exception);
} }
@@ -260,7 +257,7 @@ class Handler extends ExceptionHandler
/** /**
* Convert an authentication exception into an unauthenticated response. * Convert an authentication exception into an unauthenticated response.
* *
* @param Request $request * @param \Illuminate\Http\Request $request
*/ */
protected function unauthenticated($request, AuthenticationException $exception): JsonResponse|RedirectResponse protected function unauthenticated($request, AuthenticationException $exception): JsonResponse|RedirectResponse
{ {
@@ -294,7 +291,7 @@ class Handler extends ExceptionHandler
* *
* @return array<mixed> * @return array<mixed>
*/ */
public static function toArray(Throwable $e): array public static function toArray(\Throwable $e): array
{ {
return self::exceptionToArray($e); return self::exceptionToArray($e);
} }

View File

@@ -4,14 +4,13 @@ namespace App\Exceptions\Http;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\HttpException;
use Throwable;
class HttpForbiddenException extends HttpException class HttpForbiddenException extends HttpException
{ {
/** /**
* HttpForbiddenException constructor. * HttpForbiddenException constructor.
*/ */
public function __construct(?string $message = null, ?Throwable $previous = null) public function __construct(?string $message = null, ?\Throwable $previous = null)
{ {
parent::__construct(Response::HTTP_FORBIDDEN, $message, $previous); parent::__construct(Response::HTTP_FORBIDDEN, $message, $previous);
} }

View File

@@ -5,7 +5,6 @@ namespace App\Exceptions\Http\Server;
use App\Enums\ServerState; use App\Enums\ServerState;
use App\Models\Server; use App\Models\Server;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException; use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
use Throwable;
class ServerStateConflictException extends ConflictHttpException class ServerStateConflictException extends ConflictHttpException
{ {
@@ -13,7 +12,7 @@ class ServerStateConflictException extends ConflictHttpException
* Exception thrown when the server is in an unsupported state for API access or * Exception thrown when the server is in an unsupported state for API access or
* certain operations within the codebase. * certain operations within the codebase.
*/ */
public function __construct(Server $server, ?Throwable $previous = null) public function __construct(Server $server, ?\Throwable $previous = null)
{ {
$message = 'This server is currently in an unsupported state, please try again later.'; $message = 'This server is currently in an unsupported state, please try again later.';
if ($server->isSuspended()) { if ($server->isSuspended()) {

View File

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

View File

@@ -2,11 +2,11 @@
namespace App\Exceptions\Model; namespace App\Exceptions\Model;
use Illuminate\Support\MessageBag;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Contracts\Validation\Validator;
use App\Exceptions\PanelException; use App\Exceptions\PanelException;
use Illuminate\Contracts\Support\MessageProvider; use Illuminate\Contracts\Support\MessageProvider;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\MessageBag;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
class DataValidationException extends PanelException implements HttpExceptionInterface, MessageProvider class DataValidationException extends PanelException implements HttpExceptionInterface, MessageProvider

View File

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

View File

@@ -1,7 +0,0 @@
<?php
namespace App\Exceptions;
use Exception;
class PluginIdMismatchException extends Exception {}

View File

@@ -2,8 +2,8 @@
namespace App\Exceptions\Service; namespace App\Exceptions\Service;
use App\Exceptions\DisplayException;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use App\Exceptions\DisplayException;
class HasActiveServersException extends DisplayException class HasActiveServersException extends DisplayException
{ {

View File

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

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Exceptions\Service\User;
use App\Exceptions\DisplayException;
class TwoFactorAuthenticationTokenInvalid extends DisplayException
{
public string $title = 'Invalid 2FA Code';
public string $icon = 'tabler-2fa';
public function __construct()
{
parent::__construct('The provided two-factor authentication token was not valid.');
}
}

View File

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

View File

@@ -2,16 +2,15 @@
namespace App\Extensions\Backups; namespace App\Extensions\Backups;
use App\Extensions\Filesystem\S3Filesystem;
use Aws\S3\S3Client;
use Closure; use Closure;
use Illuminate\Foundation\Application; use Aws\S3\S3Client;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use InvalidArgumentException;
use League\Flysystem\FilesystemAdapter;
use League\Flysystem\InMemory\InMemoryFilesystemAdapter;
use Webmozart\Assert\Assert; use Webmozart\Assert\Assert;
use Illuminate\Foundation\Application;
use League\Flysystem\FilesystemAdapter;
use App\Extensions\Filesystem\S3Filesystem;
use League\Flysystem\InMemory\InMemoryFilesystemAdapter;
class BackupManager class BackupManager
{ {
@@ -65,7 +64,7 @@ class BackupManager
$config = $this->getConfig($name); $config = $this->getConfig($name);
if (empty($config['adapter'])) { if (empty($config['adapter'])) {
throw new InvalidArgumentException("Backup disk [$name] does not have a configured adapter."); throw new \InvalidArgumentException("Backup disk [$name] does not have a configured adapter.");
} }
$adapter = $config['adapter']; $adapter = $config['adapter'];
@@ -83,7 +82,7 @@ class BackupManager
return $instance; return $instance;
} }
throw new InvalidArgumentException("Adapter [$adapter] is not supported."); throw new \InvalidArgumentException("Adapter [$adapter] is not supported.");
} }
/** /**

View File

@@ -2,8 +2,8 @@
namespace App\Extensions\Captcha\Schemas; namespace App\Extensions\Captcha\Schemas;
use Filament\Forms\Components\Component;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Component;
use Illuminate\Support\Str; use Illuminate\Support\Str;
abstract class BaseSchema abstract class BaseSchema

View File

@@ -2,8 +2,7 @@
namespace App\Extensions\Captcha\Schemas; namespace App\Extensions\Captcha\Schemas;
use BackedEnum; use Filament\Forms\Components\Component;
use Filament\Schemas\Components\Component;
interface CaptchaSchemaInterface interface CaptchaSchemaInterface
{ {
@@ -25,7 +24,7 @@ interface CaptchaSchemaInterface
*/ */
public function getSettingsForm(): array; public function getSettingsForm(): array;
public function getIcon(): null|string|BackedEnum; public function getIcon(): ?string;
public function validateResponse(?string $captchaResponse = null): void; public function validateResponse(?string $captchaResponse = null): void;
} }

View File

@@ -2,13 +2,12 @@
namespace App\Extensions\Captcha\Schemas\Turnstile; namespace App\Extensions\Captcha\Schemas\Turnstile;
use App\Enums\TablerIcon;
use App\Extensions\Captcha\Schemas\BaseSchema;
use App\Extensions\Captcha\Schemas\CaptchaSchemaInterface; use App\Extensions\Captcha\Schemas\CaptchaSchemaInterface;
use BackedEnum; use App\Extensions\Captcha\Schemas\BaseSchema;
use Exception; use Exception;
use Filament\Forms\Components\Component as BaseComponent;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Toggle; use Filament\Forms\Components\Toggle;
use Filament\Infolists\Components\TextEntry;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\HtmlString; use Illuminate\Support\HtmlString;
@@ -24,7 +23,7 @@ class TurnstileSchema extends BaseSchema implements CaptchaSchemaInterface
return env('CAPTCHA_TURNSTILE_ENABLED', false); return env('CAPTCHA_TURNSTILE_ENABLED', false);
} }
public function getFormComponent(): Component public function getFormComponent(): BaseComponent
{ {
return Component::make('turnstile'); return Component::make('turnstile');
} }
@@ -40,9 +39,7 @@ class TurnstileSchema extends BaseSchema implements CaptchaSchemaInterface
} }
/** /**
* @return \Filament\Support\Components\Component[] * @return BaseComponent[]
*
* @throws Exception
*/ */
public function getSettingsForm(): array public function getSettingsForm(): array
{ {
@@ -51,21 +48,21 @@ class TurnstileSchema extends BaseSchema implements CaptchaSchemaInterface
->label(trans('admin/setting.captcha.verify')) ->label(trans('admin/setting.captcha.verify'))
->columnSpan(2) ->columnSpan(2)
->inline(false) ->inline(false)
->onIcon(TablerIcon::Check) ->onIcon('tabler-check')
->offIcon(TablerIcon::X) ->offIcon('tabler-x')
->onColor('success') ->onColor('success')
->offColor('danger') ->offColor('danger')
->default(env('CAPTCHA_TURNSTILE_VERIFY_DOMAIN', true)), ->default(env('CAPTCHA_TURNSTILE_VERIFY_DOMAIN', true)),
TextEntry::make('info') Placeholder::make('info')
->label(trans('admin/setting.captcha.info_label')) ->label(trans('admin/setting.captcha.info_label'))
->columnSpan(2) ->columnSpan(2)
->state(new HtmlString(trans('admin/setting.captcha.info'))), ->content(new HtmlString(trans('admin/setting.captcha.info'))),
]); ]);
} }
public function getIcon(): BackedEnum public function getIcon(): ?string
{ {
return TablerIcon::BrandCloudflare; return 'tabler-brand-cloudflare';
} }
/** /**

View File

@@ -2,19 +2,18 @@
namespace App\Extensions\Features\Schemas; namespace App\Extensions\Features\Schemas;
use App\Enums\SubuserPermission;
use App\Enums\TablerIcon;
use App\Extensions\Features\FeatureSchemaInterface; use App\Extensions\Features\FeatureSchemaInterface;
use App\Facades\Activity; use App\Facades\Activity;
use App\Models\Permission;
use App\Models\Server; use App\Models\Server;
use App\Models\ServerVariable; use App\Models\ServerVariable;
use App\Repositories\Daemon\DaemonServerRepository; use App\Repositories\Daemon\DaemonPowerRepository;
use Closure; use Closure;
use Exception; use Exception;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Facades\Filament; use Filament\Facades\Filament;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Blade;
@@ -37,9 +36,6 @@ class GSLTokenSchema implements FeatureSchemaInterface
return 'gsl_token'; return 'gsl_token';
} }
/**
* @throws Exception
*/
public function getAction(): Action public function getAction(): Action
{ {
/** @var Server $server */ /** @var Server $server */
@@ -55,9 +51,9 @@ class GSLTokenSchema implements FeatureSchemaInterface
->modalHeading('Invalid GSL token') ->modalHeading('Invalid GSL token')
->modalDescription('It seems like your Gameserver Login Token (GSL token) is invalid or has expired.') ->modalDescription('It seems like your Gameserver Login Token (GSL token) is invalid or has expired.')
->modalSubmitActionLabel('Update GSL Token') ->modalSubmitActionLabel('Update GSL Token')
->disabledSchema(fn () => !user()?->can(SubuserPermission::StartupUpdate, $server)) ->disabledForm(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_UPDATE, $server))
->schema([ ->form([
TextEntry::make('info') Placeholder::make('info')
->label(new HtmlString(Blade::render('You can either <x-filament::link href="https://steamcommunity.com/dev/managegameservers" target="_blank">generate a new one</x-filament::link> and enter it below or leave the field blank to remove it completely.'))), ->label(new HtmlString(Blade::render('You can either <x-filament::link href="https://steamcommunity.com/dev/managegameservers" target="_blank">generate a new one</x-filament::link> and enter it below or leave the field blank to remove it completely.'))),
TextInput::make('gsltoken') TextInput::make('gsltoken')
->label('GSL Token') ->label('GSL Token')
@@ -74,12 +70,13 @@ class GSLTokenSchema implements FeatureSchemaInterface
} }
}, },
]) ])
->hintIcon(TablerIcon::Code, fn () => implode('|', $serverVariable->variable->rules)) ->hintIcon('tabler-code')
->label(fn () => $serverVariable->variable->name) ->label(fn () => $serverVariable->variable->name)
->hintIconTooltip(fn () => implode('|', $serverVariable->variable->rules))
->prefix(fn () => '{{' . $serverVariable->variable->env_variable . '}}') ->prefix(fn () => '{{' . $serverVariable->variable->env_variable . '}}')
->helperText(fn () => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description), ->helperText(fn () => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description),
]) ])
->action(function (array $data, DaemonServerRepository $serverRepository) use ($server, $serverVariable) { ->action(function (array $data, DaemonPowerRepository $powerRepository) use ($server, $serverVariable) {
/** @var Server $server */ /** @var Server $server */
$server = Filament::getTenant(); $server = Filament::getTenant();
try { try {
@@ -101,7 +98,7 @@ class GSLTokenSchema implements FeatureSchemaInterface
->log(); ->log();
} }
$serverRepository->setServer($server)->power('restart'); $powerRepository->setServer($server)->send('restart');
Notification::make() Notification::make()
->title('GSL Token updated') ->title('GSL Token updated')

View File

@@ -2,16 +2,16 @@
namespace App\Extensions\Features\Schemas; namespace App\Extensions\Features\Schemas;
use App\Enums\SubuserPermission;
use App\Extensions\Features\FeatureSchemaInterface; use App\Extensions\Features\FeatureSchemaInterface;
use App\Facades\Activity; use App\Facades\Activity;
use App\Models\Permission;
use App\Models\Server; use App\Models\Server;
use App\Repositories\Daemon\DaemonServerRepository; use App\Repositories\Daemon\DaemonPowerRepository;
use Exception; use Exception;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Facades\Filament; use Filament\Facades\Filament;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Infolists\Components\TextEntry;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
class JavaVersionSchema implements FeatureSchemaInterface class JavaVersionSchema implements FeatureSchemaInterface
@@ -44,9 +44,9 @@ class JavaVersionSchema implements FeatureSchemaInterface
->modalHeading('Unsupported Java Version') ->modalHeading('Unsupported Java Version')
->modalDescription('This server is currently running an unsupported version of Java and cannot be started.') ->modalDescription('This server is currently running an unsupported version of Java and cannot be started.')
->modalSubmitActionLabel('Update Docker Image') ->modalSubmitActionLabel('Update Docker Image')
->disabledSchema(fn () => !user()?->can(SubuserPermission::StartupDockerImage, $server)) ->disabledForm(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_DOCKER_IMAGE, $server))
->schema([ ->form([
TextEntry::make('java') Placeholder::make('java')
->label('Please select a supported version from the list below to continue starting the server.'), ->label('Please select a supported version from the list below to continue starting the server.'),
Select::make('image') Select::make('image')
->label('Docker Image') ->label('Docker Image')
@@ -56,9 +56,10 @@ class JavaVersionSchema implements FeatureSchemaInterface
->default(fn () => $server->image) ->default(fn () => $server->image)
->notIn(fn () => $server->image) ->notIn(fn () => $server->image)
->required() ->required()
->preload(), ->preload()
->native(false),
]) ])
->action(function (array $data, DaemonServerRepository $serverRepository) use ($server) { ->action(function (array $data, DaemonPowerRepository $powerRepository) use ($server) {
try { try {
$new = $data['image']; $new = $data['image'];
$original = $server->image; $original = $server->image;
@@ -70,7 +71,7 @@ class JavaVersionSchema implements FeatureSchemaInterface
->log(); ->log();
} }
$serverRepository->setServer($server)->power('restart'); $powerRepository->setServer($server)->send('restart');
Notification::make() Notification::make()
->title('Docker image updated') ->title('Docker image updated')

View File

@@ -5,7 +5,7 @@ namespace App\Extensions\Features\Schemas;
use App\Extensions\Features\FeatureSchemaInterface; use App\Extensions\Features\FeatureSchemaInterface;
use App\Models\Server; use App\Models\Server;
use App\Repositories\Daemon\DaemonFileRepository; use App\Repositories\Daemon\DaemonFileRepository;
use App\Repositories\Daemon\DaemonServerRepository; use App\Repositories\Daemon\DaemonPowerRepository;
use Exception; use Exception;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Facades\Filament; use Filament\Facades\Filament;
@@ -35,14 +35,14 @@ class MinecraftEulaSchema implements FeatureSchemaInterface
->modalHeading('Minecraft EULA') ->modalHeading('Minecraft EULA')
->modalDescription(new HtmlString(Blade::render('By pressing "I Accept" below you are indicating your agreement to the <x-filament::link href="https://minecraft.net/eula" target="_blank">Minecraft EULA </x-filament::link>.'))) ->modalDescription(new HtmlString(Blade::render('By pressing "I Accept" below you are indicating your agreement to the <x-filament::link href="https://minecraft.net/eula" target="_blank">Minecraft EULA </x-filament::link>.')))
->modalSubmitActionLabel('I Accept') ->modalSubmitActionLabel('I Accept')
->action(function (DaemonFileRepository $fileRepository, DaemonServerRepository $serverRepository) { ->action(function (DaemonFileRepository $fileRepository, DaemonPowerRepository $powerRepository) {
try { try {
/** @var Server $server */ /** @var Server $server */
$server = Filament::getTenant(); $server = Filament::getTenant();
$fileRepository->setServer($server)->putContent('eula.txt', 'eula=true'); $fileRepository->setServer($server)->putContent('eula.txt', 'eula=true');
$serverRepository->setServer($server)->power('restart'); $powerRepository->setServer($server)->send('restart');
Notification::make() Notification::make()
->title('Minecraft EULA accepted') ->title('Minecraft EULA accepted')

View File

@@ -2,7 +2,6 @@
namespace App\Extensions\Features\Schemas; namespace App\Extensions\Features\Schemas;
use App\Enums\TablerIcon;
use App\Extensions\Features\FeatureSchemaInterface; use App\Extensions\Features\FeatureSchemaInterface;
use Filament\Actions\Action; use Filament\Actions\Action;
use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Blade;
@@ -32,10 +31,10 @@ class PIDLimitSchema implements FeatureSchemaInterface
{ {
return Action::make($this->getId()) return Action::make($this->getId())
->requiresConfirmation() ->requiresConfirmation()
->icon(TablerIcon::AlertTriangle) ->icon('tabler-alert-triangle')
->modalHeading(fn () => user()?->isAdmin() ? 'Memory or process limit reached...' : 'Possible resource limit reached...') ->modalHeading(fn () => auth()->user()->isAdmin() ? 'Memory or process limit reached...' : 'Possible resource limit reached...')
->modalDescription(new HtmlString(Blade::render( ->modalDescription(new HtmlString(Blade::render(
user()?->isAdmin() ? <<<'HTML' auth()->user()->isAdmin() ? <<<'HTML'
<p> <p>
This server has reached the maximum process or memory limit. This server has reached the maximum process or memory limit.
</p> </p>

View File

@@ -29,7 +29,7 @@ class SteamDiskSpaceSchema implements FeatureSchemaInterface
->requiresConfirmation() ->requiresConfirmation()
->modalHeading('Out of available disk space...') ->modalHeading('Out of available disk space...')
->modalDescription(new HtmlString(Blade::render( ->modalDescription(new HtmlString(Blade::render(
user()?->isAdmin() ? <<<'HTML' auth()->user()->isAdmin() ? <<<'HTML'
<p> <p>
This server has run out of available disk space and cannot complete the install or update This server has run out of available disk space and cannot complete the install or update
process. process.

View File

@@ -6,7 +6,7 @@ use App\Models\ApiKey;
use Laravel\Sanctum\NewAccessToken as SanctumAccessToken; use Laravel\Sanctum\NewAccessToken as SanctumAccessToken;
/** /**
* @property ApiKey $accessToken * @property \App\Models\ApiKey $accessToken
*/ */
class NewAccessToken extends SanctumAccessToken class NewAccessToken extends SanctumAccessToken
{ {

View File

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

View File

@@ -2,11 +2,8 @@
namespace App\Extensions\OAuth; namespace App\Extensions\OAuth;
use App\Models\User; use Filament\Forms\Components\Component;
use BackedEnum; use Filament\Forms\Components\Wizard\Step;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Wizard\Step;
use Laravel\Socialite\Contracts\User as OAuthUser;
interface OAuthSchemaInterface interface OAuthSchemaInterface
{ {
@@ -30,13 +27,11 @@ interface OAuthSchemaInterface
/** @return Step[] */ /** @return Step[] */
public function getSetupSteps(): array; public function getSetupSteps(): array;
public function getIcon(): null|string|BackedEnum; public function getIcon(): ?string;
public function getHexColor(): ?string; public function getHexColor(): ?string;
public function isEnabled(): bool; public function isEnabled(): bool;
public function shouldCreateMissingUser(OAuthUser $user): bool; public function shouldCreateMissingUsers(): bool;
public function shouldLinkMissingUser(User $user, OAuthUser $oauthUser): bool;
} }

View File

@@ -2,9 +2,7 @@
namespace App\Extensions\OAuth; namespace App\Extensions\OAuth;
use App\Models\User;
use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Event;
use Laravel\Socialite\Contracts\User as OAuthUser;
use SocialiteProviders\Manager\SocialiteWasCalled; use SocialiteProviders\Manager\SocialiteWasCalled;
class OAuthService class OAuthService
@@ -45,27 +43,4 @@ class OAuthService
$this->schemas[$schema->getId()] = $schema; $this->schemas[$schema->getId()] = $schema;
} }
public function linkUser(User $user, OAuthSchemaInterface $schema, OAuthUser $oauthUser): User
{
$oauth = $user->oauth ?? [];
$oauth[$schema->getId()] = $oauthUser->getId();
$user->update(['oauth' => $oauth]);
return $user->refresh();
}
public function unlinkUser(User $user, OAuthSchemaInterface $schema): User
{
$oauth = $user->oauth ?? [];
if (!isset($oauth[$schema->getId()])) {
return $user;
}
unset($oauth[$schema->getId()]);
$user->update(['oauth' => $oauth]);
return $user->refresh();
}
} }

View File

@@ -4,10 +4,6 @@ namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\ColorPicker; use Filament\Forms\Components\ColorPicker;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
use SocialiteProviders\Authentik\Provider; use SocialiteProviders\Authentik\Provider;
final class AuthentikSchema extends OAuthSchema final class AuthentikSchema extends OAuthSchema
@@ -24,27 +20,11 @@ final class AuthentikSchema extends OAuthSchema
public function getServiceConfig(): array public function getServiceConfig(): array
{ {
return array_merge(parent::getServiceConfig(), [ return [
'base_url' => env('OAUTH_AUTHENTIK_BASE_URL'), 'base_url' => env('OAUTH_AUTHENTIK_BASE_URL'),
]); 'client_id' => env('OAUTH_AUTHENTIK_CLIENT_ID'),
} 'client_secret' => env('OAUTH_AUTHENTIK_CLIENT_SECRET'),
];
public function getSetupSteps(): array
{
return array_merge([
Step::make('Create Authentik Application')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p>On your Authentik dashboard select <b>Applications</b>, then select <b>Create with Provider</b>.</p><p>On the creation step select <b>OAuth2/OpenID Provider</b> and on the configure step set <b>Redirect URIs/Origins</b> to the value below.</p>'))),
TextInput::make('_noenv_callback')
->label('Callback URL')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('/auth/oauth/callback/authentik')),
]),
], parent::getSetupSteps());
} }
public function getSettingsForm(): array public function getSettingsForm(): array

View File

@@ -1,47 +0,0 @@
<?php
namespace App\Extensions\OAuth\Schemas;
use App\Enums\TablerIcon;
use BackedEnum;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
final class BitbucketSchema extends OAuthSchema
{
public function getId(): string
{
return 'bitbucket';
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new Bitbucket Consumer')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud" target="_blank">Bitbucket OAuth Documentation</x-filament::link> and follow the steps in <b>Create a consumer</b>.</p><p>For the <b>Callback URL</b> use the value below.</p>'))),
TextInput::make('_noenv_callback')
->label('Callback URL')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('/auth/oauth/callback/bitbucket')),
]),
], parent::getSetupSteps());
}
public function getIcon(): BackedEnum
{
return TablerIcon::BrandBitbucketFilled;
}
public function getHexColor(): string
{
return '#205081';
}
}

View File

@@ -2,15 +2,13 @@
namespace App\Extensions\OAuth\Schemas; namespace App\Extensions\OAuth\Schemas;
use BackedEnum;
final class CommonSchema extends OAuthSchema final class CommonSchema extends OAuthSchema
{ {
public function __construct( public function __construct(
private readonly string $id, private readonly string $id,
private readonly ?string $name = null, private readonly ?string $name = null,
private readonly ?string $configName = null, private readonly ?string $configName = null,
private readonly null|string|BackedEnum $icon = null, private readonly ?string $icon = null,
private readonly ?string $hexColor = null, private readonly ?string $hexColor = null,
) {} ) {}
@@ -29,7 +27,7 @@ final class CommonSchema extends OAuthSchema
return $this->configName ?? parent::getConfigKey(); return $this->configName ?? parent::getConfigKey();
} }
public function getIcon(): null|string|BackedEnum public function getIcon(): ?string
{ {
return $this->icon; return $this->icon;
} }

View File

@@ -2,14 +2,13 @@
namespace App\Extensions\OAuth\Schemas; namespace App\Extensions\OAuth\Schemas;
use App\Enums\TablerIcon; use Filament\Forms\Components\Placeholder;
use BackedEnum;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry; use Filament\Forms\Components\Wizard\Step;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString; use Illuminate\Support\HtmlString;
use SocialiteProviders\Discord\Provider; use SocialiteProviders\Discord\Provider;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
final class DiscordSchema extends OAuthSchema final class DiscordSchema extends OAuthSchema
{ {
@@ -28,25 +27,23 @@ final class DiscordSchema extends OAuthSchema
return array_merge([ return array_merge([
Step::make('Register new Discord OAuth App') Step::make('Register new Discord OAuth App')
->schema([ ->schema([
TextEntry::make('create_application') Placeholder::make('')
->hiddenLabel() ->content(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://discord.com/developers/applications" target="_blank">Discord Developer Portal</x-filament::link> and click on <b>New Application</b>. Enter a <b>Name</b> (e.g. your panel name) and click on <b>Create</b>.</p><p>Copy the <b>Client ID</b> and the <b>Client Secret</b> from the OAuth2 tab, you will need them in the final step.</p>'))),
->state(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://discord.com/developers/applications" target="_blank">Discord Developer Portal</x-filament::link> and click on <b>New Application</b>. Enter a <b>Name</b> (e.g. your panel name) and click on <b>Create</b>.</p><p>Copy the <b>Client ID</b> and the <b>Client Secret</b> from the OAuth2 tab, you will need them in the final step.</p>'))), Placeholder::make('')
TextEntry::make('set_redirect') ->content(new HtmlString('<p>Under <b>Redirects</b> add the below URL.</p>')),
->hiddenLabel()
->state(new HtmlString('<p>Under <b>Redirects</b> add the below URL.</p>')),
TextInput::make('_noenv_callback') TextInput::make('_noenv_callback')
->label('Redirect URL') ->label('Redirect URL')
->dehydrated() ->dehydrated()
->disabled() ->disabled()
->hintCopy() ->hintAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
->formatStateUsing(fn () => url('/auth/oauth/callback/discord')), ->formatStateUsing(fn () => url('/auth/oauth/callback/discord')),
]), ]),
], parent::getSetupSteps()); ], parent::getSetupSteps());
} }
public function getIcon(): BackedEnum public function getIcon(): string
{ {
return TablerIcon::BrandDiscordFilled; return 'tabler-brand-discord-f';
} }
public function getHexColor(): string public function getHexColor(): string

View File

@@ -1,50 +0,0 @@
<?php
namespace App\Extensions\OAuth\Schemas;
use App\Enums\TablerIcon;
use BackedEnum;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
final class FacebookSchema extends OAuthSchema
{
public function getId(): string
{
return 'facebook';
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new Facebook Application')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://developers.facebook.com/apps" target="_blank">Facebook Developer Dashboard</x-filament::link> and select or create a new app you will use for authentication. Make sure to have "Authenticate and request data from users with Facebook Login" as one of the Use Cases.</p><p>Once selected go to <b>Use Cases</b> and customize "Authenticate and request data from users with Facebook Login", from there go to <b>Settings</b> and add <b>Valid OAuth Redirect URIs</b> using the value below.</p>'))),
TextInput::make('_noenv_callback')
->label('Valid OAuth Redirect URIs')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('/auth/oauth/callback/facebook')),
TextEntry::make('get_app_info')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p>To obtain the OAuth values go to <b>App Settings > Basic</b>.</p>'))),
]),
], parent::getSetupSteps());
}
public function getIcon(): BackedEnum
{
return TablerIcon::BrandFacebookFilled;
}
public function getHexColor(): string
{
return '#1877f2';
}
}

View File

@@ -2,13 +2,12 @@
namespace App\Extensions\OAuth\Schemas; namespace App\Extensions\OAuth\Schemas;
use App\Enums\TablerIcon; use Filament\Forms\Components\Placeholder;
use BackedEnum;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry; use Filament\Forms\Components\Wizard\Step;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString; use Illuminate\Support\HtmlString;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
final class GithubSchema extends OAuthSchema final class GithubSchema extends OAuthSchema
{ {
@@ -20,33 +19,30 @@ final class GithubSchema extends OAuthSchema
public function getSetupSteps(): array public function getSetupSteps(): array
{ {
return array_merge([ return array_merge([
Step::make('Register new GitHub OAuth App') Step::make('Register new Github OAuth App')
->schema([ ->schema([
TextEntry::make('create_application') Placeholder::make('')
->hiddenLabel() ->content(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://github.com/settings/developers" target="_blank">Github Developer Dashboard</x-filament::link>, go to <b>OAuth Apps</b> and click on <b>New OAuth App</b>.</p><p>Enter an <b>Application name</b> (e.g. your panel name), set <b>Homepage URL</b> to your panel url and enter the below url as <b>Authorization callback URL</b>.</p>'))),
->state(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://github.com/settings/developers" target="_blank">GitHub Developer Dashboard</x-filament::link>, go to <b>OAuth Apps</b> and click on <b>New OAuth App</b>.</p><p>Enter an <b>Application name</b> (e.g. your panel name), set <b>Homepage URL</b> to your panel url and enter the below url as <b>Authorization callback URL</b>.</p>'))),
TextInput::make('_noenv_callback') TextInput::make('_noenv_callback')
->label('Authorization callback URL') ->label('Authorization callback URL')
->dehydrated() ->dehydrated()
->disabled() ->disabled()
->hintCopy() ->hintAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
->default(fn () => url('/auth/oauth/callback/github')), ->default(fn () => url('/auth/oauth/callback/github')),
TextEntry::make('register_application') Placeholder::make('')
->hiddenLabel() ->content(new HtmlString('<p>When you filled all fields click on <b>Register application</b>.</p>')),
->state(new HtmlString('<p>When you filled all fields click on <b>Register application</b>.</p>')),
]), ]),
Step::make('Create Client Secret') Step::make('Create Client Secret')
->schema([ ->schema([
TextEntry::make('create_client_secret') Placeholder::make('')
->hiddenLabel() ->content(new HtmlString('<p>Once you registered your app, generate a new <b>Client Secret</b>.</p><p>You will also need the <b>Client ID</b>.</p>')),
->state(new HtmlString('<p>Once you registered your app, generate a new <b>Client Secret</b>.</p><p>You will also need the <b>Client ID</b>.</p>')),
]), ]),
], parent::getSetupSteps()); ], parent::getSetupSteps());
} }
public function getIcon(): BackedEnum public function getIcon(): string
{ {
return TablerIcon::BrandGithubFilled; return 'tabler-brand-github-f';
} }
public function getHexColor(): string public function getHexColor(): string

View File

@@ -2,13 +2,12 @@
namespace App\Extensions\OAuth\Schemas; namespace App\Extensions\OAuth\Schemas;
use App\Enums\TablerIcon; use Filament\Forms\Components\Placeholder;
use BackedEnum;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry; use Filament\Forms\Components\Wizard\Step;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString; use Illuminate\Support\HtmlString;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
final class GitlabSchema extends OAuthSchema final class GitlabSchema extends OAuthSchema
{ {
@@ -42,22 +41,21 @@ final class GitlabSchema extends OAuthSchema
return array_merge([ return array_merge([
Step::make('Register new Gitlab OAuth App') Step::make('Register new Gitlab OAuth App')
->schema([ ->schema([
TextEntry::make('register_application') Placeholder::make('')
->hiddenLabel() ->content(new HtmlString(Blade::render('Check out the <x-filament::link href="https://docs.gitlab.com/integration/oauth_provider/" target="_blank">Gitlab docs</x-filament::link> on how to create the oauth app.'))),
->state(new HtmlString(Blade::render('Check out the <x-filament::link href="https://docs.gitlab.com/integration/oauth_provider/" target="_blank">Gitlab docs</x-filament::link> on how to create the oauth app.'))),
TextInput::make('_noenv_callback') TextInput::make('_noenv_callback')
->label('Redirect URI') ->label('Redirect URI')
->dehydrated() ->dehydrated()
->disabled() ->disabled()
->hintCopy() ->hintAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
->default(fn () => url('/auth/oauth/callback/gitlab')), ->default(fn () => url('/auth/oauth/callback/gitlab')),
]), ]),
], parent::getSetupSteps()); ], parent::getSetupSteps());
} }
public function getIcon(): BackedEnum public function getIcon(): string
{ {
return TablerIcon::BrandGitlab; return 'tabler-brand-gitlab';
} }
public function getHexColor(): string public function getHexColor(): string

View File

@@ -1,56 +0,0 @@
<?php
namespace App\Extensions\OAuth\Schemas;
use App\Enums\TablerIcon;
use BackedEnum;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
final class GoogleSchema extends OAuthSchema
{
public function getId(): string
{
return 'google';
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new OAuth client')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://console.developers.google.com/" target="_blank">Google API Console</x-filament::link> and create or select the project you want to use.</p><p>Navigate or search <b>Credentials</b>, click on the <b>Create Credentials</b> button and select <b>OAuth client ID</b>. On the Application type select <b>Web Application</b>.</p><p>On <b>Authorized JavaScript origins</b> and <b>Authorized redirect URIs</b> add and use the values below.</p>'))),
TextInput::make('_noenv_origin')
->label('Authorized JavaScript origins')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('')),
TextInput::make('_noenv_callback')
->label('Authorized redirect URIs')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('/auth/oauth/callback/google')),
TextEntry::make('register_application')
->hiddenLabel()
->state(new HtmlString('<p>When you filled all fields click on <b>Create</b>.</p>')),
]),
], parent::getSetupSteps());
}
public function getIcon(): BackedEnum
{
return TablerIcon::BrandGoogleFilled;
}
public function getHexColor(): string
{
return '#4285f4';
}
}

View File

@@ -1,47 +0,0 @@
<?php
namespace App\Extensions\OAuth\Schemas;
use App\Enums\TablerIcon;
use BackedEnum;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
final class LinkedinSchema extends OAuthSchema
{
public function getId(): string
{
return 'linkedin';
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Obtain Linkedin App OAuth Config')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p><x-filament::link href="https://www.linkedin.com/developers/apps/new" target="_blank">Create</x-filament::link> or <x-filament::link href="https://www.linkedin.com/developers/apps" target="_blank">select</x-filament::link> the one you will be using for authentication.</p><p>Select the <b>Auth</b> tab and set <b>Authorized redirect URLs for your app</b> to the value below.</p>'))),
TextInput::make('_noenv_callback')
->label('Authorized redirect URL')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('/auth/oauth/callback/linkedin')),
]),
], parent::getSetupSteps());
}
public function getIcon(): BackedEnum
{
return TablerIcon::BrandLinkedinFilled;
}
public function getHexColor(): string
{
return '#0a66c2';
}
}

View File

@@ -2,17 +2,13 @@
namespace App\Extensions\OAuth\Schemas; namespace App\Extensions\OAuth\Schemas;
use App\Enums\TablerIcon;
use App\Extensions\OAuth\OAuthSchemaInterface; use App\Extensions\OAuth\OAuthSchemaInterface;
use App\Models\User; use Filament\Forms\Components\Component;
use BackedEnum;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle; use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Component; use Filament\Forms\Components\Wizard\Step;
use Filament\Schemas\Components\Utilities\Set; use Filament\Forms\Set;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Laravel\Socialite\Contracts\User as OAuthUser;
abstract class OAuthSchema implements OAuthSchemaInterface abstract class OAuthSchema implements OAuthSchemaInterface
{ {
@@ -61,26 +57,15 @@ abstract class OAuthSchema implements OAuthSchemaInterface
->default(env("OAUTH_{$id}_CLIENT_SECRET")), ->default(env("OAUTH_{$id}_CLIENT_SECRET")),
Toggle::make("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS") Toggle::make("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS")
->label(trans('admin/setting.oauth.create_missing_users')) ->label(trans('admin/setting.oauth.create_missing_users'))
->columnSpan(2) ->columnSpanFull()
->inline(false) ->inline(false)
->onIcon(TablerIcon::Check) ->onIcon('tabler-check')
->offIcon(TablerIcon::X) ->offIcon('tabler-x')
->onColor('success') ->onColor('success')
->offColor('danger') ->offColor('danger')
->formatStateUsing(fn ($state) => (bool) $state) ->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS", (bool) $state)) ->afterStateUpdated(fn ($state, Set $set) => $set("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS", (bool) $state))
->default(env("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS")), ->default(env("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS")),
Toggle::make("OAUTH_{$id}_SHOULD_LINK_MISSING_USERS")
->label(trans('admin/setting.oauth.link_missing_users'))
->columnSpan(2)
->inline(false)
->onIcon(TablerIcon::Check)
->offIcon(TablerIcon::X)
->onColor('success')
->offColor('danger')
->formatStateUsing(fn ($state) => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set("OAUTH_{$id}_SHOULD_LINK_MISSING_USERS", (bool) $state))
->default(env("OAUTH_{$id}_SHOULD_LINK_MISSING_USERS")),
]; ];
} }
@@ -108,7 +93,7 @@ abstract class OAuthSchema implements OAuthSchemaInterface
return "OAUTH_{$id}_ENABLED"; return "OAUTH_{$id}_ENABLED";
} }
public function getIcon(): null|string|BackedEnum public function getIcon(): ?string
{ {
return null; return null;
} }
@@ -120,20 +105,15 @@ abstract class OAuthSchema implements OAuthSchemaInterface
public function isEnabled(): bool public function isEnabled(): bool
{ {
return env($this->getConfigKey(), false); $id = Str::upper($this->getId());
return env("OAUTH_{$id}_ENABLED", false);
} }
public function shouldCreateMissingUser(OAuthUser $user): bool public function shouldCreateMissingUsers(): bool
{ {
$id = Str::upper($this->getId()); $id = Str::upper($this->getId());
return env("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS", false); return env("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS", false);
} }
public function shouldLinkMissingUser(User $user, OAuthUser $oauthUser): bool
{
$id = Str::upper($this->getId());
return env("OAUTH_{$id}_SHOULD_LINK_MISSING_USERS", false);
}
} }

View File

@@ -1,47 +0,0 @@
<?php
namespace App\Extensions\OAuth\Schemas;
use App\Enums\TablerIcon;
use BackedEnum;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
final class SlackSchema extends OAuthSchema
{
public function getId(): string
{
return 'slack';
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new Slack OAuth')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p><x-filament::link href="https://api.slack.com/apps?new_app=1" target="_blank">Create</x-filament::link> a slack app or <x-filament::link href="https://api.slack.com/apps" target="_blank">select</x-filament::link> the one you will be using for authentication.</p><p>Navigate to the <b>OAuth & Permissions</b> section and configure the <b>Redirect URL</b> using the value below.</p>'))),
TextInput::make('_noenv_callback')
->label('Redirect URL')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('/auth/oauth/callback/slack')),
]),
], parent::getSetupSteps());
}
public function getIcon(): BackedEnum
{
return TablerIcon::BrandSlack;
}
public function getHexColor(): string
{
return '#6ecadc';
}
}

View File

@@ -2,11 +2,9 @@
namespace App\Extensions\OAuth\Schemas; namespace App\Extensions\OAuth\Schemas;
use App\Enums\TablerIcon; use Filament\Forms\Components\Placeholder;
use BackedEnum;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry; use Filament\Forms\Components\Wizard\Step;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString; use Illuminate\Support\HtmlString;
use SocialiteProviders\Steam\Provider; use SocialiteProviders\Steam\Provider;
@@ -54,16 +52,15 @@ final class SteamSchema extends OAuthSchema
return array_merge([ return array_merge([
Step::make('Create API Key') Step::make('Create API Key')
->schema([ ->schema([
TextEntry::make('create_api_key') Placeholder::make('')
->hiddenLabel() ->content(new HtmlString(Blade::render('Visit <x-filament::link href="https://steamcommunity.com/dev/apikey" target="_blank">https://steamcommunity.com/dev/apikey</x-filament::link> to generate an API key.'))),
->state(new HtmlString(Blade::render('Visit <x-filament::link href="https://steamcommunity.com/dev/apikey" target="_blank">https://steamcommunity.com/dev/apikey</x-filament::link> to generate an API key.'))),
]), ]),
], parent::getSetupSteps()); ], parent::getSetupSteps());
} }
public function getIcon(): BackedEnum public function getIcon(): string
{ {
return TablerIcon::BrandSteamFilled; return 'tabler-brand-steam-f';
} }
public function getHexColor(): string public function getHexColor(): string

View File

@@ -1,56 +0,0 @@
<?php
namespace App\Extensions\OAuth\Schemas;
use App\Enums\TablerIcon;
use BackedEnum;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
final class XSchema extends OAuthSchema
{
public function getId(): string
{
return 'x';
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new X App')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://developer.x.com/en/portal/dashboard" target="_blank">X Developer Dashboard</x-filament::link> and create or select the project app you want to use.</p><p>Go to the app\'s settings and set up <b>User authentication</b> if not yet. Make sure to select <b>Web App</b> as the type of app.</p><p>For the <b>Callback URI / Redirect URL</b> and <b>Website URL</b> set it using the value below.</p>'))),
TextInput::make('_noenv_origin')
->label('Website URL')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('')),
TextInput::make('_noenv_callback')
->label('Callback URI / Redirect URL')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('/auth/oauth/callback/x')),
TextEntry::make('register_application')
->hiddenLabel()
->state(new HtmlString('<p>If you have already set this up go to your app\'s <b>Keys and tokens</b> and obtain the Client ID and Secret there.</p>')),
]),
], parent::getSetupSteps());
}
public function getIcon(): BackedEnum
{
return TablerIcon::BrandX;
}
public function getHexColor(): string
{
return '#1da1f2';
}
}

View File

@@ -2,22 +2,20 @@
namespace App\Extensions\Spatie\Fractalistic; namespace App\Extensions\Spatie\Fractalistic;
use App\Extensions\League\Fractal\Serializers\PanelSerializer;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use League\Fractal\Scope; use League\Fractal\Scope;
use League\Fractal\TransformerAbstract; use League\Fractal\TransformerAbstract;
use Spatie\Fractal\Fractal as SpatieFractal; use Spatie\Fractal\Fractal as SpatieFractal;
use Spatie\Fractalistic\Exceptions\InvalidTransformation; use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Spatie\Fractalistic\Exceptions\NoTransformerSpecified; use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use App\Extensions\League\Fractal\Serializers\PanelSerializer;
class Fractal extends SpatieFractal class Fractal extends SpatieFractal
{ {
/** /**
* Create fractal data. * Create fractal data.
* *
* @throws InvalidTransformation * @throws \Spatie\Fractalistic\Exceptions\InvalidTransformation
* @throws NoTransformerSpecified * @throws \Spatie\Fractalistic\Exceptions\NoTransformerSpecified
*/ */
public function createData(): Scope public function createData(): Scope
{ {

View File

@@ -1,32 +0,0 @@
<?php
namespace App\Extensions\Tasks\Schemas;
use App\Models\Schedule;
use App\Models\Task;
use App\Services\Backups\InitiateBackupService;
final class CreateBackupSchema extends TaskSchema
{
public function __construct(private InitiateBackupService $backupService) {}
public function getId(): string
{
return 'backup';
}
public function runTask(Task $task): void
{
$this->backupService->setIgnoredFiles(explode(PHP_EOL, $task->payload))->handle($task->server, null, true);
}
public function canCreate(Schedule $schedule): bool
{
return $schedule->server->backup_limit > 0;
}
public function getPayloadLabel(): string
{
return trans('server/schedule.tasks.actions.backup.files_to_ignore');
}
}

View File

@@ -1,26 +0,0 @@
<?php
namespace App\Extensions\Tasks\Schemas;
use App\Models\Task;
use App\Services\Files\DeleteFilesService;
final class DeleteFilesSchema extends TaskSchema
{
public function __construct(private DeleteFilesService $deleteFilesService) {}
public function getId(): string
{
return 'delete_files';
}
public function runTask(Task $task): void
{
$this->deleteFilesService->handle($task->server, explode(PHP_EOL, $task->payload));
}
public function getPayloadLabel(): string
{
return trans('server/schedule.tasks.actions.delete_files.files_to_delete');
}
}

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