Compare commits

..

107 Commits
main ... dev

Author SHA1 Message Date
Arthur Wambst
60ab10b296
actions update
Some checks failed
Build / UI (22) (push) Has been cancelled
Build / UI (20) (push) Has been cancelled
Release / Release (push) Failing after 11m21s
2025-09-17 01:58:43 +02:00
Arthur Wambst
9184512fa1
actions update
Some checks failed
Build / UI (20) (push) Failing after 5m44s
Build / UI (22) (push) Failing after 12m30s
2025-09-17 01:50:39 +02:00
Arthur Wambst
a2cc6611ce
merge to latest 2025-09-16 20:03:47 +02:00
Charles
3d2390dbcc
Remove table row icons (#1710) 2025-09-16 11:44:59 -04:00
Boy132
d5d50d4150
Collection of smaller v4 fixes (#1684)
Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
Co-authored-by: notCharles <charles@pelican.dev>
2025-09-15 23:28:57 +02:00
Boy132
cba8717188
Update security policy (#1707)
Co-authored-by: Lance Pioch <git@lance.sh>
2025-09-15 21:16:03 +02:00
danielkurek
df4543a079
Fix server owner permissions (#1703) 2025-09-15 14:13:00 -04:00
Boy132
8dc99e6390
Sanitize activity log meta data values (on frontend) (#1705) 2025-09-15 15:54:50 +02:00
MartinOscar
8f1ec20e96
Prevent rootAdmins from having other roles & being deleted via the API (#1699) 2025-09-11 12:56:21 +02:00
JoanFo
61dcb9a3ba
Fixed Allocations not calling webhooks on server creation & Object events (#1595) 2025-09-10 10:39:50 -04:00
NerdsCorpx
0e34886d7e
Fix Docker versioning (#1663) 2025-09-10 10:39:22 -04:00
Boy132
806820592f
Only disable "delete backup" when backup hasn't failed (#1686) 2025-09-09 15:01:45 +02:00
Charles
1900c04b71
Filament v4 🎉 (#1651)
Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
Co-authored-by: Lance Pioch <git@lance.sh>
2025-09-08 13:12:33 -04:00
Boy132
32eb1abd4a
Improve join_paths helper method (#1668) 2025-09-08 09:03:23 +02:00
MartinOscar
47557021fd
Remove DaemonPowerRepository (#1673) 2025-09-08 08:56:59 +02:00
MartinOscar
2ef81eae1a
Refactor & Catch DatabaseManagementService (#1671)
Co-authored-by: notCharles <charles@pelican.dev>
2025-09-06 22:57:11 +02:00
Charles
420730ba1f
Replace str_random with Str::random (#1676)
Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2025-09-06 16:47:54 -04:00
Charles
925ab26fb4
Encode file path in url for folders (#1662) 2025-09-04 17:24:58 -04:00
Charles
2952e22619
Encode file path in url (#1661) 2025-09-04 17:15:46 -04:00
MartinOscar
079eaed010
Fix finish & add translation for Installer title (#1659) 2025-09-04 21:39:10 +02:00
MartinOscar
6671d45651
Fix various Translations & add Installer & add Notifications (#1632) 2025-09-04 20:17:59 +02:00
Boy132
3543b4773a
Rename api key prefixes for better clarity (#1650) 2025-09-04 08:43:06 +02:00
IThundxr
02f788a659
Fix auto deploy docker command not including the container argument (#1584)
Co-authored-by: MartinOscar <40749467+rmartinoscar@users.noreply.github.com>
2025-09-03 22:30:18 +02:00
Boy132
7ace3978d8
Remove leftovers from activity log batch (#1649) 2025-09-03 22:26:17 +02:00
Boy132
8f277aaca0
Create custom startup variable field (#1615) 2025-09-02 09:05:36 +02:00
SaurFort
76451fa0ad
fix: Wrong conversion if decimal prefix selected (#1626)
Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2025-08-31 13:51:27 +02:00
Boy132
0104a08ba4
Create custom number format method to catch invalid languages on php 8.4 (#1623) 2025-08-31 13:48:47 +02:00
MartinOscar
5eff006843
Fix activityLog permission name (#1641) 2025-08-31 12:59:48 +02:00
MartinOscar
a8241bf9f3
Fix Installer, Admin & Exit admin redirect (#1640) 2025-08-30 14:37:59 +02:00
MartinOscar
4aae2562ea
Update bug-report logs url (#1630) 2025-08-25 12:13:27 +02:00
Boy132
42db5b328a
Fix translation for invalid schedule cron + cleanup translations for import modal (#1618) 2025-08-18 23:54:25 +02:00
Boy132
bc4dfb3e92
Fix 500 for closeable alert banners (#1620) 2025-08-18 23:53:59 +02:00
Michael (Parker) Parker
3b9c81534f
fix php ini permissions (#1619) 2025-08-17 09:34:41 -05:00
Boy132
f31aa78f6f
Fix gap for profile repeaters (api keys, ssh keys, activity logs) (#1613) 2025-08-15 14:07:23 +02:00
Boy132
b5ebd544f4
Improve translation for "link" and "unlink" (oauth) (#1612) 2025-08-15 14:06:53 +02:00
Boy132
c77a37ec89
Fix & cleanup OAuthController (#1599) 2025-08-14 08:29:58 +02:00
Michael (Parker) Parker
4d78e5dcd1
Merge pull request #1609 from parkervcp/add_fcgi_healthcheck
add missing package for healthcheck
2025-08-13 14:15:44 -05:00
Michael (Parker) Parker
15075b6ab8 re-add file server directive 2025-08-13 13:44:21 -05:00
Lance Pioch
a8f233e204
Laravel 12.23.1 Shift (#1604)
Co-authored-by: Shift <shift@laravelshift.com>
2025-08-13 08:01:48 -04:00
Boy132
795cad43b9
Server creation: Only get node_id from allocation if it is missing (#1598) 2025-08-12 15:02:49 -04:00
Charles
46934d7a85
fix eggs with [] (#1596) 2025-08-12 15:02:41 -04:00
Michael (Parker) Parker
06067f375c Add fcgi package for healthcheck
I missed adding the package to the dockerfile so the healthcheck is failing
2025-08-12 09:08:10 -05:00
Charles
d1df53c683
fix lang (#1590) 2025-08-11 18:12:33 -04:00
Charles
b03d2cf919
composer update + update jwt (#1587) 2025-08-11 16:57:59 -04:00
Boy132
27a8423f55
Fix container status caching (#1588) 2025-08-11 22:21:52 +02:00
Michael (Parker) Parker
ad70934430
Update healthcheck (#1571) 2025-08-10 15:30:58 -04:00
Boy132
900f8d0fe1
Cleanup remote api requests (#1579) 2025-08-09 17:53:45 -04:00
Lance Pioch
6a4ac515a7
Laravel 12.22.1 Shift (#1580)
Co-authored-by: Shift <shift@laravelshift.com>
2025-08-09 17:53:29 -04:00
Boy132
7c315ac995
Auto create missing users when using oauth (#1573) 2025-08-07 11:22:30 +02:00
Boy132
49e9440e0f
Fix server creation without deployment (#1569) 2025-08-07 11:16:32 +02:00
Alex Smith
02e3e43f1e
Update egg-vanilla-minecraft.yaml (#1574)
Co-authored-by: Charles <charles@pelican.dev>
2025-08-05 17:27:00 -04:00
Charles
8eddef6f04
Update minecraft eggs to support ipv4/ipv6 (#1577) 2025-08-05 17:26:49 -04:00
Boy132
d2f1936bbf
Add abstract base class for panel providers (#1576) 2025-08-05 23:17:34 +02:00
Charles
36863f94c0
Allow user selectable navigation type (#1572)
Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2025-08-05 08:56:31 -04:00
Charles
75863c50d1
Load app.css before filament styles (#1575) 2025-08-04 18:11:34 -04:00
Charles
ec0727b406
Allow eggs to be exported/imported as YAML (#1535) 2025-08-04 07:32:10 -04:00
Boy132
5b2e9d94ca
Cleanup and update node packages (#1557) 2025-08-04 11:51:18 +02:00
Charles
8840d109ef
Client area translations (#1554) 2025-08-01 07:26:14 -04:00
Boy132
71225bd2dc
Refactor AlertBanner to be ViewComponent (#1555) 2025-07-31 23:54:53 +02:00
JoanFo
bab8ec6e18
Fixed not working variables on DiscordWebhooks and headers. (#1516)
Co-authored-by: notCharles <charles@pelican.dev>
2025-07-31 15:47:46 -06:00
Awhikax
d307a2095b
Allow for backups to be renamed (#1546) 2025-07-31 15:47:15 -06:00
Hasyirin Fakhriy
a777f4e0ff
remove maxlength rule from egg variable's default_value field (#1559) 2025-07-31 15:45:28 -06:00
Boy132
86a71afc6c
Cleanup formatResource (#1563) 2025-07-31 23:02:27 +02:00
Hasyirin Fakhriy
88943563c7
Add tags field to eggs transformer. (#1550) 2025-07-22 14:39:18 -04:00
Lance Pioch
20071a64fa
Laravel 12.21.0 Shift (#1551)
Co-authored-by: Shift <shift@laravelshift.com>
2025-07-22 14:39:02 -04:00
Charles
d0d3418e03
Move header actions to iconbuttons (#1541) 2025-07-22 12:31:23 -04:00
Boy132
083e3dc62a
Update contributing guide (#1548) 2025-07-22 15:45:29 +02:00
Charles
d7e60f2456
Fix Console Fit... again (#1537) 2025-07-19 15:40:18 -04:00
Charles
38e746240d
Fix delayed status update, and graphs (#1536) 2025-07-19 14:45:50 -04:00
Lance Pioch
986063dce4
Use default startup variable value when creating server via api (#1518)
Co-authored-by: Boy132 <mail@boy132.de>
2025-07-19 13:58:04 -04:00
Charles
71d0326cb2
Call FitConsole after page load (#1534) 2025-07-19 13:04:22 -04:00
Boy132
62ca53eeaf
Server Policy: Only do owner check if checking for subuser permissions (#1521) 2025-07-19 18:52:28 +02:00
Boy132
9f2305f351
Use filaments password broker for reset link token when creating subuser (#1498) 2025-07-19 18:51:42 +02:00
Boy132
340d1b543c
Add import & export for schedules (#1530) 2025-07-19 16:48:21 +02:00
Boy132
61098b11f2
Add migration to clear password from auth:fail logs (#1533) 2025-07-19 16:47:49 +02:00
Boy132
4d03d6b948
Improve Mounts API (#1531) 2025-07-18 13:50:31 +02:00
Boy132
1f67054777
Fix phpstan (#1532) 2025-07-18 13:49:26 +02:00
Charles
4a9814f16c
Move fullscreen file editor down to not cover top bar (#1527) 2025-07-18 05:05:09 -04:00
Boy132
e0697d3288
Cleanup & fix server deployment (#1497) 2025-07-18 08:23:48 +02:00
Boy132
d165da20ec
Improve schedule form (#1514) 2025-07-18 08:23:08 +02:00
Charles
ae27b179fe
Fix memory leak caused by shift pr (#1528) 2025-07-17 17:41:41 -04:00
Rain
1113ffe0f7
Filters sensitive credential fields from auth:fail logs (#1504) 2025-07-17 16:45:38 -04:00
Lance Pioch
5531bc0ba1
Laravel 12.20.0 Shift (#1500)
Co-authored-by: Shift <shift@laravelshift.com>
2025-07-17 16:44:27 -04:00
Charles
a3819122db
Fix power actions (#1517) 2025-07-15 05:02:55 -04:00
MartinOscar
c5528a61f3
Filter out already used ips with the same port (#1496) 2025-07-10 08:59:46 +02:00
Boy132
5a7c6ac6e5
Improve turnstile error handling (+ cleanup) (#1501) 2025-07-09 13:51:43 +02:00
Boy132
5e8cccef19
Fix options for script_entry Select (#1505) 2025-07-09 09:14:46 +02:00
Charles
0ccb248d91
Add Languages (#1499)
Co-authored-by: Boy132 <mail@boy132.de>
2025-07-08 21:16:11 -04:00
Boy132
514d961c24
Add migration to match node ports (#1489) 2025-07-07 08:37:45 +02:00
Charles
f8e802afcd
Fix table view power actions (#1490) 2025-07-06 19:03:09 -04:00
Boy132
556551b4f3
Add SSH Keys to Profile (#1478) 2025-07-06 22:51:45 +02:00
Boy132
23ddded61e
Replace gethostbynamel with dns_get_record (#1479) 2025-07-06 22:42:59 +02:00
JoanFo
c5aa8a3980
DiscordWebhooks (#1355)
Co-authored-by: notCharles <charles@pelican.dev>
Co-authored-by: Lance Pioch <lancepioch@gmail.com>
Co-authored-by: Boy132 <mail@boy132.de>
Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2025-07-05 12:42:34 -04:00
MartinOscar
21ac75efae
Nullable eggFeatures in FeatureService (#1485) 2025-07-05 14:57:08 +02:00
JoanFo
9655700cde
Nullable allocation in server-entry blade² (#1486) 2025-07-05 14:25:33 +02:00
JoanFo
c9b7e979c0
Nullable allocation in server-entry blade (#1484) 2025-07-05 14:14:43 +02:00
MartinOscar
77a3b0640d
Add dehydratedWhenHidden to serverVariable TextInput & Select (#1476) 2025-07-03 08:55:18 +02:00
pelican-vehikl
de4cb38766
Refactor Providers to be a singleton (#1327) 2025-07-01 21:33:11 -04:00
Charles
74bd7f9991
Move console js to built app.js file. (#1471) 2025-07-01 17:13:44 -04:00
Charles
ba7f814300
back port power actions from v4 branch (#1470) 2025-06-28 10:41:16 -04:00
MartinOscar
cdcd1c521e
Add FileExistsException & Fix error reporting (#1417) 2025-06-26 21:04:33 +02:00
Boy132
4d0aabe91e
Schedule task improvements (#1468) 2025-06-26 17:00:37 +02:00
Boy132
68f72b9b4d
Add "egg index" and dropdown to egg importer (#1451)
Co-authored-by: notCharles <charles@pelican.dev>
2025-06-25 19:50:09 -04:00
JoanFo
dca37ccc95
Server Without Allocations (#1432)
Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2025-06-25 19:49:43 -04:00
Charles
6a088d0c4f
Tweak Grid View, Use Memory Limit, not wings reported allocation (#1462) 2025-06-25 19:49:00 -04:00
Walter van der Broek
7731f16b0f
Fix: Search for tags in correct variable (#1461) 2025-06-25 19:48:39 -04:00
Lance Pioch
9a1e7de4ae
Laravel 12.19.3 Shift (#1455)
Co-authored-by: Shift <shift@laravelshift.com>
2025-06-22 15:46:29 -04:00
884 changed files with 32007 additions and 12004 deletions

View File

@ -64,10 +64,9 @@ 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`
Wings: `sudo wings diagnostics` Panel: `tail -n 300 /var/www/pelican/storage/logs/laravel-$(date +%F).log | curl --data-binary @- https://logs.pelican.dev`
Panel: `tail -n 150 /var/www/pelican/storage/logs/laravel-$(date +%F).log | curl -X POST -F 'c=@-' paste.pelistuff.com` placeholder: "https://logs.pelican.dev/c17f750e"
placeholder: "https://pelipaste.com/a1h6z"
render: bash render: bash
validations: validations:
required: false required: false

View File

@ -4,6 +4,7 @@ on:
push: push:
branches: branches:
- main - main
- dev
pull_request: pull_request:
jobs: jobs:
@ -13,7 +14,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
node-version: [18, 20] node-version: [20, 22]
steps: steps:
- name: Code Checkout - name: Code Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4

View File

@ -4,6 +4,8 @@ on:
push: push:
tags: tags:
- "v*" - "v*"
branches:
- dev
jobs: jobs:
release: release:
@ -39,38 +41,38 @@ jobs:
- name: Build - name: Build
run: yarn build run: yarn build
- name: Create release branch and bump version # - name: Create release branch and bump version
env: # env:
REF: ${{ github.ref }} # REF: ${{ github.ref }}
run: | # run: |
BRANCH=release/${REF:10} # BRANCH=release/${REF:10}
git config --local user.email "github-actions[bot]@users.noreply.github.com" # git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]" # git config --local user.name "github-actions[bot]"
git checkout -b $BRANCH # git checkout -b $BRANCH
git push -u origin $BRANCH # git push -u origin $BRANCH
sed -i "s/ 'version' => 'canary',/ 'version' => '${REF:11}',/" config/app.php # sed -i "s/ 'version' => 'canary',/ 'version' => '${REF:11}',/" config/app.php
git add config/app.php # git add config/app.php
git commit -m "ci(release): bump version" # git commit -m "ci(release): bump version"
git push # git push
- name: Create release archive - name: Create release archive
run: | run: |
rm -rf node_modules vendor tests CODE_OF_CONDUCT.md CONTRIBUTING.md phpunit.xml shell.nix rm -rf node_modules vendor tests CODE_OF_CONDUCT.md CONTRIBUTING.md phpunit.xml shell.nix
tar -czf panel.tar.gz * .env.example tar -czf panel.tar.gz * .env.example
- name: Create checksum #- name: Create checksum
run: | # run: |
SUM=`sha256sum panel.tar.gz` # SUM=`sha256sum panel.tar.gz`
echo $SUM > checksum.txt # echo $SUM > checksum.txt
- name: Create release #- name: Create release
id: create_release # id: create_release
uses: softprops/action-gh-release@v2 # uses: softprops/action-gh-release@v2
env: # env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: # with:
draft: true # draft: true
prerelease: ${{ contains(github.ref, 'rc') || contains(github.ref, 'beta') || contains(github.ref, 'alpha') }} # prerelease: ${{ contains(github.ref, 'rc') || contains(github.ref, 'beta') || contains(github.ref, 'alpha') }}
files: | # files: |
panel.tar.gz # panel.tar.gz
checksum.txt # checksum.txt

1
.gitignore vendored
View File

@ -24,6 +24,5 @@ yarn-error.log
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

@ -63,8 +63,8 @@ FROM --platform=$TARGETOS/$TARGETARCH localhost:5000/base-php:$TARGETARCH AS fin
WORKDIR /var/www/html WORKDIR /var/www/html
# Install additional required libraries # Install additional required libraries
RUN apk update && apk add --no-cache \ RUN apk add --no-cache \
caddy ca-certificates supervisor supercronic caddy ca-certificates supervisor supercronic fcgi
COPY --chown=root:www-data --chmod=640 --from=composerbuild /build . COPY --chown=root:www-data --chmod=640 --from=composerbuild /build .
COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public
@ -85,7 +85,8 @@ RUN chown root:www-data ./ \
&& ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \ && ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \
# Allow www-data write permissions where necessary # 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 \ && 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 && chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \
&& chown -R www-data: /usr/local/etc/php/
# Configure Supervisor # Configure Supervisor
COPY docker/supervisord.conf /etc/supervisord.conf COPY docker/supervisord.conf /etc/supervisord.conf
@ -93,10 +94,11 @@ COPY docker/Caddyfile /etc/caddy/Caddyfile
# Add Laravel scheduler to crontab # Add Laravel scheduler to crontab
COPY docker/crontab /etc/supercronic/crontab COPY docker/crontab /etc/supercronic/crontab
COPY docker/entrypoint.sh ./docker/entrypoint.sh COPY docker/entrypoint.sh /entrypoint.sh
COPY docker/healthcheck.sh /healthcheck.sh
HEALTHCHECK --interval=5m --timeout=10s --start-period=5s --retries=3 \ HEALTHCHECK --interval=5m --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost/up || exit 1 CMD /bin/ash /healthcheck.sh
EXPOSE 80 443 EXPOSE 80 443
@ -104,5 +106,5 @@ VOLUME /pelican-data
USER www-data USER www-data
ENTRYPOINT [ "/bin/ash", "docker/entrypoint.sh" ] ENTRYPOINT [ "/bin/ash", "/entrypoint.sh" ]
CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ] CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ]

View File

@ -67,8 +67,8 @@ FROM --platform=$TARGETOS/$TARGETARCH base AS final
WORKDIR /var/www/html WORKDIR /var/www/html
# Install additional required libraries # Install additional required libraries
RUN apk update && apk add --no-cache \ RUN apk add --no-cache \
caddy ca-certificates supervisor supercronic caddy ca-certificates supervisor supercronic fcgi coreutils
COPY --chown=root:www-data --chmod=640 --from=composerbuild /build . COPY --chown=root:www-data --chmod=640 --from=composerbuild /build .
COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public
@ -89,7 +89,8 @@ RUN chown root:www-data ./ \
&& ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \ && ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \
# Allow www-data write permissions where necessary # 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 \ && 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 && chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \
&& chown -R www-data: /usr/local/etc/php/
# Configure Supervisor # Configure Supervisor
COPY docker/supervisord.conf /etc/supervisord.conf COPY docker/supervisord.conf /etc/supervisord.conf
@ -97,10 +98,11 @@ COPY docker/Caddyfile /etc/caddy/Caddyfile
# Add Laravel scheduler to crontab # Add Laravel scheduler to crontab
COPY docker/crontab /etc/supercronic/crontab COPY docker/crontab /etc/supercronic/crontab
COPY docker/entrypoint.sh ./docker/entrypoint.sh COPY docker/entrypoint.sh /entrypoint.sh
COPY docker/healthcheck.sh /healthcheck.sh
HEALTHCHECK --interval=5m --timeout=10s --start-period=5s --retries=3 \ HEALTHCHECK --interval=5m --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost/up || exit 1 CMD /bin/ash /healthcheck.sh
EXPOSE 80 443 EXPOSE 80 443
@ -108,5 +110,5 @@ VOLUME /pelican-data
USER www-data USER www-data
ENTRYPOINT [ "/bin/ash", "docker/entrypoint.sh" ] ENTRYPOINT [ "/bin/ash", "/entrypoint.sh" ]
CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ] CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ]

View File

@ -2,10 +2,13 @@
namespace App\Console\Commands\Egg; namespace App\Console\Commands\Egg;
use App\Enums\EggFormat;
use App\Models\Egg; 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 JsonException;
use Symfony\Component\Yaml\Yaml;
class CheckEggUpdatesCommand extends Command class CheckEggUpdatesCommand extends Command
{ {
@ -23,6 +26,9 @@ class CheckEggUpdatesCommand extends Command
} }
} }
/**
* @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)) {
@ -31,22 +37,26 @@ class CheckEggUpdatesCommand extends Command
return; return;
} }
$currentJson = json_decode($exporterService->handle($egg->id)); $ext = strtolower(pathinfo(parse_url($egg->update_url, PHP_URL_PATH), PATHINFO_EXTENSION));
unset($currentJson->exported_at); $isYaml = in_array($ext, ['yaml', 'yml']);
$updatedEgg = file_get_contents($egg->update_url); $local = $isYaml
assert($updatedEgg !== false); ? Yaml::parse($exporterService->handle($egg->id, EggFormat::YAML))
$updatedJson = json_decode($updatedEgg); : json_decode($exporterService->handle($egg->id, EggFormat::JSON), true);
unset($updatedJson->exported_at);
if (md5(json_encode($currentJson, JSON_THROW_ON_ERROR)) === md5(json_encode($updatedJson, JSON_THROW_ON_ERROR))) { $remote = file_get_contents($egg->update_url);
$this->info("$egg->name: Up-to-date"); assert($remote !== false);
cache()->put("eggs.$egg->uuid.update", false, now()->addHour());
return; $remote = $isYaml ? Yaml::parse($remote) : json_decode($remote, true);
}
$this->warn("$egg->name: Found update"); unset($local['exported_at'], $remote['exported_at']);
cache()->put("eggs.$egg->uuid.update", true, now()->addHour());
$localHash = md5(json_encode($local, JSON_THROW_ON_ERROR));
$remoteHash = md5(json_encode($remote, JSON_THROW_ON_ERROR));
$status = $localHash === $remoteHash ? 'Up-to-date' : 'Found update';
$this->{($localHash === $remoteHash) ? 'info' : 'warn'}("$egg->name: $status");
cache()->put("eggs.$egg->uuid.update", $localHash !== $remoteHash, now()->addHour());
} }
} }

View File

@ -0,0 +1,46 @@
<?php
namespace App\Console\Commands\Egg;
use Exception;
use Illuminate\Console\Command;
class UpdateEggIndexCommand extends Command
{
protected $signature = 'p:egg:update-index';
public function handle(): int
{
try {
$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) {
$this->error($exception->getMessage());
return 1;
}
$index = [];
foreach ($data['nests'] as $nest) {
$nestName = $nest['nest_type'];
$this->info("Nest: $nestName");
$nestEggs = [];
foreach ($nest['Eggs'] as $egg) {
$eggName = $egg['egg']['name'];
$this->comment("Egg: $eggName");
$nestEggs[$egg['download_url']] = $eggName;
}
$index[$nestName] = $nestEggs;
$this->info('');
}
cache()->forever('eggs.index', $index);
return 0;
}
}

View File

@ -2,6 +2,7 @@
namespace App\Console\Commands\Environment; namespace App\Console\Commands\Environment;
use PDOException;
use App\Traits\EnvironmentWriterTrait; use App\Traits\EnvironmentWriterTrait;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Kernel; use Illuminate\Contracts\Console\Kernel;
@ -105,7 +106,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'));
@ -165,7 +166,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,6 +2,7 @@
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;
@ -28,7 +29,7 @@ class EmailSettingsCommand extends Command
/** /**
* Handle command execution. * Handle command execution.
* *
* @throws \App\Exceptions\PanelException * @throws PanelException
*/ */
public function handle(): void public function handle(): void
{ {

View File

@ -2,6 +2,7 @@
namespace App\Console\Commands\Maintenance; namespace App\Console\Commands\Maintenance;
use InvalidArgumentException;
use App\Models\Backup; use App\Models\Backup;
use Carbon\CarbonImmutable; use Carbon\CarbonImmutable;
use Illuminate\Console\Command; use Illuminate\Console\Command;
@ -16,7 +17,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,6 +2,7 @@
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;
@ -34,7 +35,7 @@ class MakeNodeCommand extends Command
/** /**
* Handle the command execution process. * Handle the command execution process.
* *
* @throws \App\Exceptions\Model\DataValidationException * @throws 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 \App\Models\Node $node */ /** @var 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

@ -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.no_tasks') . " #$schedule->id: " . $exception->getMessage()); $this->error(trans('commands.schedule.process.error_message', ['schedules' => " #$schedule->id: " . $exception->getMessage()]));
} }
} }
} }

View File

@ -3,11 +3,11 @@
namespace App\Console\Commands\Server; namespace App\Console\Commands\Server;
use App\Models\Server; use App\Models\Server;
use App\Repositories\Daemon\DaemonServerRepository;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
use Illuminate\Validation\Factory as ValidatorFactory; use Illuminate\Validation\Factory as ValidatorFactory;
use App\Repositories\Daemon\DaemonPowerRepository;
use Exception; 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(DaemonPowerRepository $powerRepository, ValidatorFactory $validator): void public function handle(DaemonServerRepository $serverRepository, 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, $powerRepository, &$bar): mixed { $this->getQueryBuilder($servers, $nodes)->get()->each(function ($server, int $index) use ($action, $serverRepository, &$bar): mixed {
$bar->clear(); $bar->clear();
if (!$server instanceof Server) { if (!$server instanceof Server) {
@ -60,7 +60,7 @@ class BulkPowerActionCommand extends Command
} }
try { try {
$powerRepository->setServer($server)->send($action); $serverRepository->setServer($server)->power($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

@ -2,6 +2,9 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use Closure;
use Exception;
use Illuminate\Foundation\Application;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use App\Console\Kernel; use App\Console\Kernel;
use Symfony\Component\Process\Process; use Symfony\Component\Process\Process;
@ -28,7 +31,7 @@ class UpgradeCommand extends Command
* This places the application in maintenance mode as well while the commands * This places the application in maintenance mode as well while the commands
* are being executed. * are being executed.
* *
* @throws \Exception * @throws Exception
*/ */
public function handle(): void public function handle(): void
{ {
@ -129,9 +132,9 @@ class UpgradeCommand extends Command
}); });
}); });
/** @var \Illuminate\Foundation\Application $app */ /** @var Application $app */
$app = require __DIR__ . '/../../../bootstrap/app.php'; $app = require __DIR__ . '/../../../bootstrap/app.php';
/** @var \App\Console\Kernel $kernel */ /** @var Kernel $kernel */
$kernel = $app->make(Kernel::class); $kernel = $app->make(Kernel::class);
$kernel->bootstrap(); $kernel->bootstrap();
$this->setLaravel($app); $this->setLaravel($app);
@ -174,7 +177,7 @@ class UpgradeCommand extends Command
$this->info(trans('commands.upgrade.success')); $this->info(trans('commands.upgrade.success'));
} }
protected function withProgress(ProgressBar $bar, \Closure $callback): void protected function withProgress(ProgressBar $bar, Closure $callback): void
{ {
$bar->clear(); $bar->clear();
$callback(); $callback();

View File

@ -2,6 +2,7 @@
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;
@ -14,20 +15,22 @@ class DisableTwoFactorCommand extends Command
/** /**
* Handle command execution process. * Handle command execution process.
* *
* @throws \App\Exceptions\Model\DataValidationException * @throws 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.0') . trans('command/messages.user.2fa_help_text.1')); $this->output->warning(trans('command/messages.user.2fa_help_text'));
} }
$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::query()->where('email', $email)->firstOrFail(); $user = User::where('email', $email)->firstOrFail();
$user->use_totp = false; $user->update([
$user->totp_secret = null; 'mfa_app_secret' => null,
$user->save(); 'mfa_app_recovery_codes' => null,
'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,6 +2,7 @@
namespace App\Console\Commands\User; namespace App\Console\Commands\User;
use App\Exceptions\Model\DataValidationException;
use Exception; use Exception;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use App\Services\Users\UserCreationService; use App\Services\Users\UserCreationService;
@ -25,7 +26,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 \App\Exceptions\Model\DataValidationException * @throws DataValidationException
*/ */
public function handle(): int public function handle(): int
{ {

View File

@ -3,6 +3,7 @@
namespace App\Console; namespace App\Console;
use App\Console\Commands\Egg\CheckEggUpdatesCommand; use App\Console\Commands\Egg\CheckEggUpdatesCommand;
use App\Console\Commands\Egg\UpdateEggIndexCommand;
use App\Console\Commands\Maintenance\CleanServiceBackupFilesCommand; use App\Console\Commands\Maintenance\CleanServiceBackupFilesCommand;
use App\Console\Commands\Maintenance\PruneImagesCommand; use App\Console\Commands\Maintenance\PruneImagesCommand;
use App\Console\Commands\Maintenance\PruneOrphanedBackupsCommand; use App\Console\Commands\Maintenance\PruneOrphanedBackupsCommand;
@ -41,7 +42,9 @@ class Kernel extends ConsoleKernel
$schedule->command(CleanServiceBackupFilesCommand::class)->daily(); $schedule->command(CleanServiceBackupFilesCommand::class)->daily();
$schedule->command(PruneImagesCommand::class)->daily(); $schedule->command(PruneImagesCommand::class)->daily();
$schedule->command(CheckEggUpdatesCommand::class)->hourly();
$schedule->command(CheckEggUpdatesCommand::class)->daily();
$schedule->command(UpdateEggIndexCommand::class)->daily();
if (config('backups.prune_age')) { if (config('backups.prune_age')) {
// Every 30 minutes, run the backup pruning command so that any abandoned backups can be deleted. // Every 30 minutes, run the backup pruning command so that any abandoned backups can be deleted.

View File

@ -32,6 +32,6 @@ enum BackupStatus: string implements HasColor, HasIcon, HasLabel
public function getLabel(): string public function getLabel(): string
{ {
return str($this->value)->headline(); return trans('server/backup.backup_status.' . $this->value);
} }
} }

View File

@ -68,7 +68,7 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
public function getLabel(): string public function getLabel(): string
{ {
return str($this->value)->title(); return trans('server/console.status.' . $this->value);
} }
public function isOffline(): bool public function isOffline(): bool

View File

@ -0,0 +1,37 @@
<?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';
public function getDefaultValue(): string|int|bool
{
return match ($this) {
self::ConsoleRows => 30,
self::ConsoleFont => 'monospace',
self::ConsoleFontSize => 14,
self::ConsoleGraphPeriod => 30,
self::TopNavigation => false,
self::DashboardLayout => 'grid',
};
}
/** @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

@ -1,141 +0,0 @@
<?php
namespace App\Enums;
use Filament\Support\Contracts\HasLabel;
enum EditorLanguages: string implements HasLabel
{
case plaintext = 'plaintext';
case abap = 'abap';
case apex = 'apex';
case azcali = 'azcali';
case bat = 'bat';
case bicep = 'bicep';
case cameligo = 'cameligo';
case coljure = 'coljure';
case coffeescript = 'coffeescript';
case c = 'c';
case cpp = 'cpp';
case csharp = 'csharp';
case csp = 'csp';
case css = 'css';
case cypher = 'cypher';
case dart = 'dart';
case dockerfile = 'dockerfile';
case ecl = 'ecl';
case elixir = 'elixir';
case flow9 = 'flow9';
case fsharp = 'fsharp';
case go = 'go';
case graphql = 'graphql';
case handlebars = 'handlebars';
case hcl = 'hcl';
case html = 'html';
case ini = 'ini';
case java = 'java';
case javascript = 'javascript';
case julia = 'julia';
case json = 'json';
case kotlin = 'kotlin';
case less = 'less';
case lexon = 'lexon';
case lua = 'lua';
case liquid = 'liquid';
case m3 = 'm3';
case markdown = 'markdown';
case mdx = 'mdx';
case mips = 'mips';
case msdax = 'msdax';
case mysql = 'mysql';
case objectivec = 'objective-c';
case pascal = 'pascal';
case pascaligo = 'pascaligo';
case perl = 'perl';
case pgsql = 'pgsql';
case php = 'php';
case pla = 'pla';
case postiats = 'postiats';
case powerquery = 'powerquery';
case powershell = 'powershell';
case proto = 'proto';
case pug = 'pug';
case python = 'python';
case qsharp = 'qsharp';
case r = 'r';
case razor = 'razor';
case redis = 'redis';
case redshift = 'redshift';
case restructuredtext = 'restructuredtext';
case ruby = 'ruby';
case rust = 'rust';
case sb = 'sb';
case scala = 'scala';
case scheme = 'scheme';
case scss = 'scss';
case shell = 'shell';
case sol = 'sol';
case aes = 'aes';
case sparql = 'sparql';
case sql = 'sql';
case st = 'st';
case swift = 'swift';
case systemverilog = 'systemverilog';
case verilog = 'verilog';
case tcl = 'tcl';
case twig = 'twig';
case typescript = 'typescript';
case typespec = 'typespec';
case vb = 'vb';
case wgsl = 'wgsl';
case xml = 'xml';
case yaml = 'yaml';
public static function fromWithAlias(string $match): self
{
return match ($match) {
'h' => self::c,
'cc', 'hpp' => self::cpp,
'cs' => self::csharp,
'class' => self::java,
'htm' => self::html,
'js', 'mjs', 'cjs' => self::javascript,
'kt', 'kts' => self::kotlin,
'md' => self::markdown,
'm' => self::objectivec,
'pl', 'pm' => self::perl,
'php3', 'php4', 'php5', 'phtml' => self::php,
'py', 'pyc', 'pyo', 'pyi' => self::python,
'rdata', 'rds' => self::r,
'rb', 'erb' => self::ruby,
'sc' => self::scala,
'sh', 'zsh' => self::shell,
'ts', 'tsx' => self::typescript,
'yml' => self::yaml,
default => self::tryFrom($match) ?? self::plaintext,
};
}
public function getLabel(): string
{
return $this->name;
}
}

9
app/Enums/EggFormat.php Normal file
View File

@ -0,0 +1,9 @@
<?php
namespace App\Enums;
enum EggFormat: string
{
case YAML = 'yaml';
case JSON = 'json';
}

View File

@ -0,0 +1,27 @@
<?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,9 +2,50 @@
namespace App\Enums; namespace App\Enums;
enum ServerResourceType use App\Models\Server;
enum ServerResourceType: string
{ {
case Unit; case Uptime = 'uptime';
case Percentage; case CPU = 'cpu_absolute';
case Time; case Memory = 'memory_bytes';
case Disk = 'disk_bytes';
case CPULimit = 'cpu';
case MemoryLimit = 'memory';
case DiskLimit = 'disk';
/**
* @return int resource amount in bytes
*/
public function getResourceAmount(Server $server): int
{
if ($this->isLimit()) {
$resourceAmount = $server->{$this->value} ?? 0;
if (!$this->isPercentage()) {
// Our limits are entered as MiB/ MB so we need to convert them to bytes
$resourceAmount *= config('panel.use_binary_prefix') ? 1024 * 1024 : 1000 * 1000;
}
return $resourceAmount;
}
return $server->retrieveResources()[$this->value] ?? 0;
}
public function isLimit(): bool
{
return $this === ServerResourceType::CPULimit || $this === ServerResourceType::MemoryLimit || $this === ServerResourceType::DiskLimit;
}
public function isTime(): bool
{
return $this === ServerResourceType::Uptime;
}
public function isPercentage(): bool
{
return $this === ServerResourceType::CPU || $this === ServerResourceType::CPULimit;
}
} }

View File

@ -8,7 +8,6 @@ use Filament\Support\Contracts\HasLabel;
enum ServerState: string implements HasColor, HasIcon, HasLabel enum ServerState: string implements HasColor, HasIcon, HasLabel
{ {
case Normal = 'normal';
case Installing = 'installing'; case Installing = 'installing';
case InstallFailed = 'install_failed'; case InstallFailed = 'install_failed';
case ReinstallFailed = 'reinstall_failed'; case ReinstallFailed = 'reinstall_failed';
@ -18,7 +17,6 @@ enum ServerState: string implements HasColor, HasIcon, HasLabel
public function getIcon(): string public function getIcon(): string
{ {
return match ($this) { return match ($this) {
self::Normal => 'tabler-heart',
self::Installing => 'tabler-heart-bolt', self::Installing => 'tabler-heart-bolt',
self::InstallFailed => 'tabler-heart-x', self::InstallFailed => 'tabler-heart-x',
self::ReinstallFailed => 'tabler-heart-x', self::ReinstallFailed => 'tabler-heart-x',
@ -31,14 +29,13 @@ enum ServerState: string implements HasColor, HasIcon, HasLabel
{ {
if ($hex) { if ($hex) {
return match ($this) { return match ($this) {
self::Normal, self::Installing, self::RestoringBackup => '#2563EB', self::Installing, self::RestoringBackup => '#2563EB',
self::Suspended => '#D97706', self::Suspended => '#D97706',
self::InstallFailed, self::ReinstallFailed => '#EF4444', self::InstallFailed, self::ReinstallFailed => '#EF4444',
}; };
} }
return match ($this) { return match ($this) {
self::Normal => 'primary',
self::Installing => 'primary', self::Installing => 'primary',
self::InstallFailed => 'danger', self::InstallFailed => 'danger',
self::ReinstallFailed => 'danger', self::ReinstallFailed => 'danger',

View File

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

34
app/Enums/WebhookType.php Normal file
View File

@ -0,0 +1,34 @@
<?php
namespace App\Enums;
use Filament\Support\Contracts\HasLabel;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasIcon;
enum WebhookType: string implements HasColor, HasIcon, HasLabel
{
case Regular = 'regular';
case Discord = 'discord';
public function getLabel(): string
{
return trans('admin/webhook.' . $this->value);
}
public function getColor(): ?string
{
return match ($this) {
self::Regular => null,
self::Discord => 'blurple',
};
}
public function getIcon(): string
{
return match ($this) {
self::Regular => 'tabler-world-www',
self::Discord => 'tabler-brand-discord',
};
}
}

View File

@ -2,6 +2,7 @@
namespace App\Exceptions; namespace App\Exceptions;
use Throwable;
use Exception; use Exception;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
@ -28,7 +29,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);
} }
@ -79,11 +80,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,6 +2,9 @@
namespace App\Exceptions; namespace App\Exceptions;
use PDOException;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
@ -79,7 +82,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);
}); });
@ -88,7 +91,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) {
@ -117,11 +120,11 @@ class Handler extends ExceptionHandler
/** /**
* Render an exception into an HTTP response. * Render an exception into an HTTP response.
* *
* @param \Illuminate\Http\Request $request * @param 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);
@ -143,7 +146,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 \Illuminate\Http\Request $request * @param Request $request
*/ */
public function invalidJson($request, ValidationException $exception): JsonResponse public function invalidJson($request, ValidationException $exception): JsonResponse
{ {
@ -249,7 +252,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);
} }
@ -257,7 +260,7 @@ class Handler extends ExceptionHandler
/** /**
* Convert an authentication exception into an unauthenticated response. * Convert an authentication exception into an unauthenticated response.
* *
* @param \Illuminate\Http\Request $request * @param Request $request
*/ */
protected function unauthenticated($request, AuthenticationException $exception): JsonResponse|RedirectResponse protected function unauthenticated($request, AuthenticationException $exception): JsonResponse|RedirectResponse
{ {
@ -291,7 +294,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

@ -2,6 +2,7 @@
namespace App\Exceptions\Http; namespace App\Exceptions\Http;
use Throwable;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\HttpException;
@ -10,7 +11,7 @@ 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

@ -2,6 +2,7 @@
namespace App\Exceptions\Http\Server; namespace App\Exceptions\Http\Server;
use Throwable;
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;
@ -12,7 +13,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

@ -1,18 +0,0 @@
<?php
namespace App\Exceptions\Http;
use Illuminate\Http\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
class TwoFactorAuthRequiredException extends HttpException implements HttpExceptionInterface
{
/**
* TwoFactorAuthRequiredException constructor.
*/
public function __construct(?\Throwable $previous = null)
{
parent::__construct(Response::HTTP_BAD_REQUEST, 'Two-factor authentication is required on this account in order to access this endpoint.', $previous);
}
}

View File

@ -2,13 +2,15 @@
namespace App\Exceptions; namespace App\Exceptions;
use Exception;
use App\Exceptions\Solutions\ManifestDoesNotExistSolution;
use Spatie\Ignition\Contracts\Solution; use Spatie\Ignition\Contracts\Solution;
use Spatie\Ignition\Contracts\ProvidesSolution; use Spatie\Ignition\Contracts\ProvidesSolution;
class ManifestDoesNotExistException extends \Exception implements ProvidesSolution class ManifestDoesNotExistException extends Exception implements ProvidesSolution
{ {
public function getSolution(): Solution public function getSolution(): Solution
{ {
return new Solutions\ManifestDoesNotExistSolution(); return new ManifestDoesNotExistSolution();
} }
} }

View File

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

View File

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

View File

@ -0,0 +1,7 @@
<?php
namespace App\Exceptions\Service\Deployment;
use App\Exceptions\DisplayException;
class NoViableNodeException extends DisplayException {}

View File

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

View File

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

@ -1,42 +0,0 @@
<?php
namespace App\Extensions\Avatar;
use App\Models\User;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
abstract class AvatarProvider
{
/**
* @var array<string, static>
*/
protected static array $providers = [];
public static function getProvider(string $id): ?self
{
return Arr::get(static::$providers, $id);
}
/**
* @return array<string, static>
*/
public static function getAll(): array
{
return static::$providers;
}
public function __construct()
{
static::$providers[$this->getId()] = $this;
}
abstract public function getId(): string;
abstract public function get(User $user): ?string;
public function getName(): string
{
return Str::title($this->getId());
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace App\Extensions\Avatar;
use App\Models\User;
interface AvatarSchemaInterface
{
public function getId(): string;
public function getName(): string;
public function get(User $user): ?string;
}

View File

@ -0,0 +1,55 @@
<?php
namespace App\Extensions\Avatar;
use App\Models\User;
use Illuminate\Support\Facades\Storage;
class AvatarService
{
/** @var AvatarSchemaInterface[] */
private array $schemas = [];
public function __construct(
private readonly bool $allowUploadedAvatars,
private readonly string $activeSchema,
) {}
public function get(string $id): ?AvatarSchemaInterface
{
return array_get($this->schemas, $id);
}
public function getActiveSchema(): ?AvatarSchemaInterface
{
return $this->get($this->activeSchema);
}
public function getAvatarUrl(User $user): ?string
{
if ($this->allowUploadedAvatars) {
$path = "avatars/$user->id.png";
if (Storage::disk('public')->exists($path)) {
return Storage::url($path);
}
}
return $this->getActiveSchema()?->get($user);
}
public function register(AvatarSchemaInterface $schema): void
{
if (array_key_exists($schema->getId(), $this->schemas)) {
return;
}
$this->schemas[$schema->getId()] = $schema;
}
/** @return array<string, string> */
public function getMappings(): array
{
return collect($this->schemas)->mapWithKeys(fn ($schema) => [$schema->getId() => $schema->getName()])->all();
}
}

View File

@ -1,24 +1,24 @@
<?php <?php
namespace App\Extensions\Avatar\Providers; namespace App\Extensions\Avatar\Schemas;
use App\Extensions\Avatar\AvatarProvider; use App\Extensions\Avatar\AvatarSchemaInterface;
use App\Models\User; use App\Models\User;
class GravatarProvider extends AvatarProvider class GravatarSchema implements AvatarSchemaInterface
{ {
public function getId(): string public function getId(): string
{ {
return 'gravatar'; return 'gravatar';
} }
public function getName(): string
{
return 'Gravatar';
}
public function get(User $user): string public function get(User $user): string
{ {
return 'https://gravatar.com/avatar/' . md5($user->email); return 'https://gravatar.com/avatar/' . md5($user->email);
} }
public static function register(): self
{
return new self();
}
} }

View File

@ -1,11 +1,11 @@
<?php <?php
namespace App\Extensions\Avatar\Providers; namespace App\Extensions\Avatar\Schemas;
use App\Extensions\Avatar\AvatarProvider; use App\Extensions\Avatar\AvatarSchemaInterface;
use App\Models\User; use App\Models\User;
class UiAvatarsProvider extends AvatarProvider class UiAvatarsSchema implements AvatarSchemaInterface
{ {
public function getId(): string public function getId(): string
{ {
@ -22,9 +22,4 @@ class UiAvatarsProvider extends AvatarProvider
// UI Avatars is the default of filament so just return null here // UI Avatars is the default of filament so just return null here
return null; return null;
} }
public static function register(): self
{
return new self();
}
} }

View File

@ -2,6 +2,7 @@
namespace App\Extensions\Backups; namespace App\Extensions\Backups;
use InvalidArgumentException;
use Closure; use Closure;
use Aws\S3\S3Client; use Aws\S3\S3Client;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
@ -64,7 +65,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'];
@ -82,7 +83,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

@ -0,0 +1,48 @@
<?php
namespace App\Extensions\Captcha;
use App\Extensions\Captcha\Schemas\CaptchaSchemaInterface;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
class CaptchaService
{
/** @var array<string, CaptchaSchemaInterface> */
private array $schemas = [];
/**
* @return CaptchaSchemaInterface[]
*/
public function getAll(): array
{
return $this->schemas;
}
public function get(string $id): ?CaptchaSchemaInterface
{
return array_get($this->schemas, $id);
}
public function register(CaptchaSchemaInterface $schema): void
{
if (array_key_exists($schema->getId(), $this->schemas)) {
return;
}
config()->set('captcha.' . Str::lower($schema->getId()), $schema->getConfig());
$this->schemas[$schema->getId()] = $schema;
}
/** @return Collection<CaptchaSchemaInterface> */
public function getActiveSchemas(): Collection
{
return collect($this->schemas)
->filter(fn (CaptchaSchemaInterface $schema) => $schema->isEnabled());
}
public function getActiveSchema(): ?CaptchaSchemaInterface
{
return $this->getActiveSchemas()->first();
}
}

View File

@ -1,118 +0,0 @@
<?php
namespace App\Extensions\Captcha\Providers;
use Filament\Forms\Components\Component;
use Filament\Forms\Components\TextInput;
use Illuminate\Foundation\Application;
use Illuminate\Support\Str;
abstract class CaptchaProvider
{
/**
* @var array<string, static>
*/
protected static array $providers = [];
/**
* @return self|static[]
*/
public static function get(?string $id = null): array|self
{
return $id ? static::$providers[$id] : static::$providers;
}
protected function __construct(protected Application $app)
{
if (array_key_exists($this->getId(), static::$providers)) {
if (!$this->app->runningUnitTests()) {
logger()->warning("Tried to create duplicate Captcha provider with id '{$this->getId()}'");
}
return;
}
config()->set('captcha.' . Str::lower($this->getId()), $this->getConfig());
static::$providers[$this->getId()] = $this;
}
abstract public function getId(): string;
abstract public function getComponent(): Component;
/**
* @return array<string, string|string[]|bool|null>
*/
public function getConfig(): array
{
$id = Str::upper($this->getId());
return [
'site_key' => env("CAPTCHA_{$id}_SITE_KEY"),
'secret_key' => env("CAPTCHA_{$id}_SECRET_KEY"),
];
}
/**
* @return Component[]
*/
public function getSettingsForm(): array
{
$id = Str::upper($this->getId());
return [
TextInput::make("CAPTCHA_{$id}_SITE_KEY")
->label('Site Key')
->placeholder('Site Key')
->columnSpan(2)
->required()
->password()
->revealable()
->autocomplete(false)
->default(env("CAPTCHA_{$id}_SITE_KEY")),
TextInput::make("CAPTCHA_{$id}_SECRET_KEY")
->label('Secret Key')
->placeholder('Secret Key')
->columnSpan(2)
->required()
->password()
->revealable()
->autocomplete(false)
->default(env("CAPTCHA_{$id}_SECRET_KEY")),
];
}
public function getName(): string
{
return Str::title($this->getId());
}
public function getIcon(): ?string
{
return null;
}
public function isEnabled(): bool
{
$id = Str::upper($this->getId());
return env("CAPTCHA_{$id}_ENABLED", false);
}
/**
* @return array<string, string|bool>
*/
public function validateResponse(?string $captchaResponse = null): array
{
return [
'success' => false,
'message' => 'validateResponse not defined',
];
}
public function verifyDomain(string $hostname, ?string $requestUrl = null): bool
{
return true;
}
}

View File

@ -1,106 +0,0 @@
<?php
namespace App\Extensions\Captcha\Providers;
use App\Filament\Components\Forms\Fields\TurnstileCaptcha;
use Exception;
use Filament\Forms\Components\Component;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Toggle;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\HtmlString;
class TurnstileProvider extends CaptchaProvider
{
public function getId(): string
{
return 'turnstile';
}
public function getComponent(): Component
{
return TurnstileCaptcha::make('turnstile');
}
/**
* @return array<string, string|string[]|bool|null>
*/
public function getConfig(): array
{
return array_merge(parent::getConfig(), [
'verify_domain' => env('CAPTCHA_TURNSTILE_VERIFY_DOMAIN'),
]);
}
/**
* @return Component[]
*/
public function getSettingsForm(): array
{
return array_merge(parent::getSettingsForm(), [
Toggle::make('CAPTCHA_TURNSTILE_VERIFY_DOMAIN')
->label(trans('admin/setting.captcha.verify'))
->columnSpan(2)
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->default(env('CAPTCHA_TURNSTILE_VERIFY_DOMAIN', true)),
Placeholder::make('info')
->label(trans('admin/setting.captcha.info_label'))
->columnSpan(2)
->content(new HtmlString(trans('admin/setting.captcha.info'))),
]);
}
public function getIcon(): string
{
return 'tabler-brand-cloudflare';
}
public static function register(Application $app): self
{
return new self($app);
}
/**
* @return array<string, string|bool>
*/
public function validateResponse(?string $captchaResponse = null): array
{
$captchaResponse ??= request()->get('cf-turnstile-response');
if (!$secret = env('CAPTCHA_TURNSTILE_SECRET_KEY')) {
throw new Exception('Turnstile secret key is not defined.');
}
$response = Http::asJson()
->timeout(15)
->connectTimeout(5)
->throw()
->post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [
'secret' => $secret,
'response' => $captchaResponse,
]);
return count($response->json()) ? $response->json() : [
'success' => false,
'message' => 'Unknown error occurred, please try again',
];
}
public function verifyDomain(string $hostname, ?string $requestUrl = null): bool
{
if (!env('CAPTCHA_TURNSTILE_VERIFY_DOMAIN', true)) {
return true;
}
$requestUrl ??= request()->url;
$requestUrl = parse_url($requestUrl);
return $hostname === array_get($requestUrl, 'host');
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace App\Extensions\Captcha\Schemas;
use Filament\Schemas\Components\Component;
use Filament\Forms\Components\TextInput;
use Illuminate\Support\Str;
abstract class BaseSchema
{
abstract public function getId(): string;
public function getName(): string
{
return Str::upper($this->getId());
}
/**
* @return array<string, string|string[]|bool|null>
*/
public function getConfig(): array
{
$id = Str::upper($this->getId());
return [
'site_key' => env("CAPTCHA_{$id}_SITE_KEY"),
'secret_key' => env("CAPTCHA_{$id}_SECRET_KEY"),
];
}
/**
* @return Component[]
*/
public function getSettingsForm(): array
{
$id = Str::upper($this->getId());
return [
TextInput::make("CAPTCHA_{$id}_SITE_KEY")
->label('Site Key')
->placeholder('Site Key')
->columnSpan(2)
->required()
->password()
->revealable()
->autocomplete(false)
->default(env("CAPTCHA_{$id}_SITE_KEY")),
TextInput::make("CAPTCHA_{$id}_SECRET_KEY")
->label('Secret Key')
->placeholder('Secret Key')
->columnSpan(2)
->required()
->password()
->revealable()
->autocomplete(false)
->default(env("CAPTCHA_{$id}_SECRET_KEY")),
];
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Extensions\Captcha\Schemas;
use Filament\Schemas\Components\Component;
interface CaptchaSchemaInterface
{
public function getId(): string;
public function getName(): string;
/**
* @return array<string, string|string[]|bool|null>
*/
public function getConfig(): array;
public function isEnabled(): bool;
public function getFormComponent(): Component;
/**
* @return Component[]
*/
public function getSettingsForm(): array;
public function getIcon(): ?string;
public function validateResponse(?string $captchaResponse = null): void;
}

View File

@ -1,11 +1,10 @@
<?php <?php
namespace App\Filament\Components\Forms\Fields; namespace App\Extensions\Captcha\Schemas\Turnstile;
use App\Rules\ValidTurnstileCaptcha;
use Filament\Forms\Components\Field; use Filament\Forms\Components\Field;
class TurnstileCaptcha extends Field class Component extends Field
{ {
protected string $viewIdentifier = 'turnstile'; protected string $viewIdentifier = 'turnstile';
@ -19,8 +18,6 @@ class TurnstileCaptcha extends Field
$this->required(); $this->required();
$this->after(function (TurnstileCaptcha $component) { $this->rule(new Rule());
$component->rule(new ValidTurnstileCaptcha());
});
} }
} }

View File

@ -0,0 +1,23 @@
<?php
namespace App\Extensions\Captcha\Schemas\Turnstile;
use App\Extensions\Captcha\CaptchaService;
use Closure;
use Exception;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\App;
class Rule implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
try {
App::call(fn (CaptchaService $service) => $service->get('turnstile')->validateResponse($value));
} catch (Exception $exception) {
report($exception);
$fail('Captcha validation failed: ' . $exception->getMessage());
}
}
}

View File

@ -0,0 +1,117 @@
<?php
namespace App\Extensions\Captcha\Schemas\Turnstile;
use App\Extensions\Captcha\Schemas\CaptchaSchemaInterface;
use App\Extensions\Captcha\Schemas\BaseSchema;
use Exception;
use Filament\Infolists\Components\TextEntry;
use Filament\Forms\Components\Toggle;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\HtmlString;
class TurnstileSchema extends BaseSchema implements CaptchaSchemaInterface
{
public function getId(): string
{
return 'turnstile';
}
public function isEnabled(): bool
{
return env('CAPTCHA_TURNSTILE_ENABLED', false);
}
public function getFormComponent(): Component
{
return Component::make('turnstile');
}
/**
* @return array<string, string|string[]|bool|null>
*/
public function getConfig(): array
{
return array_merge(parent::getConfig(), [
'verify_domain' => env('CAPTCHA_TURNSTILE_VERIFY_DOMAIN'),
]);
}
/**
* @return \Filament\Support\Components\Component[]
*
* @throws Exception
*/
public function getSettingsForm(): array
{
return array_merge(parent::getSettingsForm(), [
Toggle::make('CAPTCHA_TURNSTILE_VERIFY_DOMAIN')
->label(trans('admin/setting.captcha.verify'))
->columnSpan(2)
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->default(env('CAPTCHA_TURNSTILE_VERIFY_DOMAIN', true)),
TextEntry::make('info')
->label(trans('admin/setting.captcha.info_label'))
->columnSpan(2)
->state(new HtmlString(trans('admin/setting.captcha.info'))),
]);
}
public function getIcon(): ?string
{
return 'tabler-brand-cloudflare';
}
/**
* @throws Exception
*/
public function validateResponse(?string $captchaResponse = null): void
{
$captchaResponse ??= request()->get('cf-turnstile-response');
if (!$secret = env('CAPTCHA_TURNSTILE_SECRET_KEY')) {
throw new Exception('Turnstile secret key is not defined.');
}
$response = Http::asJson()
->timeout(15)
->connectTimeout(5)
->throw()
->post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [
'secret' => $secret,
'response' => $captchaResponse,
])
->json();
if (!$response['success']) {
match ($response['error-codes'][0] ?? null) {
'missing-input-secret' => throw new Exception('The secret parameter was not passed.'),
'invalid-input-secret' => throw new Exception('The secret parameter was invalid, did not exist, or is a testing secret key with a non-testing response.'),
'missing-input-response' => throw new Exception('The response parameter (token) was not passed.'),
'invalid-input-response' => throw new Exception('The response parameter (token) is invalid or has expired.'),
'bad-request' => throw new Exception('The request was rejected because it was malformed.'),
'timeout-or-duplicate' => throw new Exception('The response parameter (token) has already been validated before.'),
default => throw new Exception('An internal error happened while validating the response.'),
};
}
if (!$this->verifyDomain($response['hostname'] ?? '')) {
throw new Exception('Domain verification failed.');
}
}
private function verifyDomain(string $hostname): bool
{
if (!env('CAPTCHA_TURNSTILE_VERIFY_DOMAIN', true)) {
return true;
}
$requestUrl = parse_url(request()->url());
return $hostname === array_get($requestUrl, 'host');
}
}

View File

@ -1,51 +0,0 @@
<?php
namespace App\Extensions\Features;
use Filament\Actions\Action;
use Illuminate\Foundation\Application;
abstract class FeatureProvider
{
/**
* @var array<string, static>
*/
protected static array $providers = [];
/**
* @param string[] $id
* @return self|static[]
*/
public static function getProviders(string|array|null $id = null): array|self
{
if (is_array($id)) {
return array_intersect_key(static::$providers, array_flip($id));
}
return $id ? static::$providers[$id] : static::$providers;
}
protected function __construct(protected Application $app)
{
if (array_key_exists($this->getId(), static::$providers)) {
if (!$this->app->runningUnitTests()) {
logger()->warning("Tried to create duplicate Feature provider with id '{$this->getId()}'");
}
return;
}
static::$providers[$this->getId()] = $this;
}
abstract public function getId(): string;
/**
* A matching subset string (case-insensitive) from the console output
*
* @return array<string>
*/
abstract public function getListeners(): array;
abstract public function getAction(): Action;
}

View File

@ -0,0 +1,15 @@
<?php
namespace App\Extensions\Features;
use Filament\Actions\Action;
interface FeatureSchemaInterface
{
/** @return string[] */
public function getListeners(): array;
public function getId(): string;
public function getAction(): Action;
}

View File

@ -0,0 +1,52 @@
<?php
namespace App\Extensions\Features;
class FeatureService
{
/** @var FeatureSchemaInterface[] */
private array $schemas = [];
/**
* @return FeatureSchemaInterface[]
*/
public function getAll(): array
{
return $this->schemas;
}
public function get(string $id): ?FeatureSchemaInterface
{
return array_get($this->schemas, $id);
}
/**
* @param ?string[] $features
* @return FeatureSchemaInterface[]
*/
public function getActiveSchemas(?array $features = []): array
{
return collect($this->schemas)->only($features)->all();
}
public function register(FeatureSchemaInterface $schema): void
{
if (array_key_exists($schema->getId(), $this->schemas)) {
return;
}
$this->schemas[$schema->getId()] = $schema;
}
/**
* @param ?string[] $features
* @return array<string, array<string>>
*/
public function getMappings(?array $features = []): array
{
return collect($this->getActiveSchemas($features))
->mapWithKeys(fn (FeatureSchemaInterface $schema) => [
$schema->getId() => $schema->getListeners(),
])->all();
}
}

View File

@ -1,32 +1,27 @@
<?php <?php
namespace App\Extensions\Features; namespace App\Extensions\Features\Schemas;
use App\Extensions\Features\FeatureSchemaInterface;
use App\Facades\Activity; use App\Facades\Activity;
use App\Models\Permission; use App\Models\Permission;
use App\Models\Server; use App\Models\Server;
use App\Models\ServerVariable; use App\Models\ServerVariable;
use App\Repositories\Daemon\DaemonPowerRepository; use App\Repositories\Daemon\DaemonServerRepository;
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\Foundation\Application;
use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Illuminate\Support\HtmlString; use Illuminate\Support\HtmlString;
class GSLToken extends FeatureProvider class GSLTokenSchema implements FeatureSchemaInterface
{ {
public function __construct(protected Application $app)
{
parent::__construct($app);
}
/** @return array<string> */ /** @return array<string> */
public function getListeners(): array public function getListeners(): array
{ {
@ -41,6 +36,9 @@ class GSLToken extends FeatureProvider
return 'gsl_token'; return 'gsl_token';
} }
/**
* @throws Exception
*/
public function getAction(): Action public function getAction(): Action
{ {
/** @var Server $server */ /** @var Server $server */
@ -56,9 +54,9 @@ class GSLToken extends FeatureProvider
->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')
->disabledForm(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_UPDATE, $server)) ->disabledSchema(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_UPDATE, $server))
->form([ ->schema([
Placeholder::make('info') TextEntry::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')
@ -75,13 +73,12 @@ class GSLToken extends FeatureProvider
} }
}, },
]) ])
->hintIcon('tabler-code') ->hintIcon('tabler-code', fn () => implode('|', $serverVariable->variable->rules))
->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, DaemonPowerRepository $powerRepository) use ($server, $serverVariable) { ->action(function (array $data, DaemonServerRepository $serverRepository) use ($server, $serverVariable) {
/** @var Server $server */ /** @var Server $server */
$server = Filament::getTenant(); $server = Filament::getTenant();
try { try {
@ -103,7 +100,7 @@ class GSLToken extends FeatureProvider
->log(); ->log();
} }
$powerRepository->setServer($server)->send('restart'); $serverRepository->setServer($server)->power('restart');
Notification::make() Notification::make()
->title('GSL Token updated') ->title('GSL Token updated')
@ -119,9 +116,4 @@ class GSLToken extends FeatureProvider
} }
}); });
} }
public static function register(Application $app): self
{
return new self($app);
}
} }

View File

@ -1,26 +1,21 @@
<?php <?php
namespace App\Extensions\Features; namespace App\Extensions\Features\Schemas;
use App\Extensions\Features\FeatureSchemaInterface;
use App\Facades\Activity; use App\Facades\Activity;
use App\Models\Permission; use App\Models\Permission;
use App\Models\Server; use App\Models\Server;
use App\Repositories\Daemon\DaemonPowerRepository; use App\Repositories\Daemon\DaemonServerRepository;
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;
use Illuminate\Foundation\Application;
class JavaVersion extends FeatureProvider class JavaVersionSchema implements FeatureSchemaInterface
{ {
public function __construct(protected Application $app)
{
parent::__construct($app);
}
/** @return array<string> */ /** @return array<string> */
public function getListeners(): array public function getListeners(): array
{ {
@ -49,9 +44,9 @@ class JavaVersion extends FeatureProvider
->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')
->disabledForm(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_DOCKER_IMAGE, $server)) ->disabledSchema(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_DOCKER_IMAGE, $server))
->form([ ->schema([
Placeholder::make('java') TextEntry::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')
@ -64,7 +59,7 @@ class JavaVersion extends FeatureProvider
->preload() ->preload()
->native(false), ->native(false),
]) ])
->action(function (array $data, DaemonPowerRepository $powerRepository) use ($server) { ->action(function (array $data, DaemonServerRepository $serverRepository) use ($server) {
try { try {
$new = $data['image']; $new = $data['image'];
$original = $server->image; $original = $server->image;
@ -76,7 +71,7 @@ class JavaVersion extends FeatureProvider
->log(); ->log();
} }
$powerRepository->setServer($server)->send('restart'); $serverRepository->setServer($server)->power('restart');
Notification::make() Notification::make()
->title('Docker image updated') ->title('Docker image updated')
@ -92,9 +87,4 @@ class JavaVersion extends FeatureProvider
} }
}); });
} }
public static function register(Application $app): self
{
return new self($app);
}
} }

View File

@ -1,25 +1,20 @@
<?php <?php
namespace App\Extensions\Features; namespace App\Extensions\Features\Schemas;
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\DaemonPowerRepository; use App\Repositories\Daemon\DaemonServerRepository;
use Exception; use Exception;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Facades\Filament; use Filament\Facades\Filament;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString; use Illuminate\Support\HtmlString;
class MinecraftEula extends FeatureProvider class MinecraftEulaSchema implements FeatureSchemaInterface
{ {
public function __construct(protected Application $app)
{
parent::__construct($app);
}
/** @return array<string> */ /** @return array<string> */
public function getListeners(): array public function getListeners(): array
{ {
@ -40,14 +35,14 @@ class MinecraftEula extends FeatureProvider
->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, DaemonPowerRepository $powerRepository) { ->action(function (DaemonFileRepository $fileRepository, DaemonServerRepository $serverRepository) {
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');
$powerRepository->setServer($server)->send('restart'); $serverRepository->setServer($server)->power('restart');
Notification::make() Notification::make()
->title('Minecraft EULA accepted') ->title('Minecraft EULA accepted')
@ -63,9 +58,4 @@ class MinecraftEula extends FeatureProvider
} }
}); });
} }
public static function register(Application $app): self
{
return new self($app);
}
} }

View File

@ -1,19 +1,14 @@
<?php <?php
namespace App\Extensions\Features; namespace App\Extensions\Features\Schemas;
use App\Extensions\Features\FeatureSchemaInterface;
use Filament\Actions\Action; use Filament\Actions\Action;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString; use Illuminate\Support\HtmlString;
class PIDLimit extends FeatureProvider class PIDLimitSchema implements FeatureSchemaInterface
{ {
public function __construct(protected Application $app)
{
parent::__construct($app);
}
/** @return array<string> */ /** @return array<string> */
public function getListeners(): array public function getListeners(): array
{ {
@ -68,9 +63,4 @@ class PIDLimit extends FeatureProvider
->modalCancelActionLabel('Close') ->modalCancelActionLabel('Close')
->action(fn () => null); ->action(fn () => null);
} }
public static function register(Application $app): self
{
return new self($app);
}
} }

View File

@ -1,19 +1,14 @@
<?php <?php
namespace App\Extensions\Features; namespace App\Extensions\Features\Schemas;
use App\Extensions\Features\FeatureSchemaInterface;
use Filament\Actions\Action; use Filament\Actions\Action;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString; use Illuminate\Support\HtmlString;
class SteamDiskSpace extends FeatureProvider class SteamDiskSpaceSchema implements FeatureSchemaInterface
{ {
public function __construct(protected Application $app)
{
parent::__construct($app);
}
/** @return array<string> */ /** @return array<string> */
public function getListeners(): array public function getListeners(): array
{ {
@ -56,9 +51,4 @@ class SteamDiskSpace extends FeatureProvider
->modalCancelActionLabel('Close') ->modalCancelActionLabel('Close')
->action(fn () => null); ->action(fn () => null);
} }
public static function register(Application $app): self
{
return new self($app);
}
} }

View File

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

View File

@ -2,6 +2,7 @@
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;
@ -20,7 +21,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

@ -0,0 +1,39 @@
<?php
namespace App\Extensions\OAuth;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Wizard\Step;
interface OAuthSchemaInterface
{
public function getId(): string;
public function getName(): string;
public function getConfigKey(): string;
/** @return ?class-string */
public function getSocialiteProvider(): ?string;
/**
* @return array<string, string|string[]|bool|null>
*/
public function getServiceConfig(): array;
/** @return Component[] */
public function getSettingsForm(): array;
/** @return Step[] */
public function getSetupSteps(): array;
public function getIcon(): ?string;
public function getHexColor(): ?string;
public function isEnabled(): bool;
public function shouldCreateMissingUsers(): bool;
public function shouldLinkMissingUsers(): bool;
}

View File

@ -0,0 +1,46 @@
<?php
namespace App\Extensions\OAuth;
use Illuminate\Support\Facades\Event;
use SocialiteProviders\Manager\SocialiteWasCalled;
class OAuthService
{
/** @var OAuthSchemaInterface[] */
private array $schemas = [];
/** @return OAuthSchemaInterface[] */
public function getAll(): array
{
return $this->schemas;
}
public function get(string $id): ?OAuthSchemaInterface
{
return array_get($this->schemas, $id);
}
/** @return OAuthSchemaInterface[] */
public function getEnabled(): array
{
return collect($this->schemas)
->filter(fn (OAuthSchemaInterface $schema) => $schema->isEnabled())
->all();
}
public function register(OAuthSchemaInterface $schema): void
{
if (array_key_exists($schema->getId(), $this->schemas)) {
return;
}
config()->set('services.' . $schema->getId(), array_merge($schema->getServiceConfig(), ['redirect' => '/auth/oauth/callback/' . $schema->getId()]));
if ($schema->getSocialiteProvider()) {
Event::listen(fn (SocialiteWasCalled $event) => $event->extendSocialite($schema->getId(), $schema->getSocialiteProvider()));
}
$this->schemas[$schema->getId()] = $schema;
}
}

View File

@ -1,38 +0,0 @@
<?php
namespace App\Extensions\OAuth\Providers;
use Illuminate\Foundation\Application;
final class CommonProvider extends OAuthProvider
{
protected function __construct(protected Application $app, private string $id, private ?string $providerClass, private ?string $icon, private ?string $hexColor)
{
parent::__construct($app);
}
public function getId(): string
{
return $this->id;
}
public function getProviderClass(): ?string
{
return $this->providerClass;
}
public function getIcon(): ?string
{
return $this->icon;
}
public function getHexColor(): ?string
{
return $this->hexColor;
}
public static function register(Application $app, string $id, ?string $providerClass = null, ?string $icon = null, ?string $hexColor = null): static
{
return new self($app, $id, $providerClass, $icon, $hexColor);
}
}

View File

@ -1,64 +0,0 @@
<?php
namespace App\Extensions\OAuth\Providers;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
use SocialiteProviders\Discord\Provider;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
final class DiscordProvider extends OAuthProvider
{
public function __construct(protected Application $app)
{
parent::__construct($app);
}
public function getId(): string
{
return 'discord';
}
public function getProviderClass(): string
{
return Provider::class;
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new Discord OAuth App')
->schema([
Placeholder::make('')
->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>'))),
Placeholder::make('')
->content(new HtmlString('<p>Under <b>Redirects</b> add the below URL.</p>')),
TextInput::make('_noenv_callback')
->label('Redirect URL')
->dehydrated()
->disabled()
->hintAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
->formatStateUsing(fn () => url('/auth/oauth/callback/discord')),
]),
], parent::getSetupSteps());
}
public function getIcon(): string
{
return 'tabler-brand-discord-f';
}
public function getHexColor(): string
{
return '#5865F2';
}
public static function register(Application $app): self
{
return new self($app);
}
}

View File

@ -1,63 +0,0 @@
<?php
namespace App\Extensions\OAuth\Providers;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
final class GithubProvider extends OAuthProvider
{
public function __construct(protected Application $app)
{
parent::__construct($app);
}
public function getId(): string
{
return 'github';
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new Github OAuth App')
->schema([
Placeholder::make('')
->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>'))),
TextInput::make('_noenv_callback')
->label('Authorization callback URL')
->dehydrated()
->disabled()
->hintAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null)
->default(fn () => url('/auth/oauth/callback/github')),
Placeholder::make('')
->content(new HtmlString('<p>When you filled all fields click on <b>Register application</b>.</p>')),
]),
Step::make('Create Client Secret')
->schema([
Placeholder::make('')
->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>')),
]),
], parent::getSetupSteps());
}
public function getIcon(): string
{
return 'tabler-brand-github-f';
}
public function getHexColor(): string
{
return '#4078c0';
}
public static function register(Application $app): self
{
return new self($app);
}
}

View File

@ -1,131 +0,0 @@
<?php
namespace App\Extensions\OAuth\Providers;
use Filament\Forms\Components\Component;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Str;
use SocialiteProviders\Manager\SocialiteWasCalled;
abstract class OAuthProvider
{
/**
* @var array<string, static>
*/
protected static array $providers = [];
/**
* @return self|static[]
*/
public static function get(?string $id = null): array|self
{
return $id ? static::$providers[$id] : static::$providers;
}
protected function __construct(protected Application $app)
{
if (array_key_exists($this->getId(), static::$providers)) {
if (!$this->app->runningUnitTests()) {
logger()->warning("Tried to create duplicate OAuth provider with id '{$this->getId()}'");
}
return;
}
config()->set('services.' . $this->getId(), array_merge($this->getServiceConfig(), ['redirect' => '/auth/oauth/callback/' . $this->getId()]));
if ($this->getProviderClass()) {
Event::listen(function (SocialiteWasCalled $event) {
$event->extendSocialite($this->getId(), $this->getProviderClass());
});
}
static::$providers[$this->getId()] = $this;
}
abstract public function getId(): string;
public function getProviderClass(): ?string
{
return null;
}
/**
* @return array<string, string|string[]|bool|null>
*/
public function getServiceConfig(): array
{
$id = Str::upper($this->getId());
return [
'client_id' => env("OAUTH_{$id}_CLIENT_ID"),
'client_secret' => env("OAUTH_{$id}_CLIENT_SECRET"),
];
}
/**
* @return Component[]
*/
public function getSettingsForm(): array
{
$id = Str::upper($this->getId());
return [
TextInput::make("OAUTH_{$id}_CLIENT_ID")
->label('Client ID')
->placeholder('Client ID')
->columnSpan(2)
->required()
->password()
->revealable()
->autocomplete(false)
->default(env("OAUTH_{$id}_CLIENT_ID")),
TextInput::make("OAUTH_{$id}_CLIENT_SECRET")
->label('Client Secret')
->placeholder('Client Secret')
->columnSpan(2)
->required()
->password()
->revealable()
->autocomplete(false)
->default(env("OAUTH_{$id}_CLIENT_SECRET")),
];
}
/**
* @return Step[]
*/
public function getSetupSteps(): array
{
return [
Step::make('OAuth Config')
->columns(4)
->schema($this->getSettingsForm()),
];
}
public function getName(): string
{
return Str::title($this->getId());
}
public function getIcon(): ?string
{
return null;
}
public function getHexColor(): ?string
{
return null;
}
public function isEnabled(): bool
{
$id = Str::upper($this->getId());
return env("OAUTH_{$id}_ENABLED", false);
}
}

View File

@ -1,25 +1,19 @@
<?php <?php
namespace App\Extensions\OAuth\Providers; 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 Illuminate\Foundation\Application;
use SocialiteProviders\Authentik\Provider; use SocialiteProviders\Authentik\Provider;
final class AuthentikProvider extends OAuthProvider final class AuthentikSchema extends OAuthSchema
{ {
public function __construct(protected Application $app)
{
parent::__construct($app);
}
public function getId(): string public function getId(): string
{ {
return 'authentik'; return 'authentik';
} }
public function getProviderClass(): string public function getSocialiteProvider(): string
{ {
return Provider::class; return Provider::class;
} }
@ -66,9 +60,4 @@ final class AuthentikProvider extends OAuthProvider
{ {
return env('OAUTH_AUTHENTIK_DISPLAY_COLOR', '#fd4b2d'); return env('OAUTH_AUTHENTIK_DISPLAY_COLOR', '#fd4b2d');
} }
public static function register(Application $app): self
{
return new self($app);
}
} }

View File

@ -0,0 +1,39 @@
<?php
namespace App\Extensions\OAuth\Schemas;
final class CommonSchema extends OAuthSchema
{
public function __construct(
private readonly string $id,
private readonly ?string $name = null,
private readonly ?string $configName = null,
private readonly ?string $icon = null,
private readonly ?string $hexColor = null,
) {}
public function getId(): string
{
return $this->id;
}
public function getName(): string
{
return $this->name ?? parent::getName();
}
public function getConfigKey(): string
{
return $this->configName ?? parent::getConfigKey();
}
public function getIcon(): ?string
{
return $this->icon;
}
public function getHexColor(): ?string
{
return $this->hexColor;
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace App\Extensions\OAuth\Schemas;
use Filament\Schemas\Components\Wizard\Step;
use Filament\Infolists\Components\TextEntry;
use Filament\Forms\Components\TextInput;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
use SocialiteProviders\Discord\Provider;
final class DiscordSchema extends OAuthSchema
{
public function getId(): string
{
return 'discord';
}
public function getSocialiteProvider(): string
{
return Provider::class;
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new Discord OAuth App')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->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>'))),
TextEntry::make('set_redirect')
->hiddenLabel()
->state(new HtmlString('<p>Under <b>Redirects</b> add the below URL.</p>')),
TextInput::make('_noenv_callback')
->label('Redirect URL')
->dehydrated()
->disabled()
->hintCopy()
->formatStateUsing(fn () => url('/auth/oauth/callback/discord')),
]),
], parent::getSetupSteps());
}
public function getIcon(): string
{
return 'tabler-brand-discord-f';
}
public function getHexColor(): string
{
return '#5865F2';
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace App\Extensions\OAuth\Schemas;
use Filament\Schemas\Components\Wizard\Step;
use Filament\Infolists\Components\TextEntry;
use Filament\Forms\Components\TextInput;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
final class GithubSchema extends OAuthSchema
{
public function getId(): string
{
return 'github';
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new Github OAuth App')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->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')
->label('Authorization callback URL')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('/auth/oauth/callback/github')),
TextEntry::make('register_application')
->hiddenLabel()
->state(new HtmlString('<p>When you filled all fields click on <b>Register application</b>.</p>')),
]),
Step::make('Create Client Secret')
->schema([
TextEntry::make('create_client_secret')
->hiddenLabel()
->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());
}
public function getIcon(): string
{
return 'tabler-brand-github-f';
}
public function getHexColor(): string
{
return '#4078c0';
}
}

View File

@ -1,22 +1,15 @@
<?php <?php
namespace App\Extensions\OAuth\Providers; namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\Placeholder; use Filament\Schemas\Components\Wizard\Step;
use Filament\Infolists\Components\TextEntry;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step;
use Illuminate\Foundation\Application;
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 GitlabProvider extends OAuthProvider final class GitlabSchema extends OAuthSchema
{ {
public function __construct(protected Application $app)
{
parent::__construct($app);
}
public function getId(): string public function getId(): string
{ {
return 'gitlab'; return 'gitlab';
@ -47,13 +40,14 @@ final class GitlabProvider extends OAuthProvider
return array_merge([ return array_merge([
Step::make('Register new Gitlab OAuth App') Step::make('Register new Gitlab OAuth App')
->schema([ ->schema([
Placeholder::make('') TextEntry::make('register_application')
->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.'))), ->hiddenLabel()
->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()
->hintAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null) ->hintCopy()
->default(fn () => url('/auth/oauth/callback/gitlab')), ->default(fn () => url('/auth/oauth/callback/gitlab')),
]), ]),
], parent::getSetupSteps()); ], parent::getSetupSteps());
@ -68,9 +62,4 @@ final class GitlabProvider extends OAuthProvider
{ {
return '#fca326'; return '#fca326';
} }
public static function register(Application $app): self
{
return new self($app);
}
} }

View File

@ -0,0 +1,137 @@
<?php
namespace App\Extensions\OAuth\Schemas;
use App\Extensions\OAuth\OAuthSchemaInterface;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Components\Wizard\Step;
use Filament\Schemas\Components\Component;
use Illuminate\Support\Str;
abstract class OAuthSchema implements OAuthSchemaInterface
{
abstract public function getId(): string;
public function getSocialiteProvider(): ?string
{
return null;
}
public function getServiceConfig(): array
{
$id = Str::upper($this->getId());
return [
'client_id' => env("OAUTH_{$id}_CLIENT_ID"),
'client_secret' => env("OAUTH_{$id}_CLIENT_SECRET"),
];
}
/**
* @return Component[]
*/
public function getSettingsForm(): array
{
$id = Str::upper($this->getId());
return [
TextInput::make("OAUTH_{$id}_CLIENT_ID")
->label('Client ID')
->placeholder('Client ID')
->columnSpan(2)
->required()
->password()
->revealable()
->autocomplete(false)
->default(env("OAUTH_{$id}_CLIENT_ID")),
TextInput::make("OAUTH_{$id}_CLIENT_SECRET")
->label('Client Secret')
->placeholder('Client Secret')
->columnSpan(2)
->required()
->password()
->revealable()
->autocomplete(false)
->default(env("OAUTH_{$id}_CLIENT_SECRET")),
Toggle::make("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS")
->label(trans('admin/setting.oauth.create_missing_users'))
->columnSpan(2)
->inline(false)
->onIcon('tabler-check')
->offIcon('tabler-x')
->onColor('success')
->offColor('danger')
->formatStateUsing(fn ($state) => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS", (bool) $state))
->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('tabler-check')
->offIcon('tabler-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")),
];
}
/**
* @return Step[]
*/
public function getSetupSteps(): array
{
return [
Step::make('OAuth Config')
->columns(4)
->schema($this->getSettingsForm()),
];
}
public function getName(): string
{
return Str::title($this->getId());
}
public function getConfigKey(): string
{
$id = Str::upper($this->getId());
return "OAUTH_{$id}_ENABLED";
}
public function getIcon(): ?string
{
return null;
}
public function getHexColor(): ?string
{
return null;
}
public function isEnabled(): bool
{
$id = Str::upper($this->getId());
return env("OAUTH_{$id}_ENABLED", false);
}
public function shouldCreateMissingUsers(): bool
{
$id = Str::upper($this->getId());
return env("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS", false);
}
public function shouldLinkMissingUsers(): bool
{
$id = Str::upper($this->getId());
return env("OAUTH_{$id}_SHOULD_LINK_MISSING_USERS", false);
}
}

View File

@ -1,28 +1,22 @@
<?php <?php
namespace App\Extensions\OAuth\Providers; namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\Placeholder; use Filament\Schemas\Components\Wizard\Step;
use Filament\Infolists\Components\TextEntry;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step;
use Illuminate\Foundation\Application;
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;
final class SteamProvider extends OAuthProvider final class SteamSchema extends OAuthSchema
{ {
public function __construct(protected Application $app)
{
parent::__construct($app);
}
public function getId(): string public function getId(): string
{ {
return 'steam'; return 'steam';
} }
public function getProviderClass(): string public function getSocialiteProvider(): string
{ {
return Provider::class; return Provider::class;
} }
@ -58,8 +52,9 @@ final class SteamProvider extends OAuthProvider
return array_merge([ return array_merge([
Step::make('Create API Key') Step::make('Create API Key')
->schema([ ->schema([
Placeholder::make('') TextEntry::make('create_api_key')
->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.'))), ->hiddenLabel()
->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());
} }
@ -73,9 +68,4 @@ final class SteamProvider extends OAuthProvider
{ {
return '#00adee'; return '#00adee';
} }
public static function register(Application $app): self
{
return new self($app);
}
} }

View File

@ -2,6 +2,8 @@
namespace App\Extensions\Spatie\Fractalistic; namespace App\Extensions\Spatie\Fractalistic;
use Spatie\Fractalistic\Exceptions\InvalidTransformation;
use Spatie\Fractalistic\Exceptions\NoTransformerSpecified;
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;
@ -14,8 +16,8 @@ class Fractal extends SpatieFractal
/** /**
* Create fractal data. * Create fractal data.
* *
* @throws \Spatie\Fractalistic\Exceptions\InvalidTransformation * @throws InvalidTransformation
* @throws \Spatie\Fractalistic\Exceptions\NoTransformerSpecified * @throws NoTransformerSpecified
*/ */
public function createData(): Scope public function createData(): Scope
{ {

View File

@ -7,7 +7,7 @@ use Filament\Pages\Dashboard as BaseDashboard;
class Dashboard extends BaseDashboard class Dashboard extends BaseDashboard
{ {
protected static ?string $navigationIcon = 'tabler-layout-dashboard'; protected static string|\BackedEnum|null $navigationIcon = 'tabler-layout-dashboard';
private SoftwareVersionService $softwareVersionService; private SoftwareVersionService $softwareVersionService;
@ -16,7 +16,7 @@ class Dashboard extends BaseDashboard
$this->softwareVersionService = $softwareVersionService; $this->softwareVersionService = $softwareVersionService;
} }
public function getColumns(): int public function getColumns(): int|array
{ {
return 1; return 1;
} }

View File

@ -13,9 +13,9 @@ use Spatie\Health\ResultStores\ResultStore;
class Health extends Page class Health extends Page
{ {
protected static ?string $navigationIcon = 'tabler-heart'; protected static string|\BackedEnum|null $navigationIcon = 'tabler-heart';
protected static string $view = 'filament.pages.health'; protected string $view = 'filament.pages.health';
/** @var array<string, string> */ /** @var array<string, string> */
protected $listeners = [ protected $listeners = [
@ -123,7 +123,7 @@ class Health extends Page
return $carry; return $carry;
}, []); }, []);
return trans('admin/health.checks.failed') . implode(', ', $failedNames); return trans('admin/health.checks.failed', ['checks' => implode(', ', $failedNames)]);
} }
public static function getNavigationIcon(): string public static function getNavigationIcon(): string

View File

@ -2,50 +2,51 @@
namespace App\Filament\Admin\Pages; namespace App\Filament\Admin\Pages;
use App\Extensions\Avatar\AvatarProvider; use App\Extensions\Avatar\AvatarService;
use App\Extensions\Captcha\Providers\CaptchaProvider; use App\Extensions\Captcha\CaptchaService;
use App\Extensions\OAuth\Providers\OAuthProvider; use App\Extensions\OAuth\OAuthService;
use App\Models\Backup; use App\Models\Backup;
use App\Notifications\MailTested; use App\Notifications\MailTested;
use App\Traits\EnvironmentWriterTrait; use App\Traits\EnvironmentWriterTrait;
use App\Traits\Filament\CanCustomizeHeaderActions; use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets; use App\Traits\Filament\CanCustomizeHeaderWidgets;
use BackedEnum;
use Exception; use Exception;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Actions\ActionGroup; use Filament\Actions\ActionGroup;
use Filament\Forms\Components\Actions;
use Filament\Forms\Components\Actions\Action as FormAction;
use Filament\Forms\Components\Component;
use Filament\Forms\Components\FileUpload; use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Group;
use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\TagsInput; use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle; use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\ToggleButtons; use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Concerns\InteractsWithForms; use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Pages\Concerns\InteractsWithHeaderActions; use Filament\Pages\Concerns\InteractsWithHeaderActions;
use Filament\Pages\Page; use Filament\Pages\Page;
use Filament\Support\Enums\MaxWidth; use Filament\Schemas\Components\Actions;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Group;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\StateCasts\BooleanStateCast;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Support\Enums\Width;
use Illuminate\Http\Client\Factory; use Illuminate\Http\Client\Factory;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Notification as MailNotification; use Illuminate\Support\Facades\Notification as MailNotification;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Filament\Schemas\Contracts\HasSchemas;
use Filament\Schemas\Schema;
/** /**
* @property Form $form * @property Schema $form
*/ */
class Settings extends Page implements HasForms class Settings extends Page implements HasSchemas
{ {
use CanCustomizeHeaderActions, InteractsWithHeaderActions { use CanCustomizeHeaderActions, InteractsWithHeaderActions {
CanCustomizeHeaderActions::getHeaderActions insteadof InteractsWithHeaderActions; CanCustomizeHeaderActions::getHeaderActions insteadof InteractsWithHeaderActions;
@ -54,9 +55,15 @@ class Settings extends Page implements HasForms
use EnvironmentWriterTrait; use EnvironmentWriterTrait;
use InteractsWithForms; use InteractsWithForms;
protected static ?string $navigationIcon = 'tabler-settings'; protected static string|\BackedEnum|null $navigationIcon = 'tabler-settings';
protected static string $view = 'filament.pages.settings'; protected string $view = 'filament.pages.settings';
protected OAuthService $oauthService;
protected AvatarService $avatarService;
protected CaptchaService $captchaService;
/** @var array<mixed>|null */ /** @var array<mixed>|null */
public ?array $data = []; public ?array $data = [];
@ -66,6 +73,13 @@ class Settings extends Page implements HasForms
$this->form->fill(); $this->form->fill();
} }
public function boot(OAuthService $oauthService, AvatarService $avatarService, CaptchaService $captchaService): void
{
$this->oauthService = $oauthService;
$this->avatarService = $avatarService;
$this->captchaService = $captchaService;
}
public static function canAccess(): bool public static function canAccess(): bool
{ {
return auth()->user()->can('view settings'); return auth()->user()->can('view settings');
@ -81,6 +95,11 @@ class Settings extends Page implements HasForms
return trans('admin/setting.title'); return trans('admin/setting.title');
} }
/**
* @return array<Component>
*
* @throws Exception
*/
protected function getFormSchema(): array protected function getFormSchema(): array
{ {
return [ return [
@ -97,7 +116,7 @@ class Settings extends Page implements HasForms
->label(trans('admin/setting.navigation.captcha')) ->label(trans('admin/setting.navigation.captcha'))
->icon('tabler-shield') ->icon('tabler-shield')
->schema($this->captchaSettings()) ->schema($this->captchaSettings())
->columns(3), ->columns(1),
Tab::make('mail') Tab::make('mail')
->label(trans('admin/setting.navigation.mail')) ->label(trans('admin/setting.navigation.mail'))
->icon('tabler-mail') ->icon('tabler-mail')
@ -106,10 +125,11 @@ class Settings extends Page implements HasForms
->label(trans('admin/setting.navigation.backup')) ->label(trans('admin/setting.navigation.backup'))
->icon('tabler-box') ->icon('tabler-box')
->schema($this->backupSettings()), ->schema($this->backupSettings()),
Tab::make('OAuth') Tab::make('oauth')
->label(trans('admin/setting.navigation.oauth')) ->label(trans('admin/setting.navigation.oauth'))
->icon('tabler-brand-oauth') ->icon('tabler-brand-oauth')
->schema($this->oauthSettings()), ->schema($this->oauthSettings())
->columns(1),
Tab::make('misc') Tab::make('misc')
->label(trans('admin/setting.navigation.misc')) ->label(trans('admin/setting.navigation.misc'))
->icon('tabler-tool') ->icon('tabler-tool')
@ -118,7 +138,9 @@ class Settings extends Page implements HasForms
]; ];
} }
/** @return Component[] */ /** @return Component[]
* @throws Exception
*/
private function generalSettings(): array private function generalSettings(): array
{ {
return [ return [
@ -131,14 +153,12 @@ class Settings extends Page implements HasForms
->schema([ ->schema([
TextInput::make('APP_LOGO') TextInput::make('APP_LOGO')
->label(trans('admin/setting.general.app_logo')) ->label(trans('admin/setting.general.app_logo'))
->hintIcon('tabler-question-mark') ->hintIcon('tabler-question-mark', trans('admin/setting.general.app_logo_help'))
->hintIconTooltip(trans('admin/setting.general.app_logo_help'))
->default(env('APP_LOGO')) ->default(env('APP_LOGO'))
->placeholder('/pelican.svg'), ->placeholder('/pelican.svg'),
TextInput::make('APP_FAVICON') TextInput::make('APP_FAVICON')
->label(trans('admin/setting.general.app_favicon')) ->label(trans('admin/setting.general.app_favicon'))
->hintIcon('tabler-question-mark') ->hintIcon('tabler-question-mark', trans('admin/setting.general.app_favicon_help'))
->hintIconTooltip(trans('admin/setting.general.app_favicon_help'))
->required() ->required()
->default(env('APP_FAVICON', '/pelican.ico')) ->default(env('APP_FAVICON', '/pelican.ico'))
->placeholder('/pelican.ico'), ->placeholder('/pelican.ico'),
@ -153,19 +173,8 @@ class Settings extends Page implements HasForms
->offIcon('tabler-x') ->offIcon('tabler-x')
->onColor('success') ->onColor('success')
->offColor('danger') ->offColor('danger')
->formatStateUsing(fn ($state): bool => (bool) $state) ->stateCast(new BooleanStateCast(false))
->afterStateUpdated(fn ($state, Set $set) => $set('APP_DEBUG', (bool) $state))
->default(env('APP_DEBUG', config('app.debug'))), ->default(env('APP_DEBUG', config('app.debug'))),
ToggleButtons::make('FILAMENT_TOP_NAVIGATION')
->label(trans('admin/setting.general.navigation'))
->inline()
->options([
false => trans('admin/setting.general.sidebar'),
true => trans('admin/setting.general.topbar'),
])
->formatStateUsing(fn ($state): bool => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set('FILAMENT_TOP_NAVIGATION', (bool) $state))
->default(env('FILAMENT_TOP_NAVIGATION', config('panel.filament.top-navigation'))),
]), ]),
Group::make() Group::make()
->columns(2) ->columns(2)
@ -173,7 +182,7 @@ class Settings extends Page implements HasForms
Select::make('FILAMENT_AVATAR_PROVIDER') Select::make('FILAMENT_AVATAR_PROVIDER')
->label(trans('admin/setting.general.avatar_provider')) ->label(trans('admin/setting.general.avatar_provider'))
->native(false) ->native(false)
->options(collect(AvatarProvider::getAll())->mapWithKeys(fn ($provider) => [$provider->getId() => $provider->getName()])) ->options($this->avatarService->getMappings())
->selectablePlaceholder(false) ->selectablePlaceholder(false)
->default(env('FILAMENT_AVATAR_PROVIDER', config('panel.filament.avatar-provider'))), ->default(env('FILAMENT_AVATAR_PROVIDER', config('panel.filament.avatar-provider'))),
Toggle::make('FILAMENT_UPLOADABLE_AVATARS') Toggle::make('FILAMENT_UPLOADABLE_AVATARS')
@ -183,19 +192,17 @@ class Settings extends Page implements HasForms
->offIcon('tabler-x') ->offIcon('tabler-x')
->onColor('success') ->onColor('success')
->offColor('danger') ->offColor('danger')
->formatStateUsing(fn ($state) => (bool) $state) ->stateCast(new BooleanStateCast(false))
->afterStateUpdated(fn ($state, Set $set) => $set('FILAMENT_UPLOADABLE_AVATARS', (bool) $state))
->default(env('FILAMENT_UPLOADABLE_AVATARS', config('panel.filament.uploadable-avatars'))), ->default(env('FILAMENT_UPLOADABLE_AVATARS', config('panel.filament.uploadable-avatars'))),
]), ]),
ToggleButtons::make('PANEL_USE_BINARY_PREFIX') ToggleButtons::make('PANEL_USE_BINARY_PREFIX')
->label(trans('admin/setting.general.unit_prefix')) ->label(trans('admin/setting.general.unit_prefix'))
->inline() ->inline()
->options([ ->options([
false => trans('admin/setting.general.decimal_prefix'), 0 => trans('admin/setting.general.decimal_prefix'),
true => trans('admin/setting.general.binary_prefix'), 1 => trans('admin/setting.general.binary_prefix'),
]) ])
->formatStateUsing(fn ($state): bool => (bool) $state) ->stateCast(new BooleanStateCast(false, true))
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_USE_BINARY_PREFIX', (bool) $state))
->default(env('PANEL_USE_BINARY_PREFIX', config('panel.use_binary_prefix'))), ->default(env('PANEL_USE_BINARY_PREFIX', config('panel.use_binary_prefix'))),
ToggleButtons::make('APP_2FA_REQUIRED') ToggleButtons::make('APP_2FA_REQUIRED')
->label(trans('admin/setting.general.2fa_requirement')) ->label(trans('admin/setting.general.2fa_requirement'))
@ -211,7 +218,7 @@ class Settings extends Page implements HasForms
Select::make('FILAMENT_WIDTH') Select::make('FILAMENT_WIDTH')
->label(trans('admin/setting.general.display_width')) ->label(trans('admin/setting.general.display_width'))
->native(false) ->native(false)
->options(MaxWidth::class) ->options(Width::class)
->selectablePlaceholder(false) ->selectablePlaceholder(false)
->default(env('FILAMENT_WIDTH', config('panel.filament.display-width'))), ->default(env('FILAMENT_WIDTH', config('panel.filament.display-width'))),
TagsInput::make('TRUSTED_PROXIES') TagsInput::make('TRUSTED_PROXIES')
@ -221,14 +228,14 @@ class Settings extends Page implements HasForms
->placeholder(trans('admin/setting.general.trusted_proxies_help')) ->placeholder(trans('admin/setting.general.trusted_proxies_help'))
->default(env('TRUSTED_PROXIES', implode(',', Arr::wrap(config('trustedproxy.proxies'))))) ->default(env('TRUSTED_PROXIES', implode(',', Arr::wrap(config('trustedproxy.proxies')))))
->hintActions([ ->hintActions([
FormAction::make('clear') Action::make('clear')
->label(trans('admin/setting.general.clear')) ->label(trans('admin/setting.general.clear'))
->color('danger') ->color('danger')
->icon('tabler-trash') ->icon('tabler-trash')
->requiresConfirmation() ->requiresConfirmation()
->authorize(fn () => auth()->user()->can('update settings')) ->authorize(fn () => auth()->user()->can('update settings'))
->action(fn (Set $set) => $set('TRUSTED_PROXIES', [])), ->action(fn (Set $set) => $set('TRUSTED_PROXIES', [])),
FormAction::make('cloudflare') Action::make('cloudflare')
->label(trans('admin/setting.general.set_to_cf')) ->label(trans('admin/setting.general.set_to_cf'))
->icon('tabler-brand-cloudflare') ->icon('tabler-brand-cloudflare')
->authorize(fn () => auth()->user()->can('update settings')) ->authorize(fn () => auth()->user()->can('update settings'))
@ -259,45 +266,39 @@ class Settings extends Page implements HasForms
/** /**
* @return Component[] * @return Component[]
*
* @throws Exception
*/ */
private function captchaSettings(): array private function captchaSettings(): array
{ {
$formFields = []; $formFields = [];
$captchaProviders = CaptchaProvider::get(); $captchaSchemas = $this->captchaService->getAll();
foreach ($captchaProviders as $captchaProvider) { foreach ($captchaSchemas as $schema) {
$id = Str::upper($captchaProvider->getId()); $id = Str::upper($schema->getId());
$name = Str::title($captchaProvider->getId());
$formFields[] = Section::make($name) $formFields[] = Section::make($schema->getName())
->columns(5) ->columns(5)
->icon($captchaProvider->getIcon() ?? 'tabler-shield') ->icon($schema->getIcon() ?? 'tabler-shield')
->collapsed(fn () => !env("CAPTCHA_{$id}_ENABLED", false)) ->collapsed(fn () => !$schema->isEnabled())
->collapsible() ->collapsible()
->schema([ ->schema([
Hidden::make("CAPTCHA_{$id}_ENABLED") Hidden::make("CAPTCHA_{$id}_ENABLED")
->live() ->live()
->default(env("CAPTCHA_{$id}_ENABLED")), ->default(env("CAPTCHA_{$id}_ENABLED")),
Actions::make([ Actions::make([
FormAction::make("disable_captcha_$id") Action::make("disable_captcha_$id")
->visible(fn (Get $get) => $get("CAPTCHA_{$id}_ENABLED")) ->visible(fn (Get $get) => $get("CAPTCHA_{$id}_ENABLED"))
->label(trans('admin/setting.captcha.disable')) ->label(trans('admin/setting.captcha.disable'))
->color('danger') ->color('danger')
->action(function (Set $set) use ($id) { ->action(fn (Set $set) => $set("CAPTCHA_{$id}_ENABLED", false)),
$set("CAPTCHA_{$id}_ENABLED", false); Action::make("enable_captcha_$id")
}),
FormAction::make("enable_captcha_$id")
->visible(fn (Get $get) => !$get("CAPTCHA_{$id}_ENABLED")) ->visible(fn (Get $get) => !$get("CAPTCHA_{$id}_ENABLED"))
->label(trans('admin/setting.captcha.enable')) ->label(trans('admin/setting.captcha.enable'))
->color('success') ->color('success')
->action(function (Set $set) use ($id, $captchaProviders) { ->action(fn (Set $set) => $set("CAPTCHA_{$id}_ENABLED", true)),
foreach ($captchaProviders as $captchaProvider) {
$loopId = Str::upper($captchaProvider->getId());
$set("CAPTCHA_{$loopId}_ENABLED", $loopId === $id);
}
}),
])->columnSpan(1), ])->columnSpan(1),
Group::make($captchaProvider->getSettingsForm()) Group::make($schema->getSettingsForm())
->visible(fn (Get $get) => $get("CAPTCHA_{$id}_ENABLED")) ->visible(fn (Get $get) => $get("CAPTCHA_{$id}_ENABLED"))
->columns(4) ->columns(4)
->columnSpan(4), ->columnSpan(4),
@ -309,6 +310,8 @@ class Settings extends Page implements HasForms
/** /**
* @return Component[] * @return Component[]
*
* @throws Exception
*/ */
private function mailSettings(): array private function mailSettings(): array
{ {
@ -328,7 +331,7 @@ class Settings extends Page implements HasForms
->live() ->live()
->default(env('MAIL_MAILER', config('mail.default'))) ->default(env('MAIL_MAILER', config('mail.default')))
->hintAction( ->hintAction(
FormAction::make('test') Action::make('test')
->label(trans('admin/setting.mail.test_mail')) ->label(trans('admin/setting.mail.test_mail'))
->icon('tabler-send') ->icon('tabler-send')
->hidden(fn (Get $get) => $get('MAIL_MAILER') === 'log') ->hidden(fn (Get $get) => $get('MAIL_MAILER') === 'log')
@ -386,6 +389,7 @@ class Settings extends Page implements HasForms
Section::make(trans('admin/setting.mail.from_settings')) Section::make(trans('admin/setting.mail.from_settings'))
->description(trans('admin/setting.mail.from_settings_help')) ->description(trans('admin/setting.mail.from_settings_help'))
->columns() ->columns()
->columnSpanFull()
->schema([ ->schema([
TextInput::make('MAIL_FROM_ADDRESS') TextInput::make('MAIL_FROM_ADDRESS')
->label(trans('admin/setting.mail.from_address')) ->label(trans('admin/setting.mail.from_address'))
@ -399,6 +403,7 @@ class Settings extends Page implements HasForms
]), ]),
Section::make(trans('admin/setting.mail.smtp.smtp_title')) Section::make(trans('admin/setting.mail.smtp.smtp_title'))
->columns() ->columns()
->columnSpanFull()
->visible(fn (Get $get) => $get('MAIL_MAILER') === 'smtp') ->visible(fn (Get $get) => $get('MAIL_MAILER') === 'smtp')
->schema([ ->schema([
TextInput::make('MAIL_HOST') TextInput::make('MAIL_HOST')
@ -435,6 +440,7 @@ class Settings extends Page implements HasForms
]), ]),
Section::make(trans('admin/setting.mail.mailgun.mailgun_title')) Section::make(trans('admin/setting.mail.mailgun.mailgun_title'))
->columns() ->columns()
->columnSpanFull()
->visible(fn (Get $get) => $get('MAIL_MAILER') === 'mailgun') ->visible(fn (Get $get) => $get('MAIL_MAILER') === 'mailgun')
->schema([ ->schema([
TextInput::make('MAILGUN_DOMAIN') TextInput::make('MAILGUN_DOMAIN')
@ -455,6 +461,8 @@ class Settings extends Page implements HasForms
/** /**
* @return Component[] * @return Component[]
*
* @throws Exception
*/ */
private function backupSettings(): array private function backupSettings(): array
{ {
@ -472,6 +480,7 @@ class Settings extends Page implements HasForms
Section::make(trans('admin/setting.backup.throttle')) Section::make(trans('admin/setting.backup.throttle'))
->description(trans('admin/setting.backup.throttle_help')) ->description(trans('admin/setting.backup.throttle_help'))
->columns() ->columns()
->columnSpanFull()
->schema([ ->schema([
TextInput::make('BACKUP_THROTTLE_LIMIT') TextInput::make('BACKUP_THROTTLE_LIMIT')
->label(trans('admin/setting.backup.limit')) ->label(trans('admin/setting.backup.limit'))
@ -519,8 +528,7 @@ class Settings extends Page implements HasForms
->onColor('success') ->onColor('success')
->offColor('danger') ->offColor('danger')
->live() ->live()
->formatStateUsing(fn ($state): bool => (bool) $state) ->stateCast(new BooleanStateCast(false))
->afterStateUpdated(fn ($state, Set $set) => $set('AWS_USE_PATH_STYLE_ENDPOINT', (bool) $state))
->default(env('AWS_USE_PATH_STYLE_ENDPOINT', config('backups.disks.s3.use_path_style_endpoint'))), ->default(env('AWS_USE_PATH_STYLE_ENDPOINT', config('backups.disks.s3.use_path_style_endpoint'))),
]), ]),
]; ];
@ -528,44 +536,44 @@ class Settings extends Page implements HasForms
/** /**
* @return Component[] * @return Component[]
*
* @throws Exception
*/ */
private function oauthSettings(): array private function oauthSettings(): array
{ {
$formFields = []; $formFields = [];
$oauthProviders = OAuthProvider::get(); $oauthSchemas = $this->oauthService->getAll();
foreach ($oauthProviders as $oauthProvider) { foreach ($oauthSchemas as $schema) {
$id = Str::upper($oauthProvider->getId()); $id = Str::upper($schema->getId());
$name = Str::title($oauthProvider->getId()); $key = $schema->getConfigKey();
$formFields[] = Section::make($name) $formFields[] = Section::make($schema->getName())
->columns(5) ->columns(5)
->icon($oauthProvider->getIcon() ?? 'tabler-brand-oauth') ->icon($schema->getIcon() ?? 'tabler-brand-oauth')
->collapsed(fn () => !env("OAUTH_{$id}_ENABLED", false)) ->collapsed(fn () => !env($key, false))
->collapsible() ->collapsible()
->schema([ ->schema([
Hidden::make("OAUTH_{$id}_ENABLED") Hidden::make($key)
->live() ->live()
->default(env("OAUTH_{$id}_ENABLED")), ->default(env($key)),
Actions::make([ Actions::make([
FormAction::make("disable_oauth_$id") Action::make("disable_oauth_$id")
->visible(fn (Get $get) => $get("OAUTH_{$id}_ENABLED")) ->visible(fn (Get $get) => $get($key))
->label(trans('admin/setting.oauth.disable')) ->label(trans('admin/setting.oauth.disable'))
->color('danger') ->color('danger')
->action(function (Set $set) use ($id) { ->action(fn (Set $set) => $set($key, false)),
$set("OAUTH_{$id}_ENABLED", false); Action::make("enable_oauth_$id")
}), ->visible(fn (Get $get) => !$get($key))
FormAction::make("enable_oauth_$id")
->visible(fn (Get $get) => !$get("OAUTH_{$id}_ENABLED"))
->label(trans('admin/setting.oauth.enable')) ->label(trans('admin/setting.oauth.enable'))
->color('success') ->color('success')
->steps($oauthProvider->getSetupSteps()) ->steps($schema->getSetupSteps())
->modalHeading(trans('admin/setting.oauth.enable') . ' ' . $name) ->modalHeading(trans('admin/setting.oauth.enable_schema', ['schema' => $schema->getName()]))
->modalSubmitActionLabel(trans('admin/setting.oauth.enable')) ->modalSubmitActionLabel(trans('admin/setting.oauth.enable'))
->modalCancelAction(false) ->modalCancelAction(false)
->action(function ($data, Set $set) use ($id) { ->action(function ($data, Set $set) use ($key) {
$data = array_merge([ $data = array_merge([
"OAUTH_{$id}_ENABLED" => 'true', $key => 'true',
], $data); ], $data);
foreach ($data as $key => $value) { foreach ($data as $key => $value) {
@ -573,8 +581,8 @@ class Settings extends Page implements HasForms
} }
}), }),
])->columnSpan(1), ])->columnSpan(1),
Group::make($oauthProvider->getSettingsForm()) Group::make($schema->getSettingsForm())
->visible(fn (Get $get) => $get("OAUTH_{$id}_ENABLED")) ->visible(fn (Get $get) => $get($key))
->columns(4) ->columns(4)
->columnSpan(4), ->columnSpan(4),
]); ]);
@ -585,6 +593,8 @@ class Settings extends Page implements HasForms
/** /**
* @return Component[] * @return Component[]
*
* @throws Exception
*/ */
private function miscSettings(): array private function miscSettings(): array
{ {
@ -603,8 +613,7 @@ class Settings extends Page implements HasForms
->offColor('danger') ->offColor('danger')
->live() ->live()
->columnSpanFull() ->columnSpanFull()
->formatStateUsing(fn ($state): bool => (bool) $state) ->stateCast(new BooleanStateCast(false))
->afterStateUpdated(fn ($state, Set $set) => $set('PANEL_CLIENT_ALLOCATIONS_ENABLED', (bool) $state))
->default(env('PANEL_CLIENT_ALLOCATIONS_ENABLED', config('panel.client_features.allocations.enabled'))), ->default(env('PANEL_CLIENT_ALLOCATIONS_ENABLED', config('panel.client_features.allocations.enabled'))),
TextInput::make('PANEL_CLIENT_ALLOCATIONS_RANGE_START') TextInput::make('PANEL_CLIENT_ALLOCATIONS_RANGE_START')
->label(trans('admin/setting.misc.auto_allocation.start')) ->label(trans('admin/setting.misc.auto_allocation.start'))
@ -695,8 +704,7 @@ class Settings extends Page implements HasForms
->onColor('success') ->onColor('success')
->offColor('danger') ->offColor('danger')
->live() ->live()
->formatStateUsing(fn ($state): bool => (bool) $state) ->stateCast(new BooleanStateCast(false))
->afterStateUpdated(fn ($state, Set $set) => $set('APP_ACTIVITY_HIDE_ADMIN', (bool) $state))
->default(env('APP_ACTIVITY_HIDE_ADMIN', config('activity.hide_admin_activity'))), ->default(env('APP_ACTIVITY_HIDE_ADMIN', config('activity.hide_admin_activity'))),
]), ]),
Section::make(trans('admin/setting.misc.api.title')) Section::make(trans('admin/setting.misc.api.title'))
@ -774,8 +782,19 @@ class Settings extends Page implements HasForms
$data = $this->form->getState(); $data = $this->form->getState();
unset($data['ConsoleFonts']); unset($data['ConsoleFonts']);
// Convert bools to a string, so they are correctly written to the .env file $data = array_map(function ($value) {
$data = array_map(fn ($value) => is_bool($value) ? ($value ? 'true' : 'false') : $value, $data); // Convert bools to a string, so they are correctly written to the .env file
if (is_bool($value)) {
return $value ? 'true' : 'false';
}
// Convert enum to its value
if ($value instanceof BackedEnum) {
return $value->value;
}
return $value;
}, $data);
$this->writeToEnvironment($data); $this->writeToEnvironment($data);

View File

@ -1,24 +1,26 @@
<?php <?php
namespace App\Filament\Admin\Resources; namespace App\Filament\Admin\Resources\ApiKeys;
use App\Filament\Admin\Resources\ApiKeyResource\Pages; use Filament\Schemas\Schema;
use App\Filament\Admin\Resources\UserResource\Pages\EditUser; use App\Filament\Admin\Resources\ApiKeys\Pages\ListApiKeys;
use App\Filament\Admin\Resources\ApiKeys\Pages\CreateApiKey;
use App\Filament\Admin\Resources\Users\Pages\EditUser;
use App\Filament\Components\Tables\Columns\DateTimeColumn; use App\Filament\Components\Tables\Columns\DateTimeColumn;
use App\Models\ApiKey; use App\Models\ApiKey;
use App\Traits\Filament\CanCustomizePages; use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations; use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanModifyForm; use App\Traits\Filament\CanModifyForm;
use App\Traits\Filament\CanModifyTable; use App\Traits\Filament\CanModifyTable;
use Filament\Forms\Components\Fieldset; use Exception;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteAction;
use Filament\Forms\Components\TagsInput; use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea; use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\ToggleButtons; use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Form;
use Filament\Resources\Pages\PageRegistration; use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables\Actions\CreateAction; use Filament\Schemas\Components\Fieldset;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
@ -32,7 +34,7 @@ class ApiKeyResource extends Resource
protected static ?string $model = ApiKey::class; protected static ?string $model = ApiKey::class;
protected static ?string $navigationIcon = 'tabler-key'; protected static string|\BackedEnum|null $navigationIcon = 'tabler-key';
public static function getNavigationLabel(): string public static function getNavigationLabel(): string
{ {
@ -66,6 +68,9 @@ class ApiKeyResource extends Resource
return trans('admin/dashboard.advanced'); return trans('admin/dashboard.advanced');
} }
/**
* @throws Exception
*/
public static function defaultTable(Table $table): Table public static function defaultTable(Table $table): Table
{ {
return $table return $table
@ -74,7 +79,7 @@ class ApiKeyResource extends Resource
->label(trans('admin/apikey.table.key')) ->label(trans('admin/apikey.table.key'))
->icon('tabler-clipboard-text') ->icon('tabler-clipboard-text')
->state(fn (ApiKey $key) => $key->identifier . $key->token) ->state(fn (ApiKey $key) => $key->identifier . $key->token)
->copyable(), ->copyable(fn () => request()->isSecure()),
TextColumn::make('memo') TextColumn::make('memo')
->label(trans('admin/apikey.table.description')) ->label(trans('admin/apikey.table.description'))
->wrap() ->wrap()
@ -88,30 +93,28 @@ class ApiKeyResource extends Resource
->sortable(), ->sortable(),
TextColumn::make('user.username') TextColumn::make('user.username')
->label(trans('admin/apikey.table.created_by')) ->label(trans('admin/apikey.table.created_by'))
->icon('tabler-user')
->url(fn (ApiKey $apiKey) => auth()->user()->can('update', $apiKey->user) ? EditUser::getUrl(['record' => $apiKey->user]) : null), ->url(fn (ApiKey $apiKey) => auth()->user()->can('update', $apiKey->user) ? EditUser::getUrl(['record' => $apiKey->user]) : null),
]) ])
->actions([ ->recordActions([
DeleteAction::make(), DeleteAction::make(),
]) ])
->emptyStateIcon('tabler-key') ->emptyStateIcon('tabler-key')
->emptyStateDescription('') ->emptyStateDescription('')
->emptyStateHeading(trans('admin/apikey.empty_table')) ->emptyStateHeading(trans('admin/apikey.empty'))
->emptyStateActions([ ->emptyStateActions([
CreateAction::make(), CreateAction::make(),
]); ]);
} }
public static function defaultForm(Form $form): Form /**
* @throws Exception
*/
public static function defaultForm(Schema $schema): Schema
{ {
return $form return $schema
->schema([ ->components([
Fieldset::make('Permissions') Fieldset::make('Permissions')
->columns([ ->columnSpanFull()
'default' => 1,
'sm' => 1,
'md' => 2,
])
->schema( ->schema(
collect(ApiKey::getPermissionList())->map(fn ($resource) => ToggleButtons::make('permissions_' . $resource) collect(ApiKey::getPermissionList())->map(fn ($resource) => ToggleButtons::make('permissions_' . $resource)
->label(str($resource)->replace('_', ' ')->title())->inline() ->label(str($resource)->replace('_', ' ')->title())->inline()
@ -156,8 +159,8 @@ class ApiKeyResource extends Resource
public static function getDefaultPages(): array public static function getDefaultPages(): array
{ {
return [ return [
'index' => Pages\ListApiKeys::route('/'), 'index' => ListApiKeys::route('/'),
'create' => Pages\CreateApiKey::route('/create'), 'create' => CreateApiKey::route('/create'),
]; ];
} }
} }

View File

@ -1,8 +1,8 @@
<?php <?php
namespace App\Filament\Admin\Resources\ApiKeyResource\Pages; namespace App\Filament\Admin\Resources\ApiKeys\Pages;
use App\Filament\Admin\Resources\ApiKeyResource; use App\Filament\Admin\Resources\ApiKeys\ApiKeyResource;
use App\Models\ApiKey; use App\Models\ApiKey;
use App\Traits\Filament\CanCustomizeHeaderActions; use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets; use App\Traits\Filament\CanCustomizeHeaderWidgets;
@ -10,6 +10,7 @@ use Filament\Actions\Action;
use Filament\Actions\ActionGroup; use Filament\Actions\ActionGroup;
use Filament\Resources\Pages\CreateRecord; use Filament\Resources\Pages\CreateRecord;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
class CreateApiKey extends CreateRecord class CreateApiKey extends CreateRecord
{ {
@ -36,7 +37,7 @@ class CreateApiKey extends CreateRecord
protected function handleRecordCreation(array $data): Model protected function handleRecordCreation(array $data): Model
{ {
$data['identifier'] = ApiKey::generateTokenIdentifier(ApiKey::TYPE_APPLICATION); $data['identifier'] = ApiKey::generateTokenIdentifier(ApiKey::TYPE_APPLICATION);
$data['token'] = str_random(ApiKey::KEY_LENGTH); $data['token'] = Str::random(ApiKey::KEY_LENGTH);
$data['user_id'] = auth()->user()->id; $data['user_id'] = auth()->user()->id;
$data['key_type'] = ApiKey::TYPE_APPLICATION; $data['key_type'] = ApiKey::TYPE_APPLICATION;

View File

@ -1,8 +1,8 @@
<?php <?php
namespace App\Filament\Admin\Resources\ApiKeyResource\Pages; namespace App\Filament\Admin\Resources\ApiKeys\Pages;
use App\Filament\Admin\Resources\ApiKeyResource; use App\Filament\Admin\Resources\ApiKeys\ApiKeyResource;
use App\Models\ApiKey; use App\Models\ApiKey;
use App\Traits\Filament\CanCustomizeHeaderActions; use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets; use App\Traits\Filament\CanCustomizeHeaderWidgets;

View File

@ -1,26 +1,30 @@
<?php <?php
namespace App\Filament\Admin\Resources; namespace App\Filament\Admin\Resources\DatabaseHosts;
use App\Filament\Admin\Resources\DatabaseHostResource\Pages; use App\Filament\Admin\Resources\DatabaseHosts\RelationManagers\DatabasesRelationManager;
use App\Filament\Admin\Resources\DatabaseHostResource\RelationManagers; use App\Filament\Admin\Resources\DatabaseHosts\Pages\ListDatabaseHosts;
use App\Filament\Admin\Resources\DatabaseHosts\Pages\CreateDatabaseHost;
use App\Filament\Admin\Resources\DatabaseHosts\Pages\ViewDatabaseHost;
use App\Filament\Admin\Resources\DatabaseHosts\Pages\EditDatabaseHost;
use App\Models\DatabaseHost; use App\Models\DatabaseHost;
use App\Traits\Filament\CanCustomizePages; use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations; use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanModifyForm; use App\Traits\Filament\CanModifyForm;
use App\Traits\Filament\CanModifyTable; use App\Traits\Filament\CanModifyTable;
use Filament\Forms\Components\Section; use Exception;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ViewAction;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Forms\Set;
use Filament\Resources\Pages\PageRegistration; use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\RelationManagers\RelationManager; use Filament\Resources\RelationManagers\RelationManager;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables\Actions\CreateAction; use Filament\Schemas\Components\Section;
use Filament\Tables\Actions\DeleteBulkAction; use Filament\Schemas\Components\Utilities\Set;
use Filament\Tables\Actions\EditAction; use Filament\Schemas\Schema;
use Filament\Tables\Actions\ViewAction;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
@ -34,7 +38,7 @@ class DatabaseHostResource extends Resource
protected static ?string $model = DatabaseHost::class; protected static ?string $model = DatabaseHost::class;
protected static ?string $navigationIcon = 'tabler-database'; protected static string|\BackedEnum|null $navigationIcon = 'tabler-database';
protected static ?string $recordTitleAttribute = 'name'; protected static ?string $recordTitleAttribute = 'name';
@ -63,6 +67,9 @@ class DatabaseHostResource extends Resource
return trans('admin/dashboard.advanced'); return trans('admin/dashboard.advanced');
} }
/**
* @throws Exception
*/
public static function defaultTable(Table $table): Table public static function defaultTable(Table $table): Table
{ {
return $table return $table
@ -77,15 +84,13 @@ class DatabaseHostResource extends Resource
->label(trans('admin/databasehost.table.username')), ->label(trans('admin/databasehost.table.username')),
TextColumn::make('databases_count') TextColumn::make('databases_count')
->counts('databases') ->counts('databases')
->icon('tabler-database')
->label(trans('admin/databasehost.databases')), ->label(trans('admin/databasehost.databases')),
TextColumn::make('nodes.name') TextColumn::make('nodes.name')
->icon('tabler-server-2')
->badge() ->badge()
->placeholder(trans('admin/databasehost.no_nodes')), ->placeholder(trans('admin/databasehost.no_nodes')),
]) ])
->checkIfRecordIsSelectableUsing(fn (DatabaseHost $databaseHost) => !$databaseHost->databases_count) ->checkIfRecordIsSelectableUsing(fn (DatabaseHost $databaseHost) => !$databaseHost->databases_count)
->actions([ ->recordActions([
ViewAction::make() ViewAction::make()
->hidden(fn ($record) => static::canEdit($record)), ->hidden(fn ($record) => static::canEdit($record)),
EditAction::make(), EditAction::make(),
@ -101,11 +106,15 @@ class DatabaseHostResource extends Resource
]); ]);
} }
public static function defaultForm(Form $form): Form /**
* @throws Exception
*/
public static function defaultForm(Schema $schema): Schema
{ {
return $form return $schema
->schema([ ->components([
Section::make() Section::make()
->columnSpanFull()
->columns([ ->columns([
'default' => 2, 'default' => 2,
'sm' => 3, 'sm' => 3,
@ -132,7 +141,7 @@ class DatabaseHostResource extends Resource
->maxValue(65535), ->maxValue(65535),
TextInput::make('max_databases') TextInput::make('max_databases')
->label(trans('admin/databasehost.max_database')) ->label(trans('admin/databasehost.max_database'))
->helpertext(trans('admin/databasehost.max_databases_help')) ->helperText(trans('admin/databasehost.max_databases_help'))
->numeric(), ->numeric(),
TextInput::make('name') TextInput::make('name')
->label(trans('admin/databasehost.display_name')) ->label(trans('admin/databasehost.display_name'))
@ -166,7 +175,7 @@ class DatabaseHostResource extends Resource
public static function getDefaultRelations(): array public static function getDefaultRelations(): array
{ {
return [ return [
RelationManagers\DatabasesRelationManager::class, DatabasesRelationManager::class,
]; ];
} }
@ -174,10 +183,10 @@ class DatabaseHostResource extends Resource
public static function getDefaultPages(): array public static function getDefaultPages(): array
{ {
return [ return [
'index' => Pages\ListDatabaseHosts::route('/'), 'index' => ListDatabaseHosts::route('/'),
'create' => Pages\CreateDatabaseHost::route('/create'), 'create' => CreateDatabaseHost::route('/create'),
'view' => Pages\ViewDatabaseHost::route('/{record}'), 'view' => ViewDatabaseHost::route('/{record}'),
'edit' => Pages\EditDatabaseHost::route('/{record}/edit'), 'edit' => EditDatabaseHost::route('/{record}/edit'),
]; ];
} }

View File

@ -1,30 +1,31 @@
<?php <?php
namespace App\Filament\Admin\Resources\DatabaseHostResource\Pages; namespace App\Filament\Admin\Resources\DatabaseHosts\Pages;
use App\Filament\Admin\Resources\DatabaseHostResource; use App\Filament\Admin\Resources\DatabaseHosts\DatabaseHostResource;
use App\Services\Databases\Hosts\HostCreationService; use App\Services\Databases\Hosts\HostCreationService;
use App\Traits\Filament\CanCustomizeHeaderActions; use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets; use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Forms\Components\Fieldset; use Exception;
use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Placeholder; use Filament\Infolists\Components\TextEntry;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle; use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\Wizard\Step; use Filament\Schemas\Components\Fieldset;
use Filament\Forms\Get; use Filament\Schemas\Components\Utilities\Get;
use Filament\Forms\Set; use Filament\Schemas\Components\Utilities\Set;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Resources\Pages\CreateRecord; use Filament\Resources\Pages\CreateRecord;
use Filament\Resources\Pages\CreateRecord\Concerns\HasWizard; use Filament\Resources\Pages\CreateRecord\Concerns\HasWizard;
use Filament\Schemas\Components\Wizard\Step;
use Filament\Support\Exceptions\Halt; use Filament\Support\Exceptions\Halt;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\HtmlString; use Illuminate\Support\HtmlString;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use PDOException; use PDOException;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction; use Throwable;
class CreateDatabaseHost extends CreateRecord class CreateDatabaseHost extends CreateRecord
{ {
@ -43,15 +44,18 @@ class CreateDatabaseHost extends CreateRecord
$this->service = $service; $this->service = $service;
} }
/** @return Step[] */ /** @return Step[]
* @throws Exception
*/
public function getSteps(): array public function getSteps(): array
{ {
return [ return [
Step::make(trans('admin/databasehost.setup.preparations')) Step::make(trans('admin/databasehost.setup.preparations'))
->columns() ->columns()
->schema([ ->schema([
Placeholder::make('') TextEntry::make('setup')
->content(trans('admin/databasehost.setup.note')), ->hiddenLabel()
->state(trans('admin/databasehost.setup.note')),
Toggle::make('different_server') Toggle::make('different_server')
->label(new HtmlString(trans('admin/databasehost.setup.different_server'))) ->label(new HtmlString(trans('admin/databasehost.setup.different_server')))
->dehydrated(false) ->dehydrated(false)
@ -84,31 +88,34 @@ class CreateDatabaseHost extends CreateRecord
->schema([ ->schema([
Fieldset::make(trans('admin/databasehost.setup.database_user')) Fieldset::make(trans('admin/databasehost.setup.database_user'))
->schema([ ->schema([
Placeholder::make('') TextEntry::make('cli_login')
->content(new HtmlString(trans('admin/databasehost.setup.cli_login'))) ->hiddenLabel()
->state(new HtmlString(trans('admin/databasehost.setup.cli_login')))
->columnSpanFull(), ->columnSpanFull(),
TextInput::make('create_user') TextInput::make('create_user')
->label(trans('admin/databasehost.setup.command_create_user')) ->label(trans('admin/databasehost.setup.command_create_user'))
->default(fn (Get $get) => "CREATE USER '{$get('username')}'@'{$get('panel_ip')}' IDENTIFIED BY '{$get('password')}';") ->default(fn (Get $get) => "CREATE USER '{$get('username')}'@'{$get('panel_ip')}' IDENTIFIED BY '{$get('password')}';")
->disabled() ->disabled()
->dehydrated(false) ->dehydrated(false)
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null) ->copyable(fn () => request()->isSecure())
->columnSpanFull(), ->columnSpanFull(),
TextInput::make('assign_permissions') TextInput::make('assign_permissions')
->label(trans('admin/databasehost.setup.command_assign_permissions')) ->label(trans('admin/databasehost.setup.command_assign_permissions'))
->default(fn (Get $get) => "GRANT ALL PRIVILEGES ON *.* TO '{$get('username')}'@'{$get('panel_ip')}' WITH GRANT OPTION;") ->default(fn (Get $get) => "GRANT ALL PRIVILEGES ON *.* TO '{$get('username')}'@'{$get('panel_ip')}' WITH GRANT OPTION;")
->disabled() ->disabled()
->dehydrated(false) ->dehydrated(false)
->suffixAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null) ->copyable(fn () => request()->isSecure())
->columnSpanFull(), ->columnSpanFull(),
Placeholder::make('') TextEntry::make('cli_exit')
->content(new HtmlString(trans('admin/databasehost.setup.cli_exit'))) ->hiddenLabel()
->state(new HtmlString(trans('admin/databasehost.setup.cli_exit')))
->columnSpanFull(), ->columnSpanFull(),
]), ]),
Fieldset::make(trans('admin/databasehost.setup.external_access')) Fieldset::make(trans('admin/databasehost.setup.external_access'))
->schema([ ->schema([
Placeholder::make('') TextEntry::make('allow_external_access')
->content(new HtmlString(trans('admin/databasehost.setup.allow_external_access'))) ->hiddenLabel()
->state(new HtmlString(trans('admin/databasehost.setup.allow_external_access')))
->columnSpanFull(), ->columnSpanFull(),
]), ]),
]), ]),
@ -136,7 +143,7 @@ class CreateDatabaseHost extends CreateRecord
->maxValue(65535), ->maxValue(65535),
TextInput::make('max_databases') TextInput::make('max_databases')
->label(trans('admin/databasehost.max_database')) ->label(trans('admin/databasehost.max_database'))
->helpertext(trans('admin/databasehost.max_databases_help')) ->helperText(trans('admin/databasehost.max_databases_help'))
->placeholder(trans('admin/databasehost.unlimited')) ->placeholder(trans('admin/databasehost.unlimited'))
->numeric(), ->numeric(),
TextInput::make('name') TextInput::make('name')
@ -155,6 +162,10 @@ class CreateDatabaseHost extends CreateRecord
]; ];
} }
/**
* @throws Halt
* @throws Throwable
*/
protected function handleRecordCreation(array $data): Model protected function handleRecordCreation(array $data): Model
{ {
try { try {

View File

@ -1,8 +1,8 @@
<?php <?php
namespace App\Filament\Admin\Resources\DatabaseHostResource\Pages; namespace App\Filament\Admin\Resources\DatabaseHosts\Pages;
use App\Filament\Admin\Resources\DatabaseHostResource; use App\Filament\Admin\Resources\DatabaseHosts\DatabaseHostResource;
use App\Models\DatabaseHost; use App\Models\DatabaseHost;
use App\Services\Databases\Hosts\HostUpdateService; use App\Services\Databases\Hosts\HostUpdateService;
use App\Traits\Filament\CanCustomizeHeaderActions; use App\Traits\Filament\CanCustomizeHeaderActions;

View File

@ -1,8 +1,8 @@
<?php <?php
namespace App\Filament\Admin\Resources\DatabaseHostResource\Pages; namespace App\Filament\Admin\Resources\DatabaseHosts\Pages;
use App\Filament\Admin\Resources\DatabaseHostResource; use App\Filament\Admin\Resources\DatabaseHosts\DatabaseHostResource;
use App\Models\DatabaseHost; use App\Models\DatabaseHost;
use App\Traits\Filament\CanCustomizeHeaderActions; use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets; use App\Traits\Filament\CanCustomizeHeaderWidgets;

View File

@ -1,8 +1,8 @@
<?php <?php
namespace App\Filament\Admin\Resources\DatabaseHostResource\Pages; namespace App\Filament\Admin\Resources\DatabaseHosts\Pages;
use App\Filament\Admin\Resources\DatabaseHostResource; use App\Filament\Admin\Resources\DatabaseHosts\DatabaseHostResource;
use App\Traits\Filament\CanCustomizeHeaderActions; use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets; use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action; use Filament\Actions\Action;

View File

@ -1,15 +1,15 @@
<?php <?php
namespace App\Filament\Admin\Resources\DatabaseHostResource\RelationManagers; namespace App\Filament\Admin\Resources\DatabaseHosts\RelationManagers;
use App\Filament\Components\Forms\Actions\RotateDatabasePasswordAction; use Filament\Actions\DeleteAction;
use Filament\Actions\ViewAction;
use Filament\Schemas\Schema;
use App\Filament\Components\Actions\RotateDatabasePasswordAction;
use App\Filament\Components\Tables\Columns\DateTimeColumn; use App\Filament\Components\Tables\Columns\DateTimeColumn;
use App\Models\Database; use App\Models\Database;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager; use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Actions\ViewAction;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
@ -17,10 +17,10 @@ class DatabasesRelationManager extends RelationManager
{ {
protected static string $relationship = 'databases'; protected static string $relationship = 'databases';
public function form(Form $form): Form public function form(Schema $schema): Schema
{ {
return $form return $schema
->schema([ ->components([
TextInput::make('database') TextInput::make('database')
->columnSpanFull(), ->columnSpanFull(),
TextInput::make('username') TextInput::make('username')
@ -49,19 +49,16 @@ class DatabasesRelationManager extends RelationManager
public function table(Table $table): Table public function table(Table $table): Table
{ {
return $table return $table
->recordTitleAttribute('servers') ->recordTitleAttribute('database')
->heading('') ->heading('')
->columns([ ->columns([
TextColumn::make('database') TextColumn::make('database'),
->icon('tabler-database'),
TextColumn::make('username') TextColumn::make('username')
->label(trans('admin/databasehost.table.username')) ->label(trans('admin/databasehost.table.username')),
->icon('tabler-user'),
TextColumn::make('remote') TextColumn::make('remote')
->label(trans('admin/databasehost.table.remote')) ->label(trans('admin/databasehost.table.remote'))
->formatStateUsing(fn (Database $record) => $record->remote === '%' ? trans('admin/databasehost.anywhere'). ' ( % )' : $record->remote), ->formatStateUsing(fn (Database $record) => $record->remote === '%' ? trans('admin/databasehost.anywhere'). ' ( % )' : $record->remote),
TextColumn::make('server.name') TextColumn::make('server.name')
->icon('tabler-brand-docker')
->url(fn (Database $database) => route('filament.admin.resources.servers.edit', ['record' => $database->server_id])), ->url(fn (Database $database) => route('filament.admin.resources.servers.edit', ['record' => $database->server_id])),
TextColumn::make('max_connections') TextColumn::make('max_connections')
->label(trans('admin/databasehost.table.max_connections')) ->label(trans('admin/databasehost.table.max_connections'))
@ -69,12 +66,10 @@ class DatabasesRelationManager extends RelationManager
DateTimeColumn::make('created_at') DateTimeColumn::make('created_at')
->label(trans('admin/databasehost.table.created_at')), ->label(trans('admin/databasehost.table.created_at')),
]) ])
->actions([ ->recordActions([
DeleteAction::make()
->authorize(fn (Database $database) => auth()->user()->can('delete', $database)),
ViewAction::make() ViewAction::make()
->color('primary') ->color('primary'),
->hidden(fn () => !auth()->user()->can('viewAny', Database::class)), DeleteAction::make(),
]); ]);
} }
} }

View File

@ -1,9 +1,12 @@
<?php <?php
namespace App\Filament\Admin\Resources; namespace App\Filament\Admin\Resources\Eggs;
use App\Filament\Admin\Resources\EggResource\Pages; use App\Enums\CustomizationKey;
use App\Filament\Admin\Resources\EggResource\RelationManagers; use App\Filament\Admin\Resources\Eggs\RelationManagers\ServersRelationManager;
use App\Filament\Admin\Resources\Eggs\Pages\ListEggs;
use App\Filament\Admin\Resources\Eggs\Pages\CreateEgg;
use App\Filament\Admin\Resources\Eggs\Pages\EditEgg;
use App\Models\Egg; use App\Models\Egg;
use App\Traits\Filament\CanCustomizePages; use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations; use App\Traits\Filament\CanCustomizeRelations;
@ -18,18 +21,18 @@ class EggResource extends Resource
protected static ?string $model = Egg::class; protected static ?string $model = Egg::class;
protected static ?string $navigationIcon = 'tabler-eggs'; protected static string|\BackedEnum|null $navigationIcon = 'tabler-eggs';
protected static ?string $recordTitleAttribute = 'name'; protected static ?string $recordTitleAttribute = 'name';
public static function getNavigationBadge(): ?string public static function getNavigationBadge(): ?string
{ {
return static::getModel()::count() ?: null; return ($count = static::getModel()::count()) > 0 ? (string) $count : null;
} }
public static function getNavigationGroup(): ?string public static function getNavigationGroup(): ?string
{ {
return config('panel.filament.top-navigation', false) ? null : trans('admin/dashboard.server'); return auth()->user()->getCustomization(CustomizationKey::TopNavigation) ? false : trans('admin/dashboard.server');
} }
public static function getNavigationLabel(): string public static function getNavigationLabel(): string
@ -56,7 +59,7 @@ class EggResource extends Resource
public static function getDefaultRelations(): array public static function getDefaultRelations(): array
{ {
return [ return [
RelationManagers\ServersRelationManager::class, ServersRelationManager::class,
]; ];
} }
@ -64,9 +67,9 @@ class EggResource extends Resource
public static function getDefaultPages(): array public static function getDefaultPages(): array
{ {
return [ return [
'index' => Pages\ListEggs::route('/'), 'index' => ListEggs::route('/'),
'create' => Pages\CreateEgg::route('/create'), 'create' => CreateEgg::route('/create'),
'edit' => Pages\EditEgg::route('/{record}/edit'), 'edit' => EditEgg::route('/{record}/edit'),
]; ];
} }
} }

View File

@ -1,9 +1,9 @@
<?php <?php
namespace App\Filament\Admin\Resources\EggResource\Pages; namespace App\Filament\Admin\Resources\Eggs\Pages;
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor; use Exception;
use App\Filament\Admin\Resources\EggResource; use App\Filament\Admin\Resources\Eggs\EggResource;
use App\Filament\Components\Forms\Fields\CopyFrom; use App\Filament\Components\Forms\Fields\CopyFrom;
use App\Models\EggVariable; use App\Models\EggVariable;
use App\Traits\Filament\CanCustomizeHeaderActions; use App\Traits\Filament\CanCustomizeHeaderActions;
@ -11,24 +11,25 @@ use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Actions\ActionGroup; use Filament\Actions\ActionGroup;
use Filament\Forms\Components\Checkbox; use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Fieldset; use Filament\Forms\Components\CodeEditor;
use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\KeyValue; use Filament\Forms\Components\KeyValue;
use Filament\Forms\Components\Repeater; use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\TagsInput; use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea; use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle; use Filament\Forms\Components\Toggle;
use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Resources\Pages\CreateRecord; use Filament\Resources\Pages\CreateRecord;
use Filament\Schemas\Components\Fieldset;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Validation\Rules\Unique; use Illuminate\Validation\Rules\Unique;
use Filament\Schemas\Schema;
class CreateEgg extends CreateRecord class CreateEgg extends CreateRecord
{ {
@ -52,12 +53,16 @@ class CreateEgg extends CreateRecord
return []; return [];
} }
public function form(Form $form): Form /**
* @throws Exception
*/
public function form(Schema $schema): Schema
{ {
return $form return $schema
->schema([ ->components([
Tabs::make()->tabs([ Tabs::make()->tabs([
Tab::make(trans('admin/egg.tabs.configuration')) Tab::make('configuration')
->label(trans('admin/egg.tabs.configuration'))
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4]) ->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
->schema([ ->schema([
TextInput::make('name') TextInput::make('name')
@ -97,8 +102,7 @@ class CreateEgg extends CreateRecord
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 1]), ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 1]),
Toggle::make('force_outgoing_ip') Toggle::make('force_outgoing_ip')
->label(trans('admin/egg.force_ip')) ->label(trans('admin/egg.force_ip'))
->hintIcon('tabler-question-mark') ->hintIcon('tabler-question-mark', trans('admin/egg.force_ip_help')),
->hintIconTooltip(trans('admin/egg.force_ip_help')),
Hidden::make('script_is_privileged') Hidden::make('script_is_privileged')
->default(1), ->default(1),
TagsInput::make('tags') TagsInput::make('tags')
@ -106,8 +110,7 @@ class CreateEgg extends CreateRecord
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]), ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
TextInput::make('update_url') TextInput::make('update_url')
->label(trans('admin/egg.update_url')) ->label(trans('admin/egg.update_url'))
->hintIcon('tabler-question-mark') ->hintIcon('tabler-question-mark', trans('admin/egg.update_url_help'))
->hintIconTooltip(trans('admin/egg.update_url_help'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]) ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2])
->url(), ->url(),
KeyValue::make('docker_images') KeyValue::make('docker_images')
@ -123,7 +126,8 @@ class CreateEgg extends CreateRecord
->helperText(trans('admin/egg.docker_help')), ->helperText(trans('admin/egg.docker_help')),
]), ]),
Tab::make(trans('admin/egg.tabs.process_management')) Tab::make('process_management')
->label(trans('admin/egg.tabs.process_management'))
->columns() ->columns()
->schema([ ->schema([
CopyFrom::make('copy_process_from') CopyFrom::make('copy_process_from')
@ -146,15 +150,15 @@ class CreateEgg extends CreateRecord
->default('{}') ->default('{}')
->helperText(trans('admin/egg.log_config_help')), ->helperText(trans('admin/egg.log_config_help')),
]), ]),
Tab::make(trans('admin/egg.tabs.egg_variables')) Tab::make('egg_variables')
->label(trans('admin/egg.tabs.egg_variables'))
->columnSpanFull() ->columnSpanFull()
->schema([ ->schema([
Repeater::make('variables') Repeater::make('variables')
->label('') ->hiddenLabel()
->addActionLabel(trans('admin/egg.add_new_variable')) ->addActionLabel(trans('admin/egg.add_new_variable'))
->grid() ->grid()
->relationship('variables') ->relationship('variables')
->name('name')
->reorderable()->orderColumn() ->reorderable()->orderColumn()
->collapsible()->collapsed() ->collapsible()->collapsed()
->columnSpan(2) ->columnSpan(2)
@ -186,7 +190,7 @@ class CreateEgg extends CreateRecord
->maxLength(255) ->maxLength(255)
->columnSpanFull() ->columnSpanFull()
->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString())) ->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString()))
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')), ignoreRecord: true) ->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
->validationMessages([ ->validationMessages([
'unique' => trans('admin/egg.error_unique'), 'unique' => trans('admin/egg.error_unique'),
]) ])
@ -197,9 +201,8 @@ class CreateEgg extends CreateRecord
->maxLength(255) ->maxLength(255)
->prefix('{{') ->prefix('{{')
->suffix('}}') ->suffix('}}')
->hintIcon('tabler-code') ->hintIcon('tabler-code', fn ($state) => "{{{$state}}}")
->hintIconTooltip(fn ($state) => "{{{$state}}}") ->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')), ignoreRecord: true)
->rules(EggVariable::getRulesForField('env_variable')) ->rules(EggVariable::getRulesForField('env_variable'))
->validationMessages([ ->validationMessages([
'unique' => trans('admin/egg.error_unique'), 'unique' => trans('admin/egg.error_unique'),
@ -207,7 +210,7 @@ class CreateEgg extends CreateRecord
'*' => trans('admin/egg.error_reserved'), '*' => trans('admin/egg.error_reserved'),
]) ])
->required(), ->required(),
TextInput::make('default_value')->label(trans('admin/egg.default_value'))->maxLength(255), TextInput::make('default_value')->label(trans('admin/egg.default_value')),
Fieldset::make(trans('admin/egg.user_permissions')) Fieldset::make(trans('admin/egg.user_permissions'))
->schema([ ->schema([
Checkbox::make('user_viewable')->label(trans('admin/egg.viewable')), Checkbox::make('user_viewable')->label(trans('admin/egg.viewable')),
@ -239,7 +242,8 @@ class CreateEgg extends CreateRecord
]), ]),
]), ]),
]), ]),
Tab::make(trans('admin/egg.tabs.install_script')) Tab::make('install_script')
->label(trans('admin/egg.tabs.install_script'))
->columns(3) ->columns(3)
->schema([ ->schema([
CopyFrom::make('copy_script_from') CopyFrom::make('copy_script_from')
@ -254,15 +258,16 @@ class CreateEgg extends CreateRecord
->native(false) ->native(false)
->selectablePlaceholder(false) ->selectablePlaceholder(false)
->default('bash') ->default('bash')
->options(['bash', 'ash', '/bin/bash']) ->options([
'bash' => 'bash',
'ash' => 'ash',
'/bin/bash' => '/bin/bash',
])
->required(), ->required(),
MonacoEditor::make('script_install') CodeEditor::make('script_install')
->label(trans('admin/egg.script_install')) ->label(trans('admin/egg.script_install'))
->columnSpanFull() ->columnSpanFull()
->fontSize('16px') ->lazy(),
->language('shell')
->lazy()
->view('filament.plugins.monaco-editor'),
]), ]),
])->columnSpanFull()->persistTabInQueryString(), ])->columnSpanFull()->persistTabInQueryString(),

View File

@ -1,9 +1,9 @@
<?php <?php
namespace App\Filament\Admin\Resources\EggResource\Pages; namespace App\Filament\Admin\Resources\Eggs\Pages;
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor; use Exception;
use App\Filament\Admin\Resources\EggResource; use App\Filament\Admin\Resources\Eggs\EggResource;
use App\Filament\Components\Actions\ExportEggAction; use App\Filament\Components\Actions\ExportEggAction;
use App\Filament\Components\Actions\ImportEggAction; use App\Filament\Components\Actions\ImportEggAction;
use App\Filament\Components\Forms\Fields\CopyFrom; use App\Filament\Components\Forms\Fields\CopyFrom;
@ -15,22 +15,23 @@ use Filament\Actions\Action;
use Filament\Actions\ActionGroup; use Filament\Actions\ActionGroup;
use Filament\Actions\DeleteAction; use Filament\Actions\DeleteAction;
use Filament\Forms\Components\Checkbox; use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Fieldset; use Filament\Forms\Components\CodeEditor;
use Filament\Schemas\Components\Fieldset;
use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\KeyValue; use Filament\Forms\Components\KeyValue;
use Filament\Forms\Components\Repeater; use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Components\TagsInput; use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea; use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle; use Filament\Forms\Components\Toggle;
use Filament\Forms\Form; use Filament\Schemas\Components\Tabs;
use Filament\Forms\Get; use Filament\Schemas\Components\Tabs\Tab;
use Filament\Forms\Set; use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Resources\Pages\EditRecord; use Filament\Resources\Pages\EditRecord;
use Illuminate\Validation\Rules\Unique; use Illuminate\Validation\Rules\Unique;
use Filament\Schemas\Schema;
class EditEgg extends EditRecord class EditEgg extends EditRecord
{ {
@ -39,12 +40,16 @@ class EditEgg extends EditRecord
protected static string $resource = EggResource::class; protected static string $resource = EggResource::class;
public function form(Form $form): Form /**
* @throws Exception
*/
public function form(Schema $schema): Schema
{ {
return $form return $schema
->schema([ ->components([
Tabs::make()->tabs([ Tabs::make()->tabs([
Tab::make(trans('admin/egg.tabs.configuration')) Tab::make('configuration')
->label(trans('admin/egg.tabs.configuration'))
->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4]) ->columns(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 4])
->icon('tabler-egg') ->icon('tabler-egg')
->schema([ ->schema([
@ -92,8 +97,7 @@ class EditEgg extends EditRecord
Toggle::make('force_outgoing_ip') Toggle::make('force_outgoing_ip')
->inline(false) ->inline(false)
->label(trans('admin/egg.force_ip')) ->label(trans('admin/egg.force_ip'))
->hintIcon('tabler-question-mark') ->hintIcon('tabler-question-mark', trans('admin/egg.force_ip_help')),
->hintIconTooltip(trans('admin/egg.force_ip_help')),
Hidden::make('script_is_privileged') Hidden::make('script_is_privileged')
->helperText('The docker images available to servers using this egg.'), ->helperText('The docker images available to servers using this egg.'),
TagsInput::make('tags') TagsInput::make('tags')
@ -102,8 +106,7 @@ class EditEgg extends EditRecord
TextInput::make('update_url') TextInput::make('update_url')
->label(trans('admin/egg.update_url')) ->label(trans('admin/egg.update_url'))
->url() ->url()
->hintIcon('tabler-question-mark') ->hintIcon('tabler-question-mark', trans('admin/egg.update_url_help'))
->hintIconTooltip(trans('admin/egg.update_url_help'))
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]), ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
KeyValue::make('docker_images') KeyValue::make('docker_images')
->label(trans('admin/egg.docker_images')) ->label(trans('admin/egg.docker_images'))
@ -115,7 +118,8 @@ class EditEgg extends EditRecord
->valueLabel(trans('admin/egg.docker_uri')) ->valueLabel(trans('admin/egg.docker_uri'))
->helperText(trans('admin/egg.docker_help')), ->helperText(trans('admin/egg.docker_help')),
]), ]),
Tab::make(trans('admin/egg.tabs.process_management')) Tab::make('process_management')
->label(trans('admin/egg.tabs.process_management'))
->columns() ->columns()
->icon('tabler-server-cog') ->icon('tabler-server-cog')
->schema([ ->schema([
@ -135,15 +139,15 @@ class EditEgg extends EditRecord
->label(trans('admin/egg.log_config')) ->label(trans('admin/egg.log_config'))
->helperText(trans('admin/egg.log_config_help')), ->helperText(trans('admin/egg.log_config_help')),
]), ]),
Tab::make(trans('admin/egg.tabs.egg_variables')) Tab::make('egg_variables')
->label(trans('admin/egg.tabs.egg_variables'))
->columnSpanFull() ->columnSpanFull()
->icon('tabler-variable') ->icon('tabler-variable')
->schema([ ->schema([
Repeater::make('variables') Repeater::make('variables')
->label('') ->hiddenLabel()
->grid() ->grid()
->relationship('variables') ->relationship('variables')
->name('name')
->reorderable() ->reorderable()
->collapsible()->collapsed() ->collapsible()->collapsed()
->orderColumn() ->orderColumn()
@ -175,7 +179,7 @@ class EditEgg extends EditRecord
->maxLength(255) ->maxLength(255)
->columnSpanFull() ->columnSpanFull()
->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString())) ->afterStateUpdated(fn (Set $set, $state) => $set('env_variable', str($state)->trim()->snake()->upper()->toString()))
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')), ignoreRecord: true) ->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
->validationMessages([ ->validationMessages([
'unique' => trans('admin/egg.error_unique'), 'unique' => trans('admin/egg.error_unique'),
]) ])
@ -186,9 +190,8 @@ class EditEgg extends EditRecord
->maxLength(255) ->maxLength(255)
->prefix('{{') ->prefix('{{')
->suffix('}}') ->suffix('}}')
->hintIcon('tabler-code') ->hintIcon('tabler-code', fn ($state) => "{{{$state}}}")
->hintIconTooltip(fn ($state) => "{{{$state}}}") ->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')))
->unique(modifyRuleUsing: fn (Unique $rule, Get $get) => $rule->where('egg_id', $get('../../id')), ignoreRecord: true)
->rules(EggVariable::getRulesForField('env_variable')) ->rules(EggVariable::getRulesForField('env_variable'))
->validationMessages([ ->validationMessages([
'unique' => trans('admin/egg.error_unique'), 'unique' => trans('admin/egg.error_unique'),
@ -196,7 +199,7 @@ class EditEgg extends EditRecord
'*' => trans('admin/egg.error_reserved'), '*' => trans('admin/egg.error_reserved'),
]) ])
->required(), ->required(),
TextInput::make('default_value')->label(trans('admin/egg.default_value'))->maxLength(255), TextInput::make('default_value')->label(trans('admin/egg.default_value')),
Fieldset::make(trans('admin/egg.user_permissions')) Fieldset::make(trans('admin/egg.user_permissions'))
->schema([ ->schema([
Checkbox::make('user_viewable')->label(trans('admin/egg.viewable')), Checkbox::make('user_viewable')->label(trans('admin/egg.viewable')),
@ -228,7 +231,8 @@ class EditEgg extends EditRecord
]), ]),
]), ]),
]), ]),
Tab::make(trans('admin/egg.tabs.install_script')) Tab::make('install_script')
->label(trans('admin/egg.tabs.install_script'))
->columns(3) ->columns(3)
->icon('tabler-file-download') ->icon('tabler-file-download')
->schema([ ->schema([
@ -243,15 +247,15 @@ class EditEgg extends EditRecord
->label(trans('admin/egg.script_entry')) ->label(trans('admin/egg.script_entry'))
->native(false) ->native(false)
->selectablePlaceholder(false) ->selectablePlaceholder(false)
->options(['bash', 'ash', '/bin/bash']) ->options([
'bash' => 'bash',
'ash' => 'ash',
'/bin/bash' => '/bin/bash',
])
->required(), ->required(),
MonacoEditor::make('script_install') CodeEditor::make('script_install')
->label(trans('admin/egg.script_install')) ->hiddenLabel()
->placeholderText('') ->columnSpanFull(),
->columnSpanFull()
->fontSize('16px')
->language('shell')
->view('filament.plugins.monaco-editor'),
]), ]),
])->columnSpanFull()->persistTabInQueryString(), ])->columnSpanFull()->persistTabInQueryString(),
]); ]);

View File

@ -1,28 +1,26 @@
<?php <?php
namespace App\Filament\Admin\Resources\EggResource\Pages; namespace App\Filament\Admin\Resources\Eggs\Pages;
use App\Filament\Admin\Resources\EggResource; use Exception;
use App\Filament\Components\Actions\ImportEggAction as ImportEggHeaderAction; use App\Filament\Admin\Resources\Eggs\EggResource;
use App\Filament\Components\Tables\Actions\ExportEggAction; use App\Filament\Components\Actions\ExportEggAction;
use App\Filament\Components\Tables\Actions\ImportEggAction; use App\Filament\Components\Actions\ImportEggAction;
use App\Filament\Components\Tables\Actions\UpdateEggAction; use App\Filament\Components\Actions\UpdateEggAction;
use App\Filament\Components\Tables\Actions\UpdateEggBulkAction; use App\Filament\Components\Actions\UpdateEggBulkAction;
use App\Filament\Components\Tables\Filters\TagsFilter; use App\Filament\Components\Tables\Filters\TagsFilter;
use App\Models\Egg; use App\Models\Egg;
use App\Traits\Filament\CanCustomizeHeaderActions; use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets; use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Actions\ActionGroup; use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction as CreateHeaderAction; use Filament\Actions\CreateAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ReplicateAction;
use Filament\Resources\Pages\ListRecords; use Filament\Resources\Pages\ListRecords;
use Filament\Tables\Actions\CreateAction;
use Filament\Tables\Actions\DeleteBulkAction;
use Filament\Tables\Actions\EditAction;
use Filament\Tables\Actions\ReplicateAction;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class ListEggs extends ListRecords class ListEggs extends ListRecords
@ -32,6 +30,9 @@ class ListEggs extends ListRecords
protected static string $resource = EggResource::class; protected static string $resource = EggResource::class;
/**
* @throws Exception
*/
public function table(Table $table): Table public function table(Table $table): Table
{ {
return $table return $table
@ -43,17 +44,15 @@ class ListEggs extends ListRecords
->hidden(), ->hidden(),
TextColumn::make('name') TextColumn::make('name')
->label(trans('admin/egg.name')) ->label(trans('admin/egg.name'))
->icon('tabler-egg')
->description(fn ($record): ?string => (strlen($record->description) > 120) ? substr($record->description, 0, 120).'...' : $record->description) ->description(fn ($record): ?string => (strlen($record->description) > 120) ? substr($record->description, 0, 120).'...' : $record->description)
->wrap() ->wrap()
->searchable() ->searchable()
->sortable(), ->sortable(),
TextColumn::make('servers_count') TextColumn::make('servers_count')
->counts('servers') ->counts('servers')
->icon('tabler-server')
->label(trans('admin/egg.servers')), ->label(trans('admin/egg.servers')),
]) ])
->actions([ ->recordActions([
EditAction::make() EditAction::make()
->iconButton() ->iconButton()
->tooltip(trans('filament-actions::edit.single.label')), ->tooltip(trans('filament-actions::edit.single.label')),
@ -62,7 +61,7 @@ class ListEggs extends ListRecords
->tooltip(trans('filament-actions::export.modal.actions.export.label')), ->tooltip(trans('filament-actions::export.modal.actions.export.label')),
UpdateEggAction::make() UpdateEggAction::make()
->iconButton() ->iconButton()
->tooltip(trans('admin/egg.update')), ->tooltip(trans_choice('admin/egg.update', 1)),
ReplicateAction::make() ReplicateAction::make()
->iconButton() ->iconButton()
->tooltip(trans('filament-actions::replicate.single.label')) ->tooltip(trans('filament-actions::replicate.single.label'))
@ -78,15 +77,15 @@ class ListEggs extends ListRecords
]) ])
->groupedBulkActions([ ->groupedBulkActions([
DeleteBulkAction::make() DeleteBulkAction::make()
->before(fn (DeleteBulkAction $action, Collection $records) => $action->records($records->filter(function ($egg) { ->before(fn (&$records) => $records = $records->filter(function ($egg) {
/** @var Egg $egg */ /** @var Egg $egg */
return $egg->servers_count <= 0; return $egg->servers_count <= 0;
}))), })),
UpdateEggBulkAction::make() UpdateEggBulkAction::make()
->before(fn (UpdateEggBulkAction $action, Collection $records) => $action->records($records->filter(function ($egg) { ->before(fn (&$records) => $records = $records->filter(function ($egg) {
/** @var Egg $egg */ /** @var Egg $egg */
return cache()->get("eggs.$egg->uuid.update", false); return cache()->get("eggs.$egg->uuid.update", false);
}))), })),
]) ])
->emptyStateIcon('tabler-eggs') ->emptyStateIcon('tabler-eggs')
->emptyStateDescription('') ->emptyStateDescription('')
@ -102,13 +101,15 @@ class ListEggs extends ListRecords
]); ]);
} }
/** @return array<Action|ActionGroup> */ /** @return array<Action|ActionGroup>
* @throws Exception
*/
protected function getDefaultHeaderActions(): array protected function getDefaultHeaderActions(): array
{ {
return [ return [
ImportEggHeaderAction::make() ImportEggAction::make()
->multiple(), ->multiple(),
CreateHeaderAction::make(), CreateAction::make(),
]; ];
} }
} }

View File

@ -1,6 +1,6 @@
<?php <?php
namespace App\Filament\Admin\Resources\EggResource\RelationManagers; namespace App\Filament\Admin\Resources\Eggs\RelationManagers;
use App\Models\Server; use App\Models\Server;
use Filament\Resources\RelationManagers\RelationManager; use Filament\Resources\RelationManagers\RelationManager;
@ -22,24 +22,22 @@ class ServersRelationManager extends RelationManager
->heading(trans('admin/egg.servers')) ->heading(trans('admin/egg.servers'))
->columns([ ->columns([
TextColumn::make('user.username') TextColumn::make('user.username')
->label('Owner') ->label(trans('admin/server.owner'))
->icon('tabler-user')
->url(fn (Server $server): string => route('filament.admin.resources.users.edit', ['record' => $server->user])) ->url(fn (Server $server): string => route('filament.admin.resources.users.edit', ['record' => $server->user]))
->sortable(), ->sortable(),
TextColumn::make('name') TextColumn::make('name')
->label(trans('admin/server.name')) ->label(trans('admin/server.name'))
->icon('tabler-brand-docker')
->url(fn (Server $server): string => route('filament.admin.resources.servers.edit', ['record' => $server])) ->url(fn (Server $server): string => route('filament.admin.resources.servers.edit', ['record' => $server]))
->sortable(), ->sortable(),
TextColumn::make('node.name') TextColumn::make('node.name')
->icon('tabler-server-2')
->url(fn (Server $server): string => route('filament.admin.resources.nodes.edit', ['record' => $server->node])), ->url(fn (Server $server): string => route('filament.admin.resources.nodes.edit', ['record' => $server->node])),
TextColumn::make('image') TextColumn::make('image')
->label(trans('admin/server.docker_image')), ->label(trans('admin/server.docker_image')),
SelectColumn::make('allocation.id') SelectColumn::make('allocation.id')
->label(trans('admin/server.primary_allocation')) ->label(trans('admin/server.primary_allocation'))
->options(fn (Server $server) => [$server->allocation->id => $server->allocation->address]) ->disabled()
->selectablePlaceholder(false) ->options(fn (Server $server) => $server->allocations->take(1)->mapWithKeys(fn ($allocation) => [$allocation->id => $allocation->address]))
->placeholder(trans('admin/server.none'))
->sortable(), ->sortable(),
]); ]);
} }

View File

@ -1,26 +1,30 @@
<?php <?php
namespace App\Filament\Admin\Resources; namespace App\Filament\Admin\Resources\Mounts;
use App\Filament\Admin\Resources\MountResource\Pages; use App\Filament\Admin\Resources\Mounts\Pages\ListMounts;
use App\Filament\Admin\Resources\Mounts\Pages\CreateMount;
use App\Filament\Admin\Resources\Mounts\Pages\ViewMount;
use App\Filament\Admin\Resources\Mounts\Pages\EditMount;
use Exception;
use App\Models\Mount; use App\Models\Mount;
use App\Traits\Filament\CanCustomizePages; use App\Traits\Filament\CanCustomizePages;
use App\Traits\Filament\CanCustomizeRelations; use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanModifyForm; use App\Traits\Filament\CanModifyForm;
use App\Traits\Filament\CanModifyTable; use App\Traits\Filament\CanModifyTable;
use Filament\Forms\Components\Group; use Filament\Actions\CreateAction;
use Filament\Forms\Components\Section; use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ViewAction;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea; use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons; use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Form;
use Filament\Resources\Pages\PageRegistration; use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables\Actions\CreateAction; use Filament\Schemas\Components\Group;
use Filament\Tables\Actions\DeleteBulkAction; use Filament\Schemas\Components\Section;
use Filament\Tables\Actions\EditAction; use Filament\Schemas\Schema;
use Filament\Tables\Actions\ViewAction;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
@ -34,7 +38,7 @@ class MountResource extends Resource
protected static ?string $model = Mount::class; protected static ?string $model = Mount::class;
protected static ?string $navigationIcon = 'tabler-layers-linked'; protected static string|\BackedEnum|null $navigationIcon = 'tabler-layers-linked';
protected static ?string $recordTitleAttribute = 'name'; protected static ?string $recordTitleAttribute = 'name';
@ -63,6 +67,9 @@ class MountResource extends Resource
return trans('admin/dashboard.advanced'); return trans('admin/dashboard.advanced');
} }
/**
* @throws Exception
*/
public static function defaultTable(Table $table): Table public static function defaultTable(Table $table): Table
{ {
return $table return $table
@ -72,12 +79,10 @@ class MountResource extends Resource
->description(fn (Mount $mount) => "$mount->source -> $mount->target") ->description(fn (Mount $mount) => "$mount->source -> $mount->target")
->sortable(), ->sortable(),
TextColumn::make('eggs.name') TextColumn::make('eggs.name')
->icon('tabler-eggs')
->label(trans('admin/mount.eggs')) ->label(trans('admin/mount.eggs'))
->badge() ->badge()
->placeholder(trans('admin/mount.table.all_eggs')), ->placeholder(trans('admin/mount.table.all_eggs')),
TextColumn::make('nodes.name') TextColumn::make('nodes.name')
->icon('tabler-server-2')
->label(trans('admin/mount.nodes')) ->label(trans('admin/mount.nodes'))
->badge() ->badge()
->placeholder(trans('admin/mount.table.all_nodes')), ->placeholder(trans('admin/mount.table.all_nodes')),
@ -88,7 +93,7 @@ class MountResource extends Resource
->color(fn ($state) => $state ? 'success' : 'warning') ->color(fn ($state) => $state ? 'success' : 'warning')
->formatStateUsing(fn ($state) => $state ? trans('admin/mount.toggles.read_only') : trans('admin/mount.toggles.writable')), ->formatStateUsing(fn ($state) => $state ? trans('admin/mount.toggles.read_only') : trans('admin/mount.toggles.writable')),
]) ])
->actions([ ->recordActions([
ViewAction::make() ViewAction::make()
->hidden(fn ($record) => static::canEdit($record)), ->hidden(fn ($record) => static::canEdit($record)),
EditAction::make(), EditAction::make(),
@ -104,10 +109,13 @@ class MountResource extends Resource
]); ]);
} }
public static function defaultForm(Form $form): Form /**
* @throws Exception
*/
public static function defaultForm(Schema $schema): Schema
{ {
return $form return $schema
->schema([ ->components([
Section::make()->schema([ Section::make()->schema([
TextInput::make('name') TextInput::make('name')
->label(trans('admin/mount.name')) ->label(trans('admin/mount.name'))
@ -176,10 +184,10 @@ class MountResource extends Resource
public static function getDefaultPages(): array public static function getDefaultPages(): array
{ {
return [ return [
'index' => Pages\ListMounts::route('/'), 'index' => ListMounts::route('/'),
'create' => Pages\CreateMount::route('/create'), 'create' => CreateMount::route('/create'),
'view' => Pages\ViewMount::route('/{record}'), 'view' => ViewMount::route('/{record}'),
'edit' => Pages\EditMount::route('/{record}/edit'), 'edit' => EditMount::route('/{record}/edit'),
]; ];
} }

View File

@ -1,8 +1,8 @@
<?php <?php
namespace App\Filament\Admin\Resources\MountResource\Pages; namespace App\Filament\Admin\Resources\Mounts\Pages;
use App\Filament\Admin\Resources\MountResource; use App\Filament\Admin\Resources\Mounts\MountResource;
use App\Traits\Filament\CanCustomizeHeaderActions; use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets; use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action; use Filament\Actions\Action;

View File

@ -1,8 +1,8 @@
<?php <?php
namespace App\Filament\Admin\Resources\MountResource\Pages; namespace App\Filament\Admin\Resources\Mounts\Pages;
use App\Filament\Admin\Resources\MountResource; use App\Filament\Admin\Resources\Mounts\MountResource;
use App\Traits\Filament\CanCustomizeHeaderActions; use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets; use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Actions\Action; use Filament\Actions\Action;

View File

@ -1,8 +1,8 @@
<?php <?php
namespace App\Filament\Admin\Resources\MountResource\Pages; namespace App\Filament\Admin\Resources\Mounts\Pages;
use App\Filament\Admin\Resources\MountResource; use App\Filament\Admin\Resources\Mounts\MountResource;
use App\Models\Mount; use App\Models\Mount;
use App\Traits\Filament\CanCustomizeHeaderActions; use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets; use App\Traits\Filament\CanCustomizeHeaderWidgets;

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