Compare commits

...

320 Commits

Author SHA1 Message Date
RMartinOscar
1d6f490c77 5min timeout 2025-09-30 16:41:00 +00:00
MartinOscar
e2851f28ca Rename shift to shift.yaml 2025-09-30 15:35:57 +02:00
MartinOscar
de14e54931 Create shift workflow 2025-09-30 15:31:47 +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
Boy132
556551b4f3 Add SSH Keys to Profile (#1478) 2025-07-06 22:51:45 +02:00
Boy132
23ddded61e Replace gethostbynamel with dns_get_record (#1479) 2025-07-06 22:42:59 +02:00
JoanFo
c5aa8a3980 DiscordWebhooks (#1355)
Co-authored-by: notCharles <charles@pelican.dev>
Co-authored-by: Lance Pioch <lancepioch@gmail.com>
Co-authored-by: Boy132 <mail@boy132.de>
Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2025-07-05 12:42:34 -04:00
MartinOscar
21ac75efae Nullable eggFeatures in FeatureService (#1485) 2025-07-05 14:57:08 +02:00
JoanFo
9655700cde Nullable allocation in server-entry blade² (#1486) 2025-07-05 14:25:33 +02:00
JoanFo
c9b7e979c0 Nullable allocation in server-entry blade (#1484) 2025-07-05 14:14:43 +02:00
MartinOscar
77a3b0640d Add dehydratedWhenHidden to serverVariable TextInput & Select (#1476) 2025-07-03 08:55:18 +02:00
pelican-vehikl
de4cb38766 Refactor Providers to be a singleton (#1327) 2025-07-01 21:33:11 -04:00
Charles
74bd7f9991 Move console js to built app.js file. (#1471) 2025-07-01 17:13:44 -04:00
Charles
ba7f814300 back port power actions from v4 branch (#1470) 2025-06-28 10:41:16 -04:00
MartinOscar
cdcd1c521e Add FileExistsException & Fix error reporting (#1417) 2025-06-26 21:04:33 +02:00
Boy132
4d0aabe91e Schedule task improvements (#1468) 2025-06-26 17:00:37 +02:00
Boy132
68f72b9b4d Add "egg index" and dropdown to egg importer (#1451)
Co-authored-by: notCharles <charles@pelican.dev>
2025-06-25 19:50:09 -04:00
JoanFo
dca37ccc95 Server Without Allocations (#1432)
Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2025-06-25 19:49:43 -04:00
Charles
6a088d0c4f Tweak Grid View, Use Memory Limit, not wings reported allocation (#1462) 2025-06-25 19:49:00 -04:00
Walter van der Broek
7731f16b0f Fix: Search for tags in correct variable (#1461) 2025-06-25 19:48:39 -04:00
Lance Pioch
9a1e7de4ae Laravel 12.19.3 Shift (#1455)
Co-authored-by: Shift <shift@laravelshift.com>
2025-06-22 15:46:29 -04:00
pelican-vehikl
c61b6920b9 Fix some tests (#1450) 2025-06-19 21:36:50 +02:00
Boy132
6107524522 Trait-ify resources and add customizable options (#1396) 2025-06-19 18:24:25 +02:00
Boy132
57a13a2701 Refactor admin dashboard widgets to use forms (#1452) 2025-06-19 18:23:32 +02:00
Boy132
4dd414ad87 Delete old csgo egg (#1448) 2025-06-19 18:18:06 +02:00
Boy132
0156ac1509 Role icons: Use correct capitalization for class names (#1447) 2025-06-12 20:27:02 +02:00
MartinOscar
387471716b Fully remove the filament-context-menu package (#1449) 2025-06-12 20:26:39 +02:00
Boy132
1dc5ec027e Cleanup & fix server list (#1433) 2025-06-12 08:54:00 +02:00
MartinOscar
b05eabfdb0 Fix Users seeing Open in admin (#1444) 2025-06-11 03:51:08 +02:00
Lance Pioch
3039c1c698 Laravel 12.18.0 Shift (#1443)
Co-authored-by: Shift <shift@laravelshift.com>
2025-06-10 21:48:21 -04:00
MartinOscar
de166bca03 Use supervisorctl instead of systemctl when running in docker (#1378) 2025-06-08 09:12:15 +02:00
JoanFo
af609994b6 Fix missing font (#1404)
Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2025-06-08 09:11:56 +02:00
Boy132
bd2a00760d Fix error handling for deleting backups (#1434) 2025-06-07 14:16:01 +02:00
pelican-vehikl
65deffc6e6 Create new description endpoint (#1136)
Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2025-06-06 23:06:28 -04:00
Boy132
34865d4288 Fix hostname env variable name in rust egg (#1435) 2025-06-06 14:19:09 +02:00
MartinOscar
2961c3e88b Refactor EnvironmentTrait to use Env Facade (#1430) 2025-06-04 22:24:17 +02:00
MartinOscar
e7a950ffcb Replace $allocation->toString() with $allocation->address (#1431) 2025-06-04 22:13:59 +02:00
Lance Pioch
ece732d9e5 Laravel 12.17.0 Shift (#1429)
Co-authored-by: Shift <shift@laravelshift.com>
2025-06-04 15:06:54 -04:00
Boy132
456c4f46bc Make sure daemon_listen and daemon_connect match when not behind proxy (#1428) 2025-06-04 08:37:04 +02:00
Boy132
0ba497a2eb Add separate port field for node connections (#1423) 2025-06-03 14:33:57 +02:00
Boy132
3b744f37dd Lazy load server entries (Grid only) (#1413) 2025-06-03 14:33:43 +02:00
Charles
b34778f736 Refactor Node Stats (#1145)
Co-authored-by: Boy132 <mail@boy132.de>
2025-06-03 07:33:08 -04:00
MartinOscar
84c351d0ae Deselect records for ListFiles DeleteAction (#1411) 2025-05-31 17:48:17 +02:00
MartinOscar
520cea7f09 Use translation for ListFiles DeleteAction (#1410) 2025-05-31 17:48:00 +02:00
Boy132
35ce1d34ab Permission check fixes (#1406) 2025-05-27 19:30:30 +02:00
Boy132
17555a1d09 Make server name and server address clickable (and copyable) (#1395) 2025-05-27 19:30:07 +02:00
Lance Pioch
837121b1fb Laravel 12.16.0 Shift (#1408)
Co-authored-by: Shift <shift@laravelshift.com>
2025-05-27 13:08:51 -04:00
Boy132
af9f2c653e Add missing </div> to monaco editor view (#1399) 2025-05-23 06:02:29 -04:00
Boy132
c22e7456b5 Move tables & forms to resources in client area (#1388) 2025-05-22 08:41:17 +02:00
Boy132
97fb66f5d6 Use app panel for password link in AccountCreated notification (#1389) 2025-05-21 08:46:27 +02:00
Lance Pioch
51037c5c20 Laravel 12.15.0 Shift (#1390)
Co-authored-by: Shift <shift@laravelshift.com>
2025-05-20 16:32:43 -04:00
MartinOscar
23d13d9e83 Fix Mount translation (#1382) 2025-05-20 11:58:16 -04:00
Boy132
6c20426757 Put whereHas-orDoesntHave in own where (#1387) 2025-05-20 08:33:33 +02:00
Boy132
1224210668 Only include "server" subjects in activity log query (#1386) 2025-05-20 08:33:16 +02:00
Boy132
258c97bf14 Add missing auth activity logs (#1372) 2025-05-19 09:12:58 +02:00
C0D3 M4513R
7034c4d013 Fix Composer warnings (#1376)
Co-authored-by: MartinOscar <40749467+rmartinoscar@users.noreply.github.com>
2025-05-15 14:39:59 -05:00
MartinOscar
e5cba893e4 Check against 2fa backup codes too in Login (#1366)
Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2025-05-12 16:14:09 +02:00
Boy132
fd49f472c3 Remove packs folders in storage (#1367) 2025-05-12 14:30:16 +02:00
MartinOscar
c8556a4c56 Use placeholder for EditServer db_delete (#1362) 2025-05-10 00:01:58 +02:00
MartinOscar
6de6306a19 Fix GSLToken id, label & query (#1361) 2025-05-09 17:57:18 -04:00
Charles
1f8a5cdd1d Fix font dropdown on EditProfile Page (#1360) 2025-05-09 17:42:39 -04:00
Charles
30ae860d69 Fix server notification body translation key (#1359) 2025-05-09 17:39:15 -04:00
Boy132
f400e2db76 Fix TRUSTED_PROXIES with * (#1358) 2025-05-09 16:22:33 -04:00
Boy132
1f7562563a Use github error format for phpstan tests (#1357) 2025-05-09 21:03:50 +02:00
Boy132
2296e41a8b Add button to view install logs (#1356)
Co-authored-by: notCharles <charles@pelican.dev>
2025-05-09 21:03:32 +02:00
MartinOscar
7971dc13fc chore: Refactor Mounts (#1236) 2025-05-09 13:18:20 -04:00
Boy132
8406f4686c Enable ipv6 on frontend (#1350) 2025-05-09 08:44:18 +02:00
Charles
67705b14b4 remove ComicMono as default set to monospace (#1352) 2025-05-08 18:00:51 -04:00
Boy132
bc115af5fd Replace File with Storage on EditProfile (#1353) 2025-05-08 22:14:53 +02:00
MartinOscar
da35703f75 Hide ChartWidgets when Server isInConflictState or Offline (#1348) 2025-05-08 20:42:14 +02:00
MartinOscar
c54bfd714b Make Tags work in StoreNodeRequest (#1349) 2025-05-08 19:08:13 +02:00
Lance Pioch
b83e3657d6 Laravel 12.13.0 Shift (#1347)
Co-authored-by: Shift <shift@laravelshift.com>
2025-05-07 15:50:41 -05:00
Boy132
e2c87a8206 Add back network chart (#1283)
* add back network chart

* don't show timestamp

* convert "total" to "real time"

* fix typo

* set min to 0

* sort data to make sure we actually get the previous value

* Fix `ServerNetworkChart`

* Many changes...

* small cleanup

---------

Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
Co-authored-by: notCharles <charles@pelican.dev>
2025-05-06 23:32:01 +02:00
Boy132
e38a736b61 Small cleanup for new egg features (#1343) 2025-05-06 13:01:34 +02:00
Boy132
26e20453bf Prevent primary allocation overwrite on save (#1344) 2025-05-06 13:01:09 +02:00
Boy132
292523d153 Cleanup files mount and fix path for global search (#1341) 2025-05-06 08:36:51 +02:00
PalmarHealer
85d625d118 Rework subuser permission loading (#1311)
* Remove open in new tab since both are on filament now.

Removing the open in new tab since both are on filament now. And the tenant: null was function default so not needed aswell

* Rework permission tab loading

Reworked permission tab loading to make it easier to expand on it in the future. This is way more friendly if extensions are planned in the future.

* Rework permission tab loading

Reworked permission tab loading to make it easier to expand on it in the future. This is way more friendly if extensions are planned in the future.

* Rework permission tab loading

Reworked permission tab loading to make it easier to expand on it in the future. This is way more friendly if extensions are planned in the future.

* Update UserResource.php

Used wrong name. It's not the name, the label has to be checked there.

* Fix: wrong name used

Used wrong name. It's not the name, the label has to be checked there.

* Update permission loading
Moved permission list to app/Models/Permission.php and made UserResource.php and ListUsers.php use it.

* Fix Pint and PHPStan error
Added comments

* Update array key
Updated array key using the lowercase name. Suggested by https://github.com/Boy132

* Correct array key
Updated array key using the lowercase. Suggested by https://github.com/Boy132

* Revert/correct array key
Updated array key using the lowercase and the correct label.

* Add 'user' key
In the old $permission array was user an entry witch is missing in permissionTabs()

* Style and return
Added @return and removed empty lines

* pin
fix pint

* fix pint
remove @return

* fix pint
add () since pint is still not happy

* remove mb_strtolower
mb_strtolower is not necessary

* remove schema for control
remove ->schema for control tab.

* Remove import

Remove unused import

* correct translation key

Co-authored-by: Boy132 <Boy132@users.noreply.github.com>

* make columns optional,
checkboxList => columns is now optional and default to 2

* move user and control registration
removed control registration since it was duplicate and move user registration to permissionTabs

* update @return on permissionTabs()

* Fix array key warning

* simplify permissions data

* revert this

* fix edit modal

* update icons

---------

Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
Co-authored-by: Boy132 <mail@boy132.de>
2025-05-05 17:35:17 -04:00
Boy132
c8230771ec Fix 500 when searching for empty term (#1340) 2025-05-05 23:31:36 +02:00
Charles
79691ba663 move redis only command to if statement (#1337) 2025-05-05 16:43:27 -04:00
Boy132
a6326f64fb Add back behind_proxy to ui (#1263)
* add back `behind_proxy` to ui

* combine `scheme` and `behind_proxy` into one component

* remove debug stuff

* update translations

* make bulky
2025-05-05 13:00:34 +02:00
Boy132
03745eb4be Allow to assign nodes to roles (node ownership) (#1231)
* allow to assign nodes to roles

* fix typo

* fix node policy

* small ui improvements

* add missing translation

* make phpstan happy

* fix migration on mysql

* also restrict mounts & database hosts to allowed nodes

* fix migration on mysql v2

* changes from review

* fix hasManyThrough

* change `accessibleNodes` to builder

Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>

---------

Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2025-05-05 12:58:55 +02:00
Charles
c0fda71e20 Font Saga Continues... (#1339)
Add back removed ??
2025-05-04 17:22:18 -04:00
Charles
f2f1026a97 Font Saga Continues (#1338)
Nuke comic, just use monospace..... make life easy
2025-05-04 17:03:45 -04:00
Charles
e1eaf805ea composer update (#1335) 2025-05-04 09:15:25 -04:00
Charles
03ec20e3a0 fix settings on mobile (#1336) 2025-05-04 09:15:12 -04:00
Charles
a5ffff8c8c Add Comic Mono to the list (#1330)
* Add Comic Mono to list and make default

* Update preview

* Create folder if missing.

* match composer lock from pr
2025-05-03 08:21:02 -04:00
Charles
82ef6c1408 Add server power actions to new context menu (#1321)
* add server power action context menu

* Update app/Filament/App/Resources/ServerResource/Pages/ListServers.php

Co-authored-by: Boy132 <Boy132@users.noreply.github.com>

* Cleanup

* Add missed enable

---------

Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2025-05-02 12:15:05 -04:00
Charles
2d581c7cbd Remove get_fonts, Fix docker container console font selection (#1329)
* Update `get_fonts`

This should fix docker, Has to be changed as we use alpine for docker which does not support GLOB_BRACE

* #2?

* #3

* FINAL BOSS FIGHT

Fixes Docker image <3

* Update resources/views/filament/components/server-console.blade.php

Co-authored-by: Lance Pioch <git@lance.sh>

---------

Co-authored-by: Lance Pioch <git@lance.sh>
2025-05-02 08:37:27 -04:00
Lance Pioch
7f0266be5e Laravel 12.12.0 Shift (#1325)
Co-authored-by: Shift <shift@laravelshift.com>
2025-05-02 03:21:21 -04:00
Charles
1ae9490b8f update filament assets (#1328) 2025-05-01 19:20:54 -04:00
MartinOscar
a53b3fda10 Append / to EditFiles (#1322) 2025-05-01 21:26:16 +02:00
MartinOscar
e9ddf80d10 Use $id as primaryKey for File Model (#1323) 2025-05-01 21:26:01 +02:00
Lance Pioch
3f1e99f1df composer update (#1320)
Co-authored-by: Shift <shift@laravelshift.com>
2025-05-01 14:28:44 -04:00
MartinOscar
435c615ff1 Add throwIf to daemonRepository (#1301) 2025-05-01 15:49:35 +02:00
Charles
3effd98013 Allow changing of the console font (#1277)
* Custom Fonts

* Update app/Filament/Pages/Auth/EditProfile.php

Co-authored-by: MartinOscar <40749467+rmartinoscar@users.noreply.github.com>

* wip

* wip

* Update app/Filament/Pages/Auth/EditProfile.php

Co-authored-by: Lance Pioch <git@lance.sh>

* Update app/helpers.php

Co-authored-by: MartinOscar <40749467+rmartinoscar@users.noreply.github.com>

* update

* add fonts folder for docker

* Add default font

* Update server console to preload the font

* Update settings/trans

---------

Co-authored-by: MartinOscar <40749467+rmartinoscar@users.noreply.github.com>
Co-authored-by: Lance Pioch <git@lance.sh>
2025-05-01 09:47:59 -04:00
Lance Pioch
e354bc9be7 Laravel 12.11.0 Shift (#1317)
Co-authored-by: Shift <shift@laravelshift.com>
2025-04-29 21:01:28 -04:00
Boy132
14d351103c Fix database & user not being deleted (#1315) 2025-04-29 17:05:49 +02:00
Boy132
92c23451af Improve file error handling (#1314)
* improve file error handling

* small cleanup

* fix typo
2025-04-29 17:05:29 +02:00
pelican-vehikl
2046fa453a Pest Test Improvements (#1137)
Co-authored-by: Lance Pioch <git@lance.sh>
Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2025-04-28 10:20:33 -04:00
Michael (Parker) Parker
b39a8186ae Resolve issue with avatar storage (#1281)
* Resolve issue with avatar storage

This resolves the issue with getting avatar storage working

updates the entrypoint to create the `pelican-data/storage` folder on start.

Adds a dev dockerfile to build locally instead of needing to update the standard dockerfile.

* Move avatar folder

Moves the avatars folder in the storage folder in-case anything else needs storage as well.

Fixes an issue in the entrypoint where it wasn't creating the sub-folder correctly.
2025-04-27 20:56:10 -04:00
Letter N
8ae3c88c91 generalize sponge installation (#1300) 2025-04-26 14:06:30 -04:00
MartinOscar
329a29f7da Add missing disabled in AllocationsRelationManager (#1304) 2025-04-26 06:42:29 -04:00
MartinOscar
98a2cab5ca Case insensitive EggFeature Listeners (#1303) 2025-04-26 06:41:59 -04:00
pelican-vehikl
8407547574 Add back Egg Features (#1271)
Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
Co-authored-by: Lance Pioch <git@lance.sh>
Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2025-04-24 18:24:18 -04:00
Lance Pioch
fccd7e5e75 composer update (#1298)
Co-authored-by: Shift <shift@laravelshift.com>
2025-04-24 15:33:26 -04:00
Lance Pioch
c0225b9e10 Laravel 12.10.1 Shift (#1294)
Co-authored-by: Shift <shift@laravelshift.com>
2025-04-24 10:08:49 -04:00
Boy132
544aaab960 Make sure 2fa requirement is enforced (#1289) 2025-04-23 16:03:10 +02:00
Boy132
914e215bc0 Separate user uploadable avatars into own setting (#1286) 2025-04-23 16:02:52 +02:00
Sebastien Green
90fd73f6a4 Change section header icon self alignment to centre (#1279) 2025-04-23 10:02:44 -04:00
Boy132
0037b4a1d4 Only use navigation groups when using sidebar (#1288)
* Revert "Remove `NavigationGroups` for Admin Navbar (#1248)"

This reverts commit a186900262.

* make navigation groups conditional
2025-04-23 16:02:21 +02:00
Boy132
3deada57c6 Remove DynamicDatabaseConnection (#1290) 2025-04-23 16:02:08 +02:00
Gabriel
6427903f9f feat(console): save command history in session (#1282) 2025-04-22 17:29:17 -04:00
PalmarHealer
b16e19b4fb Remove open in new tab since both are on filament now. (#1292) 2025-04-22 17:28:00 -04:00
Boy132
7e99d5cd8e Use Arr::dot to display multi-dimensional activity log properties (#1285) 2025-04-22 22:27:50 +02:00
Boy132
05b1a44a34 Fix metadata coming from wings activity logs (#1284) 2025-04-22 22:27:31 +02:00
Letter N
058b613c98 handle failed oauth (#1264)
* handle failed oauths

* fix linter

* small cleanup

---------

Co-authored-by: Boy132 <mail@boy132.de>
2025-04-22 15:57:44 -04:00
Boy132
0e2ab4b711 Fix activity log query (#1258) 2025-04-22 08:28:24 +02:00
Quinten
ee838316e6 Make avatars work (#1251) 2025-04-21 11:25:36 +02:00
MartinOscar
ffd94b8892 Fix develop Node Version reported as outdated (#1272) 2025-04-18 16:41:10 +02:00
MartinOscar
a186900262 Remove NavigationGroups for Admin Navbar (#1248) 2025-04-18 10:39:25 -04:00
Lance Pioch
bf14755287 Laravel 12.9.2 Shift (#1266)
Co-authored-by: Shift <shift@laravelshift.com>
2025-04-18 10:37:21 -04:00
MartinOscar
038504fbec Only chunk if rows exceeds sqlite variables limit (999) (#1270) 2025-04-17 16:24:57 -04:00
MartinOscar
22a0a52f7b Chunk Sushi inserts based on rows count (#1259) 2025-04-17 00:04:58 +02:00
Boy132
862afaa0e9 Fix api docs for server update requests (#1262)
* workaround for api docs error

* add deprecated notice
2025-04-15 23:47:31 +02:00
MartinOscar
a4dd8cca4c Add live() to KeyValue on CreateServer & EditServer (#1261) 2025-04-15 16:06:37 +02:00
Letter N
e67e0830eb Fix Node graph not rendering correctly (#1253)
* use round instead of `Number::format`

* remove unused

* also replace `Number::format` in cpu & memory charts

---------

Co-authored-by: Boy132 <mail@boy132.de>
2025-04-15 01:27:35 +02:00
Boy132
b444112085 Correctly display backup status (#1256)
* add status attribute to backup

* hide actions when backup is not successful

* small cleanup
2025-04-14 12:59:03 +02:00
Boy132
f23d4d6971 Fix action in notifications (#1257) 2025-04-14 12:57:38 +02:00
MartinOscar
2a3781f5a8 Add pdo_pgsql to Docker (#1244) 2025-04-13 02:34:27 +02:00
MartinOscar
cb245dc722 Use recommended PHP 8.4 for Docker (#1245) 2025-04-13 02:30:09 +02:00
MartinOscar
3ffbf9e46a Allow users to remove their Avatar (#1247) 2025-04-13 02:29:46 +02:00
MartinOscar
8221c80ec2 Only allow image/png mimetype for Avatar (#1246) 2025-04-13 02:27:36 +02:00
MartinOscar
702a6bb750 Restore exception_handler & error_handler for Tests (#1239) 2025-04-12 16:44:46 +02:00
MartinOscar
02d7ad04ad Fix serverVariables not saving due to join (#1235)
* Fix `serverVariables` not saving due to `join`

* Remove deprecated `viewableServerVariables`
2025-04-12 16:44:24 +02:00
Boy132
7409f020ba Add storage:link to setup command (#1233) 2025-04-11 23:23:23 +02:00
Lance Pioch
98d8510f11 Laravel 12.8.1 Shift (#1226) 2025-04-11 09:29:33 -04:00
Lance Pioch
6c6d458445 Laravel 12.7.2 Shift (#1213)
* Bump Laravel version constraint

* Bump community package dependencies

* composer update

---------

Co-authored-by: Shift <shift@laravelshift.com>
2025-04-07 21:08:27 -04:00
Lance Pioch
51fda2eaf4 These have to be nullable originally (#1222) 2025-04-07 21:08:03 -04:00
Boy132
92fbd75772 Show different roles CheckboxList for root admins and non root admins (#1219)
* show different roles checkbox list for root admins and non root admins

* simplify saveRelationshipsUsing

* remove disableOptionWhen

* add migration to remove additional roles from root admins
2025-04-07 16:10:31 +02:00
Boy132
fa8ae0aea5 Add avatar providers (#1192)
* Add avatar providers

* fix exists check for local avatar

* Use avatar in user lists

---------

Co-authored-by: Charles <charles@pelican.dev>
2025-04-07 16:06:19 +02:00
Charles
377b3f170d Change table row options (#1220)
It's a known filament issue that large tables are SUPER slow.
2025-04-06 15:03:40 -04:00
MartinOscar
566e7c1b24 Allow user to choose archive name in FileManager (#1206)
* Allow user to choose `archive` name in `FileManager`

* Rollback `file.compress` activity translation
2025-04-06 14:52:25 -04:00
tfcprivt
b9d4773bd7 Fixed the Select dropdown to use searchable on the Edit Files Page. (#1204)
Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2025-04-06 14:05:54 -04:00
Boy132
49638e75e5 Add setup wizard to database host (#1216)
* add setup wizard to database host

* make phpstan happy

* remove `.` in lang

---------

Co-authored-by: notCharles <charles@pelican.dev>
2025-04-06 14:04:20 -04:00
MartinOscar
80c404a48c Chore filament:upgrade (#1210) 2025-04-05 02:56:52 +02:00
Charles
befe6be80b Update Overview, Again. Add some customization (#1200)
* wip

* wip

* wip

* overview 2.1

* Combine 2 branches into one

* updates

* Fix 500

* use my friend JSON

* Use switch
2025-04-04 12:08:43 -04:00
Boy132
3639d7ccec Fix file writing (#1218) 2025-04-04 14:38:08 +02:00
Boy132
20f271041a Allow to register custom role permissions (#1208) 2025-04-04 09:30:45 +02:00
Boy132
c3b8b71f9c Allow to register custom console widgets (#1209) 2025-04-04 09:30:25 +02:00
Boy132
c73d0544d9 Refactor admin dashboard to use widgets (#1207) 2025-04-04 09:30:00 +02:00
MartinOscar
484a3b445a Prevent Server primary allocation dissociation (#1197) 2025-04-04 00:56:15 +02:00
MartinOscar
c0fa8c1cd8 Use afterSave instead of handleRecordUpdate & move transferServer (#1195)
* Use `afterSave` instead of `handleRecordUpdate` & move `transferServer`

* Override `getSavedNotification` instead of `save`
2025-04-03 15:59:10 +02:00
MartinOscar
e562a35057 Add unique foreign keys for EggVariable (#1196)
* Fix tests \`egg_variable\` order

* Add `EggVariable` unique foreign key for `env_variable` & `name`
2025-04-03 15:58:49 +02:00
MartinOscar
636279c6eb Add FileNotEditableException (#1135)
* Add `FileNotEditableException`

* Send `Notification` instead of Throwing

* Remove useless `function`

* Make them all `AlertBanner`
2025-04-02 21:44:51 -04:00
Boy132
ed88ce9ae3 cleanup panel config file (#1198) 2025-04-02 21:44:33 -04:00
Lance Pioch
0cce716e2c Laravel 12.6.0 Shift (#1205)
* Bump Laravel version constraint

* composer update

* Force PHP `8.2` platform

* Fix `SplFileInfo` cast in `CleanServiceBackupFilesCommand`

* Bump larastan to dev commit

* Unpin filament

---------

Co-authored-by: Shift <shift@laravelshift.com>
Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2025-04-03 03:43:01 +02:00
tfcprivt
3639f0cb50 Added Icons for the Power Actions (#1203) 2025-04-02 21:29:29 -04:00
Boy132
9c3f47590c Fix server transfer backend (#1139)
* fix notify in transfer service

* remove magical array

* fix phpstan

* better validation for allocation_additional and better docs generation

* update transfer ui

* update request body
2025-04-01 11:19:14 +02:00
Boy132
630031e1c2 Add some refreshs & notifications to EditServer action buttons (#1174)
* add some refreshs & notifications to EditServer action buttons

* reinstall server when trying to toggle failed state

* don't show modal on normal toggle install

* don't print raw exception on reinstall & suspension
2025-04-01 08:36:19 +02:00
Boy132
2c00f90ba6 remove codeowners (#1193) 2025-03-31 07:32:01 -04:00
Lance Pioch
875dca54f5 Switch inserts to proper creates (#1190)
* Switch inserts to proper creates

* Push `$token` to `$tokens[]` in `ToggleTwoFactorService`

---------

Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
2025-03-30 21:56:49 -04:00
Michael (Parker) Parker
a03b604f2d Merge pull request #1184 from JoanFo1456/main
Change console font
2025-03-30 14:53:28 -04:00
Lance Pioch
8261184b57 Officially support PostgreSQL database (#1066)
* Just skip this table because it no longer exists

* Add postgresql

* This no longer needs to be there

* These are the same output in mysql, but different in postgresql

* Fix these migrations for postgresql

* This table no longer exists

* This is expected to be a json column for json operations, required for postgresql

* Shoot for the stars

* Fix pint

* Why was this missing

* Updates

* Restore this

* This needs to be explicit

* Don’t like strings

* Fix these classes

* Use different method to compare dates

* Apparently postgresql doesn’t like case insensitivity

* Postgresql orders it backwards

* Ordered different by postgresql

* Unnecessary and breaking

* Make sure the order is correct for postresql

* Fix this with the order too

* Remove this

* Force email to be lowercased

* Update app/Models/User.php
2025-03-30 14:44:03 -04:00
MartinOscar
bca02ced86 Fix typo for SESSION_DRIVER in RedisSetupCommand (#1188) 2025-03-29 19:58:36 +01:00
Boy132
a768fadaea Reimplement password reset (#1182)
* add password reset to all panels

* remove old leftovers

* fix reset url in account created mail
2025-03-28 23:51:42 +01:00
Boy132
7471347b55 Improve alert banner fetching (#1173)
* use events for alert banner pulling

* add ids to alert banners to prevent duplicates
2025-03-28 23:50:34 +01:00
MartinOscar
1457c4bd06 Lint console.css 2025-03-28 23:09:32 +01:00
JoanFo
8b943fa160 Update server-console.blade.php
Removed font.css referrence (removed file)
2025-03-28 21:54:15 +01:00
JoanFo
5c5c9654b4 Update console.css
Added content from ./font.css
2025-03-28 21:53:29 +01:00
JoanFo
dd20cb0f11 Delete public/css/filament/server/font.css
Adding the contents on console.css
2025-03-28 21:50:50 +01:00
JoanFo
88deb35dc8 Create font.css
Forgot the font css file
2025-03-28 18:47:06 +01:00
JoanFo
0f92632c06 Update server-console.blade.php 2025-03-28 18:26:38 +01:00
JoanFo
a85fc5c88e Add files via upload 2025-03-28 18:23:36 +01:00
Charles
8d7eff13fb Update Overview and Server List (#1151)
* Update Overview and Server List

* Fix background on light mode
2025-03-28 11:57:40 -04:00
MartinOscar
c39c29e50b Must use Closure since MenuItem does not leverage Concerns (#1181)
* Must use `Closure` since `MenuItem` does not leverage Concerns

* Translate `Profile` in `ServerPanelProvider`

Co-authored-by: Boy132 <Boy132@users.noreply.github.com>

---------

Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2025-03-28 12:50:34 +01:00
MartinOscar
db3b16e609 Add Owner Filter to ListServers (#1180)
* Add `Owner` Filter to `ListServers`

* Make `Owner` filter show on `other` & `all` tabs

Co-authored-by: Boy132 <Boy132@users.noreply.github.com>

---------

Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2025-03-28 12:27:51 +01:00
MartinOscar
72b9c309d3 Fix EditProfile cannot use / in trans() (#1178) 2025-03-27 22:52:39 +01:00
MartinOscar
68a6dc45cb Set Schedule next_run_at in ListSchedules to Never if disabled (#1176) 2025-03-27 20:24:33 +01:00
MartinOscar
9a258efe53 Force app panel for EditProfile (#1162)
* Force `app` panel for `EditProfile`

* Force `app` panel for `OAuthController`

* Use translation in `AdminPanelProvider`

Co-authored-by: Boy132 <Boy132@users.noreply.github.com>

---------

Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
2025-03-27 18:54:08 +01:00
MartinOscar
3310746107 Show different emptyStateHeading when activeTab is my in ListServers (#1157) 2025-03-27 15:43:07 +01:00
MartinOscar
42706dba14 Remove duplicated activity.read & use relations in EditUser (#1165) 2025-03-27 15:40:19 +01:00
MartinOscar
ec6529ac4c Fix \compose.yml\ spelling (#1170) 2025-03-27 05:46:40 -04:00
Morpheus636
bced93c5be Removed outdated docker documentation (#1166) 2025-03-27 05:46:27 -04:00
Charles
cb1c953540 Fix Egg Variable Order (#1172)
* Fix Egg Variable Order

* Fix Client Side Startup variable order
2025-03-27 05:45:59 -04:00
Boy132
c689f6860b Disallow 0.0.0.0, 127.0.0.1 and localhost as node fqdn (#1158)
* disallow `0.0.0.0`, `127.0.01` and `localhost` as node fqdn

* use rules of model
2025-03-26 09:03:13 +01:00
MartinOscar
a73404c1b4 Fix Server ForceDelete by adding missing redirect (#1156) 2025-03-24 22:28:40 +01:00
Boy132
61cbe5465f Schedules: Update next_run_at when editing & show notification if cron is invalid (#1141)
* update `next_run_at` when editing & show notification if cron is invalid

* move getNextRun to resource
2025-03-24 09:08:51 +01:00
Charles
5bea1ea80a Fix 500 when viewing node (#1144)
* Fix node 500

* this feels better

* Update app/Models/Node.php

Co-authored-by: MartinOscar <40749467+rmartinoscar@users.noreply.github.com>

* Update app/Models/Node.php

Co-authored-by: MartinOscar <40749467+rmartinoscar@users.noreply.github.com>

* pint

---------

Co-authored-by: MartinOscar <40749467+rmartinoscar@users.noreply.github.com>
2025-03-23 17:43:51 -04:00
MartinOscar
b69136d7a4 Add Server ForceDelete (#1134) 2025-03-23 17:08:59 -04:00
Boy132
a8c3082b79 Add UI for server transfers (#1119)
* add ui for server transfers

* disable transfer button when server is in conflict state
2025-03-23 17:02:22 -04:00
Boy132
a47ad071c9 Fix activity log on rename api endpoint (#1149) 2025-03-22 17:01:11 +01:00
MartinOscar
ab953b2f4d Use composer to copy .env.example after its done installing packages. (#1073) 2025-03-22 14:06:47 +01:00
MartinOscar
03d6c88f65 Fix OAuth Modals CopyAction & use x-filament::link (#1146)
* Fix `CopyAction` & use `x-filament::link`

* PHPStan
2025-03-22 14:05:14 +01:00
PalmarHealer
b4eab02254 Remove cursor not allowed rule (#1147)
* remove cursor not allowed rule

* re-add css rule
Added the previously removed rule back and specified selector.

* Apply suggestions from code review

---------

Co-authored-by: Lance Pioch <lancepioch@gmail.com>
2025-03-22 08:31:57 -04:00
Boy132
23f39acd4e Add host config to gitlab oauth (& add setup steps) (#1142)
* add custom provider class for gitlab to allow custom hosts

* add setup steps
2025-03-21 21:23:38 +01:00
Boy132
82b0aff105 Fix SMTP scheme/ encryption (#1120) 2025-03-21 08:42:42 +01:00
Charles
adca50a372 Catch 500 on backup page when you hit the backup rate limit (#1132)
* Catch backup throwable

* phpstan

* Update notification
2025-03-20 10:38:24 -04:00
MartinOscar
c5230efad6 Catch NodeUpdateService in EditNode & NodeController (#1106) 2025-03-18 23:07:40 +01:00
Charles
e5d9d53aa3 Remove unused groupBy (#1130)
Fixes #1107
2025-03-18 18:07:35 -04:00
MartinOscar
29f3defc73 Catch DaemonFileRepository & show Alert (#1129)
* Catch `DaemonFileRepository`

Co-authored-by: notCharles <charles@pelican.dev>

* Pint

---------

Co-authored-by: notCharles <charles@pelican.dev>
2025-03-18 23:07:21 +01:00
Boy132
2dbb9a5f9b Add update egg bulk action (#1122)
* add update egg bulk action

* make phpstan happy

* use `before`
2025-03-18 17:42:04 +01:00
Boy132
a05e330b19 Fix path resolving when moving files (#1116)
* don't resolve new path when moving file

* use full path in activity log

* don't require file name when moving
2025-03-18 17:36:27 +01:00
Boy132
4a7951995e Add bulk move (#1117) 2025-03-18 17:36:13 +01:00
Boy132
3d29243cf0 Add tag filter to lists (#1124) 2025-03-18 17:35:36 +01:00
Boy132
c52439132d Add tags to egg importer & exporter, add tags to egg jsons (#1125)
* add tags to egg importer & exporter

* add tags to stock eggs
2025-03-18 17:35:15 +01:00
Boy132
517f17cbcc Add redirect after clicking reinstall (#1126) 2025-03-18 17:35:01 +01:00
Lance Pioch
f8d119b458 Update readme.md 2025-03-18 10:06:51 -04:00
MartinOscar
fbeb747fc3 Fix ImportEggAction (#1110)
* `Arr::Wrap` `data.files` cause if its unique its a string

* Use `data.url` first so it gets overwritten by `data.files`
2025-03-17 18:07:36 +01:00
Boy132
f563128237 Make sure to not sync root admin role (#1113) 2025-03-17 17:23:44 +01:00
MartinOscar
f2f3ee548f Add App Logo (#1104)
* Add `app.logo` to `Settings`

* Use `app.name` if `app.logo` is null
2025-03-17 13:28:32 +01:00
MartinOscar
0b3dce132f Add header, footer & body-end views (#1111) 2025-03-17 13:28:18 +01:00
MartinOscar
5bf23b972d Fix DaemonFileRepository in ListFiles (#1109)
* Fix `DaemonFileRepository` in `ListFiles`

* Use match for `getPermissionsFromModeBit`
2025-03-17 12:17:05 +01:00
MartinOscar
22d02c0df5 Remove NodeCreationService (#1092) 2025-03-17 05:46:33 +01:00
MartinOscar
253abf65b1 Hide Directory size in ListFiles (#1102)
* Hide `directory` size in `ListFiles`

* Use only one `DaemonFileRepository` in `ListFiles`
2025-03-16 15:51:02 +01:00
MartinOscar
d452e3d2f2 Use ContainerStatus::tryFrom in ProcessScheduleService (#1101) 2025-03-16 15:36:15 +01:00
Charles
0051370f24 Reduce svg size (#1100) 2025-03-16 10:20:19 -04:00
1347 changed files with 40357 additions and 18187 deletions

View File

@@ -3,5 +3,4 @@ APP_DEBUG=false
APP_KEY=
APP_URL=http://panel.test
APP_INSTALLED=false
APP_TIMEZONE=UTC
APP_LOCALE=en

15
.github/CODEOWNERS vendored
View File

@@ -1,15 +0,0 @@
# Lines starting with '#' are comments.
# Each line is a file pattern followed by one or more owners.
# More details are here: https://help.github.com/articles/about-codeowners/
# The '*' pattern is global owners.
# Order is important. The last matching pattern has the most precedence.
# The folders are ordered as follows:
# In each subsection folders are ordered first by depth, then alphabetically.
# This should make it easy to add new rules without breaking existing ones.
# Global
* @pelican-dev/panel

View File

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

View File

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

View File

@@ -6,12 +6,75 @@ on:
- main
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:
sqlite:
name: SQLite
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
php: [8.2, 8.3, 8.4]
env:
DB_CONNECTION: sqlite
DB_DATABASE: testing.sqlite
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: Create SQLite file
run: touch database/testing.sqlite
- 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
mysql:
name: MySQL
runs-on: ubuntu-latest
strategy:
fail-fast: false
fail-fast: true
matrix:
php: [8.2, 8.3, 8.4]
database: ["mysql:8"]
@@ -25,21 +88,10 @@ jobs:
- 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
@@ -84,7 +136,7 @@ jobs:
name: MariaDB
runs-on: ubuntu-latest
strategy:
fail-fast: false
fail-fast: true
matrix:
php: [8.2, 8.3, 8.4]
database: ["mariadb:10.6", "mariadb:10.11", "mariadb:11.4"]
@@ -98,21 +150,10 @@ jobs:
- 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
@@ -153,27 +194,35 @@ jobs:
DB_PORT: ${{ job.services.database.ports[3306] }}
DB_USERNAME: root
sqlite:
name: SQLite
postgresql:
name: PostgreSQL
runs-on: ubuntu-latest
strategy:
fail-fast: false
fail-fast: true
matrix:
php: [8.2, 8.3, 8.4]
database: ["postgres:14"]
services:
database:
image: ${{ matrix.database }}
env:
POSTGRES_DB: testing
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_HOST_AUTH_METHOD: trust
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
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_DATABASE: testing.sqlite
GUZZLE_TIMEOUT: 60
GUZZLE_CONNECT_TIMEOUT: 60
DB_CONNECTION: pgsql
DB_HOST: 127.0.0.1
DB_DATABASE: testing
DB_USERNAME: postgres
DB_PASSWORD: postgres
steps:
- name: Code Checkout
uses: actions/checkout@v4
@@ -189,7 +238,6 @@ jobs:
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
@@ -202,9 +250,6 @@ jobs:
- name: Install dependencies
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
- name: Create SQLite file
run: touch database/testing.sqlite
- name: Unit tests
run: vendor/bin/pest tests/Unit
env:

View File

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

View File

@@ -33,9 +33,9 @@ jobs:
name: PHPStan
runs-on: ubuntu-latest
strategy:
fail-fast: false
fail-fast: true
matrix:
php: [8.2, 8.3, 8.4]
php: [ 8.2, 8.3, 8.4 ]
steps:
- name: Code Checkout
uses: actions/checkout@v4
@@ -68,4 +68,4 @@ jobs:
run: composer install --no-interaction --no-suggest --no-progress --no-scripts
- name: PHPStan
run: vendor/bin/phpstan --memory-limit=-1
run: vendor/bin/phpstan --memory-limit=-1 --error-format=github

20
.github/workflows/shift.yaml vendored Normal file
View File

@@ -0,0 +1,20 @@
name: Shift
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * 5"
jobs:
shift:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Shift
run: |
curl -X POST -s -retry 5 -m 60 --fail-with-body \
https://laravelshift.com/api/run \
-H "Accept: application/json" \
-d "api_token=${{ secrets.SHIFT_TOKEN }}" \
-d "code=${{ secrets.SHIFT_CODE }}" \
-d "scs=github:${{ github.repository }}:${{ github.ref_name }}"

5
.gitignore vendored
View File

@@ -1,7 +1,6 @@
/.phpunit.cache
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/storage/pail
@@ -24,8 +23,6 @@ yarn-error.log
/.vscode
public/assets/manifest.json
/database/*.sqlite
/database/*.sqlite-journal
filament-monaco-editor/
/database/*.sqlite*
_ide_helper*
/.phpstorm.meta.php

View File

@@ -1,16 +1,9 @@
# syntax=docker.io/docker/dockerfile:1.13-labs
# Pelican Production Dockerfile
# For those who want to build this Dockerfile themselves, uncomment lines 6-12 and replace "localhost:5000/base-php:$TARGETARCH" on lines 17 and 67 with "base".
# FROM --platform=$TARGETOS/$TARGETARCH php:8.3-fpm-alpine as base
# 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
# RUN rm /usr/local/bin/install-php-extensions
##
# If you want to build this locally you want to run `docker build -f Dockerfile.dev`
##
# ================================
# Stage 1-1: Composer Install
@@ -70,8 +63,8 @@ FROM --platform=$TARGETOS/$TARGETARCH localhost:5000/base-php:$TARGETARCH AS fin
WORKDIR /var/www/html
# Install additional required libraries
RUN apk update && apk add --no-cache \
caddy ca-certificates supervisor supercronic
RUN apk add --no-cache \
caddy ca-certificates supervisor supercronic fcgi
COPY --chown=root:www-data --chmod=640 --from=composerbuild /build .
COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public
@@ -82,14 +75,18 @@ RUN chown root:www-data ./ \
&& chmod 750 ./ \
# Files should not have execute set, but directories need it
&& find ./ -type d -exec chmod 750 {} \; \
# Symlink to env/database path, as www-data won't be able to write to webroot
# 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 \
# Create necessary directories
&& mkdir -p /pelican-data /var/run/supervisord /etc/supercronic \
# Finally allow www-data write permissions where necessary
&& chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord
&& 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 \
&& chown -R www-data: /usr/local/etc/php/
# Configure Supervisor
COPY docker/supervisord.conf /etc/supervisord.conf
@@ -97,10 +94,11 @@ COPY docker/Caddyfile /etc/caddy/Caddyfile
# Add Laravel scheduler to crontab
COPY docker/crontab /etc/supercronic/crontab
COPY docker/entrypoint.sh ./docker/entrypoint.sh
COPY docker/entrypoint.sh /entrypoint.sh
COPY docker/healthcheck.sh /healthcheck.sh
HEALTHCHECK --interval=5m --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost/up || exit 1
CMD /bin/ash /healthcheck.sh
EXPOSE 80 443
@@ -108,5 +106,5 @@ VOLUME /pelican-data
USER www-data
ENTRYPOINT [ "/bin/ash", "docker/entrypoint.sh" ]
ENTRYPOINT [ "/bin/ash", "/entrypoint.sh" ]
CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ]

View File

@@ -1,10 +1,10 @@
# ================================
# Stage 0: Build PHP Base Image
# ================================
FROM --platform=$TARGETOS/$TARGETARCH php:8.3-fpm-alpine
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/
RUN install-php-extensions bcmath gd intl zip opcache pcntl posix pdo_mysql
RUN install-php-extensions bcmath gd intl zip opcache pcntl posix pdo_mysql pdo_pgsql
RUN rm /usr/local/bin/install-php-extensions
RUN rm /usr/local/bin/install-php-extensions

114
Dockerfile.dev Normal file
View File

@@ -0,0 +1,114 @@
# syntax=docker.io/docker/dockerfile:1.13-labs
# Pelican Development Dockerfile
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/
RUN install-php-extensions bcmath gd intl zip opcache pcntl posix pdo_mysql pdo_pgsql
RUN rm /usr/local/bin/install-php-extensions
# ================================
# Stage 1-1: Composer Install
# ================================
FROM --platform=$TARGETOS/$TARGETARCH base AS composer
WORKDIR /build
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
# Copy bare minimum to install Composer dependencies
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-interaction --no-autoloader --no-scripts
# ================================
# Stage 1-2: Yarn Install
# ================================
FROM --platform=$TARGETOS/$TARGETARCH node:20-alpine AS yarn
WORKDIR /build
# Copy bare minimum to install Yarn dependencies
COPY package.json yarn.lock ./
RUN yarn config set network-timeout 300000 \
&& yarn install --frozen-lockfile
# ================================
# Stage 2-1: Composer Optimize
# ================================
FROM --platform=$TARGETOS/$TARGETARCH composer AS composerbuild
# Copy full code to optimize autoload
COPY --exclude=Caddyfile --exclude=docker/ . ./
RUN composer dump-autoload --optimize
# ================================
# Stage 2-2: Build Frontend Assets
# ================================
FROM --platform=$TARGETOS/$TARGETARCH yarn AS yarnbuild
WORKDIR /build
# Copy full code
COPY --exclude=Caddyfile --exclude=docker/ . ./
COPY --from=composer /build .
RUN yarn run build
# ================================
# Stage 5: Build Final Application Image
# ================================
FROM --platform=$TARGETOS/$TARGETARCH base AS final
WORKDIR /var/www/html
# Install additional required libraries
RUN apk add --no-cache \
caddy ca-certificates supervisor supercronic fcgi coreutils
COPY --chown=root:www-data --chmod=640 --from=composerbuild /build .
COPY --chown=root:www-data --chmod=640 --from=yarnbuild /build/public ./public
# Set permissions
# First ensure all files are owned by root and restrict www-data to read access
RUN chown root:www-data ./ \
&& chmod 750 ./ \
# Files should not have execute set, but directories need it
&& find ./ -type d -exec chmod 750 {} \; \
# Create necessary directories
&& mkdir -p /pelican-data/storage /var/www/html/storage/app/public /var/run/supervisord /etc/supercronic \
# Symlinks for env, database, and avatars
&& ln -s /pelican-data/.env ./.env \
&& ln -s /pelican-data/database/database.sqlite ./database/database.sqlite \
&& ln -sf /var/www/html/storage/app/public /var/www/html/public/storage \
&& ln -s /pelican-data/storage/avatars /var/www/html/storage/app/public/avatars \
&& ln -s /pelican-data/storage/fonts /var/www/html/storage/app/public/fonts \
# Allow www-data write permissions where necessary
&& chown -R www-data:www-data /pelican-data ./storage ./bootstrap/cache /var/run/supervisord /var/www/html/public/storage \
&& chmod -R u+rwX,g+rwX,o-rwx /pelican-data ./storage ./bootstrap/cache /var/run/supervisord \
&& chown -R www-data: /usr/local/etc/php/
# Configure Supervisor
COPY docker/supervisord.conf /etc/supervisord.conf
COPY docker/Caddyfile /etc/caddy/Caddyfile
# Add Laravel scheduler to crontab
COPY docker/crontab /etc/supercronic/crontab
COPY docker/entrypoint.sh /entrypoint.sh
COPY docker/healthcheck.sh /healthcheck.sh
HEALTHCHECK --interval=5m --timeout=10s --start-period=5s --retries=3 \
CMD /bin/ash /healthcheck.sh
EXPOSE 80 443
VOLUME /pelican-data
USER www-data
ENTRYPOINT [ "/bin/ash", "/entrypoint.sh" ]
CMD [ "supervisord", "-n", "-c", "/etc/supervisord.conf" ]

View File

@@ -14,9 +14,9 @@ class NodeVersionsCheck extends Check
public function run(): Result
{
$all = Node::query()->count();
$all = Node::all();
if ($all === 0) {
if ($all->isEmpty()) {
$result = Result::make()
->notificationMessage(trans('admin/health.results.nodeversions.no_nodes_created'))
->shortSummary(trans('admin/health.results.nodeversions.no_nodes'));
@@ -25,16 +25,18 @@ class NodeVersionsCheck extends Check
return $result;
}
$latestVersion = $this->versionService->latestWingsVersion();
$outdated = Node::query()->get()
->filter(fn (Node $node) => !isset($node->systemInformation()['exception']) && $node->systemInformation()['version'] !== $latestVersion)
$outdated = $all
->filter(fn (Node $node) => !isset($node->systemInformation()['exception']) && !$this->versionService->isLatestWings($node->systemInformation()['version']))
->count();
$all = $all->count();
$latestVersion = $this->versionService->latestWingsVersion();
$result = Result::make()
->meta([
'all' => $all,
'outdated' => $outdated,
'latestVersion' => $latestVersion,
])
->shortSummary($outdated === 0 ? trans('admin/health.results.nodeversions.all_up_to_date') : trans('admin/health.results.nodeversions.outdated', ['outdated' => $outdated, 'all' => $all]));

View File

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

@@ -0,0 +1,46 @@
<?php
namespace App\Console\Commands\Egg;
use Exception;
use Illuminate\Console\Command;
class UpdateEggIndexCommand extends Command
{
protected $signature = 'p:egg:update-index';
public function handle(): int
{
try {
$data = file_get_contents('https://raw.githubusercontent.com/pelican-eggs/pelican-eggs.github.io/refs/heads/main/content/pelican.json');
$data = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
} catch (Exception $exception) {
$this->error($exception->getMessage());
return 1;
}
$index = [];
foreach ($data['nests'] as $nest) {
$nestName = $nest['nest_type'];
$this->info("Nest: $nestName");
$nestEggs = [];
foreach ($nest['Eggs'] as $egg) {
$eggName = $egg['egg']['name'];
$this->comment("Egg: $eggName");
$nestEggs[$egg['download_url']] = $eggName;
}
$index[$nestName] = $nestEggs;
$this->info('');
}
cache()->forever('eggs.index', $index);
return 0;
}
}

View File

@@ -3,7 +3,6 @@
namespace App\Console\Commands\Environment;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
class AppSettingsCommand extends Command
{
@@ -21,9 +20,13 @@ class AppSettingsCommand extends Command
if (!config('app.key')) {
$this->comment('Generating app key');
Artisan::call('key:generate');
$this->call('key:generate');
}
Artisan::call('filament:optimize');
$this->comment('Creating storage link');
$this->call('storage:link');
$this->comment('Caching components & icons');
$this->call('filament:optimize');
}
}

View File

@@ -6,6 +6,7 @@ use App\Traits\EnvironmentWriterTrait;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Database\DatabaseManager;
use PDOException;
class DatabaseSettingsCommand extends Command
{
@@ -105,7 +106,7 @@ class DatabaseSettingsCommand extends Command
]);
$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(trans('commands.database_settings.DB_error_2'));
@@ -165,7 +166,7 @@ class DatabaseSettingsCommand extends Command
]);
$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(trans('commands.database_settings.DB_error_2'));

View File

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

View File

@@ -18,6 +18,17 @@ class QueueWorkerServiceCommand extends Command
public function handle(): void
{
if (@file_exists('/.dockerenv')) {
$result = Process::run('supervisorctl restart queue-worker');
if ($result->failed()) {
$this->error('Error restarting service: ' . $result->errorOutput());
return;
}
$this->line('Queue worker service file updated successfully.');
return;
}
$serviceName = $this->option('service-name') ?? $this->ask('Queue worker service name', 'pelican-queue');
$path = '/etc/systemd/system/' . $serviceName . '.service';

View File

@@ -35,7 +35,7 @@ class RedisSetupCommand extends Command
{
$this->variables['CACHE_STORE'] = 'redis';
$this->variables['QUEUE_CONNECTION'] = 'redis';
$this->variables['SESSION_DRIVERS'] = 'redis';
$this->variables['SESSION_DRIVER'] = 'redis';
$this->requestRedisSettings();

View File

@@ -4,8 +4,9 @@ namespace App\Console\Commands\Maintenance;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory;
use Illuminate\Contracts\Filesystem\Filesystem;
use SplFileInfo;
class CleanServiceBackupFilesCommand extends Command
{
@@ -32,9 +33,10 @@ class CleanServiceBackupFilesCommand extends Command
*/
public function handle(): void
{
/** @var SplFileInfo[] */
$files = $this->disk->files('services/.bak');
collect($files)->each(function (\SplFileInfo $file) {
collect($files)->each(function ($file) {
$lastModified = Carbon::createFromTimestamp($this->disk->lastModified($file->getPath()));
if ($lastModified->diffInMinutes(Carbon::now()) > self::BACKUP_THRESHOLD_MINUTES) {
$this->disk->delete($file->getPath());

View File

@@ -5,6 +5,7 @@ namespace App\Console\Commands\Maintenance;
use App\Models\Backup;
use Carbon\CarbonImmutable;
use Illuminate\Console\Command;
use InvalidArgumentException;
class PruneOrphanedBackupsCommand extends Command
{
@@ -16,7 +17,7 @@ class PruneOrphanedBackupsCommand extends Command
{
$since = $this->option('prune-age') ?? config('backups.prune_age', 360);
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()

View File

@@ -2,8 +2,9 @@
namespace App\Console\Commands\Node;
use App\Exceptions\Model\DataValidationException;
use App\Models\Node;
use Illuminate\Console\Command;
use App\Services\Nodes\NodeCreationService;
class MakeNodeCommand extends Command
{
@@ -24,24 +25,17 @@ class MakeNodeCommand extends Command
{--overallocateCpu= : Enter the amount of cpu to overallocate (% or -1 to overallocate the maximum).}
{--uploadSize= : Enter the maximum upload filesize.}
{--daemonListeningPort= : Enter the daemon listening port.}
{--daemonConnectingPort= : Enter the daemon connecting port.}
{--daemonSFTPPort= : Enter the daemon SFTP listening port.}
{--daemonSFTPAlias= : Enter the daemon SFTP alias.}
{--daemonBase= : Enter the base folder.}';
protected $description = 'Creates a new node on the system via the CLI.';
/**
* MakeNodeCommand constructor.
*/
public function __construct(private NodeCreationService $creationService)
{
parent::__construct();
}
/**
* Handle the command execution process.
*
* @throws \App\Exceptions\Model\DataValidationException
* @throws DataValidationException
*/
public function handle(): void
{
@@ -65,11 +59,12 @@ class MakeNodeCommand extends Command
$data['cpu_overallocate'] = $this->option('overallocateCpu') ?? $this->ask(trans('commands.make_node.cpu_overallocate'), '-1');
$data['upload_size'] = $this->option('uploadSize') ?? $this->ask(trans('commands.make_node.upload_size'), '256');
$data['daemon_listen'] = $this->option('daemonListeningPort') ?? $this->ask(trans('commands.make_node.daemonListen'), '8080');
$data['daemon_connect'] = $this->option('daemonConnectingPort') ?? $this->ask(trans('commands.make_node.daemonConnect'), '8080');
$data['daemon_sftp'] = $this->option('daemonSFTPPort') ?? $this->ask(trans('commands.make_node.daemonSFTP'), '2022');
$data['daemon_sftp_alias'] = $this->option('daemonSFTPAlias') ?? $this->ask(trans('commands.make_node.daemonSFTPAlias'), '');
$data['daemon_base'] = $this->option('daemonBase') ?? $this->ask(trans('commands.make_node.daemonBase'), '/var/lib/pelican/volumes');
$node = $this->creationService->handle($data);
$node = Node::create($data);
$this->line(trans('commands.make_node.success', ['name' => $data['name'], 'id' => $node->id]));
}
}

View File

@@ -17,7 +17,7 @@ class NodeConfigurationCommand extends Command
{
$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 () {
$this->error(trans('commands.node_config.error_not_exist'));

View File

@@ -2,10 +2,10 @@
namespace App\Console\Commands\Schedule;
use Illuminate\Console\Command;
use App\Models\Schedule;
use Illuminate\Database\Eloquent\Builder;
use App\Services\Schedules\ProcessScheduleService;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Builder;
use Throwable;
class ProcessRunnableCommand extends Command
@@ -64,7 +64,7 @@ class ProcessRunnableCommand extends Command
} catch (Throwable $exception) {
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;
use App\Models\Server;
use App\Repositories\Daemon\DaemonServerRepository;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Validation\ValidationException;
use Illuminate\Validation\Factory as ValidatorFactory;
use App\Repositories\Daemon\DaemonPowerRepository;
use Exception;
use Illuminate\Validation\ValidationException;
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.';
public function handle(DaemonPowerRepository $powerRepository, ValidatorFactory $validator): void
public function handle(DaemonServerRepository $serverRepository, ValidatorFactory $validator): void
{
$action = $this->argument('action');
$nodes = empty($this->option('nodes')) ? [] : explode(',', $this->option('nodes'));
@@ -52,7 +52,7 @@ class BulkPowerActionCommand extends Command
$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();
if (!$server instanceof Server) {
@@ -60,7 +60,7 @@ class BulkPowerActionCommand extends Command
}
try {
$powerRepository->setServer($server)->send($action);
$serverRepository->setServer($server)->power($action);
} catch (Exception $exception) {
$this->output->error(trans('command/messages.server.power.action_failed', [
'name' => $server->name,

View File

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

View File

@@ -3,8 +3,8 @@
namespace App\Console\Commands\User;
use App\Models\User;
use Webmozart\Assert\Assert;
use Illuminate\Console\Command;
use Webmozart\Assert\Assert;
class DeleteUserCommand extends Command
{

View File

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

View File

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

View File

@@ -3,11 +3,11 @@
namespace App\Console;
use App\Console\Commands\Egg\CheckEggUpdatesCommand;
use App\Console\Commands\Egg\UpdateEggIndexCommand;
use App\Console\Commands\Maintenance\CleanServiceBackupFilesCommand;
use App\Console\Commands\Maintenance\PruneImagesCommand;
use App\Console\Commands\Maintenance\PruneOrphanedBackupsCommand;
use App\Console\Commands\Schedule\ProcessRunnableCommand;
use App\Jobs\NodeStatistics;
use App\Models\ActivityLog;
use App\Models\Webhook;
use Illuminate\Console\Scheduling\Schedule;
@@ -31,17 +31,20 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule): void
{
// https://laravel.com/docs/10.x/upgrade#redis-cache-tags
$schedule->command('cache:prune-stale-tags')->hourly();
if (config('cache.default') === 'redis') {
// https://laravel.com/docs/10.x/upgrade#redis-cache-tags
// This only needs to run when using redis. anything else throws an error.
$schedule->command('cache:prune-stale-tags')->hourly();
}
// Execute scheduled commands for servers every minute, as if there was a normal cron running.
$schedule->command(ProcessRunnableCommand::class)->everyMinute()->withoutOverlapping();
$schedule->command(CleanServiceBackupFilesCommand::class)->daily();
$schedule->command(PruneImagesCommand::class)->daily();
$schedule->command(CheckEggUpdatesCommand::class)->hourly();
$schedule->job(new NodeStatistics())->everyFiveSeconds()->withoutOverlapping();
$schedule->command(CheckEggUpdatesCommand::class)->daily();
$schedule->command(UpdateEggIndexCommand::class)->daily();
if (config('backups.prune_age')) {
// Every 30 minutes, run the backup pruning command so that any abandoned backups can be deleted.

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Enums;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
enum BackupStatus: string implements HasColor, HasIcon, HasLabel
{
case InProgress = 'in_progress';
case Successful = 'successful';
case Failed = 'failed';
public function getIcon(): string
{
return match ($this) {
self::InProgress => 'tabler-circle-dashed',
self::Successful => 'tabler-circle-check',
self::Failed => 'tabler-circle-x',
};
}
public function getColor(): string
{
return match ($this) {
self::InProgress => 'primary',
self::Successful => 'success',
self::Failed => 'danger',
};
}
public function getLabel(): string
{
return trans('server/backup.backup_status.' . $this->value);
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Enums;
enum ConsoleWidgetPosition: string
{
case Top = 'top';
case AboveConsole = 'above_console';
case BelowConsole = 'below_console';
case Bottom = 'bottom';
}

View File

@@ -62,13 +62,13 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
self::Removing => 'warning',
self::Missing => 'danger',
self::Stopping => 'warning',
self::Offline => 'gray',
self::Offline => 'danger',
};
}
public function getLabel(): string
{
return str($this->value)->title();
return trans('server/console.status.' . $this->value);
}
public function isOffline(): bool
@@ -88,7 +88,7 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
public function isStartable(): bool
{
return !in_array($this, [ContainerStatus::Running, ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting]);
return !in_array($this, [ContainerStatus::Running, ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting, ContainerStatus::Missing]);
}
public function isRestartable(): bool
@@ -97,18 +97,16 @@ enum ContainerStatus: string implements HasColor, HasIcon, HasLabel
return true;
}
return !in_array($this, [ContainerStatus::Offline]);
return !in_array($this, [ContainerStatus::Offline, ContainerStatus::Missing]);
}
public function isStoppable(): bool
{
return !in_array($this, [ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting, ContainerStatus::Exited, ContainerStatus::Offline]);
return !in_array($this, [ContainerStatus::Starting, ContainerStatus::Stopping, ContainerStatus::Restarting, ContainerStatus::Exited, ContainerStatus::Offline, ContainerStatus::Missing]);
}
public function isKillable(): bool
{
// [ContainerStatus::Restarting, ContainerStatus::Removing, ContainerStatus::Dead, ContainerStatus::Created]
return !in_array($this, [ContainerStatus::Offline, ContainerStatus::Running, ContainerStatus::Exited]);
return !in_array($this, [ContainerStatus::Offline, ContainerStatus::Running, ContainerStatus::Exited, ContainerStatus::Missing]);
}
}

View File

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

View File

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

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

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

View File

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

View File

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

View File

@@ -14,4 +14,24 @@ enum RolePermissionModels: string
case Server = 'server';
case User = 'user';
case Webhook = 'webhook';
public function viewAny(): string
{
return RolePermissionPrefixes::ViewAny->value . ' ' . $this->value;
}
public function view(): string
{
return RolePermissionPrefixes::View->value . ' ' . $this->value;
}
public function create(): string
{
return RolePermissionPrefixes::Create->value . ' ' . $this->value;
}
public function update(): string
{
return RolePermissionPrefixes::Update->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;
enum ServerResourceType
use App\Models\Server;
enum ServerResourceType: string
{
case Unit;
case Percentage;
case Time;
case Uptime = 'uptime';
case CPU = 'cpu_absolute';
case Memory = 'memory_bytes';
case Disk = 'disk_bytes';
case CPULimit = 'cpu';
case MemoryLimit = 'memory';
case DiskLimit = 'disk';
/**
* @return int resource amount in bytes
*/
public function getResourceAmount(Server $server): int
{
if ($this->isLimit()) {
$resourceAmount = $server->{$this->value} ?? 0;
if (!$this->isPercentage()) {
// Our limits are entered as MiB/ MB so we need to convert them to bytes
$resourceAmount *= config('panel.use_binary_prefix') ? 1024 * 1024 : 1000 * 1000;
}
return $resourceAmount;
}
return $server->retrieveResources()[$this->value] ?? 0;
}
public function isLimit(): bool
{
return $this === ServerResourceType::CPULimit || $this === ServerResourceType::MemoryLimit || $this === ServerResourceType::DiskLimit;
}
public function isTime(): bool
{
return $this === ServerResourceType::Uptime;
}
public function isPercentage(): bool
{
return $this === ServerResourceType::CPU || $this === ServerResourceType::CPULimit;
}
}

View File

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

View File

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

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

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

View File

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

View File

@@ -1,11 +0,0 @@
<?php
namespace App\Events\Auth;
use App\Models\User;
use App\Events\Event;
class DirectLogin extends Event
{
public function __construct(public User $user, public bool $remember) {}
}

View File

@@ -1,16 +0,0 @@
<?php
namespace App\Events\Auth;
use App\Events\Event;
use Illuminate\Queue\SerializesModels;
class FailedPasswordReset extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public string $ip, public string $email) {}
}

View File

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

View File

@@ -4,13 +4,14 @@ namespace App\Exceptions;
use Exception;
use Filament\Notifications\Notification;
use Illuminate\Container\Container;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Psr\Log\LoggerInterface;
use Illuminate\Http\Response;
use Illuminate\Container\Container;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Throwable;
/**
* @deprecated
@@ -28,7 +29,7 @@ class DisplayException extends PanelException implements HttpExceptionInterface
/**
* 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);
}
@@ -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
* exception is set.
*
* @throws \Throwable
* @throws Throwable
*/
public function report(): void
{
if (!$this->getPrevious() instanceof \Exception || !Handler::isReportable($this->getPrevious())) {
if (!$this->getPrevious() instanceof Exception || !Handler::isReportable($this->getPrevious())) {
return;
}

View File

@@ -2,24 +2,27 @@
namespace App\Exceptions;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Collection;
use Exception;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Container\Container;
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 Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\Mailer\Exception\TransportException;
use Illuminate\Foundation\Application;
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\Mailer\Exception\TransportException;
use Throwable;
class Handler extends ExceptionHandler
@@ -79,7 +82,7 @@ class Handler extends ExceptionHandler
$this->dontReport = [];
}
$this->reportable(function (\PDOException $ex) {
$this->reportable(function (PDOException $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 = '';
foreach ($exception->getTrace() as $index => $item) {
@@ -117,11 +120,11 @@ class Handler extends ExceptionHandler
/**
* 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);
@@ -143,7 +146,7 @@ class Handler extends ExceptionHandler
* Transform a validation exception into a consistent format to be returned for
* calls to the API.
*
* @param \Illuminate\Http\Request $request
* @param Request $request
*/
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.
*/
public static function isReportable(\Exception $exception): bool
public static function isReportable(Exception $exception): bool
{
return (new self(Container::getInstance()))->shouldReport($exception);
}
@@ -257,7 +260,7 @@ class Handler extends ExceptionHandler
/**
* Convert an authentication exception into an unauthenticated response.
*
* @param \Illuminate\Http\Request $request
* @param Request $request
*/
protected function unauthenticated($request, AuthenticationException $exception): JsonResponse|RedirectResponse
{
@@ -291,7 +294,7 @@ class Handler extends ExceptionHandler
*
* @return array<mixed>
*/
public static function toArray(\Throwable $e): array
public static function toArray(Throwable $e): array
{
return self::exceptionToArray($e);
}

View File

@@ -4,13 +4,14 @@ namespace App\Exceptions\Http;
use Illuminate\Http\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Throwable;
class HttpForbiddenException extends HttpException
{
/**
* 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);
}

View File

@@ -5,6 +5,7 @@ namespace App\Exceptions\Http\Server;
use App\Enums\ServerState;
use App\Models\Server;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
use Throwable;
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
* 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.';
if ($server->isSuspended()) {

View File

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

View File

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

View File

@@ -2,11 +2,11 @@
namespace App\Exceptions\Model;
use Illuminate\Support\MessageBag;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Contracts\Validation\Validator;
use App\Exceptions\PanelException;
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;
class DataValidationException extends PanelException implements HttpExceptionInterface, MessageProvider

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,7 @@
namespace App\Exceptions\Service;
use App\Exceptions\DisplayException;
use Throwable;
class ServiceLimitExceededException extends DisplayException
{
@@ -10,7 +11,7 @@ class ServiceLimitExceededException extends DisplayException
* Exception thrown when something goes over a defined limit, such as allocated
* 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);
}

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

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

View File

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

View File

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

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Extensions\Avatar\Schemas;
use App\Extensions\Avatar\AvatarSchemaInterface;
use App\Models\User;
class UiAvatarsSchema implements AvatarSchemaInterface
{
public function getId(): string
{
return 'uiavatars';
}
public function getName(): string
{
return 'UI Avatars';
}
public function get(User $user): ?string
{
// UI Avatars is the default of filament so just return null here
return null;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,35 +0,0 @@
<?php
namespace App\Extensions;
use App\Models\DatabaseHost;
class DynamicDatabaseConnection
{
public const DB_CHARSET = 'utf8';
public const DB_COLLATION = 'utf8_unicode_ci';
public const DB_DRIVER = 'mysql';
/**
* Adds a dynamic database connection entry to the runtime config.
*/
public function set(string $connection, DatabaseHost|int $host, string $database = 'mysql'): void
{
if (!$host instanceof DatabaseHost) {
$host = DatabaseHost::query()->findOrFail($host);
}
config()->set('database.connections.' . $connection, [
'driver' => self::DB_DRIVER,
'host' => $host->host,
'port' => $host->port,
'database' => $database,
'username' => $host->username,
'password' => $host->password,
'charset' => self::DB_CHARSET,
'collation' => self::DB_COLLATION,
]);
}
}

View File

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

View File

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

View File

@@ -0,0 +1,119 @@
<?php
namespace App\Extensions\Features\Schemas;
use App\Extensions\Features\FeatureSchemaInterface;
use App\Facades\Activity;
use App\Models\Permission;
use App\Models\Server;
use App\Models\ServerVariable;
use App\Repositories\Daemon\DaemonServerRepository;
use Closure;
use Exception;
use Filament\Actions\Action;
use Filament\Facades\Filament;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Notifications\Notification;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\HtmlString;
class GSLTokenSchema implements FeatureSchemaInterface
{
/** @return array<string> */
public function getListeners(): array
{
return [
'(gsl token expired)',
'(account not found)',
];
}
public function getId(): string
{
return 'gsl_token';
}
/**
* @throws Exception
*/
public function getAction(): Action
{
/** @var Server $server */
$server = Filament::getTenant();
/** @var ServerVariable $serverVariable */
$serverVariable = $server->serverVariables()->whereHas('variable', function (Builder $query) {
$query->where('env_variable', 'STEAM_ACC');
})->first();
return Action::make($this->getId())
->requiresConfirmation()
->modalHeading('Invalid GSL token')
->modalDescription('It seems like your Gameserver Login Token (GSL token) is invalid or has expired.')
->modalSubmitActionLabel('Update GSL Token')
->disabledSchema(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_UPDATE, $server))
->schema([
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.'))),
TextInput::make('gsltoken')
->label('GSL Token')
->rules([
fn (): Closure => function (string $attribute, $value, Closure $fail) use ($serverVariable) {
$validator = Validator::make(['validatorkey' => $value], [
'validatorkey' => $serverVariable->variable->rules,
]);
if ($validator->fails()) {
$message = str($validator->errors()->first())->replace('validatorkey', $serverVariable->variable->name);
$fail($message);
}
},
])
->hintIcon('tabler-code', fn () => implode('|', $serverVariable->variable->rules))
->label(fn () => $serverVariable->variable->name)
->prefix(fn () => '{{' . $serverVariable->variable->env_variable . '}}')
->helperText(fn () => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description),
])
->action(function (array $data, DaemonServerRepository $serverRepository) use ($server, $serverVariable) {
/** @var Server $server */
$server = Filament::getTenant();
try {
$new = $data['gsltoken'] ?? '';
$original = $serverVariable->variable_value;
$serverVariable->update([
'variable_value' => $new,
]);
if ($original !== $new) {
Activity::event('server:startup.edit')
->property([
'variable' => $serverVariable->variable->env_variable,
'old' => $original,
'new' => $new,
])
->log();
}
$serverRepository->setServer($server)->power('restart');
Notification::make()
->title('GSL Token updated')
->body('Server will restart now.')
->success()
->send();
} catch (Exception $exception) {
Notification::make()
->title('Could not update GSL Token')
->body($exception->getMessage())
->danger()
->send();
}
});
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace App\Extensions\Features\Schemas;
use App\Extensions\Features\FeatureSchemaInterface;
use App\Facades\Activity;
use App\Models\Permission;
use App\Models\Server;
use App\Repositories\Daemon\DaemonServerRepository;
use Exception;
use Filament\Actions\Action;
use Filament\Facades\Filament;
use Filament\Forms\Components\Select;
use Filament\Infolists\Components\TextEntry;
use Filament\Notifications\Notification;
class JavaVersionSchema implements FeatureSchemaInterface
{
/** @return array<string> */
public function getListeners(): array
{
return [
'java.lang.UnsupportedClassVersionError',
'unsupported major.minor version',
'has been compiled by a more recent version of the java runtime',
'minecraft 1.17 requires running the server with java 16 or above',
'minecraft 1.18 requires running the server with java 17 or above',
'minecraft 1.19 requires running the server with java 17 or above',
];
}
public function getId(): string
{
return 'java_version';
}
public function getAction(): Action
{
/** @var Server $server */
$server = Filament::getTenant();
return Action::make($this->getId())
->requiresConfirmation()
->modalHeading('Unsupported Java Version')
->modalDescription('This server is currently running an unsupported version of Java and cannot be started.')
->modalSubmitActionLabel('Update Docker Image')
->disabledSchema(fn () => !auth()->user()->can(Permission::ACTION_STARTUP_DOCKER_IMAGE, $server))
->schema([
TextEntry::make('java')
->label('Please select a supported version from the list below to continue starting the server.'),
Select::make('image')
->label('Docker Image')
->disabled(fn () => !in_array($server->image, $server->egg->docker_images))
->options(fn () => collect($server->egg->docker_images)->mapWithKeys(fn ($key, $value) => [$key => $value]))
->selectablePlaceholder(false)
->default(fn () => $server->image)
->notIn(fn () => $server->image)
->required()
->preload()
->native(false),
])
->action(function (array $data, DaemonServerRepository $serverRepository) use ($server) {
try {
$new = $data['image'];
$original = $server->image;
$server->forceFill(['image' => $new])->saveOrFail();
if ($original !== $server->image) {
Activity::event('server:startup.image')
->property(['old' => $original, 'new' => $new])
->log();
}
$serverRepository->setServer($server)->power('restart');
Notification::make()
->title('Docker image updated')
->body('Server will restart now.')
->success()
->send();
} catch (Exception $exception) {
Notification::make()
->title('Could not update docker image')
->body($exception->getMessage())
->danger()
->send();
}
});
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace App\Extensions\Features\Schemas;
use App\Extensions\Features\FeatureSchemaInterface;
use App\Models\Server;
use App\Repositories\Daemon\DaemonFileRepository;
use App\Repositories\Daemon\DaemonServerRepository;
use Exception;
use Filament\Actions\Action;
use Filament\Facades\Filament;
use Filament\Notifications\Notification;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
class MinecraftEulaSchema implements FeatureSchemaInterface
{
/** @return array<string> */
public function getListeners(): array
{
return [
'you need to agree to the eula in order to run the server',
];
}
public function getId(): string
{
return 'eula';
}
public function getAction(): Action
{
return Action::make($this->getId())
->requiresConfirmation()
->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>.')))
->modalSubmitActionLabel('I Accept')
->action(function (DaemonFileRepository $fileRepository, DaemonServerRepository $serverRepository) {
try {
/** @var Server $server */
$server = Filament::getTenant();
$fileRepository->setServer($server)->putContent('eula.txt', 'eula=true');
$serverRepository->setServer($server)->power('restart');
Notification::make()
->title('Minecraft EULA accepted')
->body('Server will restart now.')
->success()
->send();
} catch (Exception $exception) {
Notification::make()
->title('Could not accept Minecraft EULA')
->body($exception->getMessage())
->danger()
->send();
}
});
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Extensions\Features\Schemas;
use App\Extensions\Features\FeatureSchemaInterface;
use Filament\Actions\Action;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
class PIDLimitSchema implements FeatureSchemaInterface
{
/** @return array<string> */
public function getListeners(): array
{
return [
'pthread_create failed',
'failed to create thread',
'unable to create thread',
'unable to create native thread',
'unable to create new native thread',
'exception in thread "craft async scheduler management thread"',
];
}
public function getId(): string
{
return 'pid_limit';
}
public function getAction(): Action
{
return Action::make($this->getId())
->requiresConfirmation()
->icon('tabler-alert-triangle')
->modalHeading(fn () => auth()->user()->isAdmin() ? 'Memory or process limit reached...' : 'Possible resource limit reached...')
->modalDescription(new HtmlString(Blade::render(
auth()->user()->isAdmin() ? <<<'HTML'
<p>
This server has reached the maximum process or memory limit.
</p>
<p class="mt-4">
Increasing <code>container_pid_limit</code> in the wings
configuration, <code>config.yml</code>, might help resolve
this issue.
</p>
<p class="mt-4">
<b>Note: Wings must be restarted for the configuration file changes to take effect</b>
</p>
HTML
:
<<<'HTML'
<p>
This server is attempting to use more resources than allocated. Please contact the administrator
and give them the error below.
</p>
<p class="mt-4">
<code>
pthread_create failed, Possibly out of memory or process/resource limits reached
</code>
</p>
HTML
)))
->modalCancelActionLabel('Close')
->action(fn () => null);
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Extensions\Features\Schemas;
use App\Extensions\Features\FeatureSchemaInterface;
use Filament\Actions\Action;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
class SteamDiskSpaceSchema implements FeatureSchemaInterface
{
/** @return array<string> */
public function getListeners(): array
{
return [
'steamcmd needs 250mb of free disk space to update',
'0x202 after update job',
];
}
public function getId(): string
{
return 'steam_disk_space';
}
public function getAction(): Action
{
return Action::make($this->getId())
->requiresConfirmation()
->modalHeading('Out of available disk space...')
->modalDescription(new HtmlString(Blade::render(
auth()->user()->isAdmin() ? <<<'HTML'
<p>
This server has run out of available disk space and cannot complete the install or update
process.
</p>
<p class="mt-4">
Ensure the machine has enough disk space by typing{' '}
<code class="rounded py-1 px-2">df -h</code> on the machine hosting
this server. Delete files or increase the available disk space to resolve the issue.
</p>
HTML
:
<<<'HTML'
<p>
This server has run out of available disk space and cannot complete the install or update
process. Please get in touch with the administrator(s) and inform them of disk space issues.
</p>
HTML
)))
->modalCancelActionLabel('Close')
->action(fn () => null);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,63 +0,0 @@
<?php
namespace App\Extensions\OAuth\Providers;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step;
use Illuminate\Foundation\Application;
use Illuminate\Support\HtmlString;
use Illuminate\Support\Str;
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
final class GithubProvider extends OAuthProvider
{
public function __construct(protected Application $app)
{
parent::__construct($app);
}
public function getId(): string
{
return 'github';
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new Github OAuth App')
->schema([
Placeholder::make('')
->content(new HtmlString('<p>Visit the <u><a href="https://github.com/settings/developers" target="_blank">Github Developer Dashboard</a></u>, go to <b>OAuth Apps</b> and click on <b>New OAuth App</b>.</p><p>Enter an <b>Application name</b> (e.g. your panel name), set <b>Homepage URL</b> to your panel url and enter the below url as <b>Authorization callback URL</b>.</p>')),
TextInput::make('_noenv_callback')
->label('Authorization callback URL')
->dehydrated()
->disabled()
->hintAction(fn () => request()->isSecure() ? CopyAction::make() : null)
->default(fn () => config('app.url') . (Str::endsWith(config('app.url'), '/') ? '' : '/') . 'auth/oauth/callback/github'),
Placeholder::make('')
->content(new HtmlString('<p>When you filled all fields click on <b>Register application</b>.</p>')),
]),
Step::make('Create Client Secret')
->schema([
Placeholder::make('')
->content(new HtmlString('<p>Once you registered your app, generate a new <b>Client Secret</b>.</p><p>You will also need the <b>Client ID</b>.</p>')),
]),
], parent::getSetupSteps());
}
public function getIcon(): string
{
return 'tabler-brand-github-f';
}
public function getHexColor(): string
{
return '#4078c0';
}
public static function register(Application $app): self
{
return new self($app);
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Extensions\OAuth\Schemas;
use Filament\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\Discord\Provider;
final class DiscordSchema extends OAuthSchema
{
public function getId(): string
{
return 'discord';
}
public function getSocialiteProvider(): string
{
return Provider::class;
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new Discord OAuth App')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://discord.com/developers/applications" target="_blank">Discord Developer Portal</x-filament::link> and click on <b>New Application</b>. Enter a <b>Name</b> (e.g. your panel name) and click on <b>Create</b>.</p><p>Copy the <b>Client ID</b> and the <b>Client Secret</b> from the OAuth2 tab, you will need them in the final step.</p>'))),
TextEntry::make('set_redirect')
->hiddenLabel()
->state(new HtmlString('<p>Under <b>Redirects</b> add the below URL.</p>')),
TextInput::make('_noenv_callback')
->label('Redirect URL')
->dehydrated()
->disabled()
->hintCopy()
->formatStateUsing(fn () => url('/auth/oauth/callback/discord')),
]),
], parent::getSetupSteps());
}
public function getIcon(): string
{
return 'tabler-brand-discord-f';
}
public function getHexColor(): string
{
return '#5865F2';
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Extensions\OAuth\Schemas;
use Filament\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 GithubSchema extends OAuthSchema
{
public function getId(): string
{
return 'github';
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new Github OAuth App')
->schema([
TextEntry::make('create_application')
->hiddenLabel()
->state(new HtmlString(Blade::render('<p>Visit the <x-filament::link href="https://github.com/settings/developers" target="_blank">Github Developer Dashboard</x-filament::link>, go to <b>OAuth Apps</b> and click on <b>New OAuth App</b>.</p><p>Enter an <b>Application name</b> (e.g. your panel name), set <b>Homepage URL</b> to your panel url and enter the below url as <b>Authorization callback URL</b>.</p>'))),
TextInput::make('_noenv_callback')
->label('Authorization callback URL')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('/auth/oauth/callback/github')),
TextEntry::make('register_application')
->hiddenLabel()
->state(new HtmlString('<p>When you filled all fields click on <b>Register application</b>.</p>')),
]),
Step::make('Create Client Secret')
->schema([
TextEntry::make('create_client_secret')
->hiddenLabel()
->state(new HtmlString('<p>Once you registered your app, generate a new <b>Client Secret</b>.</p><p>You will also need the <b>Client ID</b>.</p>')),
]),
], parent::getSetupSteps());
}
public function getIcon(): string
{
return 'tabler-brand-github-f';
}
public function getHexColor(): string
{
return '#4078c0';
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace App\Extensions\OAuth\Schemas;
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 GitlabSchema extends OAuthSchema
{
public function getId(): string
{
return 'gitlab';
}
public function getServiceConfig(): array
{
return array_merge(parent::getServiceConfig(), [
'host' => env('OAUTH_GITLAB_HOST'),
]);
}
public function getSettingsForm(): array
{
return array_merge(parent::getSettingsForm(), [
TextInput::make('OAUTH_GITLAB_HOST')
->label('Custom Host')
->placeholder('Only set a custom host if you are self hosting gitlab')
->columnSpan(2)
->url()
->autocomplete(false)
->default(env('OAUTH_GITLAB_HOST')),
]);
}
public function getSetupSteps(): array
{
return array_merge([
Step::make('Register new Gitlab OAuth App')
->schema([
TextEntry::make('register_application')
->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')
->label('Redirect URI')
->dehydrated()
->disabled()
->hintCopy()
->default(fn () => url('/auth/oauth/callback/gitlab')),
]),
], parent::getSetupSteps());
}
public function getIcon(): string
{
return 'tabler-brand-gitlab';
}
public function getHexColor(): string
{
return '#fca326';
}
}

View File

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

View File

@@ -1,27 +1,22 @@
<?php
namespace App\Extensions\OAuth\Providers;
namespace App\Extensions\OAuth\Schemas;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Wizard\Step;
use Illuminate\Foundation\Application;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Wizard\Step;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
use SocialiteProviders\Steam\Provider;
final class SteamProvider extends OAuthProvider
final class SteamSchema extends OAuthSchema
{
public function __construct(protected Application $app)
{
parent::__construct($app);
}
public function getId(): string
{
return 'steam';
}
public function getProviderClass(): string
public function getSocialiteProvider(): string
{
return Provider::class;
}
@@ -57,8 +52,9 @@ final class SteamProvider extends OAuthProvider
return array_merge([
Step::make('Create API Key')
->schema([
Placeholder::make('')
->content(new HtmlString('Visit <u><a href="https://steamcommunity.com/dev/apikey" target="_blank">https://steamcommunity.com/dev/apikey</a></u> to generate an API key.')),
TextEntry::make('create_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());
}
@@ -72,9 +68,4 @@ final class SteamProvider extends OAuthProvider
{
return '#00adee';
}
public static function register(Application $app): self
{
return new self($app);
}
}

View File

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

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