Compare commits

...

579 Commits

Author SHA1 Message Date
Dennis Smink
52ef42a81d Merge pull request #41 from ploi/filament-v4-upgrade
Filament V4 upgrade
2026-01-14 08:16:01 +01:00
Dennis
e225499905 Assets fix 2026-01-14 08:15:15 +01:00
Dennis
9522b28b9d wip 2025-12-29 08:02:21 +01:00
Dennis
e3b944f450 updates 2025-10-28 13:34:34 +01:00
Dennis
e197a915b3 Add useragent 2025-10-28 13:31:05 +01:00
Dennis
db1240c43f Composer format & run action 2025-08-13 20:49:47 +02:00
Dennis
732ed7ea43 Merge branch 'master' of https://github.com/ploi/ploi-core 2025-08-13 20:46:37 +02:00
Dennis
e776ae299a Use Laravel prompts 💬 2025-08-13 20:46:33 +02:00
Dennis Smink
b8c07cde53 Merge pull request #38 from jelleroorda/fix-support-close-reopen
Fix closing and reopening of support tickets.
2025-08-13 20:24:23 +02:00
Dennis Smink
6eb36a84af Merge pull request #37 from jelleroorda/fix-support-ticket-mail
Use the proper route for in new ticket received mail.
2025-08-13 20:23:34 +02:00
Jelle Roorda
9622a08b74 Fix closing and reopening of support tickets. 2025-08-13 17:49:47 +02:00
Jelle Roorda
4d11e3939a Use the proper route for in new ticket received mail. 2025-08-13 17:41:15 +02:00
Dennis Smink
592bc2b0f6 Merge pull request #36 from ploi/prepare-open-source
Open Source 🔥
2025-08-12 11:39:15 +02:00
Dennis
33ccdd1b2d remove traces 2025-08-12 11:23:34 +02:00
Dennis
065773fb6f wip 2025-08-12 08:32:52 +02:00
Dennis
e26a7c2a50 Vite upgrade 2025-08-12 07:44:19 +02:00
Dennis
022caacb24 wip 2025-08-12 07:39:20 +02:00
Dennis
b5da1367d0 wip 2025-08-12 07:37:58 +02:00
Dennis
175134233b wip 2025-08-11 13:53:57 +02:00
Dennis
072018b122 wip 2025-04-11 09:10:21 +02:00
Dennis
694254cd21 prod mix 2025-01-07 10:34:38 +01:00
Dennis
8b5c5734c1 fixes 2025-01-07 10:31:44 +01:00
Dennis
528868c1df wip 2025-01-07 10:19:12 +01:00
Dennis
ba2b932bb8 wip 2025-01-07 10:15:19 +01:00
Dennis
a986d4316e wip 2024-12-19 10:06:01 +01:00
Dennis Smink
f05206f940 Merge pull request #30 from ploi/sm/71-darkmode-bug-2
Added darkmode support for ticket
2024-12-19 09:54:05 +01:00
Dennis Smink
2d6a5fa770 Merge pull request #29 from ploi/sm/72-allow-custom-name-package-webhosting-on-invoice
Changed package name in invoice pdf
2024-12-19 09:53:50 +01:00
Dennis Smink
cd29843a88 Merge pull request #28 from ploi/sm/update
Upgrade Laravel 11 + Vue upgrade
2024-12-19 09:53:17 +01:00
Stan Menten
5a6e742fd9 Added darkmode support for ticket 2024-12-18 16:50:51 +01:00
Stan Menten
6a991aa320 Changed package name in invoice pdf 2024-12-18 16:46:28 +01:00
Stan Menten
1a61a01628 Added necessary migrations 2024-12-18 13:45:00 +01:00
Stan Menten
896564990d Big version update 2024-12-18 13:41:08 +01:00
Dennis
3fa5bb7df9 Catch default errors 2024-04-04 14:44:34 +02:00
Dennis
6b6435f71b Fixes 2024-03-19 13:12:15 +01:00
Dennis
a838f1a1da fx 2024-02-16 14:57:00 +01:00
Dennis
9535f03ff1 Vite 5 upgrade 2024-02-16 14:01:09 +01:00
Dennis
879fe90f18 wip 2024-02-16 13:55:50 +01:00
Dennis
9e79b4d3c1 package updates 2024-02-16 13:45:06 +01:00
Dennis
f8929e5622 wip 2024-02-12 08:22:46 +01:00
Dennis
6556cf017a wip 2023-11-22 12:50:30 +01:00
Dennis
514c010804 fix 2023-11-20 10:12:21 +01:00
Dennis
b3a5624ad4 fx 2023-11-20 10:08:48 +01:00
Dennis
3857aa33d2 wip 2023-11-20 10:07:09 +01:00
Dennis
c625a5c967 rename this method 2023-11-20 10:06:19 +01:00
Dennis
b4d35adfb4 wip 2023-10-18 08:19:48 +02:00
Dennis
0c1d970a9c cleaning 2023-10-17 16:22:45 +02:00
Dennis
3df600bcda wip 2023-10-07 09:50:11 +02:00
Dennis Smink
c9e0bb3bda Merge pull request #24 from ploi/rjs/provider-plans-per-package
RJS/Assign provider plans to specific packages
2023-10-02 11:14:26 +02:00
Ralph J. Smit
3aae5068ce Simplify 2023-09-28 20:58:54 +02:00
Ralph J. Smit
255353763f Add clarifying comment 2023-09-28 20:57:58 +02:00
Ralph J. Smit
99a49848ca Translations 2023-09-28 20:55:20 +02:00
Ralph J. Smit
def9e3c722 Style 2023-09-28 20:50:07 +02:00
Ralph J. Smit
6cc46cf652 Allow limiting provider plans per package 2023-09-28 20:48:12 +02:00
Ralph J. Smit
9ac72ffda8 Fix headerActions package resource 2023-09-28 13:36:32 +02:00
Dennis
3f1bdb1d8e wip 2023-09-28 13:29:45 +02:00
Dennis
f3d2b0c71f wip 2023-09-28 13:28:51 +02:00
Dennis
4071ba6d49 wip 2023-09-28 13:25:32 +02:00
Dennis Smink
4ec50d7ca1 Merge pull request #23 from ploi/rjs/filament-v3
RJS/Filament V3 upgrade
2023-09-28 13:23:37 +02:00
Ralph J. Smit
51b7f28634 Style 2023-09-28 12:15:55 +02:00
Ralph J. Smit
7f6b59cd4f WIP 2023-09-28 12:15:33 +02:00
Ralph J. Smit
80b4428b72 Filament V3 2023-09-28 12:12:04 +02:00
Dennis
010ecd63ac Merge branch 'develop' of https://github.com/ploi/ploi-core into develop 2023-09-28 08:21:51 +02:00
Dennis
741104de05 wip 2023-09-28 08:21:44 +02:00
Dennis Smink
5254ca3ebe Merge pull request #22 from ploi/rjs/fix-laravel-data
RJS/Upgrade to Laravel 10 – Fix testsuite / Laravel Data implementation
2023-09-27 08:44:45 +02:00
Ralph J. Smit
aefbb5be33 Put attributes on single line 2023-09-26 23:48:48 +02:00
Ralph J. Smit
d22bb52f35 Apply style 2023-09-26 23:47:24 +02:00
Ralph J. Smit
088d951bea Fix Laravel Data/testsuite 2023-09-26 23:44:58 +02:00
Dennis
01fe642a9d wip 2023-08-31 08:18:14 +02:00
Dennis
258e7127f7 wip upgrade 2023-04-03 10:45:59 +02:00
Dennis
7e44db0e56 wip 2023-02-02 11:26:10 +01:00
Dennis Smink
e1c07d84df Merge pull request #21 from ploi-deploy/update-label-tickets
Update SupportTicketResource.php
2023-01-30 11:17:00 +01:00
Dennis Smink
145a4af407 Update SupportTicketResource.php 2023-01-30 11:15:37 +01:00
Dennis
d1a7b6002a wip 2023-01-28 20:06:06 +01:00
Dennis
a9c0bdee34 remove 2023-01-28 20:05:20 +01:00
Dennis
604b535895 wip 2023-01-28 20:04:55 +01:00
Dennis
c67546b949 Merge branch 'develop'
# Conflicts:
#	public/build/manifest.json
2023-01-28 20:00:31 +01:00
Dennis Smink
0a45a1c8b2 Merge pull request #19 from ploi-deploy/nzd-currency
NZD currency
2023-01-28 20:00:02 +01:00
Dennis
3cd83ad69c nzd 2023-01-28 19:57:43 +01:00
GitHub Actions
d27df25b3c Updated build assets 2023-01-25 12:26:54 +00:00
Dennis
e55e984f98 wip 2023-01-25 13:25:23 +01:00
Dennis
5fbc0e0a37 Merge branch 'develop'
# Conflicts:
#	public/build/manifest.json
2023-01-25 13:22:45 +01:00
Dennis Smink
3313786480 Merge pull request #17 from ploi-deploy/fix-user-creation-admin
Fix user creation admin
2023-01-25 13:22:16 +01:00
Dennis Smink
5ed532535b Merge pull request #16 from ploi-deploy/package-updates
Package updates
2023-01-25 13:21:58 +01:00
Dennis
a8361f7b22 force 2023-01-25 13:21:26 +01:00
Dennis
a190ccf8dc wip 2023-01-25 13:01:32 +01:00
GitHub Actions
3f927f9ec1 Updated build assets 2023-01-24 14:46:10 +00:00
Dennis
33692960ff Merge branch 'develop'
# Conflicts:
#	public/build/manifest.json
2023-01-24 15:44:24 +01:00
Dennis
1f79a4e790 bugfix textarea 2023-01-24 15:44:08 +01:00
GitHub Actions
903e1cccc6 Updated build assets 2023-01-12 10:47:05 +00:00
Dennis
3f7f6206bd Merge branch 'develop'
# Conflicts:
#	public/build/assets/403-e96763bb.js
#	public/build/assets/403.184d0b9c.js
#	public/build/assets/403.9643c546.js
#	public/build/assets/404-d9c28ef0.js
#	public/build/assets/404.178f769b.js
#	public/build/assets/404.6a0f48e1.js
#	public/build/assets/Aliases-a6c1e5ac.js
#	public/build/assets/Aliases.516d8088.js
#	public/build/assets/Aliases.5dffda98.js
#	public/build/assets/Apps-9ee57278.js
#	public/build/assets/Apps.68116a71.js
#	public/build/assets/Apps.cf8f7e35.js
#	public/build/assets/Article-aed7d8cd.js
#	public/build/assets/Article.225d2cc5.js
#	public/build/assets/Article.50ee5004.js
#	public/build/assets/Billing-0334c2dc.js
#	public/build/assets/Billing.e20f325d.js
#	public/build/assets/Billing.ffbd6b67.js
#	public/build/assets/BillingError-e18727e6.js
#	public/build/assets/BillingError.540cc4df.js
#	public/build/assets/BillingError.f97e8473.js
#	public/build/assets/Button-938c2a59.js
#	public/build/assets/Button.3d31b0b0.js
#	public/build/assets/Button.482f5d57.js
#	public/build/assets/Certificates-c17def4c.js
#	public/build/assets/Certificates.b2e8d10a.js
#	public/build/assets/Certificates.b7a467e5.js
#	public/build/assets/Closed-15ff2b19.js
#	public/build/assets/Closed.33434c56.js
#	public/build/assets/Closed.c034ca5a.js
#	public/build/assets/ConfirmTwoFactorAuthentication-1e147d31.js
#	public/build/assets/ConfirmTwoFactorAuthentication.227845fb.js
#	public/build/assets/ConfirmTwoFactorAuthentication.442d0995.js
#	public/build/assets/Container-45f4da93.js
#	public/build/assets/Container.44bb93ee.js
#	public/build/assets/Container.f0b4c619.js
#	public/build/assets/Cronjobs-b059a4eb.js
#	public/build/assets/Cronjobs.669e6bc0.js
#	public/build/assets/Cronjobs.686bc7da.js
#	public/build/assets/Databases-e9eba7f6.js
#	public/build/assets/Databases.602e1197.js
#	public/build/assets/Databases.92234ef8.js
#	public/build/assets/Dns-a10c5265.js
#	public/build/assets/Dns.2ab43d8b.js
#	public/build/assets/Dns.4bdaed9c.js
#	public/build/assets/DropdownListItemButton-7877064b.js
#	public/build/assets/DropdownListItemButton.573e0a20.js
#	public/build/assets/DropdownListItemButton.ef103e7c.js
#	public/build/assets/Email-79a8d6e5.js
#	public/build/assets/Email.2626d110.js
#	public/build/assets/Email.e0bd114e.js
#	public/build/assets/EmptyImage-db7f150d.js
#	public/build/assets/EmptyImage.090e8b16.js
#	public/build/assets/EmptyImage.e1281e10.js
#	public/build/assets/Form-125b83ab.js
#	public/build/assets/Form.55885a08.js
#	public/build/assets/Form.e3b24233.js
#	public/build/assets/FormInput-f09111c3.js
#	public/build/assets/FormInput.541a08d4.js
#	public/build/assets/FormInput.7a518a32.js
#	public/build/assets/FormSelect-f8b36700.js
#	public/build/assets/FormSelect.02de001d.js
#	public/build/assets/FormSelect.19ff5254.js
#	public/build/assets/FormTextarea-a69c36b6.js
#	public/build/assets/FormTextarea.d5ee4b96.js
#	public/build/assets/FormTextarea.f69b0fba.js
#	public/build/assets/IconArrowDown-8b1a8522.js
#	public/build/assets/IconArrowDown.262b4357.js
#	public/build/assets/IconArrowDown.ef2fcbd6.js
#	public/build/assets/IconStorage-18f5d16d.js
#	public/build/assets/IconStorage.9205bc74.js
#	public/build/assets/IconStorage.b8642876.js
#	public/build/assets/Index-50b7f2f7.js
#	public/build/assets/Index-65445bf1.js
#	public/build/assets/Index-ae22c003.js
#	public/build/assets/Index-b1914fc4.js
#	public/build/assets/Index-c2ad6517.js
#	public/build/assets/Index-ed2777d6.js
#	public/build/assets/Index.3aca5d17.js
#	public/build/assets/Index.42d835dc.js
#	public/build/assets/Index.4db47acb.js
#	public/build/assets/Index.6d9c51dd.js
#	public/build/assets/Index.76408cb7.js
#	public/build/assets/Index.91265e25.js
#	public/build/assets/Index.b8cdaa18.js
#	public/build/assets/Index.c4ee4e5b.js
#	public/build/assets/Index.c83252d6.js
#	public/build/assets/Index.cdf5b851.js
#	public/build/assets/Index.d203e16d.js
#	public/build/assets/Index.fe71e493.js
#	public/build/assets/InstallationIncomplete-7a0ca111.js
#	public/build/assets/InstallationIncomplete.29d458e0.js
#	public/build/assets/InstallationIncomplete.5ce48727.js
#	public/build/assets/Integrations-02935c41.js
#	public/build/assets/Integrations.c7373676.js
#	public/build/assets/Integrations.e2d78e23.js
#	public/build/assets/Login-0b8af846.js
#	public/build/assets/Login.76b08252.js
#	public/build/assets/Login.9a6f1389.js
#	public/build/assets/MainLayout-8649910c.js
#	public/build/assets/MainLayout.26b583b0.js
#	public/build/assets/MainLayout.da88ac2b.js
#	public/build/assets/ModalContainer-87ab727e.js
#	public/build/assets/ModalContainer.08e5766d.js
#	public/build/assets/ModalContainer.c73e321c.js
#	public/build/assets/Pagination-3f4890e0.js
#	public/build/assets/Pagination.00232add.js
#	public/build/assets/Pagination.a75bec58.js
#	public/build/assets/PasswordCreation-5c8b1f8e.js
#	public/build/assets/PasswordCreation.67495dc1.js
#	public/build/assets/PasswordCreation.84b14c48.js
#	public/build/assets/Privacy-815696be.js
#	public/build/assets/Privacy.99e04234.js
#	public/build/assets/Privacy.fbf4a865.js
#	public/build/assets/Redirects-c6b81f3c.js
#	public/build/assets/Redirects.b99cf7cf.js
#	public/build/assets/Redirects.f8448783.js
#	public/build/assets/Register-0403a2cd.js
#	public/build/assets/Register.a42c47dd.js
#	public/build/assets/Register.d20a1a1b.js
#	public/build/assets/Reset-ddea9d5f.js
#	public/build/assets/Reset.8ac23802.js
#	public/build/assets/Reset.d5c232ca.js
#	public/build/assets/Security-4cd2a6e1.js
#	public/build/assets/Security.0d6b2b10.js
#	public/build/assets/Security.f1214994.js
#	public/build/assets/Settings-e7582a86.js
#	public/build/assets/Settings-efc6bba8.js
#	public/build/assets/Settings-f849221a.js
#	public/build/assets/Settings.168928d6.js
#	public/build/assets/Settings.1c4b0087.js
#	public/build/assets/Settings.52d8d7b4.js
#	public/build/assets/Settings.d667c946.js
#	public/build/assets/Settings.ed66a48f.js
#	public/build/assets/Settings.fadf33ce.js
#	public/build/assets/SettingsLayout-1f4f1c24.js
#	public/build/assets/SettingsLayout.6029cb54.js
#	public/build/assets/SettingsLayout.94ea8a51.js
#	public/build/assets/SettingsSegment-70fda3a9.js
#	public/build/assets/SettingsSegment.8f3bca2b.js
#	public/build/assets/SettingsSegment.e4964203.js
#	public/build/assets/Show-07f07f81.js
#	public/build/assets/Show-2e78bbbc.js
#	public/build/assets/Show-34adf7eb.js
#	public/build/assets/Show-3c892a01.js
#	public/build/assets/Show.0ad83d86.js
#	public/build/assets/Show.4d8ab56a.js
#	public/build/assets/Show.6eaded56.js
#	public/build/assets/Show.859411a2.js
#	public/build/assets/Show.931c3baa.js
#	public/build/assets/Show.ab8d88e8.js
#	public/build/assets/Show.c67e74f4.js
#	public/build/assets/Show.f31307aa.js
#	public/build/assets/TabBar-ad9b2a96.js
#	public/build/assets/TabBar.8304c776.js
#	public/build/assets/TabBar.e7e4c38d.js
#	public/build/assets/TableData-47a3f1fd.js
#	public/build/assets/TableData.4c8a41d7.js
#	public/build/assets/TableData.764d232d.js
#	public/build/assets/Tabs-38ad7844.js
#	public/build/assets/Tabs-66e3833f.js
#	public/build/assets/Tabs-7ea7e6ee.js
#	public/build/assets/Tabs.0753e723.js
#	public/build/assets/Tabs.08e17dfd.js
#	public/build/assets/Tabs.3e1b5c30.js
#	public/build/assets/Tabs.7520b2c6.js
#	public/build/assets/Tabs.9c81a864.js
#	public/build/assets/Tabs.bba69a14.js
#	public/build/assets/Terms-dff83575.js
#	public/build/assets/Terms.0c8ae423.js
#	public/build/assets/Terms.aae13061.js
#	public/build/assets/TextDivider-8ebb8335.js
#	public/build/assets/TextDivider.ab7f414c.js
#	public/build/assets/TextDivider.f06d5c66.js
#	public/build/assets/TopBar-149e0829.js
#	public/build/assets/TopBar-43058ee7.js
#	public/build/assets/TopBar-60517658.js
#	public/build/assets/TopBar-97b50929.js
#	public/build/assets/TopBar-fa507151.js
#	public/build/assets/TopBar-fdcb98f3.js
#	public/build/assets/TopBar.23526c20.js
#	public/build/assets/TopBar.3ed45d47.js
#	public/build/assets/TopBar.42fc17aa.js
#	public/build/assets/TopBar.487eeaa9.js
#	public/build/assets/TopBar.544193fc.js
#	public/build/assets/TopBar.6e42637b.js
#	public/build/assets/TopBar.8509fd3d.js
#	public/build/assets/TopBar.a427a47e.js
#	public/build/assets/TopBar.aecff8e2.js
#	public/build/assets/TopBar.bd25c71c.js
#	public/build/assets/TopBar.ddabe973.js
#	public/build/assets/TopBar.e6994b25.js
#	public/build/assets/TwoFactorAuthentication-d10b254e.js
#	public/build/assets/TwoFactorAuthentication.bf9d9f46.js
#	public/build/assets/TwoFactorAuthentication.c3412bf0.js
#	public/build/assets/app-9a1c122c.js
#	public/build/assets/app.67ed15d7.js
#	public/build/assets/app.997ea3ab.js
#	public/build/assets/confirm-eb12c83b.js
#	public/build/assets/confirm.450efa22.js
#	public/build/assets/confirm.91e5714b.js
#	public/build/assets/notification-c544471b.js
#	public/build/assets/notification.544829a1.js
#	public/build/assets/notification.fa833402.js
#	public/build/manifest.json
2023-01-12 11:45:17 +01:00
Dennis
be00824f59 wip 2023-01-12 11:44:17 +01:00
Dennis
eb4220adc9 wip 2022-12-19 11:39:21 +01:00
GitHub Actions
3562a60461 Updated build assets 2022-10-26 06:33:52 +00:00
Dennis
4d80f26519 Merge branch 'develop' 2022-10-26 08:32:42 +02:00
Dennis
29dc893806 remove this 2022-10-26 08:32:37 +02:00
Dennis
35abe4cfd7 Merge branch 'develop' 2022-10-26 07:56:58 +02:00
Dennis
275b359b53 w 2022-10-26 07:56:53 +02:00
Dennis
0be4f4cd94 Merge branch 'develop' 2022-10-26 07:54:10 +02:00
Dennis
2ffd09877e wip 2022-10-26 07:52:43 +02:00
Dennis
a5d2445e3f Fix #10 2022-10-26 07:27:31 +02:00
Dennis
92631cdee9 Merge branch 'develop' 2022-10-17 10:40:29 +02:00
Dennis
0f63b8153a wip 2022-10-17 10:40:20 +02:00
Dennis
b6983d5377 Merge branch 'develop' 2022-10-10 13:31:28 +02:00
Dennis
78899bef61 wip 2022-10-10 13:31:22 +02:00
Dennis
9397651515 Merge branch 'develop' 2022-09-26 08:13:55 +02:00
Dennis
f692fb681a fix 2022-09-26 08:13:49 +02:00
Dennis
6de17d3e3c Merge branch 'develop' 2022-09-20 13:20:01 +02:00
Dennis
b2b24db2e6 wip 2022-09-20 13:19:56 +02:00
Dennis
2af546643e Merge branch 'develop' 2022-09-19 11:07:42 +02:00
Dennis
e63b13e5fd package updates 2022-09-19 11:07:35 +02:00
Dennis
36385c2242 wip 2022-09-11 08:41:00 +02:00
Dennis
ed67a44f5f wip 2022-09-10 16:51:00 +02:00
Dennis
fb8b2fa935 Merge branch 'develop' 2022-09-10 16:50:21 +02:00
Dennis
6e1b7613e4 bugfix 2022-09-10 16:50:08 +02:00
Dennis
175b104ebc wip 2022-08-30 10:09:06 +02:00
Dennis
a72a2466ef Merge branch 'develop' 2022-08-30 09:57:53 +02:00
Dennis
9952e2226d Fix 2022-08-30 09:57:47 +02:00
Dennis
5fa9c334ee prod 2022-08-27 20:04:00 +02:00
Dennis
d1f50c8dd9 prod 2022-08-27 20:03:25 +02:00
Dennis
557cff5e05 Merge branch 'develop' 2022-08-27 20:02:51 +02:00
Dennis
fd1d31d347 fix 2022-08-27 20:02:31 +02:00
Dennis
71d5521ca8 wip 2022-08-19 10:34:24 +02:00
Dennis
489d39099f wip 2022-08-19 10:16:54 +02:00
Dennis
cbd6a227ee Merge branch 'develop'
# Conflicts:
#	public/build/assets/app.1ce28008.css
#	public/js/1535.js
#	public/js/1587.js
#	public/js/1603.js
#	public/js/1970.js
#	public/js/1984.js
#	public/js/2035.js
#	public/js/2502.js
#	public/js/2669.js
#	public/js/2741.js
#	public/js/3067.js
#	public/js/344.js
#	public/js/3495.js
#	public/js/3506.js
#	public/js/3686.js
#	public/js/4104.js
#	public/js/4282.js
#	public/js/4482.js
#	public/js/4598.js
#	public/js/4627.js
#	public/js/4710.js
#	public/js/4967.js
#	public/js/5035.js
#	public/js/5168.js
#	public/js/547.js
#	public/js/5503.js
#	public/js/5597.js
#	public/js/5727.js
#	public/js/5867.js
#	public/js/630.js
#	public/js/6341.js
#	public/js/6366.js
#	public/js/6658.js
#	public/js/6734.js
#	public/js/6887.js
#	public/js/696.js
#	public/js/6974.js
#	public/js/7007.js
#	public/js/7022.js
#	public/js/7319.js
#	public/js/7414.js
#	public/js/7644.js
#	public/js/7790.js
#	public/js/7942.js
#	public/js/8162.js
#	public/js/8399.js
#	public/js/8502.js
#	public/js/8665.js
#	public/js/8899.js
#	public/js/8968.js
#	public/js/9039.js
#	public/js/9049.js
#	public/js/9053.js
#	public/js/917.js
#	public/js/9202.js
#	public/js/9488.js
#	public/js/9617.js
#	public/js/9737.js
#	public/js/9972.js
#	public/js/9989.js
#	public/js/9991.js
#	public/js/app.js
2022-08-19 09:39:27 +02:00
Dennis
b3d2b2ec7a demo fix 2022-08-19 09:38:37 +02:00
Dennis
2889e46685 wip 2022-08-19 09:31:12 +02:00
Dennis
761878e1b6 w 2022-08-19 09:30:44 +02:00
Dennis
4169b07b39 wip 2022-08-19 09:28:50 +02:00
Dennis
8c001cf984 wip 2022-08-19 09:25:01 +02:00
Dennis
a73adb9acf wip 2022-08-18 13:34:40 +02:00
Dennis
6ed9867f6b make this 8.1 default 2022-08-17 10:45:29 +02:00
Dennis
b1667ff445 wip 2022-08-17 09:59:02 +02:00
Dennis
db4a25fb5c fix 2022-08-17 09:55:29 +02:00
Dennis
ee21924253 fix tests 2022-08-17 09:53:08 +02:00
Dennis
37ea8aa6b0 remove old files 2022-08-17 09:33:12 +02:00
Dennis
d4c495eaee wip 2022-08-17 09:32:33 +02:00
Dennis
434046cd3e PSR formatting 2022-08-17 08:20:40 +02:00
Ralph J. Smit
eee61494d0 Fix policies interfering with admin 2022-08-16 14:27:35 +02:00
Ralph J. Smit
053864a589 Clarify that servers are also deleted 2022-08-16 13:20:55 +02:00
Ralph J. Smit
dc87d0d415 Allow completely deleting a user with all the sites etc. 2022-08-16 13:20:23 +02:00
Ralph J. Smit
ae1b41e068 Add language and requires_pasword_for_ftp to User API endpoints 2022-08-16 13:19:53 +02:00
Ralph J. Smit
277aae4bca Fix polling resets didn't preserve the form state 2022-08-16 13:19:20 +02:00
Dennis
f727c4cddb wip 2022-08-16 08:22:29 +02:00
Dennis
58c91fe7bc wip 2022-08-16 08:15:05 +02:00
Dennis
cc29f729d3 Force 8.1 2022-08-15 13:48:37 +02:00
Dennis
ff8982c2ee w 2022-08-15 10:06:14 +02:00
Dennis
aab1e86eee Wip 2022-08-15 09:29:47 +02:00
Ralph J. Smit
3388b4fdec Add packages API endpoint 2022-08-12 22:20:56 +02:00
Ralph J. Smit
8ef6e2b64c Update ServerResource.php 2022-08-12 21:14:59 +02:00
Ralph J. Smit
dd5c074976 Rework title components 2022-08-12 21:11:23 +02:00
Ralph J. Smit
9861ff3a9b Rescue Horizon exceptions 2022-08-09 11:06:39 +02:00
Ralph J. Smit
b4607d8e01 Update SiteResource.php 2022-08-09 11:00:44 +02:00
Dennis
7a98295853 wip 2022-08-09 09:23:31 +02:00
Dennis
95e7682cc2 wip 2022-08-09 09:18:37 +02:00
Dennis
251c29b4bd wip 2022-08-09 09:13:42 +02:00
Ralph J. Smit
96587db6a2 Update synchronize actions in main index pages, hide for busy servers/sites to prevent exceptions 2022-08-08 17:31:18 +02:00
Ralph J. Smit
06aecba93d Add a site relation managers 2022-08-08 17:26:42 +02:00
Ralph J. Smit
bcadd716fe Extract synchronization logic into separate pages 2022-08-08 17:21:58 +02:00
Ralph J. Smit
b2e1ee9e24 Finish reset db password 2022-08-08 12:07:01 +02:00
Ralph J. Smit
e3612ebf23 Merge branch 'V3' of https://github.com/ploi-deploy/ploi-core into V3 2022-08-08 11:57:48 +02:00
Ralph J. Smit
25d5a9617d WIP password reset 2022-08-08 11:57:37 +02:00
Dennis
b6e25806ea Merge branch 'V3' of https://github.com/ploi-deploy/ploi-core into V3 2022-08-08 11:53:59 +02:00
Dennis
f0a8e5e318 wip 2022-08-08 11:53:55 +02:00
Ralph J. Smit
c5dbfa2c4b Reset TwoFactorAuthentication 2022-08-08 11:09:13 +02:00
Ralph J. Smit
93273b5a45 Refactor to $touches 2022-08-08 10:20:08 +02:00
Ralph J. Smit
751449de5e Add Support ticket badge 2022-08-08 10:19:49 +02:00
Ralph J. Smit
e601222b4f Tweak system page design 2022-08-06 17:32:12 +02:00
Ralph J. Smit
30685c4595 Remove files related to V2 admin panel 2022-08-06 17:24:24 +02:00
Ralph J. Smit
60c951a1f8 Build files 2022-08-06 17:20:41 +02:00
Ralph J. Smit
ccc09f0967 Replace inertia link to dashboard with regular link 2022-08-06 17:19:08 +02:00
Ralph J. Smit
de9834d6a7 Upgrade to Vue 3 & use correct Inertia createApp 2022-08-06 17:11:57 +02:00
Ralph J. Smit
09dd3db506 Update GitHub actions script for Filament stylesheet 2022-08-06 15:46:06 +02:00
Ralph J. Smit
560100e592 Build assets 2022-08-06 15:45:28 +02:00
Ralph J. Smit
0f5ef71936 Create manifest.json 2022-08-06 15:43:19 +02:00
Ralph J. Smit
84b2f36440 Fix markdown support ticket 2022-08-06 15:37:37 +02:00
Ralph J. Smit
92c605d9df Cleanup 2022-08-06 15:36:36 +02:00
Ralph J. Smit
0fddf4f348 Support ticket system 2022-08-06 15:36:27 +02:00
Ralph J. Smit
ef64bdd7b3 Update DocumentationItemResource.php 2022-08-06 15:35:57 +02:00
Ralph J. Smit
465b2a524b Update setting() function to support storing multiple settings at the same time 2022-08-06 15:35:45 +02:00
Ralph J. Smit
f2c47ba2f3 Update icons and order 2022-08-05 18:29:18 +02:00
Ralph J. Smit
eab8c45f57 Update DocumentationCategory.php 2022-08-05 18:28:21 +02:00
Ralph J. Smit
2215e12717 Fix empty logo 2022-08-05 18:28:18 +02:00
Ralph J. Smit
f76e5b4d7b Add documentation functionality 2022-08-05 18:28:10 +02:00
Ralph J. Smit
9d1b12b0a3 Admin panel v1 2022-08-05 18:11:23 +02:00
Ralph J. Smit
d89482c4aa WIP 2022-08-01 20:54:52 +02:00
Ralph J. Smit
488808a7a2 WIP 2022-07-30 20:48:39 +02:00
Dennis
8e788dfd8e wip 2022-07-29 08:53:10 +02:00
Dennis
200d2bc44d wip 2022-07-29 08:47:02 +02:00
Dennis
f9424781c0 w 2022-07-29 08:42:34 +02:00
Dennis
f2441990cd w 2022-07-27 13:39:11 +02:00
Dennis
369d205d6a wip 2022-07-27 13:36:18 +02:00
Dennis
b63d4e753b Merge branch '120-move-mix-to-vite' into V3
# Conflicts:
#	composer.lock
#	package.json
#	public/css/app.css
#	public/js/app.js
#	resources/deprecated/Admin/Alerts/Create.vue
#	resources/deprecated/Admin/Alerts/Edit.vue
#	resources/deprecated/Admin/Alerts/Index.vue
#	resources/deprecated/Admin/Documentation/Articles/Create.vue
#	resources/deprecated/Admin/Documentation/Articles/Edit.vue
#	resources/deprecated/Admin/Documentation/Articles/Index.vue
#	resources/deprecated/Admin/Documentation/Create.vue
#	resources/deprecated/Admin/Documentation/Edit.vue
#	resources/deprecated/Admin/Documentation/Index.vue
#	resources/deprecated/Admin/Packages/Create.vue
#	resources/deprecated/Admin/Packages/Edit.vue
#	resources/deprecated/Admin/Packages/Index.vue
#	resources/deprecated/Admin/Servers/Index.vue
#	resources/deprecated/Admin/Services/Index.vue
#	resources/deprecated/Admin/Services/Provider/Edit.vue
#	resources/deprecated/Admin/Services/Providers.vue
#	resources/deprecated/Admin/Services/Server/Edit.vue
#	resources/deprecated/Admin/Services/Servers.vue
#	resources/deprecated/Admin/Services/Site/Edit.vue
#	resources/deprecated/Admin/Services/Sites.vue
#	resources/deprecated/Admin/Sites/Index.vue
#	resources/deprecated/Admin/Support/Index.vue
#	resources/deprecated/Admin/Support/Show.vue
#	resources/deprecated/Admin/Terms.vue
#	resources/deprecated/Admin/Users/Create.vue
#	resources/deprecated/Admin/Users/Edit.vue
#	resources/deprecated/Admin/Users/Index.vue
#	resources/deprecated/Admin/Users/Show.vue
#	resources/js/Pages/Sites/Dns.vue
2022-07-27 13:30:56 +02:00
Dennis
fbe7641cb7 wip 2022-07-27 13:30:13 +02:00
Ralph J. Smit
1ebcf75806 Improve DNS editor 2022-07-27 12:31:15 +02:00
Ralph J. Smit
88b324b47a WIP 2022-07-26 21:32:56 +02:00
Dennis
50768d5648 Merge branch 'develop' 2022-07-24 12:51:08 +02:00
Ralph J. Smit
7cfe8d64c4 Updates to endpoints 2022-07-23 16:42:23 +02:00
Ralph J. Smit
f6f5385751 Update .gitignore 2022-07-23 16:41:58 +02:00
Dennis Smink
62084f590e wip 2022-07-21 15:24:59 +02:00
Dennis Smink
c7edf262f6 Merge branch 'develop'
# Conflicts:
#	public/css/app.css
#	public/js/1042.js
#	public/js/1304.js
#	public/js/1438.js
#	public/js/1582.js
#	public/js/1973.js
#	public/js/2232.js
#	public/js/2306.js
#	public/js/2426.js
#	public/js/2658.js
#	public/js/2668.js
#	public/js/2693.js
#	public/js/2940.js
#	public/js/2999.js
#	public/js/3038.js
#	public/js/3292.js
#	public/js/3739.js
#	public/js/4144.js
#	public/js/43.js
#	public/js/4379.js
#	public/js/4485.js
#	public/js/4596.js
#	public/js/4695.js
#	public/js/47.js
#	public/js/4710.js
#	public/js/4766.js
#	public/js/4791.js
#	public/js/4804.js
#	public/js/4946.js
#	public/js/5641.js
#	public/js/565.js
#	public/js/5766.js
#	public/js/5865.js
#	public/js/6038.js
#	public/js/6282.js
#	public/js/6289.js
#	public/js/6340.js
#	public/js/6424.js
#	public/js/6511.js
#	public/js/6545.js
#	public/js/684.js
#	public/js/6861.js
#	public/js/701.js
#	public/js/7054.js
#	public/js/7210.js
#	public/js/7399.js
#	public/js/7611.js
#	public/js/7767.js
#	public/js/8027.js
#	public/js/8309.js
#	public/js/8371.js
#	public/js/8607.js
#	public/js/8786.js
#	public/js/883.js
#	public/js/908.js
#	public/js/9195.js
#	public/js/9281.js
#	public/js/9353.js
#	public/js/app.js
2022-07-21 15:24:23 +02:00
Ralph J. Smit
4294377160 WIP 2022-07-20 16:53:18 +02:00
Ralph J. Smit
1ae9519594 WIP 2022-07-20 16:47:01 +02:00
Ralph J. Smit
c0f7d572cb Update GitHub Actions trigger 2022-07-20 16:06:01 +02:00
Ralph J. Smit
247fd27368 Merge branch 'develop-test' of https://github.com/ploi-deploy/ploi-core into develop-test 2022-07-20 16:05:13 +02:00
GitHub Actions
39e5d20444 Run Laravel Mix en build front-end assets 2022-07-20 14:04:52 +00:00
Ralph J. Smit
8900e5088a Update master.yml 2022-07-20 16:03:59 +02:00
Ralph J. Smit
e52cc6205a Revert "Dummy change to trigger GH Action"
This reverts commit bffaadec1b.
2022-07-20 16:03:15 +02:00
GitHub Actions
a85be2d666 Run Laravel Mix en build front-end assets 2022-07-20 14:02:51 +00:00
Ralph J. Smit
bffaadec1b Dummy change to trigger GH Action 2022-07-20 16:01:22 +02:00
GitHub Actions
0c7398c7ac Run Laravel Mix en build front-end assets 2022-07-20 13:59:07 +00:00
Ralph J. Smit
a44ce140fc Fix test 2022-07-20 15:57:25 +02:00
Ralph J. Smit
8afe483cee Update PHP 8.0 compatible lock file 2022-07-20 15:55:09 +02:00
Ralph J. Smit
9bd3107303 Node 14 for Vite 2022-07-20 15:53:30 +02:00
Ralph J. Smit
5b124cc433 Update master.yml 2022-07-20 15:52:36 +02:00
Ralph J. Smit
661426668e Update .gitignore 2022-07-20 15:52:34 +02:00
Ralph J. Smit
2df37131d1 Build assets #1 2022-07-20 15:49:48 +02:00
Ralph J. Smit
c55b2f1f85 Merge 2022-07-20 15:49:25 +02:00
Ralph J. Smit
705739d2d3 Merge branch '120-move-mix-to-vite' into develop-test 2022-07-20 15:47:01 +02:00
Ralph J. Smit
35bf27097a Update app.blade.php 2022-07-20 15:44:46 +02:00
Ralph J. Smit
59d60a5b03 Import CSS in JS 2022-07-20 15:40:40 +02:00
Ralph J. Smit
0cd7c7e4f6 Remove Mix files 2022-07-20 15:37:55 +02:00
Ralph J. Smit
254dbaf2ec Fix page title 2022-07-20 15:36:49 +02:00
Ralph J. Smit
705104524e Fix typo 2022-07-20 15:36:46 +02:00
Ralph J. Smit
a7da48a5dd Fix undefined TabBars error 2022-07-20 15:36:35 +02:00
Ralph J. Smit
df87d541cd Fix Inertia, except TabBars 2022-07-20 15:27:47 +02:00
Ralph J. Smit
07ba298c5e WIP Vite 2022-07-20 14:50:11 +02:00
Ralph J. Smit
1ce6e8cace Fix test 2022-07-20 10:51:43 +02:00
Dennis Smink
57f783490b ww 2022-07-20 08:26:14 +02:00
Dennis Smink
eaab262629 Merge branch '61-ploi-core-design' into develop
# Conflicts:
#	public/js/app.js
2022-07-20 08:26:10 +02:00
Dennis Smink
e2e05f9cbf wip 2022-07-20 08:25:33 +02:00
Dennis Smink
24ce8bc60d Merge branch '26-duplicate-check' into develop
# Conflicts:
#	app/Http/Controllers/SiteController.php
#	package-lock.json
#	public/js/app.js
2022-07-20 08:17:59 +02:00
Dennis Smink
75592aaeb2 Do this check server sided 2022-07-20 08:16:42 +02:00
Dennis Smink
c11ad19220 w 2022-07-20 08:08:09 +02:00
Dennis Smink
db799a7d6a w 2022-07-20 08:06:53 +02:00
Dennis Smink
7fea371857 Merge branch '25-add-ssl-for-alias-domains-automatically' into develop
# Conflicts:
#	public/js/app.js
2022-07-20 08:06:48 +02:00
Dennis Smink
d7b3899e71 Few fixes 2022-07-20 08:06:37 +02:00
Dennis Smink
e2886fb67e Merge branch '131-set-up-github-action-for-ploi-core' into develop 2022-07-20 07:56:19 +02:00
Ralph J. Smit
8936e4c2d5 Update package-lock.json 2022-07-19 21:46:38 +02:00
Ralph J. Smit
92bb321a68 WIP 2022-07-19 21:46:33 +02:00
Ralph J. Smit
57c8997dd0 Fix 2022-07-19 21:32:14 +02:00
Ralph J. Smit
2df031a60f Check whether domain already exists when creating site 2022-07-19 21:17:21 +02:00
Ralph J. Smit
77384a1abe Request new certificate automatically after creating new alias 2022-07-19 19:04:01 +02:00
Ralph J. Smit
7b20082537 Implement real usernames for site system users and database name site prefix 2022-07-19 15:58:50 +02:00
Ralph J. Smit
3531e4b296 Update master.yml 2022-07-19 15:21:03 +02:00
Ralph J. Smit
4e92501985 Update master.yml 2022-07-19 15:17:20 +02:00
GitHub Actions
d7632d8289 Run Laravel Mix en build front-end assets 2022-07-19 13:12:48 +00:00
Ralph J. Smit
f404b3e9c6 Update master.yml 2022-07-19 15:11:01 +02:00
Ralph J. Smit
730f9b7451 Update run-tests.yml 2022-07-19 15:09:40 +02:00
Ralph J. Smit
2fe5fd70c9 PHP 8.0 support 2022-07-19 15:07:38 +02:00
Ralph J. Smit
6afe8738df Consistent scripts 2022-07-19 14:50:28 +02:00
Ralph J. Smit
761a940abd Add support for PHP 8.0 back 2022-07-19 14:49:08 +02:00
Ralph J. Smit
f87c1dd5ee Update run-tests.yml 2022-07-19 14:45:06 +02:00
Ralph J. Smit
de70310c90 Update run-tests.yml 2022-07-19 14:43:50 +02:00
Ralph J. Smit
11f9b1ed48 Update run-tests.yml 2022-07-19 14:42:28 +02:00
Ralph J. Smit
060a6b72a7 Update master.yml 2022-07-19 14:42:00 +02:00
Ralph J. Smit
70cc81f110 Update run-tests.yml 2022-07-19 14:41:26 +02:00
Ralph J. Smit
bcc1a9b9a8 Update master.yml 2022-07-19 14:39:10 +02:00
Ralph J. Smit
1c601a6efd Update master.yml 2022-07-19 14:36:42 +02:00
Ralph J. Smit
fee31d03a7 Update master.yml 2022-07-19 14:35:14 +02:00
Ralph J. Smit
b09dc1ba9d Update run-tests.yml 2022-07-19 14:35:11 +02:00
Ralph J. Smit
dae15c620b Update run-tests.yml 2022-07-19 14:31:36 +02:00
Ralph J. Smit
996a048a76 Update phpunit.xml 2022-07-19 14:30:46 +02:00
Ralph J. Smit
8c20f23dfd Update Pest.php 2022-07-19 14:27:27 +02:00
Ralph J. Smit
c80818df4c Update phpunit.xml 2022-07-19 14:19:10 +02:00
Ralph J. Smit
d5e77ae31f Update master.yml 2022-07-19 13:34:03 +02:00
Ralph J. Smit
a14d2c44a1 wip 2022-07-19 13:33:24 +02:00
Ralph J. Smit
3048747ed6 WIP 2022-07-18 22:45:22 +02:00
Ralph J. Smit
20bf6c4784 Compatibility with Http-facade instead of Guzzle 2022-07-18 22:42:24 +02:00
Ralph J. Smit
9b02be5be1 Update SiteController.php 2022-07-18 22:42:04 +02:00
Ralph J. Smit
d141503b6f Remove unused class 2022-07-18 22:26:31 +02:00
Ralph J. Smit
6a8e4e8edf Move Http-tests to Tests/Feature 2022-07-18 22:20:42 +02:00
Ralph J. Smit
89bbf44b3b Finish API-endpoints & tests 2022-07-18 22:19:45 +02:00
Ralph J. Smit
db1f40bf6f WIP 2022-07-16 21:44:33 +02:00
Ralph J. Smit
5933a06dd3 WIP 2022-07-05 18:50:34 +02:00
Ralph J. Smit
3c510906ee General stuff 2022-07-01 21:31:35 +02:00
Ralph J. Smit
b43f4cf292 Server testing & endpoints, general 2022-07-01 21:31:27 +02:00
Ralph J. Smit
9cc046eeed Site testing & endpoints 2022-07-01 21:30:36 +02:00
Ralph J. Smit
8291ac6714 Test and update UserController endpoint 2022-07-01 21:17:53 +02:00
Ralph J. Smit
c578ee70c0 Prepare Pest 2022-07-01 21:17:22 +02:00
Ralph J. Smit
90501e37fd Prepare data objects (hopefully remove some code after PR accepted) 2022-07-01 21:15:59 +02:00
Ralph J. Smit
ec45b0dac0 Update RouteServiceProvider.php 2022-07-01 12:01:33 +02:00
Ralph J. Smit
34b838c259 Implement & test API-authentication, simplify Api-routes 2022-07-01 12:00:16 +02:00
Dennis
28ffc8e240 Merge branch '129-add-user-friendly-404-pages' into develop 2022-07-01 07:53:44 +02:00
Dennis
c9179fbf90 proper mix 2022-07-01 07:52:51 +02:00
Dennis
fe5268971a Merge branch '114-refresh-system-version-in-system-tab' into develop
# Conflicts:
#	public/js/app.js
2022-07-01 07:52:21 +02:00
Dennis
8c246e2dba wip tests 2022-07-01 07:51:27 +02:00
Dennis
81fcfac803 Merge branch '76-the-use-of-html-tags-or-markdown-in-alert-messages' into develop
# Conflicts:
#	public/js/app.js
2022-07-01 07:50:17 +02:00
Ralph J. Smit
ff22b96a8d Fix 404 and 403 Vue error pages 2022-06-30 18:47:11 +02:00
Ralph J. Smit
4a2faf0bce Build files 2022-06-30 18:18:34 +02:00
Ralph J. Smit
5c39d07bf5 Allow version refresh in System 2022-06-30 18:14:54 +02:00
Ralph J. Smit
9870aec79f Support markdown & line breaks in Alert messages 2022-06-30 16:11:18 +02:00
Dennis
833a03e992 dev 2022-06-30 16:02:11 +02:00
Ralph J. Smit
e074ab5be4 Implement two-factor authentication 2022-06-30 15:26:44 +02:00
Dennis
b5963693e6 Prod mix 2022-06-27 10:26:40 +02:00
Dennis
1b7ea67fde Merge branch 'develop'
# Conflicts:
#	public/css/app.css
#	public/js/app.js
2022-06-27 10:25:39 +02:00
Dennis
d4f2b9839e package updates 2022-06-27 10:25:10 +02:00
Dennis
817f6a175c wip 2022-06-27 10:23:57 +02:00
Dennis
b3619e5941 wip 2022-06-20 09:14:56 +02:00
Dennis
33784410e5 wip trial 2022-06-07 13:36:49 +02:00
Dennis
6ecf7904fe wip roadmap 2022-06-07 11:31:39 +02:00
Dennis
21986f2394 add ability to run octane 2022-06-07 11:00:56 +02:00
Dennis
4d8212e56f package updates and add aws for ses emailing 2022-06-07 10:58:38 +02:00
Dennis
865f2958cf Merge branch 'develop' 2022-05-23 16:43:39 +02:00
Dennis
17890d13ad Merge branch 'develop' of https://github.com/ploi-deploy/ploi-core into develop 2022-05-23 16:43:28 +02:00
Dennis
2d33455731 Closes #1 2022-05-23 16:43:15 +02:00
Dennis
49481f9b6a Prod mix 2022-04-14 12:39:01 +02:00
Dennis
7bb800cc0a Merge branch 'develop'
# Conflicts:
#	public/css/app.css
#	public/js/app.js
2022-04-14 12:38:06 +02:00
Dennis
1b8c2c764f fx 2022-04-14 12:37:39 +02:00
Dennis
cb1a1c4c06 w 2022-04-12 10:18:47 +02:00
Dennis
ddd80a8687 site email 2022-04-11 19:13:40 +02:00
Dennis
010d4569c2 package updates 2022-04-11 18:54:44 +02:00
Dennis
62ae0f8299 wip 2022-04-09 19:06:17 +02:00
Dennis
2a3d9cabd0 wip 2022-04-09 07:54:47 +02:00
Dennis
e2a58cf2df tweaks 2022-04-07 14:48:45 +02:00
Dennis
0fd6db251b remove these 2022-04-07 14:47:31 +02:00
Dennis
94d50c11ef wip 2022-04-07 14:46:56 +02:00
Dennis
14c6faafa2 wip 2022-04-07 14:21:57 +02:00
Dennis
ea21076eda wip 2022-04-07 11:15:06 +02:00
Dennis
d378323602 wip 2022-04-07 10:52:42 +02:00
Dennis
f074dee990 wip 2022-04-05 14:59:27 +02:00
Dennis
7bd2917ec4 wip 2022-04-01 10:50:05 +02:00
Dennis
823a39ffa2 prod mix 2022-03-23 09:58:27 +01:00
Dennis
6953a8d2b2 Merge branch 'develop'
# Conflicts:
#	public/css/app.css
#	public/js/app.js
2022-03-23 09:58:01 +01:00
Dennis
fb40b450b6 BRL 2022-03-23 09:54:11 +01:00
Dennis
a28c053945 wip 2022-03-23 09:40:29 +01:00
Dennis
ea0c4ed66d wip 2022-03-23 09:06:28 +01:00
Dennis
7a0716959a Laravel 9 upgrade 2022-03-01 11:21:58 +01:00
Dennis
da322d7b1e prod mix 2022-01-21 13:57:39 +01:00
Dennis
bbff8a5403 Merge branch 'develop'
# Conflicts:
#	public/js/app.js
2022-01-21 13:57:04 +01:00
Dennis
c1ad600042 Ability to select database type, package updates, bugfixes 2022-01-21 13:56:48 +01:00
Dennis
5fc32759fd prod mix 2022-01-19 14:29:36 +01:00
Dennis
59cfd8d71b Merge branch 'develop'
# Conflicts:
#	public/css/app.css
#	public/js/app.js
2022-01-19 14:28:00 +01:00
Dennis
51d5b89df7 wip 2022-01-19 14:26:08 +01:00
Dennis
7c0dcbeb88 TW3.0 upgrade 2021-12-24 16:30:41 +01:00
Dennis
b4467f8d5b Package updates 2021-12-21 09:04:54 +01:00
Dennis
363f4ed801 default to predis here 2021-12-21 08:58:25 +01:00
Dennis
1b6b950fb5 Bugfixing 2021-12-21 08:57:55 +01:00
Dennis
90988f1538 Prod mix 2021-12-19 21:58:01 +01:00
Dennis
139ba793d0 Merge branch 'develop'
# Conflicts:
#	public/js/app.js
2021-12-19 21:57:32 +01:00
Dennis
c505dd0924 PSR 2021-12-19 21:57:23 +01:00
Dennis
ca5ee33978 Bugfix in terms page, made system available in demo 2021-12-19 21:57:10 +01:00
Dennis
c2fd5e3fa9 Prod mix 2021-12-07 15:20:10 +01:00
Dennis
b04176ce48 Merge branch 'develop'
# Conflicts:
#	public/js/app.js
2021-12-07 15:19:25 +01:00
Dennis
5136a4b9f8 Bugfix 2021-12-07 15:18:35 +01:00
Dennis
dcee703aa1 wip 2021-11-30 13:30:21 +01:00
Dennis
7c504339d9 Added favicon if logo is uploaded 2021-11-30 13:18:10 +01:00
Dennis
5ea0761fe9 prod mix 2021-11-24 11:25:14 +01:00
Dennis
c9125c3be8 Merge branch 'develop'
# Conflicts:
#	public/js/app.js
2021-11-24 11:22:00 +01:00
Dennis
84503c19db Ability to revoke card & DNS fix on site show 2021-11-24 11:20:24 +01:00
Dennis
cfd9eba5d7 Ability to remove logo and pagination to system logs 2021-11-19 14:14:20 +01:00
Dennis
10689d3d12 Ability to rotate logs in system 2021-11-19 13:52:32 +01:00
Dennis
e190fb7805 prod mix 2021-11-02 11:44:32 +01:00
Dennis
c1351f7d28 Merge branch 'develop'
# Conflicts:
#	public/js/app.js
2021-11-02 11:43:24 +01:00
Dennis
01f5469e86 Generic fixes 2021-11-02 11:43:10 +01:00
Dennis
8c5c86eb6a Preserve scroll by default 2021-11-02 11:26:53 +01:00
Dennis
995ada46aa Prod mix 2021-09-27 09:51:05 +02:00
Dennis
2e79381872 Merge branch 'develop'
# Conflicts:
#	public/js/app.js
2021-09-27 09:50:24 +02:00
Dennis
d1c7b9a418 Fixes 2021-09-27 09:49:42 +02:00
Dennis
34da2f563d Ability to configure per page number 2021-09-23 14:22:01 +02:00
Dennis
5cf77fde1c Package updates 2021-09-23 08:28:54 +02:00
Dennis
aabf6f27ac Ran PSR formatter 2021-09-23 08:27:27 +02:00
Dennis
fbcaee3bdc prod mix 2021-09-23 08:26:39 +02:00
Dennis
3082c10cdb Merge branch 'develop'
# Conflicts:
#	public/js/app.js
2021-09-23 08:26:03 +02:00
Dennis
a4d90f0017 OG image updatee readme 2021-09-23 08:25:52 +02:00
Dennis
8adfc9837e fixes 2021-09-23 08:20:22 +02:00
Dennis
aa20c8a42b prod mix 2021-09-16 09:51:17 +02:00
Dennis
0f100d6159 Merge branch 'develop'
# Conflicts:
#	public/css/app.css
#	public/js/app.js
2021-09-16 09:50:07 +02:00
Dennis
7c896243a5 wip 2021-09-16 09:49:53 +02:00
Dennis
954fef7c3e Dynamic servers tab, fix for domain uppercase 2021-09-16 09:19:41 +02:00
Dennis
105126e498 prod mix 2021-08-25 09:20:18 +02:00
Dennis
7aa5a8949d Merge branch 'develop'
# Conflicts:
#	public/js/app.js
2021-08-25 09:19:45 +02:00
Dennis
27079928a7 added demo quick-login support 2021-08-25 09:19:10 +02:00
Dennis
99968e57ec wip 2021-08-17 13:00:47 +02:00
Dennis
f75bc1a551 Production mix 2021-08-12 11:25:19 +02:00
Dennis
ef347f9381 Merge branch 'develop' 2021-08-12 11:24:30 +02:00
Dennis
f1aace3d8f Ability to select allowed regions/plans per provider 2021-08-12 11:23:01 +02:00
Dennis
b2bec62766 Merge branch 'develop'
# Conflicts:
#	public/js/app.js
2021-08-12 11:03:51 +02:00
Dennis
3a8682bbed Textual change 2021-08-12 11:03:25 +02:00
Dennis
5cfc1f97fe Logo fix 2021-08-12 10:26:29 +02:00
Dennis
cfc8220f8e Wip 2021-08-12 10:23:23 +02:00
Dennis
ac5ffefaed Horizon status to system settings tab 2021-08-10 14:04:52 +02:00
Dennis
cbd2b8e0e9 Wip on terms & privacy page configurator 2021-08-10 13:51:37 +02:00
Dennis
f0f427a7bb General improvements 2021-08-05 11:31:38 +02:00
Dennis
7b6f651015 Consistent Ploi class instantiate 2021-08-05 11:14:16 +02:00
Dennis
c793daa79a wip 2021-08-05 11:08:19 +02:00
Dennis
71b436aebe Production mix 2021-08-04 10:17:42 +02:00
Dennis
06e108da5b Merge branch 'develop'
# Conflicts:
#	public/js/app.js
2021-08-04 10:17:03 +02:00
Dennis
5677c58dd2 Package updates 2021-08-04 10:07:36 +02:00
Dennis
58ca801e30 PSR formatting 2021-08-04 09:49:47 +02:00
Dennis
e750d7caba Cleaning 2021-08-04 09:49:24 +02:00
Dennis
3c0964ef0e Bugfixes in server settings, site creation 2021-08-04 09:46:11 +02:00
Dennis
33613cdf1c Sync all sites & servers, bugfix in pagination providers 2021-08-04 09:36:28 +02:00
Dennis
abfe174825 Autofocus on login page 2021-08-04 09:19:40 +02:00
Dennis
c750f469bb Updates
- Generic demo check in base controller
- Site synchronization
- User show pagination preserve scroll
2021-08-03 11:58:38 +02:00
Dennis
c7ac56d5cc Wip custom certificates 2021-07-29 09:59:20 +02:00
Dennis
6601f44013 wip custom certificates 2021-07-29 09:07:06 +02:00
Dennis
b7baf862b6 prod mix 2021-07-27 08:33:16 +02:00
Dennis
ca79f4cf21 Merge branch 'develop'
# Conflicts:
#	public/js/app.js
2021-07-27 08:32:39 +02:00
Dennis
58ffbe8c74 URL fixes 2021-07-27 08:32:30 +02:00
Dennis
dfa7b995fc wip 2021-07-23 13:46:53 +02:00
Dennis
59c65fe6ee prod 2021-07-18 10:12:47 +02:00
Dennis
087c042c14 Merge branch 'develop'
# Conflicts:
#	public/js/app.js
2021-07-18 10:12:15 +02:00
Dennis
d1ee488ffd fixes 2021-07-18 10:11:50 +02:00
Dennis
34100bc580 prod mix 2021-07-14 09:51:24 +02:00
Dennis
22d91517fb Merge branch 'develop'
# Conflicts:
#	public/js/app.js
2021-07-14 09:50:10 +02:00
Dennis
1c088bd4e0 Bugfixes and improvements 2021-07-14 09:50:00 +02:00
Dennis
fd5bbb7f5d Merge branch 'develop' 2021-07-13 14:49:03 +02:00
Dennis
426d39bec0 wip 2021-07-13 14:48:56 +02:00
Dennis
8244c2dfb2 Merge branch 'develop' 2021-07-13 14:23:54 +02:00
Dennis
e7f9d32f68 wip 2021-07-13 14:23:49 +02:00
Dennis
71158a0030 prod 2021-07-12 12:33:07 +02:00
Dennis
573ce14b7a Merge branch 'develop'
# Conflicts:
#	public/css/app.css
#	public/js/app.js
2021-07-12 12:31:37 +02:00
Dennis
00ef1f470f add debugging 2021-07-12 12:31:19 +02:00
Dennis
a9357034c8 THB currency, annual billing added 2021-07-12 12:26:21 +02:00
Dennis
a45f5157d4 wip 2021-07-09 11:48:12 +02:00
Dennis
0d1009b8db horizon config update 2021-07-09 11:45:57 +02:00
Dennis
4f0416cd45 Added user show, pagination simplified, package updates 2021-07-09 09:08:09 +02:00
Dennis
76655d76d5 Installation command improvements 2021-07-08 14:26:28 +02:00
Dennis
3b38cbe9ac Merge branch 'develop' 2021-06-17 09:18:34 +02:00
Dennis
acf37b8850 Suppport PHP 8 2021-06-17 09:18:28 +02:00
Dennis
f47a0699d3 Merge branch 'develop' 2021-06-07 09:18:21 +02:00
Dennis
8ae429b06b Tweak 2021-06-07 09:18:14 +02:00
Dennis
626dbbcb49 Fix 2021-06-05 20:30:22 +02:00
Dennis
b623dd80fd Merge branch 'develop' 2021-05-31 15:55:32 +02:00
Dennis
3978a7c9f7 User deleting fix 2021-05-31 15:53:58 +02:00
Dennis
7d2acb7438 Merge branch 'develop' 2021-05-31 08:55:21 +02:00
Dennis
096032301c Fix when deleting user with support tickets 2021-05-31 08:55:15 +02:00
Dennis
c3e99bf2ff Merge branch 'develop' 2021-05-25 07:37:34 +02:00
Dennis
9795642bc7 Bugfix 2021-05-25 07:37:28 +02:00
Dennis
4281a432fb Merge branch 'develop' 2021-05-22 09:36:29 +02:00
Dennis
18df5589b1 Fix title 2021-05-22 09:36:25 +02:00
Dennis
79536eac2e Fresh mix 2021-05-19 09:38:13 +02:00
Dennis
6df82fca04 Clean push 2021-05-19 09:36:22 +02:00
Dennis
cc14123d27 Merge branch 'develop' 2021-05-19 09:36:04 +02:00
Dennis
bd5e7b87ff Package update && password uncomprimised 2021-05-19 09:35:54 +02:00
Dennis
42568916d6 prod mix 2021-05-18 12:17:24 +02:00
Dennis
180803cd8a Merge branch 'develop'
# Conflicts:
#	public/js/app.js
2021-05-17 12:10:01 +02:00
Dennis
08fcb0ce01 Transform currencies properly 2021-05-17 12:07:52 +02:00
Dennis
120b4c9df7 prod mix 2021-05-13 20:37:41 +02:00
Dennis
0eb66c10f6 Merge branch 'develop' 2021-05-13 20:37:09 +02:00
Dennis
1a6b14a250 Ran psr formatting 2021-05-13 20:37:03 +02:00
Dennis
072c8569eb added indian rupee 2021-05-13 20:36:41 +02:00
Dennis
5b9378255b Loading state to select country dropdown 2021-05-11 14:59:34 +02:00
Dennis
ac3cf16377 Production mix 2021-05-11 11:55:08 +02:00
Dennis
31154d20f6 wip 2021-05-11 11:54:43 +02:00
Dennis
f8031ac71a Merge branch 'develop'
# Conflicts:
#	public/css/app.css
#	public/js/2035.js
#	public/js/2407.js
#	public/js/2741.js
#	public/js/4104.js
#	public/js/4627.js
#	public/js/547.js
#	public/js/5727.js
#	public/js/630.js
#	public/js/6341.js
#	public/js/6675.js
#	public/js/6974.js
#	public/js/7438.js
#	public/js/9039.js
#	public/js/9202.js
#	public/js/9737.js
#	public/js/9989.js
#	public/js/app.js
#	public/js/app.js.LICENSE.txt
2021-05-11 11:54:09 +02:00
Dennis
898ec6a454 Stripe billing details used for india regulations 2021-05-11 11:50:41 +02:00
Dennis
9dbd54fdf6 wip 2021-05-07 11:07:48 +02:00
Dennis
934017384d wip 2021-05-07 11:04:38 +02:00
Dennis
aee3dc0d93 Merge branch 'develop' 2021-05-05 15:50:42 +02:00
Dennis
8046687ae7 Bugfix for settings 2021-05-05 15:49:05 +02:00
Dennis
a89ee796c1 Prod mix 2021-05-05 11:25:07 +02:00
Dennis
af1d3c3edc wip 2021-05-05 11:24:19 +02:00
Dennis
e1a54c2781 wip 2021-05-05 11:23:54 +02:00
Dennis
2d9f7d49b5 Merge branch 'develop' 2021-05-05 11:22:23 +02:00
Dennis
d10b046033 New features
- Few bugfixes
- Updated to TW2.0
- Updated Laravel Mix
- Documentation is now working
2021-05-05 11:21:48 +02:00
Dennis
1725c0ff65 Merge branch 'develop' 2021-05-01 08:25:59 +02:00
Dennis
b852756c82 Bugfix 2021-05-01 08:25:46 +02:00
Dennis
f164d878d7 Linking storage 2021-04-01 09:49:44 +02:00
Dennis
ecc10fffcb Merge branch 'develop' 2021-03-15 10:51:30 +01:00
Dennis
5c75b015ba Register fix 2021-03-15 10:51:23 +01:00
Dennis
65f7dc697e Production mix 2021-03-12 14:20:30 +01:00
Dennis
d981f0f899 Merge branch 'develop'
# Conflicts:
#	public/js/13.js
#	public/js/23.js
#	public/js/app.js
2021-03-12 14:20:12 +01:00
Dennis
ab50beefab Package updates & GBP currency 2021-03-12 14:20:00 +01:00
Dennis
58d1215fd6 Production mix 2021-02-10 13:56:20 +01:00
Dennis
1dc137c314 Merge branch 'develop'
# Conflicts:
#	package-lock.json
#	public/js/10.js
#	public/js/11.js
#	public/js/12.js
#	public/js/13.js
#	public/js/14.js
#	public/js/15.js
#	public/js/16.js
#	public/js/17.js
#	public/js/18.js
#	public/js/19.js
#	public/js/20.js
#	public/js/21.js
#	public/js/22.js
#	public/js/23.js
#	public/js/24.js
#	public/js/25.js
#	public/js/26.js
#	public/js/27.js
#	public/js/28.js
#	public/js/29.js
#	public/js/30.js
#	public/js/31.js
#	public/js/32.js
#	public/js/33.js
#	public/js/58.js
#	public/js/62.js
#	public/js/63.js
#	public/js/64.js
#	public/js/65.js
#	public/js/66.js
#	public/js/67.js
#	public/js/68.js
#	public/js/69.js
#	public/js/70.js
#	public/js/71.js
#	public/js/72.js
#	public/js/73.js
#	public/js/74.js
#	public/js/75.js
#	public/js/76.js
#	public/js/77.js
#	public/js/78.js
#	public/js/79.js
#	public/js/8.js
#	public/js/80.js
#	public/js/9.js
#	public/js/app.js
2021-02-10 13:55:43 +01:00
Dennis
ca51e9bf5f package updates 2021-02-10 13:55:18 +01:00
Dennis
c588583dfc Few tweaks 2021-02-10 13:54:30 +01:00
Dennis
57c7c53eae Added spanish 2021-01-20 09:18:21 +01:00
Dennis
76a62d9992 rm 2021-01-20 09:16:43 +01:00
Dennis
575aa1c6b1 wip 2020-12-29 08:39:05 +01:00
Dennis
4867a61fd0 Production mixed 2020-12-14 12:55:09 +01:00
Dennis
a63e8f350b Merge branch 'develop'
# Conflicts:
#	public/js/52.js
#	public/js/app.js
2020-12-14 12:54:39 +01:00
Dennis
6c1b4f28af Portuguese added 2020-12-14 12:54:22 +01:00
Dennis
09adccf752 Flush version data 2020-12-02 11:00:28 +01:00
Dennis
79bb522dee Don’t allow horizon in production 2020-12-02 10:58:39 +01:00
Dennis
2adc4bc7ca Production mixed 2020-11-28 11:41:34 +01:00
Dennis
0a81d58051 Merge branch 'develop'
# Conflicts:
#	public/css/app.css
#	public/js/0.js
#	public/js/1.js
#	public/js/18.js
#	public/js/19.js
#	public/js/20.js
#	public/js/21.js
#	public/js/22.js
#	public/js/23.js
#	public/js/24.js
#	public/js/25.js
#	public/js/26.js
#	public/js/27.js
#	public/js/31.js
#	public/js/49.js
#	public/js/5.js
#	public/js/50.js
#	public/js/51.js
#	public/js/52.js
#	public/js/61.js
#	public/js/71.js
#	public/js/72.js
#	public/js/73.js
#	public/js/74.js
#	public/js/app.js
2020-11-28 11:41:00 +01:00
Dennis
cb84438778 Improve ping command 2020-11-28 11:12:54 +01:00
Dennis
b0a76d311c Change this 2020-11-28 10:23:26 +01:00
Dennis
5604503d26 wip 2020-11-28 09:48:50 +01:00
Dennis
bf55092b3a wip 2020-11-28 09:13:42 +01:00
Dennis
8286e7f9af customizing 2020-11-27 19:46:53 +01:00
Dennis
1255221550 wip 2020-11-26 15:59:48 +01:00
Dennis
f4062cd6e7 wip 2020-11-26 15:23:38 +01:00
Dennis
93377ae753 Ability to allow custom styling 2020-11-25 15:56:16 +01:00
Dennis
a43cd19efd production mixing 2020-11-24 14:49:44 +01:00
Dennis
e5eec000d3 Merge branch 'develop'
# Conflicts:
#	public/js/0.js
#	public/js/1.js
#	public/js/10.js
#	public/js/11.js
#	public/js/12.js
#	public/js/13.js
#	public/js/14.js
#	public/js/15.js
#	public/js/16.js
#	public/js/17.js
#	public/js/18.js
#	public/js/19.js
#	public/js/20.js
#	public/js/21.js
#	public/js/22.js
#	public/js/23.js
#	public/js/24.js
#	public/js/25.js
#	public/js/26.js
#	public/js/27.js
#	public/js/28.js
#	public/js/29.js
#	public/js/30.js
#	public/js/31.js
#	public/js/32.js
#	public/js/33.js
#	public/js/34.js
#	public/js/35.js
#	public/js/36.js
#	public/js/37.js
#	public/js/38.js
#	public/js/39.js
#	public/js/40.js
#	public/js/41.js
#	public/js/42.js
#	public/js/43.js
#	public/js/44.js
#	public/js/45.js
#	public/js/46.js
#	public/js/47.js
#	public/js/48.js
#	public/js/49.js
#	public/js/50.js
#	public/js/51.js
#	public/js/52.js
#	public/js/53.js
#	public/js/54.js
#	public/js/55.js
#	public/js/56.js
#	public/js/57.js
#	public/js/58.js
#	public/js/59.js
#	public/js/60.js
#	public/js/61.js
#	public/js/62.js
#	public/js/63.js
#	public/js/64.js
#	public/js/65.js
#	public/js/66.js
#	public/js/67.js
#	public/js/68.js
#	public/js/69.js
#	public/js/70.js
#	public/js/71.js
#	public/js/72.js
#	public/js/73.js
#	public/js/74.js
#	public/js/75.js
#	public/js/9.js
#	public/js/app.js
2020-11-24 14:48:46 +01:00
Dennis
e5c8a62b32 wip 2020-11-24 14:48:02 +01:00
Dennis
9ae1c145b6 wip 2020-11-24 13:35:48 +01:00
Dennis
eb8b75e4f9 wip 2020-11-19 14:49:46 +01:00
Dennis
7378b82adf wip 2020-11-19 14:27:37 +01:00
Dennis
6f3b588f3d fx 2020-11-19 11:48:22 +01:00
Dennis
a2154cf37c wip 2020-11-18 12:06:34 +01:00
Dennis
9dab5f8093 wip 2020-11-18 11:22:44 +01:00
Dennis
9c9469d2f6 Title fixes 2020-11-17 13:29:35 +01:00
Dennis
94acc313b1 Alerts added 2020-11-17 12:40:39 +01:00
Dennis
448398322f Production mix 2020-11-12 20:42:40 +01:00
Dennis
3a78339396 Merge branch 'develop'
# Conflicts:
#	public/js/10.js
#	public/js/app.js
2020-11-12 20:42:04 +01:00
Dennis
a925a70448 Providers fix 2020-11-12 20:41:41 +01:00
Dennis
e283eacaa3 Production mixed 2020-11-12 13:34:27 +01:00
Dennis
3df6b6baed Merge branch 'develop'
# Conflicts:
#	public/js/0.js
#	public/js/10.js
#	public/js/11.js
#	public/js/12.js
#	public/js/13.js
#	public/js/14.js
#	public/js/15.js
#	public/js/16.js
#	public/js/17.js
#	public/js/18.js
#	public/js/19.js
#	public/js/20.js
#	public/js/21.js
#	public/js/22.js
#	public/js/23.js
#	public/js/24.js
#	public/js/25.js
#	public/js/26.js
#	public/js/27.js
#	public/js/28.js
#	public/js/29.js
#	public/js/30.js
#	public/js/31.js
#	public/js/32.js
#	public/js/33.js
#	public/js/34.js
#	public/js/35.js
#	public/js/36.js
#	public/js/37.js
#	public/js/38.js
#	public/js/39.js
#	public/js/40.js
#	public/js/41.js
#	public/js/42.js
#	public/js/43.js
#	public/js/44.js
#	public/js/45.js
#	public/js/46.js
#	public/js/47.js
#	public/js/48.js
#	public/js/49.js
#	public/js/50.js
#	public/js/52.js
#	public/js/67.js
#	public/js/68.js
#	public/js/69.js
#	public/js/75.js
#	public/js/8.js
#	public/js/9.js
#	public/js/app.js
2020-11-12 13:33:57 +01:00
Dennis
b65526e040 Stripe coupon fix && general updates 2020-11-12 13:33:34 +01:00
Dennis
673bbf73be Responsive progress 2020-11-11 16:27:16 +01:00
Dennis
094d22eaa8 Added API link here 2020-11-11 14:10:29 +01:00
Dennis
0fdba5fdec default to en here 2020-11-11 13:42:05 +01:00
Dennis
cf0730be89 Production mix 2020-11-11 13:09:48 +01:00
Dennis
221e67fd12 Merge branch 'develop' 2020-11-11 13:06:15 +01:00
Dennis
f9074309d1 Added coupon feature 2020-11-11 12:30:33 +01:00
Dennis
b2bdbb9e30 Allow sorting in billing 2020-11-11 12:07:06 +01:00
Dennis
63d0cb9626 Default language setting & fixed norwegian keys 2020-11-11 10:58:19 +01:00
Dennis
e3bb3ae4d1 Merge branch 'develop' 2020-11-06 08:42:09 +01:00
Dennis
31890005ac Fix 2020-11-06 08:41:52 +01:00
Dennis
04a216dee1 Merge branch 'develop' 2020-11-05 11:21:40 +01:00
Dennis
5d403c1202 Fix currency displayer 2020-11-05 11:21:29 +01:00
Dennis
a2d92c67b3 Merge branch 'develop' 2020-11-04 14:00:41 +01:00
Dennis
0fec3d82a3 Ugly bug 2020-11-04 14:00:36 +01:00
Dennis
6530a64f97 Production mixed 2020-11-04 13:42:50 +01:00
Dennis
656f02d652 Merge branch 'develop'
# Conflicts:
#	public/js/0.js
#	public/js/13.js
#	public/js/14.js
#	public/js/18.js
#	public/js/20.js
#	public/js/21.js
#	public/js/22.js
#	public/js/23.js
#	public/js/24.js
#	public/js/25.js
#	public/js/26.js
#	public/js/27.js
#	public/js/28.js
#	public/js/29.js
#	public/js/30.js
#	public/js/31.js
#	public/js/32.js
#	public/js/33.js
#	public/js/34.js
#	public/js/35.js
#	public/js/36.js
#	public/js/37.js
#	public/js/38.js
#	public/js/39.js
#	public/js/40.js
#	public/js/41.js
#	public/js/42.js
#	public/js/43.js
#	public/js/44.js
#	public/js/45.js
#	public/js/46.js
#	public/js/47.js
#	public/js/48.js
#	public/js/49.js
#	public/js/50.js
#	public/js/51.js
#	public/js/52.js
#	public/js/53.js
#	public/js/54.js
#	public/js/55.js
#	public/js/56.js
#	public/js/57.js
#	public/js/58.js
#	public/js/59.js
#	public/js/60.js
#	public/js/61.js
#	public/js/62.js
#	public/js/63.js
#	public/js/64.js
#	public/js/65.js
#	public/js/66.js
#	public/js/67.js
#	public/js/68.js
#	public/js/69.js
#	public/js/70.js
#	public/js/app.js
2020-11-04 13:42:17 +01:00
Dennis
0dbf3bba4d Wip 2020-11-04 13:41:46 +01:00
Dennis
ea47c0c3c6 wip 2020-11-04 12:15:19 +01:00
Dennis
b4072c7892 wip 2020-10-27 10:05:52 +01:00
Dennis
e80cd1990a Added more currencies 2020-10-27 08:40:26 +01:00
Dennis
2585cc1db4 Production mix 2020-10-26 07:51:05 +01:00
Dennis
0225828445 Add Norwegian language 2020-10-26 07:49:58 +01:00
Dennis
63af93592d Keyboard shortcuts disable-able 2020-10-22 14:06:23 +02:00
Dennis
b87fcd0f25 Danish language 2020-10-22 11:54:27 +02:00
Dennis
23a6b3cc55 Tweaks 2020-10-21 09:39:44 +02:00
Dennis
dac3d229fd Support MariaDB better 2020-10-21 09:03:35 +02:00
Dennis
e360d9c5df Add update script 2020-10-21 08:50:48 +02:00
Dennis
8f100fc027 wip 2020-10-21 08:47:06 +02:00
Dennis
06fb331ae4 Sorting 2020-10-20 15:53:59 +02:00
Dennis
2bcf02a779 Production mixed 2020-10-20 15:47:32 +02:00
Dennis
1b24664b60 Merge branch 'feature/server-management'
# Conflicts:
#	public/css/app.css
#	public/js/0.js
#	public/js/1.js
#	public/js/10.js
#	public/js/11.js
#	public/js/12.js
#	public/js/13.js
#	public/js/14.js
#	public/js/15.js
#	public/js/16.js
#	public/js/17.js
#	public/js/18.js
#	public/js/19.js
#	public/js/20.js
#	public/js/21.js
#	public/js/22.js
#	public/js/23.js
#	public/js/24.js
#	public/js/25.js
#	public/js/26.js
#	public/js/27.js
#	public/js/28.js
#	public/js/29.js
#	public/js/30.js
#	public/js/31.js
#	public/js/32.js
#	public/js/33.js
#	public/js/34.js
#	public/js/35.js
#	public/js/36.js
#	public/js/37.js
#	public/js/38.js
#	public/js/39.js
#	public/js/4.js
#	public/js/40.js
#	public/js/41.js
#	public/js/42.js
#	public/js/43.js
#	public/js/44.js
#	public/js/45.js
#	public/js/46.js
#	public/js/47.js
#	public/js/48.js
#	public/js/49.js
#	public/js/50.js
#	public/js/51.js
#	public/js/52.js
#	public/js/53.js
#	public/js/54.js
#	public/js/55.js
#	public/js/56.js
#	public/js/57.js
#	public/js/58.js
#	public/js/59.js
#	public/js/60.js
#	public/js/61.js
#	public/js/62.js
#	public/js/63.js
#	public/js/64.js
#	public/js/65.js
#	public/js/66.js
#	public/js/67.js
#	public/js/68.js
#	public/js/8.js
#	public/js/9.js
#	public/js/app.js
2020-10-20 15:46:59 +02:00
Dennis
2ce96a2062 wip 2020-10-20 15:46:18 +02:00
Dennis
5d737ba328 wip 2020-10-20 13:54:27 +02:00
Dennis
bafe8ba780 wip 2020-10-20 13:43:26 +02:00
Dennis
b7ff40fd72 wip 2020-10-20 12:30:21 +02:00
Dennis
8e036b1c01 wip 2020-10-20 12:21:13 +02:00
Dennis
b016c18880 wip 2020-10-20 12:16:40 +02:00
Dennis
d17ae49155 PSR formatting 2020-10-20 12:03:44 +02:00
Dennis
6ef5cd25f5 wip 2020-10-20 12:03:19 +02:00
Dennis
c98f077a0e wip 2020-10-20 11:25:32 +02:00
Dennis
a1f58c8e13 wip 2020-10-20 10:21:00 +02:00
Dennis
37281b01e4 wip 2020-10-14 13:19:24 +02:00
Dennis
65b0a768af Progress 2020-10-14 12:25:15 +02:00
Dennis
39af06d3b2 Tailwind 1.9 2020-10-13 15:12:21 +02:00
Dennis Smink
7d137dd612 wip 2020-10-09 12:51:29 +02:00
Dennis Smink
ce5e6c18f0 wip 2020-10-08 14:56:48 +02:00
Dennis Smink
3d445ca61a Allow attaching providers to packages 2020-10-06 12:47:20 +02:00
Dennis Smink
8566eaaa6c wip 2020-10-05 14:57:18 +02:00
Dennis Smink
db1647569c wip 2020-10-05 14:56:44 +02:00
Dennis Smink
204afb7eb1 wip 2020-10-05 14:34:24 +02:00
Dennis Smink
37f82d4579 Production mixed 2020-09-28 14:08:11 +02:00
Dennis Smink
2ed440e65f Merge branch 'develop'
# Conflicts:
#	public/js/1.js
#	public/js/10.js
#	public/js/11.js
#	public/js/12.js
#	public/js/13.js
#	public/js/14.js
#	public/js/15.js
#	public/js/16.js
#	public/js/17.js
#	public/js/18.js
#	public/js/19.js
#	public/js/2.js
#	public/js/20.js
#	public/js/21.js
#	public/js/22.js
#	public/js/23.js
#	public/js/24.js
#	public/js/25.js
#	public/js/26.js
#	public/js/27.js
#	public/js/28.js
#	public/js/29.js
#	public/js/3.js
#	public/js/30.js
#	public/js/31.js
#	public/js/32.js
#	public/js/33.js
#	public/js/34.js
#	public/js/35.js
#	public/js/36.js
#	public/js/37.js
#	public/js/38.js
#	public/js/39.js
#	public/js/4.js
#	public/js/40.js
#	public/js/41.js
#	public/js/42.js
#	public/js/43.js
#	public/js/44.js
#	public/js/45.js
#	public/js/46.js
#	public/js/47.js
#	public/js/48.js
#	public/js/49.js
#	public/js/50.js
#	public/js/51.js
#	public/js/52.js
#	public/js/53.js
#	public/js/54.js
#	public/js/55.js
#	public/js/56.js
#	public/js/57.js
#	public/js/58.js
#	public/js/59.js
#	public/js/6.js
#	public/js/60.js
#	public/js/61.js
#	public/js/62.js
#	public/js/63.js
#	public/js/64.js
#	public/js/65.js
#	public/js/66.js
#	public/js/8.js
#	public/js/9.js
#	public/js/app.js
2020-09-28 14:07:31 +02:00
Dennis Smink
57a5ec2206 Mixes 2020-09-28 13:56:23 +02:00
Dennis Smink
f14bc0494b finalize 2020-09-28 13:53:30 +02:00
Dennis Smink
c4147f0125 wip 2020-09-28 13:35:54 +02:00
Dennis Smink
cfd0c3cbe9 wip 2020-09-28 11:11:37 +02:00
Dennis Smink
0d0e2732b5 wip 2020-09-28 10:45:05 +02:00
Dennis Smink
0a7b072eeb wip 2020-09-27 19:06:33 +02:00
Dennis Smink
af6e12ca01 wip 2020-09-27 13:30:52 +02:00
Dennis Smink
5eba94fd9b wip 2020-09-25 20:44:18 +02:00
Dennis Smink
e9756494d9 wip 2020-09-25 15:17:18 +02:00
Dennis Smink
f7f919b5de Update columns 2020-09-25 15:00:04 +02:00
Dennis Smink
6f434f3b07 Initial start on stripe 2020-09-25 14:56:01 +02:00
Dennis Smink
8820851afa wip 2020-09-25 14:42:20 +02:00
Dennis Smink
4582e955d0 wip 2020-09-25 14:05:37 +02:00
Dennis Smink
e07395b3d5 Wip 2020-09-25 09:45:59 +02:00
Dennis Smink
3ad7d06976 Less logging days 2020-09-25 09:37:54 +02:00
Dennis Smink
f57cbb76e3 Ability to read up into laravel logs 2020-09-25 09:29:57 +02:00
Dennis Smink
bb3151a2fe Progress 2020-09-24 16:06:56 +02:00
Dennis Smink
5b48d204a0 Wip 2020-09-24 15:26:26 +02:00
Dennis Smink
680d96882a Progress 2020-09-24 15:09:09 +02:00
Dennis Smink
7347356646 Progress on improved permissions 2020-09-24 11:49:00 +02:00
Dennis Smink
2652e7ed71 Improve installation 2020-09-23 21:48:27 +02:00
Dennis Smink
a068da2a54 Production mixed 2020-09-22 14:11:52 +02:00
Dennis Smink
de69a2687a Merge branch 'develop'
# Conflicts:
#	public/js/41.js
#	public/js/42.js
#	public/js/43.js
#	public/js/44.js
#	public/js/45.js
#	public/js/46.js
#	public/js/47.js
#	public/js/48.js
#	public/js/49.js
#	public/js/50.js
#	public/js/51.js
#	public/js/52.js
#	public/js/53.js
#	public/js/54.js
#	public/js/55.js
#	public/js/56.js
#	public/js/57.js
#	public/js/58.js
#	public/js/59.js
#	public/js/6.js
#	public/js/60.js
#	public/js/61.js
#	public/js/62.js
#	public/js/63.js
#	public/js/app.js
2020-09-22 14:11:20 +02:00
Dennis Smink
59552cf8e5 Progress on server management for users 2020-09-22 13:56:34 +02:00
904 changed files with 112117 additions and 32448 deletions

0
.ai/mcp/mcp.json Normal file
View File

View File

@@ -4,9 +4,11 @@ APP_KEY=
APP_DEBUG=false
APP_URL=http://localhost
APP_DEMO=false
APP_DATE_TIME_FORMAT="Y-m-d H:i:s"
PLOI_TOKEN=
PLOI_CORE_TOKEN=
IMPERSONATION=false
LOG_CHANNEL=stack
@@ -26,6 +28,7 @@ SESSION_LIFETIME=120
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
REDIS_CLIENT=predis
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
@@ -36,6 +39,10 @@ MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"
STRIPE_KEY=your-stripe-key
STRIPE_SECRET=your-stripe-secret
CASHIER_MODEL=App\Models\User
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
@@ -46,5 +53,5 @@ PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

33
.github/workflows/run-tests.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: "Run tests"
on:
push:
workflow_call:
jobs:
test:
name: Pest (PHP ${{ matrix.php }} ${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
php: [8.4]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo, mysql
coverage: none
- name: Install dependencies
run: |
composer install
- name: Execute tests
run: vendor/bin/pest

10
.gitignore vendored
View File

@@ -1,8 +1,10 @@
/node_modules
/node_modules.nosync
/public/hot
/public/storage
/storage/*.key
/vendor
/vendor.nosync
.env
.env.backup
.phpunit.result.cache
@@ -12,3 +14,11 @@ npm-debug.log
yarn-error.log
.idea
.php_cs.cache
.php-cs-fixer.cache
/public/js/resources*.js
/storage/views/header.blade.php
/storage/views/footer.blade.php
rr
.rr.yaml
.DS_Store
.phpunit.cache

551
.junie/guidelines.md Normal file
View File

@@ -0,0 +1,551 @@
<laravel-boost-guidelines>
=== foundation rules ===
# Laravel Boost Guidelines
The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications.
## Foundational Context
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
- php - 8.4.10
- filament/filament (FILAMENT) - v3
- inertiajs/inertia-laravel (INERTIA) - v2
- laravel/cashier (CASHIER) - v15
- laravel/framework (LARAVEL) - v11
- laravel/horizon (HORIZON) - v5
- laravel/octane (OCTANE) - v2
- laravel/prompts (PROMPTS) - v0
- livewire/livewire (LIVEWIRE) - v3
- tightenco/ziggy (ZIGGY) - v1
- laravel/dusk (DUSK) - v8
- laravel/mcp (MCP) - v0
- pestphp/pest (PEST) - v3
- phpunit/phpunit (PHPUNIT) - v11
- @inertiajs/vue3 (INERTIA) - v2
- tailwindcss (TAILWINDCSS) - v3
- vue (VUE) - v3
## Conventions
- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, naming.
- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`.
- Check for existing components to reuse before writing a new one.
## Verification Scripts
- Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important.
## Application Structure & Architecture
- Stick to existing directory structure - don't create new base folders without approval.
- Do not change the application's dependencies without approval.
## Frontend Bundling
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them.
## Replies
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
## Documentation Files
- You must only create documentation files if explicitly requested by the user.
=== boost rules ===
## Laravel Boost
- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them.
## Artisan
- Use the `list-artisan-commands` tool when you need to call an Artisan command to double check the available parameters.
## URLs
- Whenever you share a project URL with the user you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain / IP, and port.
## Tinker / Debugging
- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly.
- Use the `database-query` tool when you only need to read from the database.
## Reading Browser Logs With the `browser-logs` Tool
- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost.
- Only recent browser logs will be useful - ignore old logs.
## Searching Documentation (Critically Important)
- Boost comes with a powerful `search-docs` tool you should use before any other approaches. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation specific for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
- The 'search-docs' tool is perfect for all Laravel related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc.
- You must use this tool to search for Laravel-ecosystem documentation before falling back to other approaches.
- Search the documentation before making code changes to ensure we are taking the correct approach.
- Use multiple, broad, simple, topic based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`.
- Do not add package names to queries - package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`.
### Available Search Syntax
- You can and should pass multiple queries at once. The most relevant results will be returned first.
1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'
2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit"
3. Quoted Phrases (Exact Position) - query="infinite scroll" - Words must be adjacent and in that order
4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit"
5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms
=== php rules ===
## PHP
- Always use curly braces for control structures, even if it has one line.
### Constructors
- Use PHP 8 constructor property promotion in `__construct()`.
- <code-snippet>public function __construct(public GitHub $github) { }</code-snippet>
- Do not allow empty `__construct()` methods with zero parameters.
### Type Declarations
- Always use explicit return type declarations for methods and functions.
- Use appropriate PHP type hints for method parameters.
<code-snippet name="Explicit Return Types and Method Params" lang="php">
protected function isAccessible(User $user, ?string $path = null): bool
{
...
}
</code-snippet>
## Comments
- Prefer PHPDoc blocks over comments. Never use comments within the code itself unless there is something _very_ complex going on.
## PHPDoc Blocks
- Add useful array shape type definitions for arrays when appropriate.
## Enums
- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
=== tests rules ===
## Test Enforcement
- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test` with a specific filename or filter.
=== inertia-laravel/core rules ===
## Inertia Core
- Inertia.js components should be placed in the `resources/js/Pages` directory unless specified differently in the JS bundler (vite.config.js).
- Use `Inertia::render()` for server-side routing instead of traditional Blade views.
- Use `search-docs` for accurate guidance on all things Inertia.
<code-snippet lang="php" name="Inertia::render Example">
// routes/web.php example
Route::get('/users', function () {
return Inertia::render('Users/Index', [
'users' => User::all()
]);
});
</code-snippet>
=== inertia-laravel/v2 rules ===
## Inertia v2
- Make use of all Inertia features from v1 & v2. Check the documentation before making any changes to ensure we are taking the correct approach.
### Inertia v2 New Features
- Polling
- Prefetching
- Deferred props
- Infinite scrolling using merging props and `WhenVisible`
- Lazy loading data on scroll
### Deferred Props & Empty States
- When using deferred props on the frontend, you should add a nice empty state with pulsing / animated skeleton.
### Inertia Form General Guidance
- Build forms using the `useForm` helper. Use the code examples and `search-docs` tool with a query of `useForm helper` for guidance.
=== laravel/core rules ===
## Do Things the Laravel Way
- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool.
- If you're creating a generic PHP class, use `php artisan make:class`.
- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior.
### Database
- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins.
- Use Eloquent models and relationships before suggesting raw database queries
- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them.
- Generate code that prevents N+1 query problems by using eager loading.
- Use Laravel's query builder for very complex database operations.
### Model Creation
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`.
### APIs & Eloquent Resources
- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.
### Controllers & Validation
- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages.
- Check sibling Form Requests to see if the application uses array or string based validation rules.
### Queues
- Use queued jobs for time-consuming operations with the `ShouldQueue` interface.
### Authentication & Authorization
- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.).
### URL Generation
- When generating links to other pages, prefer named routes and the `route()` function.
### Configuration
- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`.
### Testing
- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`.
- When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.
### Vite Error
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`.
=== laravel/v11 rules ===
## Laravel 11
- Use the `search-docs` tool to get version specific documentation.
- This project upgraded from Laravel 10 without migrating to the new streamlined Laravel 11 file structure.
- This is **perfectly fine** and recommended by Laravel. Follow the existing structure from Laravel 10. We do not to need migrate to the Laravel 11 structure unless the user explicitly requests that.
### Laravel 10 Structure
- Middleware typically live in `app/Http/Middleware/` and service providers in `app/Providers/`.
- There is no `bootstrap/app.php` application configuration in a Laravel 10 structure:
- Middleware registration is in `app/Http/Kernel.php`
- Exception handling is in `app/Exceptions/Handler.php`
- Console commands and schedule registration is in `app/Console/Kernel.php`
- Rate limits likely exist in `RouteServiceProvider` or `app/Http/Kernel.php`
### Database
- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost.
- Laravel 11 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`.
### Models
- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models.
### New Artisan Commands
- List Artisan commands using Boost's MCP tool, if available. New commands available in Laravel 11:
- `php artisan make:enum`
- `php artisan make:class `
- `php artisan make:interface `
=== livewire/core rules ===
## Livewire Core
- Use the `search-docs` tool to find exact version specific documentation for how to write Livewire & Livewire tests.
- Use the `php artisan make:livewire [Posts\CreatePost]` artisan command to create new components
- State should live on the server, with the UI reflecting it.
- All Livewire requests hit the Laravel backend, they're like regular HTTP requests. Always validate form data, and run authorization checks in Livewire actions.
## Livewire Best Practices
- Livewire components require a single root element.
- Use `wire:loading` and `wire:dirty` for delightful loading states.
- Add `wire:key` in loops:
```blade
@foreach ($items as $item)
<div wire:key="item-{{ $item->id }}">
{{ $item->name }}
</div>
@endforeach
```
- Prefer lifecycle hooks like `mount()`, `updatedFoo()` for initialization and reactive side effects:
<code-snippet name="Lifecycle hook examples" lang="php">
public function mount(User $user) { $this->user = $user; }
public function updatedSearch() { $this->resetPage(); }
</code-snippet>
## Testing Livewire
<code-snippet name="Example Livewire component test" lang="php">
Livewire::test(Counter::class)
->assertSet('count', 0)
->call('increment')
->assertSet('count', 1)
->assertSee(1)
->assertStatus(200);
</code-snippet>
<code-snippet name="Testing a Livewire component exists within a page" lang="php">
$this->get('/posts/create')
->assertSeeLivewire(CreatePost::class);
</code-snippet>
=== livewire/v3 rules ===
## Livewire 3
### Key Changes From Livewire 2
- These things changed in Livewire 2, but may not have been updated in this application. Verify this application's setup to ensure you conform with application conventions.
- Use `wire:model.live` for real-time updates, `wire:model` is now deferred by default.
- Components now use the `App\Livewire` namespace (not `App\Http\Livewire`).
- Use `$this->dispatch()` to dispatch events (not `emit` or `dispatchBrowserEvent`).
- Use the `components.layouts.app` view as the typical layout path (not `layouts.app`).
### New Directives
- `wire:show`, `wire:transition`, `wire:cloak`, `wire:offline`, `wire:target` are available for use. Use the documentation to find usage examples.
### Alpine
- Alpine is now included with Livewire, don't manually include Alpine.js.
- Plugins included with Alpine: persist, intersect, collapse, and focus.
### Lifecycle Hooks
- You can listen for `livewire:init` to hook into Livewire initialization, and `fail.status === 419` for the page expiring:
<code-snippet name="livewire:load example" lang="js">
document.addEventListener('livewire:init', function () {
Livewire.hook('request', ({ fail }) => {
if (fail && fail.status === 419) {
alert('Your session expired');
}
});
Livewire.hook('message.failed', (message, component) => {
console.error(message);
});
});
</code-snippet>
=== pest/core rules ===
## Pest
### Testing
- If you need to verify a feature is working, write or update a Unit / Feature test.
### Pest Tests
- All tests must be written using Pest. Use `php artisan make:test --pest {name}`.
- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files - these are core to the application.
- Tests should test all of the happy paths, failure paths, and weird paths.
- Tests live in the `tests/Feature` and `tests/Unit` directories.
- Pest tests look and behave like this:
<code-snippet name="Basic Pest Test Example" lang="php">
it('is true', function () {
expect(true)->toBeTrue();
});
</code-snippet>
### Running Tests
- Run the minimal number of tests using an appropriate filter before finalizing code edits.
- To run all tests: `php artisan test`.
- To run all tests in a file: `php artisan test tests/Feature/ExampleTest.php`.
- To filter on a particular test name: `php artisan test --filter=testName` (recommended after making a change to a related file).
- When the tests relating to your changes are passing, ask the user if they would like to run the entire test suite to ensure everything is still passing.
### Pest Assertions
- When asserting status codes on a response, use the specific method like `assertForbidden` and `assertNotFound` instead of using `assertStatus(403)` or similar, e.g.:
<code-snippet name="Pest Example Asserting postJson Response" lang="php">
it('returns all', function () {
$response = $this->postJson('/api/docs', []);
$response->assertSuccessful();
});
</code-snippet>
### Mocking
- Mocking can be very helpful when appropriate.
- When mocking, you can use the `Pest\Laravel\mock` Pest function, but always import it via `use function Pest\Laravel\mock;` before using it. Alternatively, you can use `$this->mock()` if existing tests do.
- You can also create partial mocks using the same import or self method.
### Datasets
- Use datasets in Pest to simplify tests which have a lot of duplicated data. This is often the case when testing validation rules, so consider going with this solution when writing tests for validation rules.
<code-snippet name="Pest Dataset Example" lang="php">
it('has emails', function (string $email) {
expect($email)->not->toBeEmpty();
})->with([
'james' => 'james@laravel.com',
'taylor' => 'taylor@laravel.com',
]);
</code-snippet>
=== inertia-vue/core rules ===
## Inertia + Vue
- Vue components must have a single root element.
- Use `router.visit()` or `<Link>` for navigation instead of traditional links.
<code-snippet name="Inertia Client Navigation" lang="vue">
import { Link } from '@inertiajs/vue3'
<Link href="/">Home</Link>
</code-snippet>
=== inertia-vue/v2/forms rules ===
## Inertia + Vue Forms
<code-snippet name="Inertia Vue useForm example" lang="vue">
<script setup>
import { useForm } from '@inertiajs/vue3'
const form = useForm({
email: null,
password: null,
remember: false,
})
</script>
<template>
<form @submit.prevent="form.post('/login')">
<!-- email -->
<input type="text" v-model="form.email">
<div v-if="form.errors.email">{{ form.errors.email }}</div>
<!-- password -->
<input type="password" v-model="form.password">
<div v-if="form.errors.password">{{ form.errors.password }}</div>
<!-- remember me -->
<input type="checkbox" v-model="form.remember"> Remember Me
<!-- submit -->
<button type="submit" :disabled="form.processing">Login</button>
</form>
</template>
</code-snippet>
=== tailwindcss/core rules ===
## Tailwind Core
- Use Tailwind CSS classes to style HTML, check and use existing tailwind conventions within the project before writing your own.
- Offer to extract repeated patterns into components that match the project's conventions (i.e. Blade, JSX, Vue, etc..)
- Think through class placement, order, priority, and defaults - remove redundant classes, add classes to parent or child carefully to limit repetition, group elements logically
- You can use the `search-docs` tool to get exact examples from the official documentation when needed.
### Spacing
- When listing items, use gap utilities for spacing, don't use margins.
<code-snippet name="Valid Flex Gap Spacing Example" lang="html">
<div class="flex gap-8">
<div>Superior</div>
<div>Michigan</div>
<div>Erie</div>
</div>
</code-snippet>
### Dark Mode
- If existing pages and components support dark mode, new pages and components must support dark mode in a similar way, typically using `dark:`.
=== tailwindcss/v3 rules ===
## Tailwind 3
- Always use Tailwind CSS v3 - verify you're using only classes supported by this version.
=== filament/filament rules ===
## Filament
- Filament is used by this application, check how and where to follow existing application conventions.
- Filament is a Server-Driven UI (SDUI) framework for Laravel. It allows developers to define user interfaces in PHP using structured configuration objects. It is built on top of Livewire, Alpine.js, and Tailwind CSS.
- You can use the `search-docs` tool to get information from the official Filament documentation when needed. This is very useful for Artisan command arguments, specific code examples, testing functionality, relationship management, and ensuring you're following idiomatic practices.
- Utilize static `make()` methods for consistent component initialization.
### Artisan
- You must use the Filament specific Artisan commands to create new files or components for Filament. You can find these with the `list-artisan-commands` tool, or with `php artisan` and the `--help` option.
- Inspect the required options, always pass `--no-interaction`, and valid arguments for other options when applicable.
### Filament's Core Features
- Actions: Handle doing something within the application, often with a button or link. Actions encapsulate the UI, the interactive modal window, and the logic that should be executed when the modal window is submitted. They can be used anywhere in the UI and are commonly used to perform one-time actions like deleting a record, sending an email, or updating data in the database based on modal form input.
- Forms: Dynamic forms rendered within other features, such as resources, action modals, table filters, and more.
- Infolists: Read-only lists of data.
- Notifications: Flash notifications displayed to users within the application.
- Panels: The top-level container in Filament that can include all other features like pages, resources, forms, tables, notifications, actions, infolists, and widgets.
- Resources: Static classes that are used to build CRUD interfaces for Eloquent models. Typically live in `app/Filament/Resources`.
- Schemas: Represent components that define the structure and behavior of the UI, such as forms, tables, or lists.
- Tables: Interactive tables with filtering, sorting, pagination, and more.
- Widgets: Small component included within dashboards, often used for displaying data in charts, tables, or as a stat.
### Relationships
- Determine if you can use the `relationship()` method on form components when you need `options` for a select, checkbox, repeater, or when building a `Fieldset`:
<code-snippet name="Relationship example for Form Select" lang="php">
Forms\Components\Select::make('user_id')
->label('Author')
->relationship('author')
->required(),
</code-snippet>
## Testing
- It's important to test Filament functionality for user satisfaction.
- Ensure that you are authenticated to access the application within the test.
- Filament uses Livewire, so start assertions with `livewire()` or `Livewire::test()`.
### Example Tests
<code-snippet name="Filament Table Test" lang="php">
livewire(ListUsers::class)
->assertCanSeeTableRecords($users)
->searchTable($users->first()->name)
->assertCanSeeTableRecords($users->take(1))
->assertCanNotSeeTableRecords($users->skip(1))
->searchTable($users->last()->email)
->assertCanSeeTableRecords($users->take(-1))
->assertCanNotSeeTableRecords($users->take($users->count() - 1));
</code-snippet>
<code-snippet name="Filament Create Resource Test" lang="php">
livewire(CreateUser::class)
->fillForm([
'name' => 'Howdy',
'email' => 'howdy@example.com',
])
->call('create')
->assertNotified()
->assertRedirect();
assertDatabaseHas(User::class, [
'name' => 'Howdy',
'email' => 'howdy@example.com',
]);
</code-snippet>
<code-snippet name="Testing Multiple Panels (setup())" lang="php">
use Filament\Facades\Filament;
Filament::setCurrentPanel('app');
</code-snippet>
<code-snippet name="Calling an Action in a Test" lang="php">
livewire(EditInvoice::class, [
'invoice' => $invoice,
])->callAction('send');
expect($invoice->refresh())->isSent()->toBeTrue();
</code-snippet>
## Version 3 Changes To Focus On
- Resources are located in `app/Filament/Resources/` directory.
- Resource pages (List, Create, Edit) are auto-generated within the resource's directory - e.g., `app/Filament/Resources/PostResource/Pages/`.
- Forms use the `Forms\Components` namespace for form fields.
- Tables use the `Tables\Columns` namespace for table columns.
- A new `Filament\Forms\Components\RichEditor` component is available.
- Form and table schemas now use fluent method chaining.
- Added `php artisan filament:optimize` command for production optimization.
- Requires implementing `FilamentUser` contract for production access control.
</laravel-boost-guidelines>

11
.junie/mcp/mcp.json Normal file
View File

@@ -0,0 +1,11 @@
{
"mcpServers": {
"laravel-boost": {
"command": "/opt/homebrew/Cellar/php/8.4.10/bin/php",
"args": [
"/Users/dennissmink/Workspace/ploi-core/artisan",
"boost:mcp"
]
}
}
}

11
.mcp.json Normal file
View File

@@ -0,0 +1,11 @@
{
"mcpServers": {
"laravel-boost": {
"command": "php",
"args": [
"artisan",
"boost:mcp"
]
}
}
}

View File

@@ -8,11 +8,12 @@ $finder = Symfony\Component\Finder\Finder::create()
->name('*.php')
->notName('*.blade.php');
return PhpCsFixer\Config::create()
return (new PhpCsFixer\Config)
->setRules([
'@PSR2' => true,
'array_syntax' => ['syntax' => 'short'],
'ordered_imports' => ['sortAlgorithm' => 'length'],
'ordered_imports' => ['sort_algorithm' => 'length'],
'no_unused_imports' => true,
])
->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect())
->setFinder($finder);

View File

@@ -9,5 +9,5 @@ php:
js:
finder:
not-name:
- webpack.mix.js
- vite.config.js
css: true

676
CLAUDE.md Normal file
View File

@@ -0,0 +1,676 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Ploi Core is a Laravel-based webhosting management platform that allows users to launch their own webhosting service using ploi.io as the backend infrastructure.
## Essential Commands
### Development
```bash
# Start development server
npm run dev
# Build for production
npm run build
# Watch for changes
npm run watch
# Format PHP code
composer format
# Run tests
php artisan test
php artisan test --filter=TestName
# Run browser tests
php artisan dusk
# Clear all caches
php artisan cache:clear
php artisan config:clear
php artisan route:clear
php artisan view:clear
# Queue management
php artisan horizon
php artisan queue:work
```
### Database
```bash
# Run migrations
php artisan migrate
# Rollback migrations
php artisan migrate:rollback
# Fresh migration with seeders
php artisan migrate:fresh --seed
```
### Custom Artisan Commands
```bash
php artisan core:install # Initial installation
php artisan core:synchronize # Sync with Ploi API
php artisan core:cleanup # Clean up resources
php artisan core:trial # Manage trials
```
## Architecture Overview
### Technology Stack
- **Backend**: Laravel 11 (PHP 8.2+), Filament v3 admin panel
- **Frontend**: Vue 3 with Inertia.js, Tailwind CSS, Vite
- **Queue**: Laravel Horizon with Redis
- **Payments**: Laravel Cashier (Stripe)
- **Testing**: Pest PHP, Laravel Dusk
### Key Directories
- `app/Services/Ploi/` - Ploi.io API integration layer
- `app/Filament/` - Admin panel resources and pages
- `app/Http/Controllers/` - Web and API controllers
- `app/Jobs/` - Async queue jobs for Ploi API operations
- `resources/js/Pages/` - Inertia.js Vue pages
- `resources/js/components/` - Reusable Vue components
### Ploi API Integration
The application heavily integrates with the Ploi.io API. Key service class is at `app/Services/Ploi/Ploi.php`. All server/site management operations go through this API layer. Use queue jobs for long-running operations to avoid timeouts.
### Database Structure
Main entities: Users, Packages, Servers, Sites, Databases, Certificates, Cronjobs. Multi-tenancy through user-server-site relationships. Role-based access: admin, reseller, user.
### Frontend Architecture
- Inertia.js handles the Vue-Laravel bridge
- Pages are in `resources/js/Pages/` following Laravel route structure
- Shared data is passed via Inertia middleware
- Vuex store modules in `resources/js/store/`
- Form handling uses Inertia forms
### Testing Approach
- Feature tests use Pest PHP syntax
- Database tests use RefreshDatabase trait
- API calls should be mocked using Http::fake()
- Browser tests in `tests/Browser/` using Dusk
### Important Environment Variables
```
PLOI_TOKEN= # Ploi API token
APP_DEMO=false # Demo mode toggle
STRIPE_KEY= # Stripe public key
STRIPE_SECRET= # Stripe secret key
```
### Development Workflow
1. Always run `npm run dev` for frontend changes
2. Use queue workers for Ploi API operations
3. Clear caches when changing config or routes
4. Format code with `composer format` before commits
5. Test with `php artisan test` for unit/feature tests
### Common Patterns
- Use Actions (`app/Actions/`) for business logic
- API responses follow Laravel's resource pattern
- Filament resources handle admin CRUD operations
- Queue jobs for async Ploi API calls
- Service classes for external integrations
### Deployment
Production deployment uses the `update.sh` script which handles git pull, composer install, migrations, and cache clearing. Laravel Horizon manages queues in production.
===
<laravel-boost-guidelines>
=== foundation rules ===
# Laravel Boost Guidelines
The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications.
## Foundational Context
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
- php - 8.4.10
- filament/filament (FILAMENT) - v3
- inertiajs/inertia-laravel (INERTIA) - v2
- laravel/cashier (CASHIER) - v15
- laravel/framework (LARAVEL) - v11
- laravel/horizon (HORIZON) - v5
- laravel/octane (OCTANE) - v2
- laravel/prompts (PROMPTS) - v0
- livewire/livewire (LIVEWIRE) - v3
- tightenco/ziggy (ZIGGY) - v1
- laravel/dusk (DUSK) - v8
- laravel/mcp (MCP) - v0
- pestphp/pest (PEST) - v3
- phpunit/phpunit (PHPUNIT) - v11
- @inertiajs/vue3 (INERTIA) - v2
- tailwindcss (TAILWINDCSS) - v3
- vue (VUE) - v3
## Conventions
- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, naming.
- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`.
- Check for existing components to reuse before writing a new one.
## Verification Scripts
- Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important.
## Application Structure & Architecture
- Stick to existing directory structure - don't create new base folders without approval.
- Do not change the application's dependencies without approval.
## Frontend Bundling
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them.
## Replies
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
## Documentation Files
- You must only create documentation files if explicitly requested by the user.
=== boost rules ===
## Laravel Boost
- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them.
## Artisan
- Use the `list-artisan-commands` tool when you need to call an Artisan command to double check the available parameters.
## URLs
- Whenever you share a project URL with the user you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain / IP, and port.
## Tinker / Debugging
- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly.
- Use the `database-query` tool when you only need to read from the database.
## Reading Browser Logs With the `browser-logs` Tool
- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost.
- Only recent browser logs will be useful - ignore old logs.
## Searching Documentation (Critically Important)
- Boost comes with a powerful `search-docs` tool you should use before any other approaches. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation specific for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
- The 'search-docs' tool is perfect for all Laravel related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc.
- You must use this tool to search for Laravel-ecosystem documentation before falling back to other approaches.
- Search the documentation before making code changes to ensure we are taking the correct approach.
- Use multiple, broad, simple, topic based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`.
- Do not add package names to queries - package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`.
### Available Search Syntax
- You can and should pass multiple queries at once. The most relevant results will be returned first.
1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'
2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit"
3. Quoted Phrases (Exact Position) - query="infinite scroll" - Words must be adjacent and in that order
4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit"
5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms
=== php rules ===
## PHP
- Always use curly braces for control structures, even if it has one line.
### Constructors
- Use PHP 8 constructor property promotion in `__construct()`.
- <code-snippet>public function __construct(public GitHub $github) { }</code-snippet>
- Do not allow empty `__construct()` methods with zero parameters.
### Type Declarations
- Always use explicit return type declarations for methods and functions.
- Use appropriate PHP type hints for method parameters.
<code-snippet name="Explicit Return Types and Method Params" lang="php">
protected function isAccessible(User $user, ?string $path = null): bool
{
...
}
</code-snippet>
## Comments
- Prefer PHPDoc blocks over comments. Never use comments within the code itself unless there is something _very_ complex going on.
## PHPDoc Blocks
- Add useful array shape type definitions for arrays when appropriate.
## Enums
- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
=== tests rules ===
## Test Enforcement
- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test` with a specific filename or filter.
=== inertia-laravel/core rules ===
## Inertia Core
- Inertia.js components should be placed in the `resources/js/Pages` directory unless specified differently in the JS bundler (vite.config.js).
- Use `Inertia::render()` for server-side routing instead of traditional Blade views.
- Use `search-docs` for accurate guidance on all things Inertia.
<code-snippet lang="php" name="Inertia::render Example">
// routes/web.php example
Route::get('/users', function () {
return Inertia::render('Users/Index', [
'users' => User::all()
]);
});
</code-snippet>
=== inertia-laravel/v2 rules ===
## Inertia v2
- Make use of all Inertia features from v1 & v2. Check the documentation before making any changes to ensure we are taking the correct approach.
### Inertia v2 New Features
- Polling
- Prefetching
- Deferred props
- Infinite scrolling using merging props and `WhenVisible`
- Lazy loading data on scroll
### Deferred Props & Empty States
- When using deferred props on the frontend, you should add a nice empty state with pulsing / animated skeleton.
### Inertia Form General Guidance
- Build forms using the `useForm` helper. Use the code examples and `search-docs` tool with a query of `useForm helper` for guidance.
=== laravel/core rules ===
## Do Things the Laravel Way
- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool.
- If you're creating a generic PHP class, use `php artisan make:class`.
- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior.
### Database
- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins.
- Use Eloquent models and relationships before suggesting raw database queries
- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them.
- Generate code that prevents N+1 query problems by using eager loading.
- Use Laravel's query builder for very complex database operations.
### Model Creation
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`.
### APIs & Eloquent Resources
- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.
### Controllers & Validation
- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages.
- Check sibling Form Requests to see if the application uses array or string based validation rules.
### Queues
- Use queued jobs for time-consuming operations with the `ShouldQueue` interface.
### Authentication & Authorization
- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.).
### URL Generation
- When generating links to other pages, prefer named routes and the `route()` function.
### Configuration
- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`.
### Testing
- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`.
- When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.
### Vite Error
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`.
=== laravel/v11 rules ===
## Laravel 11
- Use the `search-docs` tool to get version specific documentation.
- This project upgraded from Laravel 10 without migrating to the new streamlined Laravel 11 file structure.
- This is **perfectly fine** and recommended by Laravel. Follow the existing structure from Laravel 10. We do not to need migrate to the Laravel 11 structure unless the user explicitly requests that.
### Laravel 10 Structure
- Middleware typically live in `app/Http/Middleware/` and service providers in `app/Providers/`.
- There is no `bootstrap/app.php` application configuration in a Laravel 10 structure:
- Middleware registration is in `app/Http/Kernel.php`
- Exception handling is in `app/Exceptions/Handler.php`
- Console commands and schedule registration is in `app/Console/Kernel.php`
- Rate limits likely exist in `RouteServiceProvider` or `app/Http/Kernel.php`
### Database
- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost.
- Laravel 11 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`.
### Models
- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models.
### New Artisan Commands
- List Artisan commands using Boost's MCP tool, if available. New commands available in Laravel 11:
- `php artisan make:enum`
- `php artisan make:class `
- `php artisan make:interface `
=== livewire/core rules ===
## Livewire Core
- Use the `search-docs` tool to find exact version specific documentation for how to write Livewire & Livewire tests.
- Use the `php artisan make:livewire [Posts\CreatePost]` artisan command to create new components
- State should live on the server, with the UI reflecting it.
- All Livewire requests hit the Laravel backend, they're like regular HTTP requests. Always validate form data, and run authorization checks in Livewire actions.
## Livewire Best Practices
- Livewire components require a single root element.
- Use `wire:loading` and `wire:dirty` for delightful loading states.
- Add `wire:key` in loops:
```blade
@foreach ($items as $item)
<div wire:key="item-{{ $item->id }}">
{{ $item->name }}
</div>
@endforeach
```
- Prefer lifecycle hooks like `mount()`, `updatedFoo()` for initialization and reactive side effects:
<code-snippet name="Lifecycle hook examples" lang="php">
public function mount(User $user) { $this->user = $user; }
public function updatedSearch() { $this->resetPage(); }
</code-snippet>
## Testing Livewire
<code-snippet name="Example Livewire component test" lang="php">
Livewire::test(Counter::class)
->assertSet('count', 0)
->call('increment')
->assertSet('count', 1)
->assertSee(1)
->assertStatus(200);
</code-snippet>
<code-snippet name="Testing a Livewire component exists within a page" lang="php">
$this->get('/posts/create')
->assertSeeLivewire(CreatePost::class);
</code-snippet>
=== livewire/v3 rules ===
## Livewire 3
### Key Changes From Livewire 2
- These things changed in Livewire 2, but may not have been updated in this application. Verify this application's setup to ensure you conform with application conventions.
- Use `wire:model.live` for real-time updates, `wire:model` is now deferred by default.
- Components now use the `App\Livewire` namespace (not `App\Http\Livewire`).
- Use `$this->dispatch()` to dispatch events (not `emit` or `dispatchBrowserEvent`).
- Use the `components.layouts.app` view as the typical layout path (not `layouts.app`).
### New Directives
- `wire:show`, `wire:transition`, `wire:cloak`, `wire:offline`, `wire:target` are available for use. Use the documentation to find usage examples.
### Alpine
- Alpine is now included with Livewire, don't manually include Alpine.js.
- Plugins included with Alpine: persist, intersect, collapse, and focus.
### Lifecycle Hooks
- You can listen for `livewire:init` to hook into Livewire initialization, and `fail.status === 419` for the page expiring:
<code-snippet name="livewire:load example" lang="js">
document.addEventListener('livewire:init', function () {
Livewire.hook('request', ({ fail }) => {
if (fail && fail.status === 419) {
alert('Your session expired');
}
});
Livewire.hook('message.failed', (message, component) => {
console.error(message);
});
});
</code-snippet>
=== pest/core rules ===
## Pest
### Testing
- If you need to verify a feature is working, write or update a Unit / Feature test.
### Pest Tests
- All tests must be written using Pest. Use `php artisan make:test --pest {name}`.
- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files - these are core to the application.
- Tests should test all of the happy paths, failure paths, and weird paths.
- Tests live in the `tests/Feature` and `tests/Unit` directories.
- Pest tests look and behave like this:
<code-snippet name="Basic Pest Test Example" lang="php">
it('is true', function () {
expect(true)->toBeTrue();
});
</code-snippet>
### Running Tests
- Run the minimal number of tests using an appropriate filter before finalizing code edits.
- To run all tests: `php artisan test`.
- To run all tests in a file: `php artisan test tests/Feature/ExampleTest.php`.
- To filter on a particular test name: `php artisan test --filter=testName` (recommended after making a change to a related file).
- When the tests relating to your changes are passing, ask the user if they would like to run the entire test suite to ensure everything is still passing.
### Pest Assertions
- When asserting status codes on a response, use the specific method like `assertForbidden` and `assertNotFound` instead of using `assertStatus(403)` or similar, e.g.:
<code-snippet name="Pest Example Asserting postJson Response" lang="php">
it('returns all', function () {
$response = $this->postJson('/api/docs', []);
$response->assertSuccessful();
});
</code-snippet>
### Mocking
- Mocking can be very helpful when appropriate.
- When mocking, you can use the `Pest\Laravel\mock` Pest function, but always import it via `use function Pest\Laravel\mock;` before using it. Alternatively, you can use `$this->mock()` if existing tests do.
- You can also create partial mocks using the same import or self method.
### Datasets
- Use datasets in Pest to simplify tests which have a lot of duplicated data. This is often the case when testing validation rules, so consider going with this solution when writing tests for validation rules.
<code-snippet name="Pest Dataset Example" lang="php">
it('has emails', function (string $email) {
expect($email)->not->toBeEmpty();
})->with([
'james' => 'james@laravel.com',
'taylor' => 'taylor@laravel.com',
]);
</code-snippet>
=== inertia-vue/core rules ===
## Inertia + Vue
- Vue components must have a single root element.
- Use `router.visit()` or `<Link>` for navigation instead of traditional links.
<code-snippet name="Inertia Client Navigation" lang="vue">
import { Link } from '@inertiajs/vue3'
<Link href="/">Home</Link>
</code-snippet>
=== inertia-vue/v2/forms rules ===
## Inertia + Vue Forms
<code-snippet name="Inertia Vue useForm example" lang="vue">
<script setup>
import { useForm } from '@inertiajs/vue3'
const form = useForm({
email: null,
password: null,
remember: false,
})
</script>
<template>
<form @submit.prevent="form.post('/login')">
<!-- email -->
<input type="text" v-model="form.email">
<div v-if="form.errors.email">{{ form.errors.email }}</div>
<!-- password -->
<input type="password" v-model="form.password">
<div v-if="form.errors.password">{{ form.errors.password }}</div>
<!-- remember me -->
<input type="checkbox" v-model="form.remember"> Remember Me
<!-- submit -->
<button type="submit" :disabled="form.processing">Login</button>
</form>
</template>
</code-snippet>
=== tailwindcss/core rules ===
## Tailwind Core
- Use Tailwind CSS classes to style HTML, check and use existing tailwind conventions within the project before writing your own.
- Offer to extract repeated patterns into components that match the project's conventions (i.e. Blade, JSX, Vue, etc..)
- Think through class placement, order, priority, and defaults - remove redundant classes, add classes to parent or child carefully to limit repetition, group elements logically
- You can use the `search-docs` tool to get exact examples from the official documentation when needed.
### Spacing
- When listing items, use gap utilities for spacing, don't use margins.
<code-snippet name="Valid Flex Gap Spacing Example" lang="html">
<div class="flex gap-8">
<div>Superior</div>
<div>Michigan</div>
<div>Erie</div>
</div>
</code-snippet>
### Dark Mode
- If existing pages and components support dark mode, new pages and components must support dark mode in a similar way, typically using `dark:`.
=== tailwindcss/v3 rules ===
## Tailwind 3
- Always use Tailwind CSS v3 - verify you're using only classes supported by this version.
=== filament/filament rules ===
## Filament
- Filament is used by this application, check how and where to follow existing application conventions.
- Filament is a Server-Driven UI (SDUI) framework for Laravel. It allows developers to define user interfaces in PHP using structured configuration objects. It is built on top of Livewire, Alpine.js, and Tailwind CSS.
- You can use the `search-docs` tool to get information from the official Filament documentation when needed. This is very useful for Artisan command arguments, specific code examples, testing functionality, relationship management, and ensuring you're following idiomatic practices.
- Utilize static `make()` methods for consistent component initialization.
### Artisan
- You must use the Filament specific Artisan commands to create new files or components for Filament. You can find these with the `list-artisan-commands` tool, or with `php artisan` and the `--help` option.
- Inspect the required options, always pass `--no-interaction`, and valid arguments for other options when applicable.
### Filament's Core Features
- Actions: Handle doing something within the application, often with a button or link. Actions encapsulate the UI, the interactive modal window, and the logic that should be executed when the modal window is submitted. They can be used anywhere in the UI and are commonly used to perform one-time actions like deleting a record, sending an email, or updating data in the database based on modal form input.
- Forms: Dynamic forms rendered within other features, such as resources, action modals, table filters, and more.
- Infolists: Read-only lists of data.
- Notifications: Flash notifications displayed to users within the application.
- Panels: The top-level container in Filament that can include all other features like pages, resources, forms, tables, notifications, actions, infolists, and widgets.
- Resources: Static classes that are used to build CRUD interfaces for Eloquent models. Typically live in `app/Filament/Resources`.
- Schemas: Represent components that define the structure and behavior of the UI, such as forms, tables, or lists.
- Tables: Interactive tables with filtering, sorting, pagination, and more.
- Widgets: Small component included within dashboards, often used for displaying data in charts, tables, or as a stat.
### Relationships
- Determine if you can use the `relationship()` method on form components when you need `options` for a select, checkbox, repeater, or when building a `Fieldset`:
<code-snippet name="Relationship example for Form Select" lang="php">
Forms\Components\Select::make('user_id')
->label('Author')
->relationship('author')
->required(),
</code-snippet>
## Testing
- It's important to test Filament functionality for user satisfaction.
- Ensure that you are authenticated to access the application within the test.
- Filament uses Livewire, so start assertions with `livewire()` or `Livewire::test()`.
### Example Tests
<code-snippet name="Filament Table Test" lang="php">
livewire(ListUsers::class)
->assertCanSeeTableRecords($users)
->searchTable($users->first()->name)
->assertCanSeeTableRecords($users->take(1))
->assertCanNotSeeTableRecords($users->skip(1))
->searchTable($users->last()->email)
->assertCanSeeTableRecords($users->take(-1))
->assertCanNotSeeTableRecords($users->take($users->count() - 1));
</code-snippet>
<code-snippet name="Filament Create Resource Test" lang="php">
livewire(CreateUser::class)
->fillForm([
'name' => 'Howdy',
'email' => 'howdy@example.com',
])
->call('create')
->assertNotified()
->assertRedirect();
assertDatabaseHas(User::class, [
'name' => 'Howdy',
'email' => 'howdy@example.com',
]);
</code-snippet>
<code-snippet name="Testing Multiple Panels (setup())" lang="php">
use Filament\Facades\Filament;
Filament::setCurrentPanel('app');
</code-snippet>
<code-snippet name="Calling an Action in a Test" lang="php">
livewire(EditInvoice::class, [
'invoice' => $invoice,
])->callAction('send');
expect($invoice->refresh())->isSent()->toBeTrue();
</code-snippet>
## Version 3 Changes To Focus On
- Resources are located in `app/Filament/Resources/` directory.
- Resource pages (List, Create, Edit) are auto-generated within the resource's directory - e.g., `app/Filament/Resources/PostResource/Pages/`.
- Forms use the `Forms\Components` namespace for form fields.
- Tables use the `Tables\Columns` namespace for table columns.
- A new `Filament\Forms\Components\RichEditor` component is available.
- Form and table schemas now use fluent method chaining.
- Added `php artisan filament:optimize` command for production optimization.
- Requires implementing `FilamentUser` contract for production access control.
</laravel-boost-guidelines>

View File

@@ -3,7 +3,7 @@
With Ploi Core, you'll power-launch your webhosting company.
Using the ploi.io system as backbone you will be able to serve your customers your custom panel & feeling.
<p align="center"><img src="https://ploi-core.io/images/featured.png" width="100%"></p>
<p align="center"><img src="https://ploi-core.io/images/og.jpg" width="100%"></p>
## Documentation
@@ -17,4 +17,4 @@ https://ploi.io
The contribution guide can be found inside our documentation:
https://docs.ploi-core.io/getting-started/contribution-guide
https://docs.ploi-core.io/261-getting-started/639-contribution-guide

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Actions\Provider;
use App\Models\Provider;
use App\Services\Ploi\Ploi;
class SynchronizeProviderAction
{
public function execute(int $ploiProviderId): Provider
{
$ploiProvider = Ploi::make()->user()->serverProviders($ploiProviderId)->getData();
$provider = Provider::updateOrCreate([
'ploi_id' => $ploiProvider->id,
], [
'label' => $ploiProvider->label,
'name' => $ploiProvider->name,
]);
foreach ($ploiProvider->provider->plans as $plan) {
$provider->plans()->updateOrCreate([
'plan_id' => $plan->id,
], [
'label' => $plan->name,
]);
}
foreach ($ploiProvider->provider->regions as $region) {
$provider->regions()->updateOrCreate([
'region_id' => $region->id,
], [
'label' => $region->name,
]);
}
return $provider;
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Actions\Server;
use App\Models\User;
use App\Models\Server;
use App\Jobs\Servers\CreateServer;
use Illuminate\Support\Facades\Mail;
use App\DataTransferObjects\ServerData;
use App\Mail\Admin\Server\AdminServerCreatedEmail;
class CreateServerAction
{
public function execute(ServerData $serverData): Server
{
[$provider, $providerRegion, $providerPlan] = $this->determineProviderRegionPlan($serverData);
$server = $serverData->getUser()->servers()->create([
'name' => $serverData->name,
'database_type' => $serverData->database_type,
'os' => setting('default_os', Server::OS_UBUNTU_22)
]);
$server->provider()->associate($provider);
$server->providerRegion()->associate($providerRegion);
$server->providerPlan()->associate($providerPlan);
$server->save();
dispatch(new CreateServer($server));
$this->sendAdminServerCreatedEmails($server);
return $server;
}
protected function determineProviderRegionPlan(ServerData $serverData): array
{
$provider = $serverData->getUser()->package->providers()->findOrFail($serverData->provider_id);
$region = $provider->regions()->findOrFail($serverData->provider_region_id);
$plan = $provider->plans()->findOrFail($serverData->provider_plan_id);
return [$provider, $region, $plan];
}
protected function sendAdminServerCreatedEmails(Server $server): void
{
if (! setting('receive_email_on_server_creation')) {
return;
}
$admins = User::query()->where('role', User::ADMIN)->get();
foreach ($admins as $admin) {
Mail::to($admin)->send(new AdminServerCreatedEmail(auth()->user(), $server));
}
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Actions\Server;
use Throwable;
use App\Models\Server;
use App\Services\Ploi\Ploi;
use Filament\Notifications\Notification;
class SynchronizeServerAction
{
public function execute(int $ploiServerId): Server|null
{
try {
$serverData = Ploi::make()->server()->get($ploiServerId)->getData();
} catch (Throwable $exception) {
Notification::make()
->title('An error has occurred: ' . $exception->getMessage())
->danger()
->send();
return null;
}
if (!$serverData) {
Notification::make()
->title('Server synchronization')
->body('It was not possible to synchronize servers, it seems the API key has the wrong scopes. Please make sure the Ploi API key you\'ve entered has all the scopes enabled.')
->danger()
->send();
return null;
}
try {
$server = Server::query()
->updateOrCreate([
'ploi_id' => $serverData->id,
], [
'status' => $serverData->status,
'name' => $serverData->name,
'ip' => $serverData->ip_address,
'ssh_port' => $serverData->ssh_port,
'internal_ip' => $serverData->internal_ip,
'available_php_versions' => $serverData->installed_php_versions,
]);
} catch (Throwable $exception) {
Notification::make()
->title('An error has occurred: ' . $exception->getMessage())
->danger()
->send();
return null;
}
Notification::make()
->title(__('Server :server synchronized successfully.', ['server' => $server->name]))
->success()
->send();
return $server;
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace App\Actions\Site;
use App\Models\Site;
use App\Models\User;
use App\Models\Server;
use App\Jobs\Sites\CreateSite;
use Illuminate\Support\Facades\Mail;
use App\DataTransferObjects\SiteData;
use Illuminate\Database\Eloquent\Model;
use App\Mail\Admin\Site\AdminSiteCreatedEmail;
class CreateSiteAction
{
public function execute(SiteData $siteData): ?Site
{
$server = $this->determineServer($siteData);
if (! $server) {
return null;
}
$site = $server->sites()->create($siteData->toArray());
$siteData->getUser()->sites()->save($site);
dispatch(new CreateSite($site));
$siteData->getUser()->systemLogs()->create([
'title' => 'New site :site created',
'description' => 'A new site has been created',
])->model()->associate($site)->save();
$this->sendAdminSiteCreatedEmails($server, $site, $siteData->getUser());
return $site;
}
protected function determineServer(SiteData $siteData): ?Server
{
if ($siteData->server_id) {
return $siteData->getUser()->servers()->findOrFail($siteData->server_id);
}
$server = Server::query()
->where('maximum_sites', '>', 0)
->where(function ($query) use ($siteData) {
return $query
->where(fn ($query) => $query->whereHas('users', fn ($query) => $query->where('user_id', $siteData->getUser()->id)))
->orWhere(function ($query) {
return $query->doesntHave('users');
});
})
->withCount('sites')
->inRandomOrder()
->first();
return $server && $server->sites_count < $server->maximum_sites
? $server
: null;
}
protected function sendAdminSiteCreatedEmails(Server $server, Model|Site $site, User $user): void
{
if (! setting('receive_email_on_site_creation')) {
return;
}
$admins = User::where('role', User::ADMIN)->get();
foreach ($admins as $admin) {
Mail::to($admin)->send(new AdminSiteCreatedEmail(user: $user, server: $server, site: $site));
}
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace App\Actions\Site;
use App\Models\Site;
use App\Models\Server;
use App\Services\Ploi\Ploi;
use Filament\Notifications\Notification;
class SynchronizeSiteAction
{
public function execute(int $ploiServerId, int $ploiSiteId): Site
{
$siteData = Ploi::make()->server($ploiServerId)->sites()->get($ploiSiteId)->getData();
$server = Server::query()
->where('ploi_id', $siteData->server_id)
->firstOrFail();
$site = Site::query()
->updateOrCreate([
'ploi_id' => $siteData->id,
], [
'domain' => $siteData->domain,
'php_version' => $siteData->php_version,
'project' => $siteData->project_type,
]);
$site->status = $siteData->status;
$site->server_id = $server->id;
$site->save();
$certificates = Ploi::make()->server($siteData->server_id)->sites($siteData->id)->certificates()->get()->getData();
if ($certificates) {
foreach ($certificates as $certificate) {
$site->certificates()->updateOrCreate([
'ploi_id' => $certificate->id,
], [
'status' => $certificate->status,
'ploi_id' => $certificate->id,
'domain' => $certificate->domain,
'type' => $certificate->type,
]);
}
}
Notification::make()
->title(__('Site :site synchronized successfully.', ['site' => $site->domain]))
->success()
->send();
return $site;
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Actions\User;
use App\Models\Site;
use App\Models\User;
use App\Models\Server;
use App\Jobs\Sites\DeleteSite;
use App\Jobs\Servers\DeleteServer;
class DeleteUserAction
{
public function execute(User $user, bool $removeAllData): void
{
if ($removeAllData) {
$this->removeAllData($user);
}
// The next items are already being deleted by the "deleting" event:
// systemLogs, servers detached, sites detached, supportTickets, supportTicketReplies, userProviders
$user->delete();
}
protected function removeAllData(User $user): void
{
$user
->sites()
->withCount('users')
->get()
->filter(fn (Site $site) => $site->users_count === 1)
->each(function (Site $site) {
dispatch(new DeleteSite($site->server->ploi_id, $site->ploi_id));
// Deletes databases, redirects, cronjobs, certificates.
$site->delete();
});
$user
->servers()
->withCount('users')
->get()
->filter(fn (Server $server) => $server->users_count === 1)
->each(function (Server $server) {
dispatch(new DeleteServer($server->ploi_id));
// Deletes databases, redirects, cronjobs, certificates.
$server->delete();
});
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Casts;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
class Encrypted implements CastsAttributes
@@ -9,7 +10,7 @@ class Encrypted implements CastsAttributes
/**
* Cast the given value.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param Model $model
* @param string $key
* @param mixed $value
* @param array $attributes
@@ -23,7 +24,7 @@ class Encrypted implements CastsAttributes
/**
* Prepare the given value for storage.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param Model $model
* @param string $key
* @param array $value
* @param array $attributes

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Casts;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
class PermissionCast implements CastsAttributes
{
/**
* Cast the given value.
*
* @param Model $model
* @param string $key
* @param mixed $value
* @param array $attributes
* @return mixed
*/
public function get($model, $key, $value, $attributes)
{
if (!$value) {
return [
'create' => false,
'update' => false,
'delete' => false,
];
}
return json_decode($value, true);
}
/**
* Prepare the given value for storage.
*
* @param Model $model
* @param string $key
* @param array $value
* @param array $attributes
* @return mixed
*/
public function set($model, $key, $value, $attributes)
{
return json_encode($value);
}
}

26
app/Casts/SiteAlias.php Normal file
View File

@@ -0,0 +1,26 @@
<?php
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
class SiteAlias implements CastsAttributes
{
public function get($model, string $key, $value, array $attributes)
{
if (!$value) {
return [];
}
$data = json_decode($value, true);
sort($data);
return $data;
}
public function set($model, string $key, $value, array $attributes)
{
return json_encode($value);
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Console\Commands\Core;
use App\Models\SystemLog;
use Illuminate\Console\Command;
class Cleanup extends Command
{
protected $signature = 'core:cleanup';
protected $description = 'Clean up any old logs';
public function handle()
{
if (!setting('rotate_logs_after')) {
return Command::SUCCESS;
}
$rotationDate = $this->getRotationDate();
$rotated = SystemLog::query()
->where('created_at', '<', $rotationDate)
->delete();
$this->info('Rotated ' . $rotated . ' system logs!');
return Command::SUCCESS;
}
protected function getRotationDate()
{
switch (setting('rotate_logs_after')) {
case 'weeks-1':
return now()->subWeek();
break;
case 'months-1':
return now()->subMonth();
break;
case 'months-3':
return now()->subMonths(3);
break;
case 'months-6':
return now()->subMonths(6);
break;
case 'years-1':
return now()->subYear();
break;
case 'years-2':
return now()->subYears(2);
break;
case 'years-3':
return now()->subYears(3);
break;
case 'years-4':
return now()->subYears(4);
break;
}
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Console\Commands\Core;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
class Css extends Command
{
protected $signature = 'core:css';
protected $description = 'Generates an theme.css file for you to customize';
public function handle()
{
if (file_exists(storage_path('app/public/theme.css')) && !$this->confirm('You seem to already have a theme.css published, are you sure you want to overwrite?')) {
$this->warn('Aborted publishing of theme.css.');
return 0;
}
$this->info('Publishing theme.css file..');
(new Filesystem)->copy(
__DIR__ . '/stubs/theme.css',
storage_path('app/public/theme.css')
);
$this->info('Done! You can edit the theme.css file inside storage/public/theme.css');
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Console\Commands\Core;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
class CssBackup extends Command
{
protected $signature = 'core:css-backup';
protected $description = 'Creates a backup from your own created theme.css';
public function handle()
{
if (!file_exists(storage_path('app/public/theme.css'))) {
$this->warn('There is no custom theme.css, aborting backup.');
return 0;
}
$this->info('Backing up theme.css file..');
(new Filesystem)->copy(
storage_path('app/public/theme.css'),
storage_path('app/public/theme-backup.css')
);
$this->info('Done! You can find the CSS backup file here storage/public/theme-backup.css');
}
}

View File

@@ -7,109 +7,196 @@ use App\Models\User;
use RuntimeException;
use App\Models\Package;
use App\Services\Ploi\Ploi;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Console\Command;
use App\Services\VersionChecker;
use function Laravel\Prompts\info;
use function Laravel\Prompts\note;
use function Laravel\Prompts\spin;
use function Laravel\Prompts\text;
use Illuminate\Support\Facades\DB;
use function Laravel\Prompts\error;
use function Laravel\Prompts\intro;
use function Laravel\Prompts\outro;
use function Laravel\Prompts\select;
use Illuminate\Support\Facades\Http;
use function Laravel\Prompts\confirm;
use function Laravel\Prompts\warning;
use function Laravel\Prompts\password;
class Install extends Command
{
protected $company;
protected $signature = 'core:install';
protected $signature = 'core:install {--force}';
protected $description = 'Installation command for Ploi Core';
protected $versionChecker;
protected $installationFile = 'app/installation';
public function __construct()
{
parent::__construct();
$this->versionChecker = (new VersionChecker)->getVersions();
}
public function handle()
{
$this->intro();
$this->isInstalled();
$this->checkApplicationKey();
$this->checkDatabaseConnection();
$this->runDatabaseMigrations();
$this->checkCredentials();
$this->askAboutAdministrationAccount();
$this->askAboutDefaultPackages();
$this->checkApplicationUrl();
$this->createInstallationFile();
try {
$this->init();
$this->intro();
$this->isInstalled();
$this->checkApplicationKey();
$this->checkDatabaseConnection();
$this->runDatabaseMigrations();
$this->checkCredentials();
$this->askAboutAdministrationAccount();
$this->askAboutDefaultPackages();
$this->checkApplicationUrl();
$this->createInstallationFile();
$this->linkStorage();
$this->info('Succes! Installation has finished.');
$this->info('Visit your platform at ' . env('APP_URL'));
outro('🎉 Installation completed successfully!');
note(
"Next steps:\n\n" .
"📧 Setup email: https://docs.ploi-core.io/261-getting-started/918-setting-up-email\n" .
"⚙️ Setup cron & queue: https://docs.ploi-core.io/261-getting-started/638-installation\n\n" .
"Visit your platform at: " . env('APP_URL')
);
return Command::SUCCESS;
} catch (Exception $e) {
error('Installation failed: ' . $e->getMessage());
return Command::FAILURE;
}
}
protected function init()
{
$this->versionChecker = (new VersionChecker)->getVersions();
}
protected function askAboutAdministrationAccount()
{
$this->info('Let\'s start by setting up your administration account.');
if (!User::query()->where('role', User::ADMIN)->count()) {
note('Let\'s set up your administration account');
$name = $this->ask('What is your name', $this->company['user_name']);
$email = $this->ask('What is your e-mail address', $this->company['email']);
$password = $this->secret('What password do you desire');
$name = text(
label: 'What is your name?',
default: $this->company['user_name'],
required: true
);
$check = User::where('email', $email)->count();
$email = text(
label: 'What is your email address?',
default: $this->company['email'],
required: true,
validate: fn (string $value) => match (true) {
!filter_var($value, FILTER_VALIDATE_EMAIL) => 'Please enter a valid email address.',
User::where('email', $value)->exists() => 'This email is already registered in the system.',
default => null
}
);
if ($check) {
$this->line('');
$this->comment('This user is already present in your system, please refresh your database or use different credentials.');
$this->comment('Aborting installation..');
$password = password(
label: 'Choose a secure password',
required: true,
validate: fn (string $value) => match (true) {
strlen($value) < 8 => 'Password must be at least 8 characters.',
default => null
}
);
exit();
spin(
function () use ($name, $email, $password) {
User::forceCreate([
'name' => $name,
'email' => $email,
'password' => $password,
'role' => User::ADMIN
]);
},
'Creating administrator account...'
);
info('✓ Administrator account created successfully');
} else {
note('Administrator account already exists. Use existing credentials to login.');
}
User::forceCreate([
'name' => $name,
'email' => $email,
'password' => $password,
'role' => User::ADMIN
]);
}
protected function askAboutDefaultPackages()
{
$basicPackages = $this->confirm(
'Do you want to create the basic packages which you can edit later?',
true
$createPackages = confirm(
label: 'Would you like to create default packages?',
default: true,
hint: 'Basic (5 sites), Professional (30 sites), and Unlimited packages'
);
if (!$basicPackages) {
if (!$createPackages) {
return false;
}
Package::create([
'name' => 'Basic',
'maximum_sites' => 5,
]);
spin(
function () {
Package::create([
'name' => 'Basic',
'maximum_sites' => 5,
'site_permissions' => [
'create' => true,
'update' => true,
'delete' => true
],
'server_permissions' => [
'create' => false,
'update' => false,
'delete' => false
]
]);
Package::create([
'name' => 'Professional',
'maximum_sites' => 5,
]);
Package::create([
'name' => 'Professional',
'maximum_sites' => 30,
'site_permissions' => [
'create' => true,
'update' => true,
'delete' => true
],
'server_permissions' => [
'create' => false,
'update' => false,
'delete' => false
]
]);
Package::create([
'name' => 'Unlimited',
'maximum_sites' => 0,
]);
Package::create([
'name' => 'Unlimited',
'maximum_sites' => 0,
'site_permissions' => [
'create' => true,
'update' => true,
'delete' => true
],
'server_permissions' => [
'create' => false,
'update' => false,
'delete' => false
]
]);
},
'Creating default packages...'
);
info('✓ Created 3 default packages');
}
protected function getCompany($ploiCoreKey, $token)
protected function getCompany($token)
{
$response = Http::withHeaders([
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'X-Ploi-Core-Key' => $ploiCoreKey
'Content-Type' => 'application/json'
])
->withToken($token)
->get((new Ploi)->url . 'ping');
if (!$response->ok() || !$response->json()) {
return false;
return [
'error' => Arr::get($response->json(), 'message', 'An unknown error has occurred.')
];
}
return $response->json();
@@ -124,28 +211,26 @@ class Install extends Command
protected function intro()
{
$this->info('*---------------------------------------------------------------------------*');
$this->line('Ploi Core Installation');
$this->line('Ploi Core version: ' . $this->versionChecker->currentVersion);
$this->line('Ploi Core remote: ' . $this->versionChecker->remoteVersion);
$this->line('Laravel version: ' . app()->version());
$this->line('PHP version: ' . trim(phpversion()));
$this->line(' ');
$this->line('Website: https://ploi-core.io');
$this->line('E-mail: core@ploi.io');
$this->line('Terms of service: https://ploi-core.io/terms');
$this->info('*---------------------------------------------------------------------------*');
$this->line('');
intro('🚀 Ploi Core Installation');
note(
"Ploi Core v{$this->versionChecker->currentVersion} (Remote: v{$this->versionChecker->remoteVersion})\n" .
"Laravel v" . app()->version() . " | PHP v" . trim(phpversion()) . "\n\n" .
"Website: https://ploi-core.io\n" .
"E-mail: core@ploi.io\n" .
"Terms: https://ploi-core.io/terms"
);
}
protected function isInstalled()
{
if (file_exists(storage_path($this->installationFile))) {
$this->line('');
$this->comment('Ploi Core has already been installed before.');
$this->comment('If you still want to start installation, remove this file to continue: ./storage/' . $this->installationFile);
$this->comment('Aborting installation..');
if (file_exists(storage_path($this->installationFile)) && !$this->option('force')) {
warning('Ploi Core has already been installed before.');
error(
"To reinstall, either:\n" .
"• Remove the file: ./storage/{$this->installationFile}\n" .
"• Or run with --force flag"
);
exit();
}
@@ -155,121 +240,215 @@ class Install extends Command
protected function checkApplicationKey(): void
{
if (!config('app.key')) {
$this->call('key:generate');
spin(
fn () => $this->call('key:generate', [], $this->getOutput()),
'Generating application key...'
);
info('✓ Application key has been set');
}
$this->info('Application key has been set');
}
protected function checkApplicationUrl()
{
// Ask about URL
$url = $this->ask('What URL is this platform using?', env('APP_URL'));
$url = text(
label: 'What URL will this platform use?',
default: env('APP_URL', 'https://example.com'),
required: true,
validate: fn (string $value) => match (true) {
!filter_var($value, FILTER_VALIDATE_URL) => 'Please enter a valid URL.',
!str_starts_with($value, 'http://') && !str_starts_with($value, 'https://') => 'URL must start with http:// or https://',
default => null
},
hint: 'Include the protocol (http:// or https://)'
);
$this->writeToEnvironmentFile('APP_URL', $url);
try {
$this->writeToEnvironmentFile('APP_URL', $url);
info('✓ Application URL configured');
} catch (Exception $e) {
error('Failed to save application URL: ' . $e->getMessage());
exit(1);
}
}
protected function createInstallationFile()
{
file_put_contents(storage_path($this->installationFile), json_encode($this->getInstallationPayload(), JSON_PRETTY_PRINT));
try {
$path = storage_path($this->installationFile);
$content = json_encode($this->getInstallationPayload(), JSON_PRETTY_PRINT);
if (file_put_contents($path, $content) === false) {
error('Failed to create installation file');
exit(1);
}
info('✓ Installation marker created');
} catch (Exception $e) {
error('Error creating installation file: ' . $e->getMessage());
exit(1);
}
}
protected function linkStorage()
{
// Create storage symlink
$publicPath = public_path('storage');
$storagePath = storage_path('app/public');
// Remove existing symlink if it exists
if (is_link($publicPath)) {
unlink($publicPath);
}
// Create new symlink
if (!file_exists($publicPath)) {
try {
symlink($storagePath, $publicPath);
info('✓ Storage symlink created');
} catch (Exception $e) {
warning('Could not create storage symlink (may need manual creation)');
}
} else {
info('✓ Storage path already exists');
}
}
protected function createDatabaseCredentials(): bool
{
$storeCredentials = $this->confirm(
'Unable to connect to your database. Would you like to enter your credentials now?',
true
$storeCredentials = confirm(
label: 'Would you like to configure database credentials now?',
default: true
);
if (!$storeCredentials) {
return false;
}
$connection = $this->choice('Type', ['mysql', 'pgsql'], 0);
$connection = select(
label: 'Select database type',
options: [
'mysql' => 'MySQL / MariaDB',
'pgsql' => 'PostgreSQL'
],
default: 'mysql'
);
$defaultPort = $connection === 'mysql' ? '3306' : '5432';
$variables = [
'DB_CONNECTION' => $connection,
'DB_HOST' => $this->anticipate(
'Host',
['127.0.0.1', 'localhost'],
config("database.connections.{$connection}.host", '127.0.0.1')
'DB_HOST' => text(
label: 'Database host',
default: config("database.connections.{$connection}.host", '127.0.0.1'),
required: true,
hint: 'Usually 127.0.0.1 or localhost'
),
'DB_PORT' => $this->ask(
'Port',
config("database.connections.{$connection}.port", '3306')
'DB_PORT' => text(
label: 'Database port',
default: config("database.connections.{$connection}.port", $defaultPort),
required: true
),
'DB_DATABASE' => $this->ask(
'Database',
config("database.connections.{$connection}.database")
'DB_DATABASE' => text(
label: 'Database name',
default: config("database.connections.{$connection}.database", 'ploi_core'),
required: true
),
'DB_USERNAME' => $this->ask(
'Username',
config("database.connections.{$connection}.username")
'DB_USERNAME' => text(
label: 'Database username',
default: config("database.connections.{$connection}.username", 'root'),
required: true
),
'DB_PASSWORD' => $this->secret(
'Password',
config("database.connections.{$connection}.password")
),
'DB_PASSWORD' => password(
label: 'Database password',
hint: 'Leave empty if no password is set'
) ?: '',
];
$this->persistVariables($variables);
spin(
fn () => $this->persistVariables($variables),
'Saving database configuration...'
);
return true;
}
protected function checkCredentials()
{
do {
$ploiApiToken = $this->ask('Enter the Ploi API token', env('PLOI_TOKEN'));
} while (empty($ploiApiToken));
$ploiApiToken = text(
label: 'Enter your Ploi API token',
default: env('PLOI_TOKEN'),
required: true,
hint: 'You can find this in your Ploi account settings'
);
do {
$ploiCoreKey = $this->ask('Enter the Ploi Core key', env('PLOI_CORE_TOKEN'));
} while (empty($ploiCoreKey));
$this->company = $this->getCompany($ploiCoreKey, $ploiApiToken);
$this->company = spin(
fn () => $this->getCompany($ploiApiToken),
'Authenticating with Ploi API...'
);
if (!$this->company) {
$this->error('Could not authenticate with ploi.io, please retry by running this command again.');
error('Could not authenticate with ploi.io');
exit();
}
if (isset($this->company['error'])) {
error($this->company['error']);
exit();
}
if ($this->company['user']['subscription'] !== 'unlimited') {
error('Your Ploi subscription does not support Ploi Core.');
warning('Please upgrade to the Unlimited plan at https://ploi.io');
exit();
}
info('✓ Successfully authenticated with Ploi');
$this->writeToEnvironmentFile('PLOI_TOKEN', $ploiApiToken);
$this->writeToEnvironmentFile('PLOI_CORE_TOKEN', $ploiCoreKey);
$name = $this->ask('What is the name of your company? (Press enter to keep the name here)', $this->company['name']);
$name = text(
label: 'What is the name of your company?',
default: $this->company['name'],
required: true
);
$this->writeToEnvironmentFile('APP_NAME', $name);
setting(['name' => $name]);
}
protected function runDatabaseMigrations()
{
$this->info('Running database migrations..');
$this->call('migrate');
$this->info('Database migrations successful');
spin(
fn () => $this->call('migrate', ['--force' => true], $this->getOutput()),
'Running database migrations...'
);
info('✓ Database migrations completed');
}
protected function checkDatabaseConnection(): void
{
try {
DB::connection()->getPdo();
$this->info('Database connection successful.');
spin(
fn () => DB::connection()->getPdo(),
'Testing database connection...'
);
info('✓ Database connection successful');
} catch (Exception $e) {
warning('Unable to connect to database');
try {
if (!$this->createDatabaseCredentials()) {
$this->error('A database connection could not be established. Please update your configuration and try again.');
error('Database connection could not be established.');
$this->printDatabaseConfig();
exit();
}
} catch (RuntimeException $e) {
$this->error('Failed to persist environment configuration.');
error('Failed to persist environment configuration.');
exit();
}
@@ -281,14 +460,15 @@ class Install extends Command
{
$connection = config('database.default');
$this->line('');
$this->info('Database Configuration:');
$this->line("- Connection: {$connection}");
$this->line('- Host: ' . config("database.connections.{$connection}.host"));
$this->line('- Port: ' . config("database.connections.{$connection}.port"));
$this->line('- Database: ' . config("database.connections.{$connection}.database"));
$this->line('- Username: ' . config("database.connections.{$connection}.username"));
$this->line('- Password: ' . config("database.connections.{$connection}.password"));
note(
"Current Database Configuration:\n" .
" Connection: {$connection}\n" .
" Host: " . config("database.connections.{$connection}.host") . "\n" .
" Port: " . config("database.connections.{$connection}.port") . "\n" .
" Database: " . config("database.connections.{$connection}.database") . "\n" .
" Username: " . config("database.connections.{$connection}.username") . "\n" .
" Password: " . (config("database.connections.{$connection}.password") ? '***' : '(not set)')
);
}
protected function persistVariables(array $connectionData): void
@@ -343,4 +523,5 @@ class Install extends Command
{
$this->laravel['config'][$key] = $value;
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Console\Commands\Core;
use App\Models\Provider;
use App\Services\Ploi\Ploi;
use Illuminate\Console\Command;
@@ -9,7 +10,7 @@ class Synchronize extends Command
{
protected $signature = 'core:synchronize';
protected $description = 'Synchronze data';
protected $description = 'Synchronize data';
public function handle()
{
@@ -18,10 +19,10 @@ class Synchronize extends Command
$data = collect($ploi->user()->serverProviders()->getData());
foreach ($data as $apiProvider) {
$provider = \App\Models\Provider::where('ploi_id', $apiProvider->id)->first();
$provider = Provider::where('ploi_id', $apiProvider->id)->first();
if (!$provider) {
$provider = \App\Models\Provider::create([
$provider = Provider::create([
'ploi_id' => $apiProvider->id,
'label' => $apiProvider->label,
'name' => $apiProvider->label

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Console\Commands\Core;
use App\Models\User;
use Illuminate\Console\Command;
class Trial extends Command
{
protected $signature = 'core:trial';
protected $description = 'Check for expired trials';
public function handle()
{
User::query()
->where('trial_ends_at', '<', now())
->each(function (User $user) {
$user->trial_ends_at = null;
$user->package_id = setting('default_package');
$user->save();
});
}
}

View File

@@ -0,0 +1,78 @@
:root {
--font-body: 'Inter', sans-serif;
--color-white: #fff;
--color-gray-1: #f7f7f7;
--color-gray-2: #e6e6e6;
--color-gray-3: #cacaca;
--color-gray-4: #888;
--color-gray-5: #666;
--color-gray-6: #2f2f2f;
--color-gray-7: #1b1a1a;
--color-gray-8: #101010;
--color-primary: #1b8ae8;
--color-success: #17b35d;
--color-warning: #f5a623;
--color-danger: #c90c4c;
--color-text-high-emphasis: var(--color-gray-8);
--color-text-medium-emphasis: var(--color-gray-5);
--color-text-low-emphasis: var(--color-gray-3);
--color-text-on-primary: var(--color-white);
--color-text-on-success: var(--color-gray-8);
--color-text-on-warning: var(--color-white);
--color-text-on-danger: var(--color-white);
--color-border-high-emphasis: var(--color-gray-4);
--color-border-medium-emphasis: var(--color-gray-3);
--color-border-low-emphasis: var(--color-gray-2);
--color-backdrop: rgba(0, 0, 0, 0.5);
--color-overlay: rgba(255, 255, 255, 0.8);
--color-surface-1: var(--color-white);
--color-surface-2: var(--color-gray-1);
--color-surface-3: var(--color-white);
--border-radius: 0.5rem;
--border-radius-avatar: 4rem;
--border-radius-circle: 100%;
--top-bar-container: 64rem;
--top-bar-logo-height: 3.5rem;
--top-bar-background-color: var(--color-surface-1);
--top-bar-text-color: var(--color-text-medium-emphasis);
--tab-bar-background-color: var(--color-surface-2);
--tab-bar-item-active-background-color: var(--color-surface-1);
--tab-bar-item-text-color: var(--color-text-medium-emphasis);
--tab-bar-item-active-text-color: var(--color-text-high-emphasis);
--breadcrumbs-text-color: var(--color-text-medium-emphasis);
}
.theme--dark {
--color-primary: #63a6f5;
--color-success: #50e3c2;
--color-warning: #f5a623;
--color-danger: #d4667c;
--color-text-high-emphasis: var(--color-white);
--color-text-medium-emphasis: var(--color-gray-3);
--color-text-low-emphasis: var(--color-gray-5);
--color-text-on-primary: var(--color-gray-7);
--color-text-on-success: var(--color-gray-7);
--color-text-on-warning: var(--color-gray-7);
--color-text-on-danger: var(--color-gray-7);
--color-border-high-emphasis: var(--color-gray-4);
--color-border-medium-emphasis: var(--color-gray-5);
--color-border-low-emphasis: var(--color-gray-6);
--color-surface-1: var(--color-gray-7);
--color-surface-2: var(--color-gray-6);
--color-surface-3: var(--color-gray-6);
--color-overlay: rgba(0, 0, 0, 0.8);
}

View File

@@ -3,7 +3,11 @@
namespace App\Console;
use App\Jobs\Core\Ping;
use App\Console\Commands\Core\Css;
use App\Console\Commands\Core\Trial;
use App\Console\Commands\Core\Cleanup;
use App\Console\Commands\Core\Install;
use App\Console\Commands\Core\CssBackup;
use App\Console\Commands\Core\Synchronize;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
@@ -11,8 +15,12 @@ use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
protected $commands = [
Css::class,
CssBackup::class,
Install::class,
Synchronize::class,
Cleanup::class,
Trial::class,
];
protected function schedule(Schedule $schedule)
@@ -20,5 +28,8 @@ class Kernel extends ConsoleKernel
$schedule->call(function () {
dispatch(new Ping())->delay(now()->addMinutes(rand(1, 30)));
})->dailyAt('02:00');
$schedule->command('core:cleanup')->daily();
$schedule->command('core:trial')->dailyAt('10:00');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\DataTransferObjects;
use Illuminate\Support\Carbon;
use App\DataTransferObjects\Support\Data;
class PackageData extends Data
{
public function __construct(
// Add validation attributes to this class if we add additional API-endpoints.
public int $id,
public ?string $name,
public ?int $maximum_servers,
public ?int $maximum_sites,
public float $price_hourly,
public float $price_monthly,
public float $price_yearly,
public ?string $stripe_plan_id,
public string $currency,
public array $server_permissions,
public array $site_permissions,
public Carbon $created_at,
) {
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace App\DataTransferObjects;
use App\Models\User;
use App\Models\Server;
use App\Models\Provider;
use App\Models\ProviderPlan;
use App\Models\ProviderRegion;
use Illuminate\Support\Carbon;
use App\DataTransferObjects\Support\Data;
use Spatie\LaravelData\Attributes\Validation\In;
use Spatie\LaravelData\Attributes\Validation\Max;
use Spatie\LaravelData\Attributes\Validation\NotIn;
use Spatie\LaravelData\Attributes\Validation\Exists;
use Spatie\LaravelData\Attributes\Validation\AlphaDash;
use Spatie\LaravelData\Attributes\Validation\StringType;
use Spatie\LaravelData\Attributes\Validation\IntegerType;
use App\DataTransferObjects\Support\Concerns\BelongsToUser;
class ServerData extends Data
{
use BelongsToUser;
public function __construct(
public ?int $id = null,
#[StringType]
public ?string $status = null,
#[StringType, AlphaDash, Max(40)]
public string $name,
#[NotIn(0), Exists(Provider::class, 'id')]
public int $provider_id,
#[NotIn(0), Exists(ProviderRegion::class, 'id')]
public int $provider_region_id,
#[NotIn(0), Exists(ProviderPlan::class, 'id')]
public int $provider_plan_id,
#[StringType, In(['mysql', 'mariadb', 'postgresql', 'postgresql13'])]
public string $database_type,
#[Exists(User::class, 'id'), IntegerType]
public ?int $user_id = null,
public ?Carbon $created_at = null,
) {
}
public static function fromModel(Server $server): static
{
return static::from(array_merge($server->toArray(), ['user_id' => $server->user->id]));
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace App\DataTransferObjects;
use App\Models\Site;
use App\Models\User;
use App\Models\Server;
use App\Rules\Hostname;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use App\Rules\ValidateMaximumSites;
use App\DataTransferObjects\Support\Data;
use Spatie\LaravelData\Attributes\Validation\Exists;
use App\DataTransferObjects\Support\Rules\CustomRule;
use Spatie\LaravelData\Attributes\Validation\StringType;
use Spatie\LaravelData\Attributes\Validation\IntegerType;
use App\DataTransferObjects\Support\Concerns\BelongsToUser;
class SiteData extends Data
{
use BelongsToUser;
public function __construct(
public ?int $id = null,
public ?string $status = null,
#[Exists(Server::class, 'id'), IntegerType]
public ?int $server_id = null,
#[StringType, CustomRule(Hostname::class, ValidateMaximumSites::class)]
public ?string $domain = null,
#[Exists(User::class, 'id'), IntegerType]
public ?int $user_id = null,
public ?Carbon $created_at = null,
) {
}
public static function authorize(): bool
{
if (auth()->guest()) {
return true;
}
return auth()->user()->can('create', Site::class);
}
public static function fromModel(Site $site): static
{
return static::from(array_merge($site->toArray(), ['user_id' => $site->user->id]));
}
public function toArray(): array
{
return Arr::only(parent::toArray(), [
'id',
'status',
'server_id',
'domain',
'user_id',
'created_at',
]);
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\DataTransferObjects\Support\Casts;
use Illuminate\Support\Carbon;
use Spatie\LaravelData\Casts\Cast;
use Spatie\LaravelData\Support\DataProperty;
class CarbonCast implements Cast
{
public function cast(DataProperty $property, mixed $value, array $context): Carbon
{
return Carbon::parse($value);
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace App\DataTransferObjects\Support\Concerns;
use App\Models\User;
trait BelongsToUser
{
public function getUser(): ?User
{
return User::find($this->user_id);
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\DataTransferObjects\Support;
class Data extends \Spatie\LaravelData\Data
{
/**
* When working with paginated data, we want to include pagination details in JSON
* responses from the API. However, due to legacy requirements Ploi Core is using
* a different structure than this package assumes. Therefore, we will override
* the data collection, register a custom transformer and output the structure.
*/
protected static string $_paginatedCollectionClass = PaginatedDataCollection::class;
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\DataTransferObjects\Support;
class DataCollectableTransformer extends \Spatie\LaravelData\Transformers\DataCollectableTransformer
{
protected function wrapPaginatedArray(array $paginated): array
{
$wrapKey = $this->wrap->getKey() ?? 'data';
return [
$wrapKey => $paginated['data'],
'links' => [
'first' => $paginated['first_page_url'],
'last' => $paginated['last_page_url'],
'prev' => $paginated['prev_page_url'],
'next' => $paginated['next_page_url'],
],
'meta' => [
'current_page' => $paginated['current_page'],
'from' => $paginated['from'],
'last_page' => $paginated['last_page'],
'links' => $paginated['links'],
'path' => $paginated['path'],
'per_page' => $paginated['per_page'],
'to' => $paginated['to'],
'total' => $paginated['total'],
],
];
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\DataTransferObjects\Support;
use Spatie\LaravelData\Support\Wrapping\WrapExecutionType;
class PaginatedDataCollection extends \Spatie\LaravelData\PaginatedDataCollection
{
public function transform(bool $transformValues = true, WrapExecutionType $wrapExecutionType = WrapExecutionType::Disabled, bool $mapPropertyNames = true): array
{
$transformer = new DataCollectableTransformer(
$this->dataClass,
$transformValues,
$wrapExecutionType,
$mapPropertyNames,
$this->getPartialTrees(),
$this->items,
$this->getWrap(),
);
return $transformer->transform();
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\DataTransferObjects\Support\Rules;
use Attribute;
use Spatie\LaravelData\Support\Validation\ValidationPath;
use Spatie\LaravelData\Attributes\Validation\CustomValidationAttribute;
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
class CustomRule extends CustomValidationAttribute
{
protected array $rules = [];
public function __construct(...$rules)
{
$this->rules = $rules;
}
/**
* @return array<object|string>|object|string
*/
public function getRules(ValidationPath $path): array|object|string
{
return array_map(
fn (string $ruleClass) => new $ruleClass(),
$this->rules
);
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\DataTransferObjects\Support\Transformers;
use Illuminate\Support\Carbon;
use Spatie\LaravelData\Support\DataProperty;
use Spatie\LaravelData\Transformers\Transformer;
class CarbonTransformer implements Transformer
{
public function transform(DataProperty $property, mixed $value): string
{
/** @var Carbon $value */
return $value->toISOString();
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace App\DataTransferObjects;
use App\Models\User;
use App\Models\Package;
use Illuminate\Support\Carbon;
use App\DataTransferObjects\Support\Data;
use Spatie\LaravelData\Attributes\Validation\Max;
use Spatie\LaravelData\Attributes\Validation\Email;
use Spatie\LaravelData\Attributes\Validation\Exists;
use Spatie\LaravelData\Attributes\Validation\Unique;
use Spatie\LaravelData\Attributes\Validation\StringType;
use Spatie\LaravelData\Attributes\Validation\BooleanType;
use Spatie\LaravelData\Attributes\Validation\IntegerType;
class UserData extends Data
{
public function __construct(
public ?int $id = null,
public ?string $avatar = null,
#[StringType, Max(255)]
public ?string $name = null,
#[StringType, Email, Max(255), Unique(User::class)]
public ?string $email = null,
#[Exists(Package::class, 'id'), IntegerType]
public ?int $package_id = null,
#[StringType]
public ?string $blocked = null,
#[StringType]
public ?string $language = 'en',
#[BooleanType]
public ?bool $requires_password_for_ftp = true,
public ?Carbon $created_at = null,
) {
}
}

View File

@@ -2,7 +2,12 @@
namespace App\Exceptions;
use Exception;
use Throwable;
use Illuminate\Http\Request;
use App\Http\Middleware\SetLocale;
use App\Http\Middleware\HandleInertiaRequests;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
class Handler extends ExceptionHandler
@@ -29,10 +34,10 @@ class Handler extends ExceptionHandler
/**
* Report or log an exception.
*
* @param \Throwable $exception
* @param Throwable $exception
* @return void
*
* @throws \Exception
* @throws Exception
*/
public function report(Throwable $exception)
{
@@ -42,24 +47,25 @@ class Handler extends ExceptionHandler
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Throwable $exception
* @return \Symfony\Component\HttpFoundation\Response
* @param Request $request
* @param Throwable $exception
* @return Response
*
* @throws \Throwable
* @throws Throwable
*/
public function render($request, Throwable $exception)
{
$response = parent::render($request, $exception);
if (in_array($response->status(), [404, 403])) {
\Route::any($request->path(), function () use ($exception, $request) {
return parent::render($request, $exception);
})->middleware('web');
// Only return an Inertia-response when there are special Vue-templates (403 and 404) and when it isn't an API request.
if (in_array($response->status(), [403, 404]) && ! $request->routeIs('api.*')) {
inertia()->share([
'translations' => SetLocale::getTranslations()
]);
return inertia()->render('Errors/' . $response->status(), ['status' => $response->status()])
->toResponse($request)
->setStatusCode($response->status());
return app(HandleInertiaRequests::class)
->handle($request, fn () => inertia()->render('Errors/' . $response->status(), ['status' => $response->status()])
->toResponse($request));
}
return $response;

View File

@@ -0,0 +1,189 @@
<?php
namespace App\Filament\Pages;
use Filament\Schemas\Schema;
use Filament\Schemas\Components\Grid;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Actions\Action;
use Filament\Forms;
use App\Models\Server;
use App\Models\Package;
use Filament\Pages\Page;
use Illuminate\Support\Str;
use Illuminate\Support\HtmlString;
use Illuminate\Support\Facades\Storage;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
class Settings extends Page implements HasForms
{
use InteractsWithForms;
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-cog';
protected string $view = 'filament.pages.settings';
protected static string | \UnitEnum | null $navigationGroup = 'Settings';
protected static ?int $navigationSort = 1;
public array $data = [];
public function mount(): void
{
cache()->forget('core.settings');
$this->form->fill([
'logo' => setting('logo'),
'name' => setting('name'),
'email' => setting('email'),
'support_emails' => setting('support_emails'),
'default_package' => setting('default_package'),
'default_language' => setting('default_language'),
'rotate_logs_after' => setting('rotate_logs_after'),
'trial' => (bool)setting('trial'),
'support' => (bool)setting('support'),
'documentation' => (bool)setting('documentation'),
'allow_registration' => (bool)setting('allow_registration'),
'receive_email_on_server_creation' => (bool)setting('receive_email_on_server_creation'),
'receive_email_on_site_creation' => (bool)setting('receive_email_on_site_creation'),
'enable_api' => (bool)setting('enable_api'),
'api_token' => setting('api_token'),
'isolate_per_site_per_user' => (bool)setting('isolate_per_site_per_user'),
'default_os' => setting('default_os', Server::OS_UBUNTU_22),
]);
}
public function form(Schema $schema): Schema
{
return $schema
->statePath('data')
->components([
Grid::make(2)
->schema([
Grid::make(2)
->schema([
TextInput::make('name')
->label(__('Company name'))
->required(),
TextInput::make('email')
->label(__('E-mail address'))
->email(),
TextInput::make('support_emails')
->label(__('Support email address'))
->helperText('Separate by comma to allow more email addresses'),
])
->columnSpan(2),
Select::make('default_package')
->options(fn () => Package::orderBy('name')->pluck('name', 'id'))
->label(__('Select default package'))
->helperText(__('Select the default package a user should get when you create or they register')),
Select::make('default_language')
->options(collect(languages())->mapWithKeys(fn (string $language) => [$language => $language]))
->label('Select default language')
->helperText('Select the default language a user should get when you create or they register'),
FileUpload::make('logo')
->label(__('Logo'))
->disk('logos')
->columnSpan(2),
Select::make('rotate_logs_after')
->label(__('This will rotate any logs older than selected, this helps cleanup your database'))
->options([
null => __("Don't rotate logs"),
'weeks-1' => __('Older than 1 week'),
'months-1' => __('Older than 1 month'),
'months-3' => __('Older than 3 months'),
'months-6' => __('Older than 6 months'),
'years-1' => __('Older than 1 year'),
'years-2' => __('Older than 2 years'),
'years-3' => __('Older than 3 years'),
'years-4' => __('Older than 4 years'),
])
->columnSpan(1),
Select::make('default_os')
->label(__('Select the default OS that should be used when users create a server'))
->default(Server::OS_UBUNTU_22)
->options([
Server::OS_UBUNTU_18 => __('Ubuntu 18'),
Server::OS_UBUNTU_20 => __('Ubuntu 20'),
Server::OS_UBUNTU_22 => __('Ubuntu 22'),
])
->columnSpan(1),
Toggle::make('trial')
->label(__('Enable trial'))
->helperText(__('This will allow you to have users with trials.')),
Toggle::make('allow_registration')
->label(__('Allow registration'))
->helperText(__('Allow customer registration')),
Toggle::make('support')
->label(__('Enable support platform'))
->helperText(__('This will allow your customers to make support requests to you.')),
Toggle::make('documentation')
->label(__('Enable documentation platform'))
->helperText(__('This will allow you to create articles for your users to look at.')),
Toggle::make('receive_email_on_server_creation')
->label(__('Receive email when customers create server'))
->helperText(__('This will send an email to all admins notifying them about a new server installation.')),
Toggle::make('receive_email_on_site_creation')
->label(__('Receive email when customers create site'))
->helperText(__('This will send an email to all admins notifying them about a new site installation.')),
Toggle::make('enable_api')
->label(__('Enable API'))
->helperText(new HtmlString(__('This will allow you to interact with your system via the API. ') . '<a href="https://docs.ploi-core.io/304-core-api/737-introduction" target="_blank" class="text-primary-600">' . __('More information') . '</a>')),
TextInput::make('api_token')
->label(__('API token'))
->afterStateHydrated(function (?string $state, TextInput $component) {
$state = filled($state) ? decrypt($state) : null;
$component->state($state);
})
->dehydrateStateUsing(function (?string $state) {
return filled($state) ? encrypt($state) : null;
})
->registerActions([
'generate' => $generateAction = Action::make('generate')
->label(__('Generate'))
->icon('heroicon-o-key')
->action(function (TextInput $component) {
$component->state(Str::random(20));
})
->tooltip('Generate'),
])
->suffixAction($generateAction),
Toggle::make('isolate_per_site_per_user')
->label(__('Enable site isolation per site & user'))
->helperText(__('This will make sure each site created by one user is always isolated from another.')),
]),
]);
}
public function save(): void
{
$state = $this->form->getState();
$oldLogo = setting('logo');
$oldDocumentation = setting('documentation');
$oldSupport = setting('support');
if ($state['logo'] === null && $oldLogo) {
Storage::disk('logos')->delete($oldLogo);
}
setting($state);
cache()->forget('core.settings');
Notification::make()
->success()
->title(__('Settings saved.'))
->send();
if ($state['logo'] !== $oldLogo || $state['documentation'] !== $oldDocumentation || $state['support'] !== $oldSupport) {
$this->redirect(Settings::getUrl());
}
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Filament\Pages;
use Filament\Pages\Page;
use App\Services\VersionChecker;
use Filament\Notifications\Notification;
use Laravel\Horizon\Contracts\MasterSupervisorRepository;
class System extends Page
{
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-adjustments-vertical';
protected string $view = 'filament.pages.system';
protected static string | \UnitEnum | null $navigationGroup = 'Settings';
protected static ?int $navigationSort = 2;
public function getCurrentVersion(): string
{
return app(VersionChecker::class)->getVersions()->currentVersion;
}
public function getRemoteVersion(): string
{
return app(VersionChecker::class)->getVersions()->remoteVersion;
}
public function refreshRemoteVersion(): void
{
app(VersionChecker::class)->flushVersionData();
Notification::make()
->success()
->title(__('Refreshed versions'))
->send();
}
public function getHorizonWorkerStatus(): bool
{
return rescue(fn () => (bool)app(MasterSupervisorRepository::class)->all(), false, false);
}
public function hasAvailableUpdate(): bool
{
return app(VersionChecker::class)->getVersions()->isOutOfDate();
}
public static function getNavigationBadge(): ?string
{
$systemChecker = app(VersionChecker::class);
if ($systemChecker->isOutOfDate()) {
return 'Update available';
}
return null;
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace App\Filament\Pages;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\MarkdownEditor;
use Filament\Actions\Action;
use Filament\Forms;
use Filament\Actions;
use Filament\Pages\Page;
use Filament\Schemas\Schema;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Illuminate\Support\Str;
use Filament\Notifications\Notification;
class Terms extends Page implements HasForms
{
use InteractsWithForms;
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-document-text';
protected string $view = 'filament.pages.terms';
protected static string | \UnitEnum | null $navigationGroup = 'Settings';
protected static ?int $navigationSort = 3;
public array $data = [];
public function mount(): void
{
cache()->forget('core.settings');
$this->form->fill([
'accept_terms_required' => (bool)setting('accept_terms_required'),
'terms' => setting('terms'),
'privacy' => setting('privacy'),
]);
}
public function form(Schema $schema): Schema
{
return $schema
->statePath('data')
->components([
Toggle::make('accept_terms_required')
->label(__(' Require users to accept terms of service on registration'))
->helperText(__('This will require newly registered users to accept the terms of service.')),
MarkdownEditor::make('terms')
->label(__('Content Terms Of Service')),
MarkdownEditor::make('privacy')
->label(__('Content Privacy Policy')),
]);
}
protected function getHeaderActions(): array
{
return [
Action::make('load_terms_template')
->label(__('Load Terms Of Service Template'))
->action(function (self $livewire) {
$template = Str::of(file_get_contents(storage_path('templates/terms-of-service.md')))
->replace([
'{NAME}',
'{WEBSITE}',
'{DATE}',
], [
setting('name'),
config('app.url'),
date('Y-m-d'),
])
->value();
$livewire->data['terms'] = $template;
Notification::make()
->success()
->title(__('Loaded Terms Of Service Template'))
->send();
}),
];
}
public function save(): void
{
$state = $this->form->getState();
setting($state);
cache()->forget('core.settings');
Notification::make()
->success()
->title(__('Terms saved.'))
->send();
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace App\Filament\Resources\Alerts;
use Filament\Schemas\Schema;
use Filament\Forms\Components\MarkdownEditor;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\DateTimePicker;
use Filament\Tables\Columns\TextColumn;
use App\Filament\Resources\Alerts\Pages\ListAlerts;
use App\Filament\Resources\Alerts\Pages\CreateAlert;
use App\Filament\Resources\Alerts\Pages\EditAlert;
use Filament\Forms;
use Filament\Tables;
use App\Models\Alert;
use Filament\Tables\Table;
use Illuminate\Support\Str;
use Filament\Resources\Resource;
use Illuminate\Support\HtmlString;
use App\Filament\Resources\AlertResource\Pages;
class AlertResource extends Resource
{
protected static ?string $model = Alert::class;
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-bell';
protected static string | \UnitEnum | null $navigationGroup = 'Settings';
protected static ?int $navigationSort = 4;
protected static ?string $recordTitleAttribute = 'message';
public static function form(Schema $schema): Schema
{
return $schema
->components([
MarkdownEditor::make('message')
->label(__('Content'))
->columnSpan(2)
->required(),
Select::make('type')
->label(__('Type'))
->options([
Alert::TYPE_INFO => __('Informational'),
Alert::TYPE_WARNING => __('Warning'),
Alert::TYPE_DANGER => __('Danger'),
])
->required(),
DateTimePicker::make('expires_at')
->label(__('Expires at'))
->seconds(false),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('message')
->label(__('Content'))
->formatStateUsing(fn (?string $state) => new HtmlString(Str::markdown($state))),
TextColumn::make('type')
->label(__('Type'))
->badge()
->formatStateUsing(fn (string $state) => match ($state) {
Alert::TYPE_INFO => __('Informational'),
Alert::TYPE_WARNING => __('Warning'),
Alert::TYPE_DANGER => __('Danger'),
default => __('Unknown status')
})
->colors([
'primary' => Alert::TYPE_INFO,
'warning' => Alert::TYPE_WARNING,
'danger' => Alert::TYPE_DANGER,
]),
TextColumn::make('expires_at')
->label('Expires')
->default('-'),
]);
}
public static function getPages(): array
{
return [
'index' => ListAlerts::route('/'),
'create' => CreateAlert::route('/create'),
'edit' => EditAlert::route('/{record}/edit'),
];
}
public static function getGloballySearchableAttributes(): array
{
return [];
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\Alerts\Pages;
use App\Filament\Resources\Alerts\AlertResource;
use Filament\Resources\Pages\CreateRecord;
class CreateAlert extends CreateRecord
{
protected static string $resource = AlertResource::class;
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Filament\Resources\Alerts\Pages;
use Filament\Actions\DeleteAction;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
use App\Filament\Resources\Alerts\AlertResource;
class EditAlert extends EditRecord
{
protected static string $resource = AlertResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Filament\Resources\Alerts\Pages;
use Filament\Actions\CreateAction;
use Filament\Actions;
use App\Filament\Resources\Alerts\AlertResource;
use Filament\Resources\Pages\ListRecords;
class ListAlerts extends ListRecords
{
protected static string $resource = AlertResource::class;
protected ?string $subheading = 'Alerts are meant to inform your users about things that are going on. For example server migrations, pricing changes. They will display as top-banner inside the panel.';
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace App\Filament\Resources\Certificates;
use Filament\Schemas\Schema;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Tables\Columns\TextColumn;
use Filament\Actions\EditAction;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use App\Filament\Resources\Certificates\Pages\ListCertificates;
use Filament\Forms;
use Filament\Tables;
use Filament\Tables\Table;
use App\Models\Certificate;
use Filament\Resources\Resource;
use Illuminate\Support\HtmlString;
use Illuminate\Database\Eloquent\Builder;
use App\Filament\Resources\CertificateResource\Pages;
class CertificateResource extends Resource
{
protected static ?string $model = Certificate::class;
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-chat-bubble-bottom-center-text';
protected static string | \UnitEnum | null $navigationGroup = 'Site management';
protected static ?int $navigationSort = 2;
public static function form(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('site.name'),
TextInput::make('server_id'),
TextInput::make('status'),
TextInput::make('ploi_id'),
TextInput::make('domain'),
Textarea::make('certificate'),
Textarea::make('private'),
TextInput::make('type'),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('server.name')
->searchable()
->label(__('Server')),
TextColumn::make('site.domain')
->searchable()
->label(__('Main domain')),
TextColumn::make('type')
->label('Type'),
TextColumn::make('status')
->badge()
->formatStateUsing(fn (string $state) => match ($state) {
Certificate::STATUS_BUSY => __('Busy'),
Certificate::STATUS_ACTIVE => __('Active'),
default => __('Unknown status')
})
->colors([
'warning' => Certificate::STATUS_BUSY,
'success' => Certificate::STATUS_ACTIVE,
])
->label(__('Status')),
TextColumn::make('domain')
->searchable()
->wrap()
->getStateUsing(function (Certificate $record) {
$state = str($record->domain)->explode(',')->implode(', ');
return new HtmlString($state);
})
->label('Domains & aliases'),
TextColumn::make('created_at')
->label(__('Date'))
->sortable()
->dateTime(),
])
->filters([
//
])
->recordActions([
EditAction::make(),
DeleteAction::make(),
])
->toolbarActions([
DeleteBulkAction::make(),
])
->defaultSort('created_at', 'desc');
}
public static function getEloquentQuery(): Builder
{
return parent::getEloquentQuery()
->orderBy('domain');
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListCertificates::route('/'),
];
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Filament\Resources\Certificates\Pages;
use Filament\Actions\CreateAction;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use App\Filament\Resources\Certificates\CertificateResource;
class ListCertificates extends ListRecords
{
protected static string $resource = CertificateResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace App\Filament\Resources\Cronjobs;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use App\Filament\Resources\Cronjobs\Pages\ListCronjobs;
use Filament\Tables;
use App\Models\Cronjob;
use Filament\Tables\Table;
use Filament\Resources\Resource;
use App\Filament\Resources\CronjobResource\Pages;
class CronjobResource extends Resource
{
protected static ?string $model = Cronjob::class;
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-clock';
protected static string | \UnitEnum | null $navigationGroup = 'Site management';
protected static ?int $navigationSort = 3;
public static function form(Schema $schema): Schema
{
return $schema
->components([
//
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('site.domain')
->searchable()
->label(__('Site')),
TextColumn::make('status')
->badge()
->formatStateUsing(fn (string $state) => match ($state) {
Cronjob::STATUS_BUSY => __('Busy'),
Cronjob::STATUS_ACTIVE => __('Active'),
default => __('Unknown status')
})
->colors([
'warning' => Cronjob::STATUS_BUSY,
'success' => Cronjob::STATUS_ACTIVE,
])
->label(__('Status')),
TextColumn::make('server.name')
->searchable()
->label(__('Server')),
TextColumn::make('command')
->searchable()
->label(__('Command')),
TextColumn::make('frequency')
->label(__('Frequency')),
TextColumn::make('created_at')
->label(__('Date'))
->sortable()
->dateTime(),
])
->filters([
//
])
->recordActions([
//
])
->toolbarActions([
//
])
->defaultSort('created_at', 'desc');
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListCronjobs::route('/'),
];
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\Cronjobs\Pages;
use Filament\Resources\Pages\CreateRecord;
use App\Filament\Resources\Cronjobs\CronjobResource;
class CreateCronjob extends CreateRecord
{
protected static string $resource = CronjobResource::class;
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Filament\Resources\Cronjobs\Pages;
use Filament\Actions\DeleteAction;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
use App\Filament\Resources\Cronjobs\CronjobResource;
class EditCronjob extends EditRecord
{
protected static string $resource = CronjobResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Filament\Resources\Cronjobs\Pages;
use Filament\Actions\CreateAction;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use App\Filament\Resources\Cronjobs\CronjobResource;
class ListCronjobs extends ListRecords
{
protected static string $resource = CronjobResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace App\Filament\Resources\Databases;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Actions\EditAction;
use App\Filament\Resources\Databases\Pages\ListDatabases;
use App\Filament\Resources\Databases\Pages\EditDatabase;
use Filament\Tables;
use App\Models\Database;
use Filament\Tables\Table;
use Filament\Resources\Resource;
use App\Filament\Resources\DatabaseResource\Pages;
class DatabaseResource extends Resource
{
protected static ?string $model = Database::class;
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-circle-stack';
protected static string | \UnitEnum | null $navigationGroup = 'Site management';
protected static ?int $navigationSort = 4;
public static function form(Schema $schema): Schema
{
return $schema
->components([
//
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->label(__('Name'))
->searchable(),
TextColumn::make('server.name')
->label(__('Server'))
->searchable(),
TextColumn::make('site.domain')
->label(__('Site'))
->searchable(),
TextColumn::make('status')
->badge()
->formatStateUsing(fn (string $state) => match ($state) {
Database::STATUS_BUSY => __('Busy'),
Database::STATUS_ACTIVE => __('Active'),
default => __('Unknown status')
})
->colors([
'warning' => Database::STATUS_BUSY,
'success' => Database::STATUS_ACTIVE,
])
->label(__('Status')),
TextColumn::make('created_at')
->label(__('Date'))
->sortable()
->dateTime(),
])
->filters([
//
])
->recordActions([
EditAction::make(),
])
->toolbarActions([
//
])
->defaultSort('created_at', 'desc');
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListDatabases::route('/'),
'edit' => EditDatabase::route('/{record}'),
];
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace App\Filament\Resources\Databases\Pages;
use App\Models\Database;
use App\Services\Ploi\Ploi;
use Filament\Resources\Pages\Page;
use Filament\Schemas\Schema;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use App\Mail\Database\PasswordReset;
use Illuminate\Support\Facades\Mail;
use Filament\Forms\Components\Toggle;
use Filament\Notifications\Notification;
use App\Filament\Resources\Databases\DatabaseResource;
class EditDatabase extends Page implements HasForms
{
use InteractsWithForms;
protected static string $resource = DatabaseResource::class;
protected string $view = 'filament.resources.database-resource.pages.edit-database';
public Database $record;
protected ?string $recentlyUpdatedPassword = null;
public function mount(): void
{
$this->resetDatabasePasswordForm->fill();
}
public ?array $resetDatabasePasswordData = [];
public function resetDatabasePasswordForm(Schema $schema): Schema
{
return $schema
->statePath('resetDatabasePasswordData')
->components([
Toggle::make('send_new_password_to_user')
->label(__('Email new password to user')),
]);
}
public function resetDatabasePassword(): void
{
$state = $this->resetDatabasePasswordForm->getState();
$data = Ploi::make()
->server($this->record->server->ploi_id)
->databases($this->record->ploi_id)
->users($this->record->users->first()->ploi_id)
->resetPassword()
->getData();
$this->recentlyUpdatedPassword = $data->new_password;
Notification::make()
->title(__('Successfully reset database password.'))
->success()
->send();
if ($state['send_new_password_to_user']) {
Mail::to($this->record->site->users)->send(new PasswordReset($this->record, $this->recentlyUpdatedPassword));
}
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Filament\Resources\Databases\Pages;
use Filament\Actions\CreateAction;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use App\Filament\Resources\Databases\DatabaseResource;
class ListDatabases extends ListRecords
{
protected static string $resource = DatabaseResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace App\Filament\Resources\DocumentationCategories;
use Filament\Schemas\Schema;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\MarkdownEditor;
use Filament\Tables\Columns\TextColumn;
use App\Filament\Resources\DocumentationCategories\RelationManagers\DocumentationItemsRelationManager;
use App\Filament\Resources\DocumentationCategories\Pages\ListDocumentationCategories;
use App\Filament\Resources\DocumentationCategories\Pages\CreateDocumentationCategory;
use App\Filament\Resources\DocumentationCategories\Pages\EditDocumentationCategory;
use Filament\Forms;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Support\Str;
use Filament\Resources\Resource;
use Illuminate\Support\HtmlString;
use App\Models\DocumentationCategory;
use App\Filament\Resources\DocumentationCategoryResource\Pages;
use App\Filament\Resources\DocumentationCategoryResource\RelationManagers;
class DocumentationCategoryResource extends Resource
{
protected static ?string $model = DocumentationCategory::class;
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-tag';
protected static string | \UnitEnum | null $navigationGroup = 'Documentation';
protected static ?int $navigationSort = 2;
protected static ?string $pluralLabel = 'Categories';
protected static ?string $label = 'Category';
public static function shouldRegisterNavigation(): bool
{
return (bool)setting('documentation');
}
public static function form(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('title')
->label(__('Title'))
->unique(table: DocumentationCategory::class, column: 'title', ignoreRecord: true)
->required()
->columnSpan(2),
MarkdownEditor::make('description')
->label(__('Description'))
->required()
->columnSpan(2),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('title')
->searchable()
->sortable()
->label(__('Title')),
TextColumn::make('description')
->label(__('Description'))
->formatStateUsing(fn (string $state) => new HtmlString(Str::markdown($state))),
]);
}
public static function getRelations(): array
{
return [
DocumentationItemsRelationManager::class,
];
}
public static function getPages(): array
{
return [
'index' => ListDocumentationCategories::route('/'),
'create' => CreateDocumentationCategory::route('/create'),
'edit' => EditDocumentationCategory::route('/{record}/edit'),
];
}
public static function getGloballySearchableAttributes(): array
{
return ['title'];
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\DocumentationCategories\Pages;
use Filament\Resources\Pages\CreateRecord;
use App\Filament\Resources\DocumentationCategories\DocumentationCategoryResource;
class CreateDocumentationCategory extends CreateRecord
{
protected static string $resource = DocumentationCategoryResource::class;
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Filament\Resources\DocumentationCategories\Pages;
use Filament\Actions\DeleteAction;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
use App\Filament\Resources\DocumentationCategories\DocumentationCategoryResource;
class EditDocumentationCategory extends EditRecord
{
protected static string $resource = DocumentationCategoryResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Filament\Resources\DocumentationCategories\Pages;
use Filament\Actions\CreateAction;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use App\Filament\Resources\DocumentationCategories\DocumentationCategoryResource;
class ListDocumentationCategories extends ListRecords
{
protected static string $resource = DocumentationCategoryResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Filament\Resources\DocumentationCategories\RelationManagers;
use Filament\Schemas\Schema;
use Filament\Forms\Components\TextInput;
use Filament\Tables\Columns\TextColumn;
use Filament\Actions\CreateAction;
use Filament\Actions\EditAction;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Forms;
use Filament\Tables;
use Filament\Tables\Table;
use Filament\Resources\RelationManagers\RelationManager;
class DocumentationItemsRelationManager extends RelationManager
{
protected static string $relationship = 'items';
protected static ?string $recordTitleAttribute = 'title';
protected static ?string $label = 'Article';
protected static ?string $pluralLabel = 'Articles';
public function form(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('title')
->required()
->maxLength(255),
]);
}
public function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('title'),
])
->filters([
//
])
->headerActions([
CreateAction::make(),
])
->recordActions([
EditAction::make(),
DeleteAction::make(),
])
->toolbarActions([
DeleteBulkAction::make(),
]);
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace App\Filament\Resources\DocumentationItems;
use Filament\Schemas\Schema;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\MarkdownEditor;
use Filament\Tables\Columns\TextColumn;
use App\Filament\Resources\DocumentationItems\Pages\ListDocumentationItems;
use App\Filament\Resources\DocumentationItems\Pages\CreateDocumentationItem;
use App\Filament\Resources\DocumentationItems\Pages\EditDocumentationItem;
use Filament\Forms;
use Filament\Tables;
use Filament\Tables\Table;
use Filament\Resources\Resource;
use App\Models\DocumentationItem;
use App\Filament\Resources\DocumentationItemResource\Pages;
class DocumentationItemResource extends Resource
{
protected static ?string $model = DocumentationItem::class;
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-document-duplicate';
protected static string | \UnitEnum | null $navigationGroup = 'Documentation';
protected static ?int $navigationSort = 1;
protected static ?string $pluralLabel = 'Articles';
protected static ?string $label = 'Article';
public static function shouldRegisterNavigation(): bool
{
return (bool)setting('documentation');
}
public static function form(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('title')
->label(__('Title'))
->required(),
Select::make('documentation_category_id')
->relationship('category', 'title')
->searchable()
->preload(),
MarkdownEditor::make('content')
->label(__('Content'))
->required()
->columnSpan(2),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('title')
->searchable()
->sortable(),
TextColumn::make('category.title')
->searchable()
->sortable(),
]);
}
public static function getPages(): array
{
return [
'index' => ListDocumentationItems::route('/'),
'create' => CreateDocumentationItem::route('/create'),
'edit' => EditDocumentationItem::route('/{record}/edit'),
];
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\DocumentationItems\Pages;
use Filament\Resources\Pages\CreateRecord;
use App\Filament\Resources\DocumentationItems\DocumentationItemResource;
class CreateDocumentationItem extends CreateRecord
{
protected static string $resource = DocumentationItemResource::class;
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Filament\Resources\DocumentationItems\Pages;
use Filament\Actions\DeleteAction;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
use App\Filament\Resources\DocumentationItems\DocumentationItemResource;
class EditDocumentationItem extends EditRecord
{
protected static string $resource = DocumentationItemResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Filament\Resources\DocumentationItems\Pages;
use Filament\Actions\CreateAction;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use App\Filament\Resources\DocumentationItems\DocumentationItemResource;
class ListDocumentationItems extends ListRecords
{
protected static string $resource = DocumentationItemResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,279 @@
<?php
namespace App\Filament\Resources\Packages;
use Filament\Schemas\Schema;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Select;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\CheckboxList;
use Filament\Schemas\Components\Actions;
use Filament\Actions\Action;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Forms\Components\Placeholder;
use Filament\Tables\Columns\TextColumn;
use Filament\Actions\EditAction;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use App\Filament\Resources\Packages\RelationManagers\UsersRelationManager;
use App\Filament\Resources\Packages\Pages\ListPackages;
use App\Filament\Resources\Packages\Pages\CreatePackage;
use App\Filament\Resources\Packages\Pages\EditPackage;
use Filament\Forms;
use Filament\Tables;
use App\Models\Package;
use App\Models\Provider;
use Filament\Tables\Table;
use App\Models\ProviderPlan;
use Filament\Resources\Resource;
use Illuminate\Support\HtmlString;
use Filament\Notifications\Notification;
use App\Filament\Resources\Servers\ServerResource;
use App\Filament\Resources\Sites\SiteResource;
use App\Filament\Resources\Providers\ProviderResource;
use App\Filament\Resources\PackageResource\Pages;
use App\Filament\Resources\PackageResource\RelationManagers;
class PackageResource extends Resource
{
protected static ?string $model = Package::class;
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-swatch';
protected static ?int $navigationSort = 3;
public static function form(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('name')
->label(__('Name'))
->required()
->columnSpan(2),
TextInput::make('maximum_sites')
->helperText(__('Set to 0 for unlimited'))
->integer()
->required(),
TextInput::make('maximum_servers')
->helperText(__('Set to 0 for unlimited'))
->integer()
->required(),
TextInput::make('stripe_plan_id')
->helperText(new HtmlString(__('Enter the pricing ID from Stripe here') . ' - <a href="https://docs.ploi-core.io/263-digging-deeper/743-using-stripe" target="ploi-docs-stripe" class="text-primary-500">How does this work?</a>'))
->label(__('Stripe ID'))
->columnSpan(2),
TextInput::make('price_monthly')
->label(__('Monthly price'))
->helperText(__('Fill this in if you want it to be monthly payments'))
->required(),
TextInput::make('price_yearly')
->label(__('Yearly price'))
->helperText(__('Fill this in if you want it to be yearly payments'))
->required(),
Select::make('currency')
->label(__('Currency'))
->options([
'usd' => 'USD $',
'eur' => 'Euro €',
'gbp' => 'GBP £',
'nok' => 'NOK (Norwegian Krone)',
'aud' => 'AUD (Australian dollar)',
'cad' => 'CAD (Canadian dollar)',
'inr' => 'INR (Indian ₹ rupee)',
'thb' => 'THB (Thai Baht)',
'brl' => 'BRL R$ (Brazilian Real)',
'nz' => 'NZD $ (New Zealand Dollar)',
])
->required(),
Grid::make()
->schema([
Section::make(__('Server permissions'))
->icon(ServerResource::getNavigationIcon())
->schema([
Checkbox::make('server_permissions.create')
->reactive()
->label('Allow server creation')
->helperText('This will allow users to create servers'),
Checkbox::make('server_permissions.update')
->label('Allow server updates')
->helperText('This will allow users to update servers'),
Checkbox::make('server_permissions.delete')
->label('Allow server deletion')
->helperText('This will allow users to delete servers'),
])
->columnSpan(1),
Section::make(__('Site permissions'))
->icon(SiteResource::getNavigationIcon())
->schema([
Checkbox::make('site_permissions.create')
->label('Allow site creation')
->helperText('This will allow users to create sites'),
Checkbox::make('site_permissions.update')
->label('Allow site updates')
->helperText('This will allow users to update sites'),
Checkbox::make('site_permissions.delete')
->label('Allow site deletion')
->helperText('This will allow users to delete sites'),
])
->columnSpan(1),
]),
Grid::make()
->schema([
Section::make(__('Available server providers'))
->description(__('These server providers will be available for users that are attached to this package.'))
->icon(ProviderResource::getNavigationIcon())
->schema([
CheckboxList::make('providers')
->relationship('providers', 'name')
->reactive(),
Grid::make(1)
->schema([
Actions::make([
Action::make('manage_provider_plans')
->label(__('Manage provider plans'))
->icon('heroicon-o-adjustments-horizontal')
->schema(function (Package $record) {
return $record->providers->sortBy('name')->map(function (Provider $provider) {
return Section::make($provider->label)
->description(__('Select the plans that should be available for this provider on this package.'))
->icon(ProviderResource::getNavigationIcon())
->statePath($provider->id)
->schema([
Toggle::make('select_specific_provider_plans')
->label(__('Select subset'))
->helperText(__('Check this box if you want to limit the provider plans available on this package.'))
->default(false)
->reactive()
->afterStateUpdated(function (Toggle $component, Set $set) use ($provider) {
$set(
path: "provider_plans",
state: $component->getState() ? $provider->plans->pluck('id') : [],
);
}),
CheckboxList::make("provider_plans")
->label(__('Select plans'))
->options(fn () => $provider->plans->mapWithKeys(fn (ProviderPlan $providerPlan) => [$providerPlan->id => $providerPlan->label ?? $providerPlan->plan_id])->all())
->visible(fn (Get $get) => $get('select_specific_provider_plans'))
->reactive()
->bulkToggleable()
->columns(2)
])
->collapsible();
})->all();
})
->fillForm(function (Package $record) {
return $record->providers->mapWithKeys(function (Provider $provider) use ($record) {
$providerPlanIds = $record->providerPlans()->whereBelongsTo($provider)->pluck('provider_plans.id');
return [$provider->id => [
'select_specific_provider_plans' => $providerPlanIds->isNotEmpty(),
'provider_plans' => $providerPlanIds->all(),
]];
})->all();
})
->action(function (Package $record, array $data) {
$providerPlanIds = collect($data)
// If `select_specific_provider_plans`, all provider plans are available. It could be that this
// option was deselected, and that we have some left over provider plans in the field that
// is now hidden. We will not include theSE IDs so that they ARE detached automatically.
->where('select_specific_provider_plans', true)
->pluck('provider_plans')
->flatten();
// Detaches provider plans not specifically selected.
$record->providerPlans()->sync($providerPlanIds);
Notification::make()
->title(__('Provider plans saved'))
->success()
->send();
})
->modalSubmitActionLabel(__('Save'))
->color('gray')
->disabled(function (Package $record, Get $get) {
$providers = collect($get('providers'))
->map(fn (string $id): int => (int)$id)
->sort();
return $record->providers->pluck('id')->map(fn (string $id): int => (int)$id)->sort()->toArray() !== $providers->all();
})
]),
Placeholder::make('save_warning')
->content(__('You\'ve changed the available server providers. Please save your changes before you can manage the provider plans.'))
->visible(function (Package $record, Get $get) {
$providers = collect($get('providers'))
->map(fn (string $id): int => (int)$id)
->sort();
return $record->providers->pluck('id')->map(fn (string $id): int => (int)$id)->sort()->toArray() !== $providers->all();
})
->hiddenLabel(),
])
->hiddenOn('create'),
])
->columnSpan(1)
])
->visible(function ($get) {
return $get('server_permissions')['create'];
})
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('id')
->label('ID')
->searchable(),
TextColumn::make('name')
->label(__('Name'))
->description(function (Package $record) {
if (!$record->stripe_plan_id) {
return __('Not attached to Stripe.');
}
return "Attached to stripe - {$record->price_monthly} {$record->currency}";
}),
TextColumn::make('maximum_sites')
->formatStateUsing(fn (int $state) => $state === 0 ? __('Unlimited') : $state)
->label(__('Maximum sites')),
TextColumn::make('maximum_servers')
->formatStateUsing(fn (int $state) => $state === 0 ? __('Unlimited') : $state)
->label(__('Maximum servers')),
TextColumn::make('users_count')
->counts('users'),
])
->filters([
//
])
->recordActions([
EditAction::make(),
DeleteAction::make(),
])
->toolbarActions([
DeleteBulkAction::make(),
]);
}
public static function getRelations(): array
{
return [
UsersRelationManager::class,
];
}
public static function getPages(): array
{
return [
'index' => ListPackages::route('/'),
'create' => CreatePackage::route('/create'),
'edit' => EditPackage::route('/{record}/edit'),
];
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\Packages\Pages;
use Filament\Resources\Pages\CreateRecord;
use App\Filament\Resources\Packages\PackageResource;
class CreatePackage extends CreateRecord
{
protected static string $resource = PackageResource::class;
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Filament\Resources\Packages\Pages;
use Filament\Actions\DeleteAction;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
use App\Filament\Resources\Packages\PackageResource;
class EditPackage extends EditRecord
{
protected static string $resource = PackageResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
public function afterSave(): void
{
// Necessary to refresh, in order to load the updated saved relationships and
// correctly show or hide the "manage provider plans" warning placeholder.
$this->getRecord()->refresh();
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Filament\Resources\Packages\Pages;
use Filament\Actions\CreateAction;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use App\Filament\Resources\Packages\PackageResource;
class ListPackages extends ListRecords
{
protected static string $resource = PackageResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Filament\Resources\Packages\RelationManagers;
use Filament\Schemas\Schema;
use Filament\Actions\Action;
use App\Models\User;
use Filament\Tables\Table;
use Filament\Forms\Components\Select;
use App\Filament\Resources\Users\UserResource;
use Filament\Resources\RelationManagers\RelationManager;
class UsersRelationManager extends RelationManager
{
protected static string $relationship = 'users';
protected static ?string $recordTitleAttribute = 'name';
public function form(Schema $schema): Schema
{
return UserResource::form($schema);
}
public function table(Table $table): Table
{
return UserResource::table($table)
->headerActions([
...$table->getHeaderActions(),
Action::make('add_user')
->label(__('Add user'))
->schema(fn (self $livewire) => [
Select::make('user_id')
->label('User')
->options(User::orderBy('name')->get()->mapWithKeys(fn (User $user) => [$user->id => $user->name]))
->preload()
->searchable()
->required(),
])
->action(function (array $data, self $livewire) {
$user = User::find($data['user_id']);
$user->update([
'package_id' => $livewire->ownerRecord->id,
]);
})
->button(),
]);
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Filament\Resources\ProviderPlans\Pages;
use Filament\Resources\Pages\ListRecords;
use App\Filament\Resources\ProviderPlans\ProviderPlanResource;
class ListProviderPlans extends ListRecords
{
protected static string $resource = ProviderPlanResource::class;
protected function getHeaderActions(): array
{
return [
//
];
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace App\Filament\Resources\ProviderPlans;
use Filament\Schemas\Schema;
use Filament\Forms\Components\TextInput;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Actions\EditAction;
use App\Filament\Resources\ProviderPlans\Pages\ListProviderPlans;
use Filament\Forms;
use Filament\Tables;
use App\Models\Provider;
use Filament\Tables\Table;
use App\Models\ProviderPlan;
use Filament\Resources\Resource;
use App\Filament\Resources\ProviderPlanResource\Pages;
class ProviderPlanResource extends Resource
{
protected static ?string $model = ProviderPlan::class;
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-cube';
protected static string | \UnitEnum | null $navigationGroup = 'Providers';
protected static ?int $navigationSort = 2;
public static function form(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('label'),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('id')
->label('ID')
->searchable(),
TextColumn::make('provider.name')
->label(__('Provider'))
->searchable(),
TextColumn::make('plan_id')
->label(__('Plan ID'))
->searchable(),
TextColumn::make('label')
->label(__('Label'))
->searchable(),
TextColumn::make('created_at')
->label(__('Date'))
->sortable()
->dateTime(),
])
->filters([
SelectFilter::make('provider_id')
->label(__('Provider'))
->options(fn () => Provider::orderBy('name')->get()->mapWithKeys(fn (Provider $provider) => [$provider->id => $provider->name])),
])
->recordActions([
EditAction::make(),
])
->toolbarActions([
//
])
->defaultSort('created_at', 'desc');
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListProviderPlans::route('/'),
];
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Filament\Resources\ProviderRegions\Pages;
use Filament\Actions\CreateAction;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use App\Filament\Resources\ProviderRegions\ProviderRegionResource;
class ListProviderRegions extends ListRecords
{
protected static string $resource = ProviderRegionResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace App\Filament\Resources\ProviderRegions;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use App\Filament\Resources\ProviderRegions\Pages\ListProviderRegions;
use Filament\Tables;
use App\Models\Provider;
use Filament\Tables\Table;
use App\Models\ProviderRegion;
use Filament\Resources\Resource;
use App\Filament\Resources\ProviderRegionResource\Pages;
class ProviderRegionResource extends Resource
{
protected static ?string $model = ProviderRegion::class;
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-globe-americas';
protected static string | \UnitEnum | null $navigationGroup = 'Providers';
protected static ?int $navigationSort = 3;
public static function form(Schema $schema): Schema
{
return $schema
->components([
//
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('id')
->label('ID')
->searchable(),
TextColumn::make('provider.name')
->label(__('Provider'))
->searchable(),
TextColumn::make('region_id')
->searchable()
->label(__('Region')),
TextColumn::make('label')
->searchable()
->label(__('Label')),
])
->filters([
SelectFilter::make('provider_id')
->label(__('Provider'))
->options(fn () => Provider::orderBy('name')->pluck('name', 'id'))
])
->recordActions([
//
])
->toolbarActions([
//
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListProviderRegions::route('/'),
];
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\Providers\Pages;
use Filament\Resources\Pages\EditRecord;
use App\Filament\Resources\Providers\ProviderResource;
class EditProvider extends EditRecord
{
protected static string $resource = ProviderResource::class;
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Filament\Resources\Providers\Pages;
use Filament\Actions\Action;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use App\Filament\Resources\Providers\ProviderResource;
class ListProviders extends ListRecords
{
protected static string $resource = ProviderResource::class;
protected function getHeaderActions(): array
{
return [
Action::make('synchronize_providers')
->label(__('Synchronize providers'))
->icon('heroicon-o-arrow-path')
->color('gray')
->url(ProviderResource::getUrl('synchronize')),
];
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Filament\Resources\Providers\Pages;
use App\Filament\Resources\Providers\Widgets\AvailableProvidersOverview;
use Filament\Resources\Pages\Page;
use App\Filament\Resources\Providers\ProviderResource;
class SynchronizeProviders extends Page
{
protected static string $resource = ProviderResource::class;
protected string $view = 'filament.resources.provider-resource.pages.synchronize-providers';
protected static ?string $title = 'Synchronize providers';
protected function getHeaderWidgets(): array
{
return [
AvailableProvidersOverview::class,
];
}
protected function getHeaderActions(): array
{
return [
//
];
}
}

View File

@@ -0,0 +1,128 @@
<?php
namespace App\Filament\Resources\Providers;
use Filament\Schemas\Schema;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\CheckboxList;
use Filament\Tables\Columns\TextColumn;
use Filament\Actions\EditAction;
use Filament\Actions\Action;
use App\Filament\Resources\Providers\Pages\ListProviders;
use App\Filament\Resources\Providers\Pages\SynchronizeProviders;
use App\Filament\Resources\Providers\Pages\EditProvider;
use Filament\Forms;
use Filament\Tables;
use App\Models\Provider;
use Filament\Tables\Table;
use App\Models\ProviderPlan;
use Filament\Resources\Resource;
use Filament\Notifications\Notification;
use Illuminate\Database\Eloquent\Builder;
use App\Filament\Resources\ProviderResource\Pages;
use App\Actions\Provider\SynchronizeProviderAction;
use App\Filament\Resources\Providers\Widgets\AvailableProvidersOverview;
class ProviderResource extends Resource
{
protected static ?string $model = Provider::class;
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-cloud-arrow-up';
protected static string | \UnitEnum | null $navigationGroup = 'Providers';
protected static ?int $navigationSort = 1;
public static function form(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('name')
->label(__('Name'))
->required()
->columnSpan(2),
CheckboxList::make('allowed_plans')
->options(function (Provider $record) {
return $record->plans->mapWithKeys(fn (ProviderPlan $plan) => [$plan->id => $plan->label ?? $plan->plan_id]);
})
->label(__('Allowed Plans')),
CheckboxList::make('allowed_regions')
->options(fn (Provider $record) => $record->regions->pluck('label', 'id'))
->label(__('Allowed Regions')),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('id')
->label('ID')
->searchable(),
TextColumn::make('name')
->description(function (Provider $record) {
return "{$record->plans_count} plan(s) · {$record->regions_count} region(s)";
})
->label(__('Name'))
->searchable(),
TextColumn::make('label')
->label(__('Label'))
->searchable(),
TextColumn::make('created_at')
->label(__('Date'))
->sortable()
->dateTime(),
])
->filters([
//
])
->recordActions([
EditAction::make(),
Action::make('synchronize_provider')
->label(__('Synchronize'))
->tooltip(__('This will synchronize the latest data from this provider to your Ploi Core installation'))
->icon('heroicon-o-arrow-path')
->action(function (Provider $record) {
$provider = app(SynchronizeProviderAction::class)->execute($record->ploi_id);
Notification::make()
->title(__('Provider :provider synchronized successfully.', ['provider' => $provider->name]))
->success()
->send();
}),
])
->toolbarActions([
//
])
->defaultSort('created_at', 'desc');
}
public static function getEloquentQuery(): Builder
{
return parent::getEloquentQuery()
->withCount(['plans', 'regions']);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getWidgets(): array
{
return [
AvailableProvidersOverview::class,
];
}
public static function getPages(): array
{
return [
'index' => ListProviders::route('/'),
'synchronize' => SynchronizeProviders::route('/synchronize'),
'edit' => EditProvider::route('/{record}'),
];
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Filament\Resources\Providers\Widgets;
use Filament\Actions\Action;
use App\Models\AvailableProvider;
use Filament\Widgets\TableWidget;
use Filament\Tables\Columns\TextColumn;
use Filament\Notifications\Notification;
use Illuminate\Database\Eloquent\Builder;
use App\Actions\Provider\SynchronizeProviderAction;
use Illuminate\Database\Eloquent\Relations\Relation;
class AvailableProvidersOverview extends TableWidget
{
protected int|string|array $columnSpan = 'full';
protected static ?string $heading = 'Available Providers';
protected function getTableDescription(): ?string
{
return 'These are all the providers available from your ploi.io account which you can synchronize to your Ploi Core installation.';
}
protected function getTableQuery(): Builder|Relation
{
return AvailableProvider::query();
}
protected function getTableColumns(): array
{
return [
TextColumn::make('name')
->label(__('Name')),
TextColumn::make('label')
->label(__('Label')),
];
}
protected function getTableActions(): array
{
return [
Action::make('synchronize_provider')
->label(__('Synchronize'))
->icon('heroicon-o-arrow-path')
->action(function (AvailableProvider $record, self $livewire) {
$provider = app(SynchronizeProviderAction::class)->execute($record->id);
$livewire->dispatch('$refresh');
Notification::make()
->title(__('Provider :provider synchronized successfully.', ['provider' => $provider->name]))
->success()
->send();
}),
];
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Filament\Resources\Redirects\Pages;
use Filament\Actions\CreateAction;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use App\Filament\Resources\Redirects\RedirectResource;
class ListRedirects extends ListRecords
{
protected static string $resource = RedirectResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,102 @@
<?php
namespace App\Filament\Resources\Redirects;
use Filament\Schemas\Schema;
use Filament\Forms\Components\TextInput;
use Filament\Tables\Columns\TextColumn;
use App\Filament\Resources\Redirects\Pages\ListRedirects;
use Filament\Forms;
use Filament\Tables;
use App\Models\Redirect;
use Filament\Tables\Table;
use Filament\Resources\Resource;
use App\Filament\Resources\RedirectResource\Pages;
class RedirectResource extends Resource
{
protected static ?string $model = Redirect::class;
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-arrow-top-right-on-square';
protected static string | \UnitEnum | null $navigationGroup = 'Site management';
protected static ?int $navigationSort = 5;
public static function form(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('site_id'),
TextInput::make('server_id'),
TextInput::make('status'),
TextInput::make('ploi_id'),
TextInput::make('redirect_from'),
TextInput::make('redirect_to'),
TextInput::make('type'),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('redirect_from')
->wrap()
->label(__('Redirect from'))
->searchable(),
TextColumn::make('redirect_to')
->wrap()
->label(__('Redirect to'))
->searchable(),
TextColumn::make('server.name')
->label(__('Server'))
->searchable(),
TextColumn::make('site.domain')
->label(__('Site'))
->searchable(),
TextColumn::make('type')
->label(__('Type')),
TextColumn::make('status')
->badge()
->formatStateUsing(fn (string $state) => match ($state) {
Redirect::STATUS_BUSY => __('Busy'),
Redirect::STATUS_ACTIVE => __('Active'),
default => __('Unknown status')
})
->colors([
'warning' => Redirect::STATUS_BUSY,
'success' => Redirect::STATUS_ACTIVE,
])
->label(__('Status')),
TextColumn::make('created_at')
->label(__('Date'))
->sortable()
->dateTime(),
])
->filters([
//
])
->recordActions([
//
])
->toolbarActions([
//
])
->defaultSort('created_at', 'desc');
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListRedirects::route('/'),
];
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\Servers\Pages;
use App\Filament\Resources\Servers\ServerResource;
use Filament\Resources\Pages\CreateRecord;
class CreateServer extends CreateRecord
{
protected static string $resource = ServerResource::class;
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Filament\Resources\Servers\Pages;
use Filament\Actions\DeleteAction;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
use App\Filament\Resources\Servers\ServerResource;
class EditServer extends EditRecord
{
protected static string $resource = ServerResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Filament\Resources\Servers\Pages;
use Filament\Actions\Action;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use App\Filament\Resources\Servers\ServerResource;
class ListServers extends ListRecords
{
protected static string $resource = ServerResource::class;
protected function getHeaderActions(): array
{
return [
Action::make('synchronize_servers')
->label(__('Synchronize servers'))
->icon('heroicon-o-arrow-path')
->color('gray')
->url(ServerResource::getUrl('synchronize')),
...parent::getHeaderActions(),
];
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Filament\Resources\Servers\Pages;
use App\Filament\Resources\Servers\Widgets\AvailableServersOverview;
use Filament\Actions\Action;
use Filament\Actions;
use App\Models\Server;
use App\Services\Ploi\Ploi;
use Filament\Resources\Pages\Page;
use Filament\Notifications\Notification;
use App\Filament\Resources\Servers\ServerResource;
class SynchronizeServers extends Page
{
protected static string $resource = ServerResource::class;
protected string $view = 'filament.resources.server-resource.pages.synchronize-servers';
protected static ?string $title = 'Synchronize servers';
protected function getHeaderWidgets(): array
{
return [
AvailableServersOverview::class,
];
}
protected function getHeaderActions(): array
{
return [
Action::make('synchronize_servers')
->label(__('Synchronize all servers'))
->icon('heroicon-o-arrow-path')
->requiresConfirmation()
->modalHeading('Synchronize servers')
->modalDescription('This will synchronize all the servers that are listed in the table, to your Ploi Core installation.')
->action(function () {
$availableServers = Ploi::make()->synchronize()->servers()->getData();
foreach ($availableServers as $availableServer) {
Server::query()
->updateOrCreate([
'ploi_id' => $availableServer->id,
], [
'status' => $availableServer->status,
'name' => $availableServer->name,
'ip' => $availableServer->ip_address,
'ssh_port' => $availableServer->ssh_port,
'internal_ip' => $availableServer->internal_ip,
'available_php_versions' => $availableServer->installed_php_versions,
]);
}
Notification::make()
->title(__('Servers synchronized successfully.'))
->success()
->send();
}),
];
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Filament\Resources\Servers\RelationManagers;
use Filament\Schemas\Schema;
use Filament\Tables\Table;
use App\Filament\Resources\Sites\SiteResource;
use Filament\Resources\RelationManagers\RelationManager;
class SitesRelationManager extends RelationManager
{
protected static string $relationship = 'sites';
protected static ?string $recordTitleAttribute = 'domain';
public static function getLabel(): ?string
{
return __('Site');
}
protected static function getPluralModelLabel(): string
{
return __('Sites');
}
public function form(Schema $schema): Schema
{
return SiteResource::form($schema);
}
public function table(Table $table): Table
{
return SiteResource::table($table);
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Filament\Resources\Servers\RelationManagers;
use Filament\Schemas\Schema;
use Filament\Actions\AttachAction;
use Filament\Actions\DetachAction;
use Filament\Tables;
use Filament\Tables\Table;
use App\Filament\Resources\Users\UserResource;
use Filament\Resources\RelationManagers\RelationManager;
class UsersRelationManager extends RelationManager
{
protected static string $relationship = 'users';
protected static ?string $recordTitleAttribute = 'name';
public static function getLabel(): ?string
{
return __('User');
}
protected static function getPluralModelLabel(): string
{
return __('Users');
}
public function form(Schema $schema): Schema
{
return UserResource::form($schema);
}
public function table(Table $table): Table
{
return UserResource::table($table)
->headerActions([
...$table->getHeaderActions(),
AttachAction::make()
->preloadRecordSelect(),
])
->recordActions([
...$table->getActions(),
DetachAction::make(),
]);
}
}

View File

@@ -0,0 +1,158 @@
<?php
namespace App\Filament\Resources\Servers;
use Filament\Schemas\Schema;
use Filament\Forms\Components\TextInput;
use Filament\Tables\Columns\TextColumn;
use Filament\Actions\EditAction;
use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use App\Filament\Resources\Servers\RelationManagers\UsersRelationManager;
use App\Filament\Resources\Servers\RelationManagers\SitesRelationManager;
use App\Filament\Resources\Servers\Pages\ListServers;
use App\Filament\Resources\Servers\Pages\EditServer;
use App\Filament\Resources\Servers\Pages\SynchronizeServers;
use Filament\Forms;
use App\Models\User;
use Filament\Tables;
use App\Models\Server;
use Filament\Tables\Table;
use Filament\Resources\Resource;
use Illuminate\Support\HtmlString;
use Illuminate\Database\Eloquent\Builder;
use App\Actions\Server\SynchronizeServerAction;
use App\Filament\Resources\ServerResource\Pages;
use App\Filament\Resources\ServerResource\RelationManagers;
class ServerResource extends Resource
{
protected static ?string $model = Server::class;
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-server';
protected static string | \UnitEnum | null $navigationGroup = 'Server management';
protected static ?string $recordTitleAttribute = 'name';
public static function getGloballySearchableAttributes(): array
{
return ['name', 'ip', 'internal_ip', 'id'];
}
public static function form(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('name')
->required()
->label(__('Name'))
->columnSpan(2),
TextInput::make('ip')
->required()
->label('IP address')
->columnSpan(2),
TextInput::make('internal_ip')
->required()
->label('Internal IP address')
->columnSpan(2),
TextInput::make('maximum_sites')
->label(__('Maximum sites'))
->integer()
->required()
->columnSpan(2),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->label(__('Name'))
->searchable(),
TextColumn::make('status')
->label(__('Status'))
->badge()
->formatStateUsing(fn (string $state) => match ($state) {
Server::STATUS_BUSY => __('Busy'),
Server::STATUS_ACTIVE => __('Active'),
default => __('Unknown status')
})
->colors([
'warning' => Server::STATUS_BUSY,
'success' => Server::STATUS_ACTIVE,
]),
TextColumn::make('users')
->label(__('Users'))
->wrap()
->formatStateUsing(function (Server $record) {
$state = $record
->users
->map(function (User $user) {
return '<a href="' . UserResource::getUrl('edit', ['record' => $user]) . '" class="text-primary-600" style="white-space: nowrap">' . $user->name . '</a>';
})
->implode(', ') ?: '-';
return new HtmlString($state);
})
->searchable(query: function (Builder $query, string $search) {
return $query->whereHas('users', function (Builder $query) use ($search) {
return $query
->where('name', 'LIKE', "%{$search}%")
->orWhere('email', 'LIKE', "%{$search}%");
});
}),
TextColumn::make('maximum_sites')
->label(__('Max sites'))
->formatStateUsing(fn (Server $record) => $record->maximum_sites . " (Current: {$record->sites_count})")
->counts('sites'),
TextColumn::make('ip')
->label(__('IP')),
TextColumn::make('created_at')
->label(__('Date'))
->dateTime(),
])
->filters([
//
])
->recordActions([
EditAction::make(),
Action::make('synchronize_server')
->label(__('Synchronize'))
->tooltip(__('This will synchronize the latest data from this provider to your Ploi Core installation'))
->icon('heroicon-o-arrow-path')
->action(fn (Server $record) => app(SynchronizeServerAction::class)->execute($record->ploi_id))
->visible(fn (Server $record) => $record->status === Server::STATUS_ACTIVE),
DeleteAction::make(),
])
->toolbarActions([
DeleteBulkAction::make(),
]);
}
public static function getEloquentQuery(): Builder
{
return parent::getEloquentQuery()
->with(['users:id,name'])
->withCount(['sites']);
}
public static function getRelations(): array
{
return [
UsersRelationManager::class,
SitesRelationManager::class,
];
}
public static function getPages(): array
{
return [
'index' => ListServers::route('/'),
'edit' => EditServer::route('/{record}/edit'),
'synchronize' => SynchronizeServers::route('/synchronize'),
];
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Filament\Resources\Servers\Widgets;
use Filament\Actions\Action;
use App\Models\AvailableServer;
use Filament\Widgets\TableWidget;
use Filament\Tables\Columns\TextColumn;
use Illuminate\Database\Eloquent\Builder;
use App\Actions\Server\SynchronizeServerAction;
use Illuminate\Database\Eloquent\Relations\Relation;
class AvailableServersOverview extends TableWidget
{
protected int|string|array $columnSpan = 'full';
protected static ?string $heading = 'Available servers';
protected function getTableQuery(): Builder|Relation
{
return AvailableServer::query();
}
protected function getTableDescription(): ?string
{
return 'These are all the servers available from your ploi.io account which you can synchronize to your Ploi Core installation.';
}
protected function getTableColumns(): array
{
return [
TextColumn::make('name')->label(__('Name')),
TextColumn::make('ip_address')->label(__('IP address')),
TextColumn::make('sites_count')->label(__('Sites')),
];
}
protected function getTableActions(): array
{
return [
Action::make('synchronize_server')
->label(__('Synchronize'))
->icon('heroicon-o-arrow-path')
->action(function (AvailableServer $record) {
app(SynchronizeServerAction::class)->execute($record->id);
}),
];
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Filament\Resources\SiteSystemUsers\Pages;
use Filament\Actions\CreateAction;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use App\Filament\Resources\SiteSystemUsers\SiteSystemUserResource;
class ListSiteSystemUsers extends ListRecords
{
protected static string $resource = SiteSystemUserResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace App\Filament\Resources\SiteSystemUsers;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use App\Filament\Resources\SiteSystemUsers\Pages\ListSiteSystemUsers;
use Filament\Tables;
use Filament\Tables\Table;
use App\Models\SiteSystemUser;
use Filament\Resources\Resource;
use App\Filament\Resources\SiteSystemUserResource\Pages;
class SiteSystemUserResource extends Resource
{
protected static ?string $model = SiteSystemUser::class;
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-user-group';
protected static string | \UnitEnum | null $navigationGroup = 'Site management';
protected static ?int $navigationSort = 6;
public static function form(Schema $schema): Schema
{
return $schema
->components([
//
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('user_name')
->label(__('Username'))
->searchable(),
TextColumn::make('site.domain')
->label(__('Site'))
->searchable(),
TextColumn::make('created_at')
->label(__('Date'))
->dateTime()
->sortable(),
])
->filters([
//
])
->recordActions([
//
])
->toolbarActions([
//
])
->defaultSort('created_at', 'desc');
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListSiteSystemUsers::route('/'),
];
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\Sites\Pages;
use App\Filament\Resources\Sites\SiteResource;
use Filament\Resources\Pages\CreateRecord;
class CreateSite extends CreateRecord
{
protected static string $resource = SiteResource::class;
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Filament\Resources\Sites\Pages;
use Filament\Actions\DeleteAction;
use Filament\Actions;
use App\Filament\Resources\Sites\SiteResource;
use Filament\Resources\Pages\EditRecord;
class EditSite extends EditRecord
{
protected static string $resource = SiteResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Filament\Resources\Sites\Pages;
use Filament\Actions\Action;
use Filament\Actions;
use App\Traits\HasPloi;
use App\Filament\Resources\Sites\SiteResource;
use Filament\Resources\Pages\ListRecords;
class ListSites extends ListRecords
{
use HasPloi;
protected static string $resource = SiteResource::class;
protected function getHeaderActions(): array
{
return [
Action::make('synchronize_sites')
->label(__('Synchronize sites'))
->icon('heroicon-o-arrow-path')
->color('gray')
->url(SiteResource::getUrl('synchronize')),
...parent::getHeaderActions()
];
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Filament\Resources\Sites\Pages;
use App\Filament\Resources\Sites\Widgets\AvailableSitesOverview;
use Filament\Actions\Action;
use App\Models\Site;
use Filament\Actions;
use App\Models\Server;
use App\Services\Ploi\Ploi;
use Filament\Resources\Pages\Page;
use App\Filament\Resources\Sites\SiteResource;
use Filament\Notifications\Notification;
class SynchronizeSites extends Page
{
protected static string $resource = SiteResource::class;
protected string $view = 'filament.resources.site-resource.pages.synchronize-sites';
public function getHeaderWidgets(): array
{
return [
AvailableSitesOverview::class,
];
}
protected function getHeaderActions(): array
{
return [
Action::make('synchronize_sites')
->label(__('Synchronize all sites'))
->icon('heroicon-o-arrow-path')
->requiresConfirmation()
->modalHeading('Synchronize sites')
->modalDescription('This will synchronize all the sites that are listed in the table, to your Ploi Core installation.')
->action(function () {
$availableSites = Ploi::make()->synchronize()->sites()->getData();
foreach ($availableSites as $availableSite) {
$server = Server::query()->where('ploi_id', $availableSite->server_id)->firstOrFail();
$site = Site::query()
->updateOrCreate([
'ploi_id' => $availableSite->id,
], [
'domain' => $availableSite->domain,
'php_version' => $availableSite->php_version,
]);
$site->status = $availableSite->status;
$site->server_id = $server->id;
$site->save();
}
Notification::make()
->title(__('Sites synchronized successfully.'))
->success()
->send();
}),
];
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Filament\Resources\Sites\RelationManagers;
use Filament\Schemas\Schema;
use Filament\Tables\Table;
use App\Filament\Resources\Certificates\CertificateResource;
use Filament\Resources\RelationManagers\RelationManager;
class CertificatesRelationManager extends RelationManager
{
protected static string $relationship = 'certificates';
protected static ?string $recordTitleAttribute = 'domain';
public function form(Schema $schema): Schema
{
return CertificateResource::form($schema);
}
public function table(Table $table): Table
{
return CertificateResource::table($table);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Filament\Resources\Sites\RelationManagers;
use Filament\Schemas\Schema;
use Filament\Tables\Table;
use App\Filament\Resources\Cronjobs\CronjobResource;
use Filament\Resources\RelationManagers\RelationManager;
class CronjobsRelationManager extends RelationManager
{
protected static string $relationship = 'cronjobs';
protected static ?string $recordTitleAttribute = 'command';
public function form(Schema $schema): Schema
{
return CronjobResource::form($schema);
}
public function table(Table $table): Table
{
return CronjobResource::table($table);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Filament\Resources\Sites\RelationManagers;
use Filament\Schemas\Schema;
use Filament\Tables\Table;
use App\Filament\Resources\Databases\DatabaseResource;
use Filament\Resources\RelationManagers\RelationManager;
class DatabasesRelationManager extends RelationManager
{
protected static string $relationship = 'databases';
protected static ?string $recordTitleAttribute = 'name';
public function form(Schema $schema): Schema
{
return DatabaseResource::form($schema);
}
public function table(Table $table): Table
{
return DatabaseResource::table($table);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Filament\Resources\Sites\RelationManagers;
use Filament\Schemas\Schema;
use Filament\Tables\Table;
use App\Filament\Resources\Redirects\RedirectResource;
use Filament\Resources\RelationManagers\RelationManager;
class RedirectsRelationManager extends RelationManager
{
protected static string $relationship = 'redirects';
protected static ?string $recordTitleAttribute = 'from';
public function form(Schema $schema): Schema
{
return RedirectResource::form($schema);
}
public function table(Table $table): Table
{
return RedirectResource::table($table);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Filament\Resources\Sites\RelationManagers;
use Filament\Schemas\Schema;
use Filament\Tables\Table;
use App\Filament\Resources\SiteSystemUsers\SiteSystemUserResource;
use Filament\Resources\RelationManagers\RelationManager;
class SystemUsersRelationManager extends RelationManager
{
protected static string $relationship = 'systemUsers';
protected static ?string $recordTitleAttribute = 'user_name';
public function form(Schema $schema): Schema
{
return SiteSystemUserResource::form($schema);
}
public function table(Table $table): Table
{
return SiteSystemUserResource::table($table);
}
}

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