Compare commits

...

359 Commits

Author SHA1 Message Date
Boy132
2bbfb0eef9 Fix "undefined key" error for TagsFilter (#2312) 2026-05-04 10:48:34 +02:00
Boy132
2a64ea8536 Cleanup node fqdn/ip validation (#2307) 2026-05-04 09:01:03 +02:00
Boy132
4fdbbff74b Correctly store falsy startup variable values (#2305) 2026-04-28 09:35:19 +02:00
Boy132
2ed891633c Add subuser permission for changing server icon (#2304) 2026-04-28 09:34:38 +02:00
Michael (Parker) Parker
58dcbeac0b install plugin packages on start (#2298) 2026-04-23 08:41:44 -05:00
Boy132
91c5ddb2bd Catch icon exception when importing egg (#2299) 2026-04-22 13:52:31 +02:00
Lance Pioch
562be98b20 Add user_mountable functionality for Mounts (#2077) (#2181)
Co-authored-by: Boy132 <mail@boy132.de>
2026-04-21 22:03:10 -04:00
Lance Pioch
fd3b8a7ab3 Laravel 12.56.0 Shift (#2283)
Co-authored-by: Shift <shift@laravelshift.com>
2026-04-21 22:02:48 -04:00
Boy132
1817383bf5 Add changes from upstream (#2293)
Co-authored-by: DaneEveritt <dane@daneeveritt.com>
Co-authored-by: danny6167 <danielb@purpleflaghosting.com>
Co-authored-by: MrSoulPenguin <28676680+MrSoulPenguin@users.noreply.github.com>
2026-04-20 17:25:54 +02:00
Boy132
d39a0c4464 Allow to add translation prefix to custom subuser permissions (#2265) 2026-04-20 17:24:24 +02:00
Boy132
2e48095379 Refactor egg & server icon uploading (#2281) 2026-04-20 17:23:41 +02:00
Jørgen Vatle
06c662988a Feature: Add support for user-configurable egg/nests index file. (#2287)
Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2026-04-20 05:06:39 -04:00
Neil Hanlon
98d7158dfc Fix PATCH /api/application/nodes returning 500 due to null timestamps (#2286) 2026-04-20 05:06:01 -04:00
JUBE
e01d9f2cf3 Add sortable feature to name column in ListServers (#2282) 2026-04-08 19:50:01 -04:00
Boy132
b693d0e728 Do not allow all file extensions for egg image in egg importer (#2279) 2026-03-22 19:18:46 +01:00
Boy132
612041e1f8 Run composer update and update model phpdocs (#2275) 2026-03-17 09:09:01 +01:00
Boy132
64bcdb514b Add jobs for plugin install, update and uninstall (#2267) 2026-03-17 09:08:28 +01:00
Lance Pioch
c1105db702 Fix encodings with new tested helper (#2238) 2026-03-16 16:48:13 -04:00
Lance Pioch
761492d09f Laravel 12.53.0 Shift (#2266)
Co-authored-by: Shift <shift@laravelshift.com>
2026-02-26 23:56:15 -05:00
Boy132
e35ce1e79d Use folder name as id when having id mismatch to allow deletion (#2263) 2026-02-23 20:25:19 +01:00
Charles
42c127c004 Fix invisible force delete button (#2262) 2026-02-23 13:41:15 -05:00
Boy132
593f209142 Fix stock egg UUID migration (#2259) 2026-02-22 17:23:51 +01:00
Boy132
9bf5b2cf0a Do not throw error when checking for egg updates (#2256) 2026-02-22 17:23:09 +01:00
Lance Pioch
f76e864a30 Laravel 12.52.0 Shift (#2247)
Co-authored-by: Shift <shift@laravelshift.com>
2026-02-22 11:11:18 -05:00
Boy132
01cfa31ee1 Limit activity logs on profile page (#2253) 2026-02-19 14:57:48 +01:00
Boy132
dead664e4d Make plugin id in artisan commands also case insensitive (#2252) 2026-02-19 14:57:19 +01:00
Charles
1bbbcd0e25 Update server/egg icon url supported file types (#2249) 2026-02-18 16:30:24 -05:00
Michael (Parker) Parker
677d2f742c docker env fixes (#2234)
Co-authored-by: Charles <charles@pelican.dev>
Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2026-02-18 14:40:46 -05:00
Charles
650fb16d2d exclude bulk actions in list eggs (#2239) 2026-02-17 06:17:28 -05:00
Boy132
58814ea782 Allow to delete errored plugins (#2246) 2026-02-17 11:52:25 +01:00
hallo123wert
0918ed308b exclude bulk actions (#2240) 2026-02-16 06:34:14 -05:00
Lance Pioch
85d5f2ec3f Fix alert banners (#1492) (#2177) 2026-02-15 15:34:09 -05:00
Lance Pioch
810f237547 Remove ignition, just use new default error handler (#2241) 2026-02-15 15:03:55 -05:00
Lance Pioch
8f191890a1 Fix null backup limit exception (#2242) 2026-02-15 15:03:37 -05:00
Boy132
160e0e54f5 More action fixes (#2237)
Co-authored-by: notCharles <charles@pelican.dev>
2026-02-15 17:31:50 +01:00
Charles
fdcfbb00ca Revert "Fix UTF double encoding in file editor" (#2236) 2026-02-15 11:08:01 -05:00
Lance Pioch
44f6cf8928 Fix UTF double encoding in file editor (#2199) 2026-02-14 10:33:35 -05:00
Boy132
cf2a26bbf0 Improve plugin loading (#2233) 2026-02-14 14:06:50 +01:00
gOOvER
adb6678eee Convert YAML boolean defaults to strings (#2232) 2026-02-13 18:12:44 -05:00
Lance Pioch
9539e21b39 Handle invalid svg icon in generic-oidc provider gracefully (#2216) 2026-02-13 10:06:07 -05:00
Charles
33660f635f Fix yet another action (#2230) 2026-02-12 21:00:26 -05:00
Lance Pioch
8c475ed95f Add “reachable” column for Client -> Wings connections for Nodes (#2200) 2026-02-12 17:06:38 -05:00
Lance Pioch
d43cb1d180 Fix empty egg config_files causing fatal 500 error (#2195) (#2197) 2026-02-12 17:06:19 -05:00
Lance Pioch
fe55dbd200 Ignore oauth redirects for spa (#2224) 2026-02-12 16:31:35 -05:00
Charles
0bb4503c2b fix unsuspend button (#2227) 2026-02-12 16:31:20 -05:00
Lance Pioch
3c1168beb5 Update job runners (#2225) 2026-02-12 10:29:33 -05:00
Quinten
1a092aedc8 Docker: chown composer.json and composer.lock for plugins (#2220) 2026-02-12 10:02:12 -05:00
Charles
8c99a8030f Fix more actions (#2208) 2026-02-12 09:56:56 -05:00
Lance Pioch
6e53b1cd7d Allow custom ips to still be entered (#2223) 2026-02-12 09:49:54 -05:00
Charles
4042e0416b Revert toolbar change (#2218) 2026-02-11 17:54:58 -05:00
Lance Pioch
cc8973cf00 Laravel 12.51.0 Shift (#2213)
Co-authored-by: Shift <shift@laravelshift.com>
2026-02-11 11:18:33 -05:00
Quinten
8ebe75b947 fix: composer not been installed in the docker image (#2211) 2026-02-10 16:00:18 -05:00
Hythera
f8144407d1 fix: composer content hash (#2209) 2026-02-09 22:26:01 -05:00
Charles
e431ccb66a add rounding to list-files header (#2207) 2026-02-09 22:19:10 -05:00
Lance Pioch
9291bb4477 Fix webhook processing for event objects (#2198) 2026-02-09 08:47:09 -05:00
Lance Pioch
e8c80ae420 Fix SFTP access denied for subuser when view role is assigned (#2196) 2026-02-09 08:46:47 -05:00
stdpi
f1be003276 Change file browser sticky header again... (#2203) 2026-02-08 15:17:48 -05:00
Charles
e532a9a180 composer upgrade (#2167)
Co-authored-by: Lance Pioch <git@lance.sh>
2026-02-07 14:33:51 -05:00
Charles
41fdd7bc8e Exclude create client api key button (#2169) 2026-02-07 14:33:32 -05:00
Lance Pioch
9b01203b7c Add per-user toggle to redirect admins to /admin after login (#2191) 2026-02-07 13:21:14 -05:00
Lance Pioch
ab4eadec32 Fix Exporting an egg in yaml format (#2172) 2026-02-07 08:10:00 -05:00
Lance Pioch
789c4c7284 Localize email notifications (#2043) (#2178) 2026-02-06 10:37:15 -05:00
Lance Pioch
b1a39f1724 Handle X-Forwarded-Proto in .htaccess for SSL-terminating proxies (#2171) 2026-02-06 09:54:24 -05:00
Lance Pioch
6a548c09a0 Clarify OAuth error when provider account has no linked email (#2179) 2026-02-06 07:50:37 -05:00
Lance Pioch
55bda569cc Implement flexible caching for node statuses (#2174) 2026-02-06 07:50:20 -05:00
Lance Pioch
adf1249086 Fix Egg Feature modals not working (#2175) 2026-02-06 07:49:56 -05:00
Lance Pioch
dbf77bf146 Implement single file move to support Unix mv semantics (#1984) (#2176) 2026-02-06 07:49:40 -05:00
Frogperson
a34bf9fd49 Add Daemon Base Directory field (#2151)
Co-authored-by: Boy132 <mail@boy132.de>
2026-02-05 08:00:25 -05:00
Boy132
7a9deba0e1 Fix notifications for DeleteAction on EditEgg page (#2165) 2026-02-04 22:20:22 +01:00
Charles
159bfe2210 exclude node actions (#2164) 2026-02-04 06:48:13 -05:00
stdpi
a821db8aae Improve file browser UI (#2086) 2026-02-04 05:40:45 -05:00
Lance Pioch
1556f8efb8 Allow all permissions to be toggled at once for api tokens (#2154) 2026-02-02 08:41:10 -05:00
Lance Pioch
57c2aa6f21 Fix the tabbing for username to password on login (#2155)
Co-authored-by: notCharles <charles@pelican.dev>
2026-02-02 08:40:55 -05:00
Charles
36de4c3786 composer update (#2161) 2026-01-31 19:37:54 -05:00
Boy132
26312e3897 Add basic api for plugins (#2146) 2026-02-01 00:10:57 +01:00
Boy132
a477c89025 Move username sanitization to model and make it less strict (#2150) 2026-02-01 00:07:26 +01:00
Boy132
93e81c26a9 Send server id to wings (#2157) 2026-01-31 22:54:06 +01:00
Boy132
23e91e8df3 Fix "visit website" button being invisible (#2158) 2026-01-31 22:53:56 +01:00
Boy132
833294bfaf Invisible button and tooltip fixes (#2149) 2026-01-29 15:37:05 +01:00
Lance Pioch
abaeeff86d Laravel 12.49.0 Shift (#2145)
Co-authored-by: Shift <shift@laravelshift.com>
2026-01-27 23:40:20 -05:00
Charles
dd77555c42 Add tooltips to actions across admin area (#2134)
Co-authored-by: Boy132 <mail@boy132.de>
2026-01-27 20:07:18 -05:00
Boy132
297ecb544d Replace icon strings with enum (#2113) 2026-01-27 11:36:07 +01:00
Boy132
e14bb7d030 Fix oauth provider "enabled" checks (#2142) 2026-01-27 11:27:19 +01:00
Boy132
c770937880 Migration to convert former stock egg uuids (#2108) 2026-01-23 16:40:24 +01:00
Boy132
426643eaa6 Add allocation to role permission models & make sure user can target node of allocation (#2124) 2026-01-23 16:37:01 +01:00
Boy132
3ca0f64e6e Set failed plugin installs to "not_installed" instead of errored (#2129) 2026-01-23 16:36:38 +01:00
Michael (Parker) Parker
8e8ce3b50f fix plugins in entrypoint (#2122) 2026-01-19 09:15:18 -05:00
Charles
b1e9cadc10 Revert "Update to filament v5, Livewire v4" (#2121) 2026-01-18 17:17:23 -05:00
Charles
7bf1f18c2d Update to filament v5, Livewire v4 (#2114)
Co-authored-by: Lance Pioch <git@lance.sh>
2026-01-18 17:04:13 -05:00
Charles
6fe7d29960 composer update (#2120) 2026-01-18 16:44:16 -05:00
Charles
15172b1d86 Add github eggs to egg importer (#2116) 2026-01-18 16:33:09 -05:00
Boy132
9f744d39a2 Add traits for customizing tabs (#2101) 2026-01-18 22:32:18 +01:00
Boy132
b79511568e Fix allocation policy for admins and update checks (#2090) 2026-01-18 22:26:15 +01:00
Lance Pioch
adeb1b4217 Add parallel flags to github ci (#2109) 2026-01-18 16:24:39 -05:00
JoanFo
d064bf9734 Allow backup transfers (#2068)
Co-authored-by: MartinOscar <40749467+rmartinoscar@users.noreply.github.com>
2026-01-18 16:23:21 -05:00
Michael (Parker) Parker
107286d618 Multiple Container Fixes (#2063)
Co-authored-by: MartinOscar <40749467+rmartinoscar@users.noreply.github.com>
2026-01-18 16:21:33 -05:00
Lance Pioch
a3203f7dda Update readme.md 2026-01-16 19:14:05 -05:00
Boy132
e9abd56f7a Add traits for customizing wizard steps (#2100) 2026-01-15 20:55:53 +01:00
PalmarHealer
675ab057b0 fix: Enhance feedback notifications for egg actions (#2042)
Co-authored-by: Charles <charles@pelican.dev>
2026-01-15 12:32:50 -05:00
Boy132
943d9d3ef5 Update translations from crowdin (#2110) 2026-01-15 07:59:55 -05:00
Lance Pioch
c06a525be2 Laravel 12.47.0 Shift (#2103)
Co-authored-by: Shift <shift@laravelshift.com>
2026-01-15 07:57:57 -05:00
Boy132
2ff5fdf831 Fix columns for mount form (#2105) 2026-01-15 13:57:37 +01:00
Boy132
0e810f3110 Throw yarn errors when installing themes (#2104) 2026-01-14 08:23:24 +01:00
Charles
eadbe6e8fd fix client side view database unlimited state (#2047)
Co-authored-by: Boy132 <mail@boy132.de>
2026-01-13 05:33:20 -05:00
Boy132
53aa49b11a Add changes from upstream (#2076)
Co-authored-by: DaneEveritt <dane@daneeveritt.com>
2026-01-13 08:39:50 +01:00
Boy132
6ae4f007c8 Make sure custom pages/relations don't override default pages/relations (#2099) 2026-01-12 18:00:37 +01:00
Boy132
6b9d683f06 Update database config to remove deprecation warning on php 8.5 (#2089) 2026-01-09 14:39:22 +01:00
Boy132
3b24e22316 Set plugin status to "errored" if it errored (#2084) 2026-01-08 17:43:31 +01:00
Boy132
bd012f52a9 Add tests for php 8.5 (#2079) 2026-01-08 17:32:23 +01:00
Boy132
af202d9827 Add user to shouldLink and shouldCreate oauth functions (#2083) 2026-01-08 15:13:15 +01:00
Boy132
6ebeb40ba0 Make rule for user language less restrictive (#2075) 2026-01-06 08:45:53 +01:00
Boy132
333eeda065 Disable field if server variable is not user_editable (#2074) 2026-01-06 08:45:40 +01:00
MartinOscar
fcfafadec7 Return if no egg was selected in the Installer (#2073) 2026-01-05 14:21:34 +01:00
Boy132
76b6118fd1 Fix typo in method name (#2062) 2026-01-04 15:17:48 -05:00
PalmarHealer
3141fe61b4 fix: plugin migration rollback and cache clearing on uninstall (#2033)
Co-authored-by: Boy132 <mail@boy132.de>
2026-01-03 23:44:33 +01:00
Charles
bed9dbeb2b Add Eggs to Installer (#2004)
Co-authored-by: Boy132 <mail@boy132.de>
2025-12-29 17:24:02 -05:00
Boy132
976cb00c0d Replace Artisan::call in plugin service for better error handling (#2031) 2025-12-28 14:44:39 +01:00
Quinten
e3534bbb29 Bungeecord: Fix Download (#2055) 2025-12-28 13:48:22 +01:00
xDev789
5740c93032 Per request cache for permission checks (#2029)
Co-authored-by: MartinOscar <40749467+rmartinoscar@users.noreply.github.com>
Co-authored-by: Lance Pioch <lancepioch@gmail.com>
2025-12-28 02:00:59 +01:00
MartinOscar
d72e075977 chore: Prevent users from caching Config (#2048) 2025-12-28 01:50:36 +01:00
Boy132
9af608f808 Fix relation managers for admin server resource (#2050) 2025-12-25 00:44:30 +01:00
Boy132
ac36e7a4b5 Fix oauth providers with no color (#2044) 2025-12-24 14:38:47 +01:00
Boy132
b1c64e2ef1 Add error notification when plugin install, update or uninstall fails (#2032) 2025-12-24 14:38:25 +01:00
PalmarHealer
da2e930d4d Correct bounty link (#2039)
Co-authored-by: MartinOscar <40749467+rmartinoscar@users.noreply.github.com>
2025-12-23 15:46:00 -05:00
Charles
460a5dfaf8 composer update (#2030) 2025-12-22 19:01:52 -05:00
killerbite95
576f04be58 fix: use correct log path for upload action (#2016)
Co-authored-by: Charles <charles@pelican.dev>
2025-12-22 19:01:44 -05:00
Boy132
43fb030133 Don't log yarn exceptions as error but warning (#2022) 2025-12-21 15:37:21 +01:00
Boy132
ae054f6e9b Fix actions when plugin is "errored" (#2027) 2025-12-21 15:37:07 +01:00
Boy132
fef91791c3 Fix plugin settings not showing on non-admin plugins (#2023) 2025-12-21 15:36:39 +01:00
Boy132
1d5ace3a6d Clear filament cache when installing a plugin (#2017) 2025-12-20 02:00:57 +01:00
Boy132
242a75bf3d Plugin system (#1866) 2025-12-20 00:32:13 +01:00
Charles
2ab4c81e2a Replace CodeEditor with MonacoEditor (#2013)
Co-authored-by: Boy132 <mail@boy132.de>
2025-12-19 18:31:55 -05:00
Boy132
5a47948a93 Use recipient language for database notifications (#2008) 2025-12-17 20:34:12 +01:00
Boy132
9d1e7f510f Add toggle for externally managed users (#1825) 2025-12-17 14:09:17 -05:00
hallo123wert
be55e75109 Fix: egg images are not loading (#2009) 2025-12-17 10:47:18 +01:00
Charles
8b5f33ee71 Change images from being stored in base64 to files (#1993)
Co-authored-by: Boy132 <mail@boy132.de>
2025-12-16 11:52:58 -05:00
DaNussi
014e866d0e Egg API Import/Delete (#1947)
Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
Co-authored-by: Boy132 <mail@boy132.de>
2025-12-16 06:28:12 -05:00
gOOvER
4a1ecb1adc changed docker panel restart to unless-stopped (#1995) 2025-12-15 12:11:21 -05:00
Michael (Parker) Parker
e2529ab436 Fix migrations in docker container (#1999) 2025-12-14 15:02:06 -05:00
Charles M
cd3f3a97ac Fix Docker build command in comments (#2003) 2025-12-14 14:22:36 -05:00
Charles
2f5790b121 Fix Egg Importer Upload File Type Filter (#2000) 2025-12-13 22:46:03 -05:00
Charles
59f0fe1959 Fix console duplicating with spa (#1990) 2025-12-13 21:49:58 -05:00
Charles
fdd9faaaa3 Fix schedule actions (#1992) 2025-12-12 18:31:46 -05:00
Boy132
9449d78144 Don't convert Windows-1252 encoding (#1991) 2025-12-13 00:15:45 +01:00
Charles
a391d21043 Fix progress bar max value in table view (#1989) 2025-12-12 16:55:09 -05:00
Quinten
b13fcfd644 Update paper egg to use their new domain (#1986)
Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2025-12-12 16:16:30 -05:00
Boy132
760aaf9bfb Refactor subuser permissions (#1961)
Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2025-12-11 14:34:27 +01:00
MartinOscar
1ab4ddb07c Fix File global search path & rename to nested search (#1985) 2025-12-11 13:48:34 +01:00
MartinOscar
f278041bc0 EditServer select_startup refactor (#1983) 2025-12-11 13:48:29 +01:00
Boy132
cdc928a15b Consolidate policies and use Subuser model for subuser resource (#1978) 2025-12-11 13:16:57 +01:00
MartinOscar
3939c409c1 Followup Stock Eggs #1973 (#1982) 2025-12-10 20:41:56 +01:00
MartinOscar
091ca5447a Fix CreateWebhookConfiguration HeaderActions (#1979) 2025-12-10 20:39:57 +01:00
JoanFo
57c4172c74 Fix settings Translation typo (#1981) 2025-12-10 19:56:17 +01:00
Charles
dfd6dbfe26 Update Stock Egg Images (#1973) 2025-12-09 17:53:07 -05:00
Charles
b4f331e4b2 composer update (#1972) 2025-12-09 17:09:06 -05:00
Charles
7a95712ed0 composer update (#1966) 2025-12-08 10:46:33 -05:00
MartinOscar
b6aeb954c4 Disable Captcha & Oauth Settings actions when read only (#1968) 2025-12-08 11:33:29 +01:00
MartinOscar
7c0d53c796 Use Policies rather then overriding can*() functions (#1837)
Co-authored-by: Boy132 <mail@boy132.de>
2025-12-07 14:53:13 -05:00
MartinOscar
71bd267166 Fix docker entrypoint ASSET_URL not APP_ASSET (#1965) 2025-12-06 20:54:40 +01:00
MartinOscar
25d8adbcc6 Add ignoreRecord to CopyFrom relationships (#1964) 2025-12-06 20:17:05 +01:00
Michael (Parker) Parker
27b896c6d2 Update docker image (#1917)
Co-authored-by: MartinOscar <40749467+rmartinoscar@users.noreply.github.com>
2025-12-05 22:50:49 -05:00
MartinOscar
bda2f9a699 Fix Save Notification icon & Cleanup (#1959) 2025-12-03 02:23:09 +01:00
Boy132
04375439d7 Add pagination to server list (#1955) 2025-12-02 08:26:45 +01:00
Boy132
0fe8917668 Only allow server transfers to accessible nodes (#1951) 2025-12-02 08:26:19 +01:00
Boy132
c312ef493f Replace file_get_contents with Http (#1953) 2025-12-02 08:25:53 +01:00
PalmarHealer
6c02f9a663 feat: Add toggle for automatic allocation creation in panel settings (#1884) 2025-12-01 08:59:07 +01:00
Charles
2dd6e3d4fc Add progress bars to client area (#1924) 2025-11-28 18:04:40 -05:00
Quinten
575e5bdb0d Fix typo in suspend method documentation (#1944) 2025-11-28 18:39:49 +01:00
Boy132
efa8eef57c Add custom render hooks to our footer (#1942) 2025-11-27 23:55:59 +01:00
MartinOscar
d16e7dd876 Better Role icons (#1936)
Fix `Role` class path for `::getNavigationIcon()`
Allow to register custom model icons
Co-authored-by: Boy132 <mail@boy132.de>
2025-11-27 23:51:57 +01:00
Charles
897b95ec13 Change Admin Actions to IconButtons (#1900) 2025-11-27 16:44:05 -05:00
MartinOscar
97f5a0f20b Fix Policies modelname are case sensitive (#1937) 2025-11-27 17:51:16 +01:00
MartinOscar
d0af45a0c7 Delete ssh keys shouldn't be a POST & Cleanup routes (#1934) 2025-11-27 16:26:47 +01:00
MartinOscar
78ab098d02 Fix Egg select_startup default & update state (#1933) 2025-11-27 16:26:40 +01:00
Charles
cdccca8fa2 composer update (#1928) 2025-11-24 15:34:33 -05:00
Boy132
bb33bcca4f Refactor schedule tasks (#1911) 2025-11-24 14:42:47 +01:00
Boy132
611b8649e0 Improve "first task" checks (#1926) 2025-11-24 00:48:32 +01:00
MartinOscar
b1b723485f Fix EditFiles breadcrumbs incorrect url (#1925) 2025-11-24 00:42:04 +01:00
hallo123wert
25c8ff3f1f Fix: No live preview for fonts (#1921) 2025-11-24 00:06:08 +01:00
Boy132
07763d912b Add back 2fa requirement middleware (#1897)
Co-authored-by: MartinOscar <40749467+rmartinoscar@users.noreply.github.com>
2025-11-24 00:01:29 +01:00
Charles
65bb99e2b0 Add server icons (#1906)
Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2025-11-21 16:48:20 -05:00
MartinOscar
a195b56f93 Fix permission checks on Client side (#1913) 2025-11-19 22:28:13 +01:00
Boy132
d78c977d75 Make sure to load FilamentServiceProvider before panel providers (#1907) 2025-11-17 11:41:11 +01:00
PalmarHealer
5e25ea4a43 fix: use port range on free allocation lookup (#1882)
Co-authored-by: MartinOscar <40749467+rmartinoscar@users.noreply.github.com>
Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2025-11-17 10:56:48 +01:00
Luke
886836c60a Remove 'required' rule from egg-garrys-mod.yaml (#1902) 2025-11-16 11:59:01 -05:00
Charles
f575e3edfa composer update (#1901) 2025-11-15 07:17:29 -05:00
Boy132
1a66b3fab4 Encode file contents to utf-8 (#1896) 2025-11-13 19:05:23 +01:00
Boy132
0f1efcfd15 Remove old update command (#1898) 2025-11-13 19:05:04 +01:00
PalmarHealer
3f89c6ddd8 fix: bypass tenant scoping in allocation queries (#1883) 2025-11-13 04:48:25 +00:00
mristau
20cb7850ef don't try to bulk update if egg doesn't even have a url (#1887) 2025-11-13 04:47:38 +00:00
hallo123wert
108dad09fb Fix: Duplicate bulk deletion notifications (#1881) 2025-11-13 04:46:55 +00:00
Boy132
445c9364bc Make sure case for role permissions is correct (#1892) 2025-11-11 18:18:29 +01:00
MartinOscar
acec117b1e Use public disk for console fonts upload (#1893) 2025-11-11 18:13:52 +01:00
Boy132
89199dfbe5 Fix jar mime type (#1891) 2025-11-11 11:23:56 +01:00
Boy132
216a3484f1 Fix node_ids rule for database host (#1885) 2025-11-10 12:25:58 +01:00
Boy132
5c3b0919aa Fix allocations by admins aren't locked by default (#1879) 2025-11-09 18:29:46 +01:00
Charles
f4ee33fa4f Hide new allocation action if server has 0 allocations. (#1878) 2025-11-09 12:11:14 -05:00
Charles
d8368c4cec Do no use stock notifications on actions (#1877) 2025-11-09 12:08:25 -05:00
Charles
aa35d7d001 Fix creating mounts (#1876) 2025-11-09 11:14:44 -05:00
JoanFo
3c25b43b46 Repair webhooks once again (#1815)
Co-authored-by: MartinOscar <40749467+rmartinoscar@users.noreply.github.com>
2025-11-09 09:35:00 -05:00
Charles
0891db5342 Reimplement Drag & Drop for file uploading 🎉 (#1858) 2025-11-09 09:24:12 -05:00
exefer
172436e012 Fix typo in failed upload message (#1874) 2025-11-09 12:58:56 +00:00
Charles
2b5403a4da Replace current panel log viewer with new and improved log viewer (#1834) 2025-11-08 19:31:51 -05:00
Charles
a30c45fbbe Add session key to use last used node, instead of latest created node (#1869)
Co-authored-by: Lance Pioch <git@lance.sh>
2025-11-08 17:09:41 -05:00
Copilot
b06df23823 Add bulk IP update action for node allocations (#1845)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: notAreYouScared <1757840+notAreYouScared@users.noreply.github.com>
Co-authored-by: Charles <charles@pelican.dev>
2025-11-08 16:53:12 -05:00
exefer
1ff965611e Fix typo in DNS help text (#1868)
Authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2025-11-08 22:40:23 +01:00
Boy132
cec141889a Allow admins to "lock" allocations (#1811) 2025-11-08 21:54:41 +01:00
Charles
6ed84b5584 Add wings diagnostics retrieving to Edit Node page (#1865)
Co-authored-by: Boy132 <mail@boy132.de>
2025-11-08 15:47:40 -05:00
Lance Pioch
49f24e37b6 Laravel 12.37.0 Shift (#1864)
Co-authored-by: Shift <shift@laravelshift.com>
2025-11-06 08:43:02 -05:00
Boy132
e0c4e47a6c Fix directAccessibleServers returning duplicates (#1862) 2025-11-05 16:19:03 +01:00
Boy132
4bda7cba75 Allow to "embed" server list (#1860) 2025-11-05 16:18:44 +01:00
Boy132
852f7beb39 Allow to register "special file" alert banners (#1861) 2025-11-04 12:48:18 +01:00
mristau
d61583cd7b add server description to grid view too (#1851) 2025-11-04 06:03:50 -05:00
Charles
21f9f259d0 Add Egg Images (#1849) 2025-11-03 12:32:11 -05:00
M41den
b2aff5445b Fix admin serverlist search (#1854) 2025-11-03 06:50:08 -05:00
Boy132
1f26750a2a Add api endpoint for updating username (#1826) 2025-11-03 08:31:07 +01:00
Charles
6d83c6d908 composer update (#1856) 2025-11-02 18:53:24 -05:00
Copilot
574a391e73 Add border-radius to activity log avatars (#1848)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: notAreYouScared <1757840+notAreYouScared@users.noreply.github.com>
2025-11-02 15:13:36 -05:00
PalmarHealer
605fcbe61a feat: Add mixed navigation type with admin-configurable defaults (#1850) 2025-10-31 14:12:54 -04:00
Letter N
0214b127e4 Add setup wizard to all oauth providers (#1801) 2025-10-31 14:09:20 -04:00
MartinOscar
e6aa76ef2c Refactor: add FilamentServiceProvider & globally make Select native(false) (#1836) 2025-10-29 23:23:18 +01:00
Boy132
d38075e3cb Add boolean cast to read_only toggle buttons (#1844) 2025-10-28 16:06:33 +01:00
M41den
0fec6adc3e Fix 500 "No route found" when creating db host (#1841) 2025-10-28 08:48:46 -04:00
M41den
5e3c22ea5e Fix weird postgres behavior when selecting mounts (#1842)
Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2025-10-28 08:48:35 -04:00
MartinOscar
d1a808a746 Hide User reset password Action on create Operation (#1840) 2025-10-28 01:38:37 +01:00
MartinOscar
3bcdeea800 Leverage user() helper (#1832) 2025-10-26 16:24:34 +01:00
Charles
e6bd6e416f Add archive extension selection (#1828) 2025-10-24 12:39:30 -04:00
Boy132
8e006ac32d Fix user permissions service (#1819) 2025-10-22 16:00:51 +02:00
Boy132
430f28a847 Add "cancel" button to profile (#1821) 2025-10-22 16:00:31 +02:00
Charles
1a4fa5e67a Replace Xtermjs canvas with webgl (#1807)
Co-authored-by: MartinOscar <40749467+rmartinoscar@users.noreply.github.com>
2025-10-14 20:35:26 -04:00
Charles
a65469b33b Remove duplicate translation entries (#1812) 2025-10-14 06:58:33 -04:00
Charles
d587cf3ee5 composer update (#1806) 2025-10-13 17:34:21 -04:00
Boy132
2cd9fa2cde Only keep the last 120 stored stats (#1805) 2025-10-13 22:50:16 +02:00
MartinOscar
d735e858a2 Rename Create actions in EditProfile (#1804) 2025-10-13 00:58:22 +02:00
MartinOscar
317fa46894 Use tenantMiddleware instead of manually fetching tenant query param (#1799) 2025-10-12 18:07:10 +02:00
Letter N
e589f972fb Add changelog preview when a new update is available (#1792)
Co-authored-by: Boy132 <mail@boy132.de>
2025-10-11 21:34:38 -04:00
MartinOscar
266e3779d5 Fix 500 when oauth is null (#1798) 2025-10-11 22:06:51 +02:00
MartinOscar
4652680a7b Add cpu helper on EditServer & move helperText to hintIcon on Create (#1795) 2025-10-10 22:46:47 +02:00
JoanFo
e99f7179c6 Topbar removed if using sidebar (#1789)
Co-authored-by: Boy132 <mail@boy132.de>
2025-10-10 16:37:14 -04:00
Charles
1f56b8e114 Language Update (#1784) 2025-10-08 16:00:47 -04:00
Charles
574e03a986 composer update (#1782) 2025-10-08 11:12:13 -04:00
Charles
05f3422dda Add Laravel/Filament Log Viewer (#1778)
Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2025-10-08 06:18:20 -04:00
Charles
dbe4bdd62d General Edit User Improvements (#1779)
Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
Co-authored-by: Boy132 <mail@boy132.de>
2025-10-08 05:04:52 -04:00
Boy132
f6710dbbe4 Improve time offset ux (#1772)
Co-authored-by: Lance Pioch <git@lance.sh>
2025-10-08 08:55:37 +02:00
Charles
e4f807b297 Change node config to use Code Entry (#1781) 2025-10-07 22:25:16 -04:00
Boy132
cd965678b7 Allow multiple startup commands per egg (#1656) 2025-10-07 23:42:28 +02:00
Boy132
a58ae874f3 Add own endpoint for exporting eggs (#1760) 2025-10-07 23:41:28 +02:00
Charles
432fb8a514 Filament v4.1.4 (#1780) 2025-10-07 17:40:26 -04:00
MartinOscar
bb02ec4c6c Add user() helper (#1768) 2025-10-07 17:12:31 -04:00
Charles
69b669e345 v4.1.2 + upgrade (#1775) 2025-10-06 06:20:18 -04:00
Boy132
80993f38a9 Add sudo to crontab command (#1773) 2025-10-03 00:03:22 +02:00
Boy132
19103b16b8 Allow both nodes for server requests when doing transfers (#1701) 2025-10-02 17:55:20 +02:00
Boy132
246997754e Remove "custom" email views (#1763) 2025-10-01 10:31:01 +02:00
Boy132
df75dbe2ad Fix mime type for jar files (#1757) 2025-10-01 10:30:49 +02:00
Charles
f02b58c320 Filament v4.1 (#1761) 2025-09-29 09:29:16 -04:00
Boy132
8aa0fc7fc2 Refresh page after file updates (#1759)
Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2025-09-29 15:26:17 +02:00
Boy132
2fc30e14fd Make sure default variable value is set and that variables are created when viewing server (#1758) 2025-09-29 15:14:18 +02:00
Charles
ec5fd3262a Add xtermjs Canvas (#1756) 2025-09-28 15:17:02 -04:00
Boy132
81178f81b4 Redirect to previous page when clicking "cancel" on EditFiles page (#1747) 2025-09-28 19:12:05 +02:00
Boy132
5373f1e30a Switch tenant slug back to short uuid (#1732) 2025-09-28 19:11:41 +02:00
Boy132
9f35f1c3ee Enable "ordered imports" (#1746) 2025-09-24 13:34:19 +02:00
MartinOscar
a5858a6d9b Allow clipboard.writeText without HTTPS (#1723) 2025-09-24 01:22:29 +02:00
MartinOscar
e3b3c92dcb Make tests fail-fast & common env (#1724) 2025-09-24 01:22:19 +02:00
Lance Pioch
42c84c2df5 Laravel 12.31.1 Shift (#1739)
Co-authored-by: Shift <shift@laravelshift.com>
Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2025-09-24 01:22:15 +02:00
Boy132
4792542f20 Fix refresh action for egg index select & add refresh action to allocation ip selects (#1736) 2025-09-23 14:56:49 +02:00
Boy132
bb40a5273f Url encode username in sftp connection string (#1731) 2025-09-22 12:58:54 +02:00
Boy132
e5c24fe8b6 Remove username rules and allow to change it in profile (#1702) 2025-09-21 00:37:42 +02:00
Boy132
c10280af4b Make allocation select on users server relation manager functional (#1719) 2025-09-19 08:43:29 +02:00
JoanFo
6db1d82738 Fixed webhooks on v4 and nested values (#1704)
Co-authored-by: MartinOscar <40749467+rmartinoscar@users.noreply.github.com>
2025-09-18 16:40:24 +02:00
MartinOscar
68f8244298 Fix powerActions visible while loading (#1708) 2025-09-18 16:22:23 +02:00
Boy132
ce393af7a6 Fix join_paths for absolute linux paths (#1715) 2025-09-17 12:35:20 +02:00
Boy132
932809fec5 Add state cast for server condition (#1713) 2025-09-16 21:34:23 +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
1668 changed files with 70573 additions and 21490 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

@@ -11,9 +11,9 @@ jobs:
name: UI name: UI
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: false fail-fast: true
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

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

View File

@@ -66,8 +66,6 @@ jobs:
permissions: permissions:
contents: read contents: read
packages: write packages: write
strategy:
fail-fast: false
# Start a temp local registry because workflow can not pull from localy loaded images # Start a temp local registry because workflow can not pull from localy loaded images
services: services:
registry: registry:
@@ -134,6 +132,11 @@ jobs:
docker push localhost:5000/base-php:arm64 docker push localhost:5000/base-php:arm64
rm base-php-arm64.tar base-php-amd64.tar rm base-php-arm64.tar base-php-amd64.tar
- name: Update version in config/app.php (tag)
if: "github.event_name == 'release' && github.event.action == 'published'"
run: |
sed -i "s/'version' => 'canary',/'version' => '${{ steps.build_info.outputs.version_tag }}',/" config/app.php
- name: Build and Push (tag) - name: Build and Push (tag)
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
if: "github.event_name == 'release' && github.event.action == 'published'" if: "github.event_name == 'release' && github.event.action == 'published'"

View File

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

2
.gitignore vendored
View File

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

View File

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

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

View File

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

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

View File

@@ -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 Illuminate\Support\Facades\Http;
use Symfony\Component\Yaml\Yaml;
class CheckEggUpdatesCommand extends Command class CheckEggUpdatesCommand extends Command
{ {
@@ -18,11 +21,12 @@ class CheckEggUpdatesCommand extends Command
try { try {
$this->check($egg, $exporterService); $this->check($egg, $exporterService);
} catch (Exception $exception) { } catch (Exception $exception) {
$this->error("{$egg->name}: Error ({$exception->getMessage()})"); $this->error("$egg->name: Error ({$exception->getMessage()})");
} }
} }
} }
/** @throws Exception */
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 +35,30 @@ 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 = Http::timeout(5)->connectTimeout(1)->get($egg->update_url);
$this->info("$egg->name: Up-to-date");
cache()->put("eggs.$egg->uuid.update", false, now()->addHour());
return; if ($remote->failed()) {
throw new Exception("HTTP request returned status code {$remote->status()}");
} }
$this->warn("$egg->name: Found update"); $remote = $remote->body();
cache()->put("eggs.$egg->uuid.update", true, now()->addHour()); $remote = $isYaml ? Yaml::parse($remote) : json_decode($remote, true);
unset($local['exported_at'], $remote['exported_at']);
$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

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

View File

@@ -6,6 +6,7 @@ use App\Traits\EnvironmentWriterTrait;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Kernel; use Illuminate\Contracts\Console\Kernel;
use Illuminate\Database\DatabaseManager; use Illuminate\Database\DatabaseManager;
use PDOException;
class DatabaseSettingsCommand extends Command class DatabaseSettingsCommand extends Command
{ {
@@ -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

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

View File

@@ -5,6 +5,7 @@ namespace App\Console\Commands\Maintenance;
use App\Models\Backup; use App\Models\Backup;
use Carbon\CarbonImmutable; use Carbon\CarbonImmutable;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use InvalidArgumentException;
class PruneOrphanedBackupsCommand extends Command class PruneOrphanedBackupsCommand extends Command
{ {
@@ -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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,12 +3,12 @@
namespace App\Console\Commands\Server; namespace App\Console\Commands\Server;
use App\Models\Server; use App\Models\Server;
use App\Repositories\Daemon\DaemonServerRepository;
use Exception;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Validation\ValidationException;
use Illuminate\Validation\Factory as ValidatorFactory; use Illuminate\Validation\Factory as ValidatorFactory;
use App\Repositories\Daemon\DaemonPowerRepository; use Illuminate\Validation\ValidationException;
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

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

View File

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

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,9 +2,10 @@
namespace App\Console\Commands\User; namespace App\Console\Commands\User;
use App\Exceptions\Model\DataValidationException;
use App\Services\Users\UserCreationService;
use Exception; use Exception;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use App\Services\Users\UserCreationService;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
class MakeUserCommand extends Command class MakeUserCommand extends Command
@@ -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

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

View File

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

View File

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

@@ -2,6 +2,7 @@
namespace App\Enums; namespace App\Enums;
use BackedEnum;
use Filament\Support\Contracts\HasColor; use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasIcon; use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel; use Filament\Support\Contracts\HasLabel;
@@ -23,20 +24,20 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
// HTTP Based // HTTP Based
case Missing = 'missing'; case Missing = 'missing';
public function getIcon(): string public function getIcon(): BackedEnum
{ {
return match ($this) { return match ($this) {
self::Created => 'tabler-heart-plus', self::Created => TablerIcon::HeartPlus,
self::Starting => 'tabler-heart-up', self::Starting => TablerIcon::HeartUp,
self::Running => 'tabler-heartbeat', self::Running => TablerIcon::Heartbeat,
self::Restarting => 'tabler-heart-bolt', self::Restarting => TablerIcon::HeartBolt,
self::Exited => 'tabler-heart-exclamation', self::Exited => TablerIcon::HeartExclamation,
self::Paused => 'tabler-heart-pause', self::Paused => TablerIcon::HeartPause,
self::Dead, self::Offline => 'tabler-heart-x', self::Dead, self::Offline => TablerIcon::HeartX,
self::Removing => 'tabler-heart-down', self::Removing => TablerIcon::HeartDown,
self::Missing => 'tabler-heart-search', self::Missing => TablerIcon::HeartSearch,
self::Stopping => 'tabler-heart-minus', self::Stopping => TablerIcon::HeartMinus,
}; };
} }
@@ -68,7 +69,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,9 @@
<?php
namespace App\Enums;
enum CustomRenderHooks: string
{
case FooterStart = 'pelican::footer.start';
case FooterEnd = 'pelican::footer.end';
}

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -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

@@ -2,28 +2,26 @@
namespace App\Enums; namespace App\Enums;
use BackedEnum;
use Filament\Support\Contracts\HasColor; use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasIcon; use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel; use Filament\Support\Contracts\HasLabel;
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';
case Suspended = 'suspended'; case Suspended = 'suspended';
case RestoringBackup = 'restoring_backup'; case RestoringBackup = 'restoring_backup';
public function getIcon(): string public function getIcon(): BackedEnum
{ {
return match ($this) { return match ($this) {
self::Normal => 'tabler-heart', self::Installing => TablerIcon::HeartBolt,
self::Installing => 'tabler-heart-bolt', self::InstallFailed, self::ReinstallFailed => TablerIcon::HeartX,
self::InstallFailed => 'tabler-heart-x', self::Suspended => TablerIcon::HeartCancel,
self::ReinstallFailed => 'tabler-heart-x', self::RestoringBackup => TablerIcon::HeartUp,
self::Suspended => 'tabler-heart-cancel',
self::RestoringBackup => 'tabler-heart-up',
}; };
} }
@@ -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';
}

View File

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

View File

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

View File

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

5997
app/Enums/TablerIcon.php Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Events\User;
use App\Events\Event;
use App\Models\User;
use Illuminate\Queue\SerializesModels;
class Deleting extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public User $user) {}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace App\Events\User;
use App\Models\User;
use Illuminate\Foundation\Events\Dispatchable;
final class PasswordChanged
{
use Dispatchable;
public function __construct(public readonly User $user) {}
}

View File

@@ -4,13 +4,14 @@ namespace App\Exceptions;
use Exception; use Exception;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Illuminate\Container\Container;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Psr\Log\LoggerInterface;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Illuminate\Container\Container; use Psr\Log\LoggerInterface;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Throwable;
/** /**
* @deprecated * @deprecated
@@ -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,24 +2,27 @@
namespace App\Exceptions; namespace App\Exceptions;
use Illuminate\Support\Arr; use Exception;
use Illuminate\Support\Str; use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse; use Illuminate\Auth\AuthenticationException;
use Illuminate\Support\Collection;
use Illuminate\Container\Container; use Illuminate\Container\Container;
use Illuminate\Database\Connection; use Illuminate\Database\Connection;
use Illuminate\Http\RedirectResponse;
use Illuminate\Foundation\Application;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Session\TokenMismatchException;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Database\Eloquent\ModelNotFoundException;
use Symfony\Component\HttpKernel\Exception\HttpException; use Illuminate\Foundation\Application;
use Symfony\Component\Mailer\Exception\TransportException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Session\TokenMismatchException;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use PDOException;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\Mailer\Exception\TransportException;
use Throwable; use Throwable;
class Handler extends ExceptionHandler class Handler extends ExceptionHandler
@@ -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

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

View File

@@ -5,6 +5,7 @@ namespace App\Exceptions\Http\Server;
use App\Enums\ServerState; use App\Enums\ServerState;
use App\Models\Server; use App\Models\Server;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException; use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
use Throwable;
class ServerStateConflictException extends ConflictHttpException class ServerStateConflictException extends ConflictHttpException
{ {
@@ -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,14 +0,0 @@
<?php
namespace App\Exceptions;
use Spatie\Ignition\Contracts\Solution;
use Spatie\Ignition\Contracts\ProvidesSolution;
class ManifestDoesNotExistException extends \Exception implements ProvidesSolution
{
public function getSolution(): Solution
{
return new Solutions\ManifestDoesNotExistSolution();
}
}

View File

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

View File

@@ -2,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;
use Exception;
class PluginIdMismatchException extends Exception {}

View File

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

View File

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

View File

@@ -2,15 +2,16 @@
namespace App\Extensions\Backups; namespace App\Extensions\Backups;
use Closure; use App\Extensions\Filesystem\S3Filesystem;
use Aws\S3\S3Client; use Aws\S3\S3Client;
use Closure;
use Illuminate\Foundation\Application;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Webmozart\Assert\Assert; use InvalidArgumentException;
use Illuminate\Foundation\Application;
use League\Flysystem\FilesystemAdapter; use League\Flysystem\FilesystemAdapter;
use App\Extensions\Filesystem\S3Filesystem;
use League\Flysystem\InMemory\InMemoryFilesystemAdapter; use League\Flysystem\InMemory\InMemoryFilesystemAdapter;
use Webmozart\Assert\Assert;
class BackupManager class BackupManager
{ {
@@ -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

@@ -2,8 +2,8 @@
namespace App\Extensions\Captcha\Schemas; namespace App\Extensions\Captcha\Schemas;
use Filament\Forms\Components\Component;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Component;
use Illuminate\Support\Str; use Illuminate\Support\Str;
abstract class BaseSchema abstract class BaseSchema
@@ -56,9 +56,4 @@ abstract class BaseSchema
->default(env("CAPTCHA_{$id}_SECRET_KEY")), ->default(env("CAPTCHA_{$id}_SECRET_KEY")),
]; ];
} }
public function verifyDomain(string $hostname, ?string $requestUrl = null): bool
{
return true;
}
} }

View File

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

View File

@@ -4,6 +4,7 @@ namespace App\Extensions\Captcha\Schemas\Turnstile;
use App\Extensions\Captcha\CaptchaService; use App\Extensions\Captcha\CaptchaService;
use Closure; use Closure;
use Exception;
use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
@@ -11,10 +12,12 @@ class Rule implements ValidationRule
{ {
public function validate(string $attribute, mixed $value, Closure $fail): void public function validate(string $attribute, mixed $value, Closure $fail): void
{ {
$response = App::call(fn (CaptchaService $service) => $service->getActiveSchema()->validateResponse($value)); try {
App::call(fn (CaptchaService $service) => $service->get('turnstile')->validateResponse($value));
} catch (Exception $exception) {
report($exception);
if (!$response['success']) { $fail('Captcha validation failed: ' . $exception->getMessage());
$fail($response['message'] ?? 'Unknown error occurred, please try again');
} }
} }
} }

View File

@@ -2,12 +2,13 @@
namespace App\Extensions\Captcha\Schemas\Turnstile; namespace App\Extensions\Captcha\Schemas\Turnstile;
use App\Extensions\Captcha\Schemas\CaptchaSchemaInterface; use App\Enums\TablerIcon;
use App\Extensions\Captcha\Schemas\BaseSchema; use App\Extensions\Captcha\Schemas\BaseSchema;
use App\Extensions\Captcha\Schemas\CaptchaSchemaInterface;
use BackedEnum;
use Exception; use Exception;
use Filament\Forms\Components\Component as BaseComponent;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Toggle; use Filament\Forms\Components\Toggle;
use Filament\Infolists\Components\TextEntry;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\HtmlString; use Illuminate\Support\HtmlString;
@@ -23,7 +24,7 @@ class TurnstileSchema extends BaseSchema implements CaptchaSchemaInterface
return env('CAPTCHA_TURNSTILE_ENABLED', false); return env('CAPTCHA_TURNSTILE_ENABLED', false);
} }
public function getFormComponent(): BaseComponent public function getFormComponent(): Component
{ {
return Component::make('turnstile'); return Component::make('turnstile');
} }
@@ -39,7 +40,9 @@ class TurnstileSchema extends BaseSchema implements CaptchaSchemaInterface
} }
/** /**
* @return BaseComponent[] * @return \Filament\Support\Components\Component[]
*
* @throws Exception
*/ */
public function getSettingsForm(): array public function getSettingsForm(): array
{ {
@@ -48,27 +51,27 @@ class TurnstileSchema extends BaseSchema implements CaptchaSchemaInterface
->label(trans('admin/setting.captcha.verify')) ->label(trans('admin/setting.captcha.verify'))
->columnSpan(2) ->columnSpan(2)
->inline(false) ->inline(false)
->onIcon('tabler-check') ->onIcon(TablerIcon::Check)
->offIcon('tabler-x') ->offIcon(TablerIcon::X)
->onColor('success') ->onColor('success')
->offColor('danger') ->offColor('danger')
->default(env('CAPTCHA_TURNSTILE_VERIFY_DOMAIN', true)), ->default(env('CAPTCHA_TURNSTILE_VERIFY_DOMAIN', true)),
Placeholder::make('info') TextEntry::make('info')
->label(trans('admin/setting.captcha.info_label')) ->label(trans('admin/setting.captcha.info_label'))
->columnSpan(2) ->columnSpan(2)
->content(new HtmlString(trans('admin/setting.captcha.info'))), ->state(new HtmlString(trans('admin/setting.captcha.info'))),
]); ]);
} }
public function getIcon(): ?string public function getIcon(): BackedEnum
{ {
return 'tabler-brand-cloudflare'; return TablerIcon::BrandCloudflare;
} }
/** /**
* @return array<string, string|bool> * @throws Exception
*/ */
public function validateResponse(?string $captchaResponse = null): array public function validateResponse(?string $captchaResponse = null): void
{ {
$captchaResponse ??= request()->get('cf-turnstile-response'); $captchaResponse ??= request()->get('cf-turnstile-response');
@@ -83,22 +86,33 @@ class TurnstileSchema extends BaseSchema implements CaptchaSchemaInterface
->post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [ ->post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [
'secret' => $secret, 'secret' => $secret,
'response' => $captchaResponse, 'response' => $captchaResponse,
]); ])
->json();
return count($response->json()) ? $response->json() : [ if (!$response['success']) {
'success' => false, match ($response['error-codes'][0] ?? null) {
'message' => 'Unknown error occurred, please try again', '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.');
}
} }
public function verifyDomain(string $hostname, ?string $requestUrl = null): bool private function verifyDomain(string $hostname): bool
{ {
if (!env('CAPTCHA_TURNSTILE_VERIFY_DOMAIN', true)) { if (!env('CAPTCHA_TURNSTILE_VERIFY_DOMAIN', true)) {
return true; return true;
} }
$requestUrl ??= request()->url; $requestUrl = parse_url(request()->url());
$requestUrl = parse_url($requestUrl);
return $hostname === array_get($requestUrl, 'host'); return $hostname === array_get($requestUrl, 'host');
} }

View File

@@ -2,18 +2,19 @@
namespace App\Extensions\Features\Schemas; namespace App\Extensions\Features\Schemas;
use App\Enums\SubuserPermission;
use App\Enums\TablerIcon;
use App\Extensions\Features\FeatureSchemaInterface; use App\Extensions\Features\FeatureSchemaInterface;
use App\Facades\Activity; use App\Facades\Activity;
use App\Models\Permission;
use App\Models\Server; use App\Models\Server;
use App\Models\ServerVariable; use App\Models\ServerVariable;
use App\Repositories\Daemon\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\Support\Facades\Blade; use Illuminate\Support\Facades\Blade;
@@ -36,6 +37,9 @@ class GSLTokenSchema implements FeatureSchemaInterface
return 'gsl_token'; return 'gsl_token';
} }
/**
* @throws Exception
*/
public function getAction(): Action public function getAction(): Action
{ {
/** @var Server $server */ /** @var Server $server */
@@ -51,9 +55,9 @@ class GSLTokenSchema implements FeatureSchemaInterface
->modalHeading('Invalid GSL token') ->modalHeading('Invalid GSL token')
->modalDescription('It seems like your Gameserver Login Token (GSL token) is invalid or has expired.') ->modalDescription('It seems like your Gameserver Login Token (GSL token) is invalid or has expired.')
->modalSubmitActionLabel('Update GSL Token') ->modalSubmitActionLabel('Update GSL Token')
->disabledForm(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_UPDATE, $server)) ->disabledSchema(fn () => !user()?->can(SubuserPermission::StartupUpdate, $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')
@@ -70,13 +74,12 @@ class GSLTokenSchema implements FeatureSchemaInterface
} }
}, },
]) ])
->hintIcon('tabler-code') ->hintIcon(TablerIcon::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 {
@@ -98,7 +101,7 @@ class GSLTokenSchema implements FeatureSchemaInterface
->log(); ->log();
} }
$powerRepository->setServer($server)->send('restart'); $serverRepository->setServer($server)->power('restart');
Notification::make() Notification::make()
->title('GSL Token updated') ->title('GSL Token updated')

View File

@@ -2,16 +2,16 @@
namespace App\Extensions\Features\Schemas; namespace App\Extensions\Features\Schemas;
use App\Enums\SubuserPermission;
use App\Extensions\Features\FeatureSchemaInterface; use App\Extensions\Features\FeatureSchemaInterface;
use App\Facades\Activity; use App\Facades\Activity;
use App\Models\Permission;
use App\Models\Server; use App\Models\Server;
use App\Repositories\Daemon\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;
class JavaVersionSchema implements FeatureSchemaInterface class JavaVersionSchema implements FeatureSchemaInterface
@@ -44,9 +44,9 @@ class JavaVersionSchema implements FeatureSchemaInterface
->modalHeading('Unsupported Java Version') ->modalHeading('Unsupported Java Version')
->modalDescription('This server is currently running an unsupported version of Java and cannot be started.') ->modalDescription('This server is currently running an unsupported version of Java and cannot be started.')
->modalSubmitActionLabel('Update Docker Image') ->modalSubmitActionLabel('Update Docker Image')
->disabledForm(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_DOCKER_IMAGE, $server)) ->disabledSchema(fn () => !user()?->can(SubuserPermission::StartupDockerImage, $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')
@@ -56,10 +56,9 @@ class JavaVersionSchema implements FeatureSchemaInterface
->default(fn () => $server->image) ->default(fn () => $server->image)
->notIn(fn () => $server->image) ->notIn(fn () => $server->image)
->required() ->required()
->preload() ->preload(),
->native(false),
]) ])
->action(function (array $data, 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;
@@ -71,7 +70,7 @@ class JavaVersionSchema implements FeatureSchemaInterface
->log(); ->log();
} }
$powerRepository->setServer($server)->send('restart'); $serverRepository->setServer($server)->power('restart');
Notification::make() Notification::make()
->title('Docker image updated') ->title('Docker image updated')

View File

@@ -5,7 +5,7 @@ namespace App\Extensions\Features\Schemas;
use App\Extensions\Features\FeatureSchemaInterface; use App\Extensions\Features\FeatureSchemaInterface;
use App\Models\Server; use App\Models\Server;
use App\Repositories\Daemon\DaemonFileRepository; use App\Repositories\Daemon\DaemonFileRepository;
use App\Repositories\Daemon\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;
@@ -35,14 +35,14 @@ class MinecraftEulaSchema implements FeatureSchemaInterface
->modalHeading('Minecraft EULA') ->modalHeading('Minecraft EULA')
->modalDescription(new HtmlString(Blade::render('By pressing "I Accept" below you are indicating your agreement to the <x-filament::link href="https://minecraft.net/eula" target="_blank">Minecraft EULA </x-filament::link>.'))) ->modalDescription(new HtmlString(Blade::render('By pressing "I Accept" below you are indicating your agreement to the <x-filament::link href="https://minecraft.net/eula" target="_blank">Minecraft EULA </x-filament::link>.')))
->modalSubmitActionLabel('I Accept') ->modalSubmitActionLabel('I Accept')
->action(function (DaemonFileRepository $fileRepository, 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')

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ use App\Models\ApiKey;
use Laravel\Sanctum\NewAccessToken as SanctumAccessToken; use Laravel\Sanctum\NewAccessToken as SanctumAccessToken;
/** /**
* @property \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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,13 +2,14 @@
namespace App\Extensions\OAuth\Schemas; namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\Placeholder; use App\Enums\TablerIcon;
use BackedEnum;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step; use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString; use Illuminate\Support\HtmlString;
use SocialiteProviders\Discord\Provider; use SocialiteProviders\Discord\Provider;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
final class DiscordSchema extends OAuthSchema final class DiscordSchema extends OAuthSchema
{ {
@@ -27,23 +28,25 @@ final class DiscordSchema extends OAuthSchema
return array_merge([ return array_merge([
Step::make('Register new Discord OAuth App') Step::make('Register new Discord OAuth App')
->schema([ ->schema([
Placeholder::make('') TextEntry::make('create_application')
->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>'))), ->hiddenLabel()
Placeholder::make('') ->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>'))),
->content(new HtmlString('<p>Under <b>Redirects</b> add the below URL.</p>')), TextEntry::make('set_redirect')
->hiddenLabel()
->state(new HtmlString('<p>Under <b>Redirects</b> add the below URL.</p>')),
TextInput::make('_noenv_callback') TextInput::make('_noenv_callback')
->label('Redirect URL') ->label('Redirect URL')
->dehydrated() ->dehydrated()
->disabled() ->disabled()
->hintAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null) ->hintCopy()
->formatStateUsing(fn () => url('/auth/oauth/callback/discord')), ->formatStateUsing(fn () => url('/auth/oauth/callback/discord')),
]), ]),
], parent::getSetupSteps()); ], parent::getSetupSteps());
} }
public function getIcon(): string public function getIcon(): BackedEnum
{ {
return 'tabler-brand-discord-f'; return TablerIcon::BrandDiscordFilled;
} }
public function getHexColor(): string public function getHexColor(): string

View File

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

View File

@@ -2,12 +2,13 @@
namespace App\Extensions\OAuth\Schemas; namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\Placeholder; use App\Enums\TablerIcon;
use BackedEnum;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step; use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString; use Illuminate\Support\HtmlString;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
final class GithubSchema extends OAuthSchema final class GithubSchema extends OAuthSchema
{ {
@@ -19,30 +20,33 @@ final class GithubSchema extends OAuthSchema
public function getSetupSteps(): array public function getSetupSteps(): array
{ {
return array_merge([ return array_merge([
Step::make('Register new Github OAuth App') Step::make('Register new GitHub OAuth App')
->schema([ ->schema([
Placeholder::make('') TextEntry::make('create_application')
->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>'))), ->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') TextInput::make('_noenv_callback')
->label('Authorization callback URL') ->label('Authorization callback URL')
->dehydrated() ->dehydrated()
->disabled() ->disabled()
->hintAction(fn (string $state) => request()->isSecure() ? CopyAction::make()->copyable($state) : null) ->hintCopy()
->default(fn () => url('/auth/oauth/callback/github')), ->default(fn () => url('/auth/oauth/callback/github')),
Placeholder::make('') TextEntry::make('register_application')
->content(new HtmlString('<p>When you filled all fields click on <b>Register application</b>.</p>')), ->hiddenLabel()
->state(new HtmlString('<p>When you filled all fields click on <b>Register application</b>.</p>')),
]), ]),
Step::make('Create Client Secret') Step::make('Create Client Secret')
->schema([ ->schema([
Placeholder::make('') TextEntry::make('create_client_secret')
->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>')), ->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()); ], parent::getSetupSteps());
} }
public function getIcon(): string public function getIcon(): BackedEnum
{ {
return 'tabler-brand-github-f'; return TablerIcon::BrandGithubFilled;
} }
public function getHexColor(): string public function getHexColor(): string

View File

@@ -2,12 +2,13 @@
namespace App\Extensions\OAuth\Schemas; namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\Placeholder; use App\Enums\TablerIcon;
use BackedEnum;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step; use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString; use Illuminate\Support\HtmlString;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
final class GitlabSchema extends OAuthSchema final class GitlabSchema extends OAuthSchema
{ {
@@ -41,21 +42,22 @@ final class GitlabSchema extends OAuthSchema
return array_merge([ return array_merge([
Step::make('Register new Gitlab OAuth App') Step::make('Register new Gitlab OAuth App')
->schema([ ->schema([
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());
} }
public function getIcon(): string public function getIcon(): BackedEnum
{ {
return 'tabler-brand-gitlab'; return TablerIcon::BrandGitlab;
} }
public function getHexColor(): string public function getHexColor(): string

View File

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

View File

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

View File

@@ -2,11 +2,17 @@
namespace App\Extensions\OAuth\Schemas; namespace App\Extensions\OAuth\Schemas;
use App\Enums\TablerIcon;
use App\Extensions\OAuth\OAuthSchemaInterface; use App\Extensions\OAuth\OAuthSchemaInterface;
use Filament\Forms\Components\Component; use App\Models\User;
use BackedEnum;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step; use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Laravel\Socialite\Contracts\User as OAuthUser;
abstract class OAuthSchema implements OAuthSchemaInterface abstract class OAuthSchema implements OAuthSchemaInterface
{ {
@@ -53,6 +59,28 @@ abstract class OAuthSchema implements OAuthSchemaInterface
->revealable() ->revealable()
->autocomplete(false) ->autocomplete(false)
->default(env("OAUTH_{$id}_CLIENT_SECRET")), ->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(TablerIcon::Check)
->offIcon(TablerIcon::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(TablerIcon::Check)
->offIcon(TablerIcon::X)
->onColor('success')
->offColor('danger')
->formatStateUsing(fn ($state) => (bool) $state)
->afterStateUpdated(fn ($state, Set $set) => $set("OAUTH_{$id}_SHOULD_LINK_MISSING_USERS", (bool) $state))
->default(env("OAUTH_{$id}_SHOULD_LINK_MISSING_USERS")),
]; ];
} }
@@ -80,7 +108,7 @@ abstract class OAuthSchema implements OAuthSchemaInterface
return "OAUTH_{$id}_ENABLED"; return "OAUTH_{$id}_ENABLED";
} }
public function getIcon(): ?string public function getIcon(): null|string|BackedEnum
{ {
return null; return null;
} }
@@ -91,9 +119,21 @@ abstract class OAuthSchema implements OAuthSchemaInterface
} }
public function isEnabled(): bool public function isEnabled(): bool
{
return env($this->getConfigKey(), false);
}
public function shouldCreateMissingUser(OAuthUser $user): bool
{ {
$id = Str::upper($this->getId()); $id = Str::upper($this->getId());
return env("OAUTH_{$id}_ENABLED", false); return env("OAUTH_{$id}_SHOULD_CREATE_MISSING_USERS", false);
}
public function shouldLinkMissingUser(User $user, OAuthUser $oauthUser): bool
{
$id = Str::upper($this->getId());
return env("OAUTH_{$id}_SHOULD_LINK_MISSING_USERS", false);
} }
} }

View File

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

View File

@@ -2,9 +2,11 @@
namespace App\Extensions\OAuth\Schemas; namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\Placeholder; use App\Enums\TablerIcon;
use BackedEnum;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step; use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString; use Illuminate\Support\HtmlString;
use SocialiteProviders\Steam\Provider; use SocialiteProviders\Steam\Provider;
@@ -52,15 +54,16 @@ final class SteamSchema extends OAuthSchema
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());
} }
public function getIcon(): string public function getIcon(): BackedEnum
{ {
return 'tabler-brand-steam-f'; return TablerIcon::BrandSteamFilled;
} }
public function getHexColor(): string public function getHexColor(): string

View File

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

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